mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2025-04-24 17:27:17 +00:00
initial commit
This commit is contained in:
commit
9f19519b75
96
.gitignore
vendored
Normal file
96
.gitignore
vendored
Normal file
@ -0,0 +1,96 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
parts/
|
||||
sdist/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log*
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# IPython Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# dotenv
|
||||
.env
|
||||
|
||||
# virtualenv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
*.deb
|
||||
|
||||
.idea/
|
||||
|
||||
.mypy_cache/
|
||||
|
||||
/cache
|
||||
|
||||
*.db
|
38
migrations/20190830_01_sYYZL-init-tables.py
Normal file
38
migrations/20190830_01_sYYZL-init-tables.py
Normal file
@ -0,0 +1,38 @@
|
||||
'''
|
||||
init tables
|
||||
'''
|
||||
|
||||
from yoyo import step
|
||||
|
||||
__depends__ = {}
|
||||
|
||||
steps = [
|
||||
step('''create table players (
|
||||
player_id integer primary key,
|
||||
created integer not null,
|
||||
nick text not null,
|
||||
job text not null,
|
||||
bis_link text,
|
||||
priority integer not null default 1
|
||||
)'''),
|
||||
step('''create unique index players_nick_job_idx on players(nick, job)'''),
|
||||
|
||||
step('''create table loot (
|
||||
loot_id integer primary key,
|
||||
player_id integer not null,
|
||||
created integer not null,
|
||||
piece text not null,
|
||||
is_tome integer not null,
|
||||
foreign key (player_id) references players(player_id) on delete cascade
|
||||
)'''),
|
||||
step('''create index loot_owner_idx on loot(player_id)'''),
|
||||
|
||||
step('''create table bis (
|
||||
player_id integer not null,
|
||||
created integer not null,
|
||||
piece text not null,
|
||||
is_tome integer not null,
|
||||
foreign key (player_id) references players(player_id) on delete cascade
|
||||
)'''),
|
||||
step('''create unique index bis_piece_player_id_idx on bis(player_id, piece)''')
|
||||
]
|
9
package/ini/ffxivbis.ini
Normal file
9
package/ini/ffxivbis.ini
Normal file
@ -0,0 +1,9 @@
|
||||
[settings]
|
||||
include = ffxivbis.ini.d
|
||||
logging = ffxivbis.ini.d/logging.ini
|
||||
database = sqlite
|
||||
priority = is_required loot_count_bis loot_count_total loot_count loot_priority
|
||||
|
||||
[web]
|
||||
host = 0.0.0.0
|
||||
port = 8000
|
44
package/ini/ffxivbis.ini.d/logging.ini
Normal file
44
package/ini/ffxivbis.ini.d/logging.ini
Normal file
@ -0,0 +1,44 @@
|
||||
[loggers]
|
||||
keys = root,application,database,http
|
||||
|
||||
[handlers]
|
||||
keys = file_handler
|
||||
|
||||
[formatters]
|
||||
keys = generic_format
|
||||
|
||||
[handler_console_handler]
|
||||
class = StreamHandler
|
||||
level = INFO
|
||||
formatter = generic_format
|
||||
args = (sys.stdout,)
|
||||
|
||||
[handler_file_handler]
|
||||
class = logging.handlers.RotatingFileHandler
|
||||
level = INFO
|
||||
formatter = generic_format
|
||||
args = ('ffxivbis.log', 'a', 20971520, 20)
|
||||
|
||||
[formatter_generic_format]
|
||||
format = [%(levelname)s] [%(asctime)s] [%(threadName)s] [%(name)s] [%(funcName)s]: %(message)s
|
||||
datefmt =
|
||||
|
||||
[logger_root]
|
||||
level = INFO
|
||||
handlers = file_handler
|
||||
qualname = root
|
||||
|
||||
[logger_application]
|
||||
level = INFO
|
||||
handlers = file_handler
|
||||
qualname = application
|
||||
|
||||
[logger_database]
|
||||
level = INFO
|
||||
handlers = file_handler
|
||||
qualname = database
|
||||
|
||||
[logger_http]
|
||||
level = INFO
|
||||
handlers = file_handler
|
||||
qualname = http
|
3
package/ini/ffxivbis.ini.d/sqlite.ini
Normal file
3
package/ini/ffxivbis.ini.d/sqlite.ini
Normal file
@ -0,0 +1,3 @@
|
||||
[sqlite]
|
||||
database_path = /home/arcanis/Documents/github/ffxivbis/ffxivbis.db
|
||||
migrations_path = /home/arcanis/Documents/github/ffxivbis/migrations
|
44
setup.py
Normal file
44
setup.py
Normal file
@ -0,0 +1,44 @@
|
||||
from distutils.util import convert_path
|
||||
from setuptools import setup, find_packages
|
||||
from os import path
|
||||
|
||||
|
||||
here = path.abspath(path.dirname(__file__))
|
||||
metadata = dict()
|
||||
with open(convert_path('src/service/core/version.py')) as metadata_file:
|
||||
exec(metadata_file.read(), metadata)
|
||||
|
||||
|
||||
setup(
|
||||
name='ffxivbis',
|
||||
|
||||
version=metadata['__version__'],
|
||||
zip_safe=False,
|
||||
|
||||
description='Helper to handle loot drop',
|
||||
|
||||
author='Evgeniy Alekseev',
|
||||
author_email='i@arcanis.me',
|
||||
|
||||
license='BSD',
|
||||
|
||||
packages=find_packages(exclude=['contrib', 'docs', 'tests']),
|
||||
|
||||
install_requires=[
|
||||
'aiohttp',
|
||||
'requests',
|
||||
'yoyo_migrations'
|
||||
],
|
||||
setup_requires=[
|
||||
'pytest-runner'
|
||||
],
|
||||
tests_require=[
|
||||
'pytest', 'pytest-aiohttp'
|
||||
],
|
||||
|
||||
include_package_data=True,
|
||||
|
||||
extras_require={
|
||||
'test': ['coverage', 'pytest'],
|
||||
},
|
||||
)
|
0
src/service/__init__.py
Normal file
0
src/service/__init__.py
Normal file
0
src/service/api/__init__.py
Normal file
0
src/service/api/__init__.py
Normal file
26
src/service/api/json.py
Normal file
26
src/service/api/json.py
Normal file
@ -0,0 +1,26 @@
|
||||
from enum import Enum
|
||||
from json import JSONEncoder
|
||||
from typing import Any
|
||||
|
||||
|
||||
class HttpEncoder(JSONEncoder):
|
||||
def default(self, obj: Any) -> Any:
|
||||
if isinstance(obj, dict):
|
||||
data = {}
|
||||
for key, value in obj.items():
|
||||
data[key] = self.default(value)
|
||||
return data
|
||||
elif isinstance(obj, Enum):
|
||||
return obj.name
|
||||
elif hasattr(obj, '_ast'):
|
||||
return self.default(obj._ast())
|
||||
elif hasattr(obj, '__iter__') and not isinstance(obj, str):
|
||||
return [self.default(value) for value in obj]
|
||||
elif hasattr(obj, '__dict__'):
|
||||
data = {
|
||||
key: self.default(value)
|
||||
for key, value in obj.__dict__.items()
|
||||
if not callable(value) and not key.startswith('_')}
|
||||
return data
|
||||
else:
|
||||
return obj
|
18
src/service/api/routes.py
Normal file
18
src/service/api/routes.py
Normal file
@ -0,0 +1,18 @@
|
||||
from aiohttp.web import Application
|
||||
|
||||
from .views.bis import BiSView
|
||||
from .views.loot import LootView
|
||||
from .views.player import PlayerView
|
||||
|
||||
|
||||
def setup_routes(app: Application) -> None:
|
||||
app.router.add_get('/api/v1/party', PlayerView)
|
||||
app.router.add_post('/api/v1/party', PlayerView)
|
||||
|
||||
app.router.add_get('/api/v1/party/bis', BiSView)
|
||||
app.router.add_post('/api/v1/party/bis', BiSView)
|
||||
app.router.add_put('/api/v1/party/bis', BiSView)
|
||||
|
||||
app.router.add_get('/api/v1/party/loot', LootView)
|
||||
app.router.add_post('/api/v1/party/loot', LootView)
|
||||
app.router.add_put('/api/v1/party/loot', LootView)
|
30
src/service/api/utils.py
Normal file
30
src/service/api/utils.py
Normal file
@ -0,0 +1,30 @@
|
||||
import json
|
||||
|
||||
from aiohttp.web import Response
|
||||
from typing import Any, Mapping, List
|
||||
|
||||
from .json import HttpEncoder
|
||||
|
||||
|
||||
def make_json(response: Any, args: Mapping[str, Any], code: int = 200) -> str:
|
||||
return json.dumps({
|
||||
'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:
|
||||
return wrap_json({'message': repr(exception)}, args, code)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def wrap_json(response: Any, args: Mapping[str, Any], code: int = 200) -> Response:
|
||||
return Response(
|
||||
text=make_json(response, args, code),
|
||||
status=code,
|
||||
content_type='application/json'
|
||||
)
|
0
src/service/api/views/__init__.py
Normal file
0
src/service/api/views/__init__.py
Normal file
89
src/service/api/views/bis.py
Normal file
89
src/service/api/views/bis.py
Normal file
@ -0,0 +1,89 @@
|
||||
from aiohttp.web import Response, View
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from service.core.ariyala_parser import AriyalaParser
|
||||
from service.models.job import Job
|
||||
from service.models.piece import Piece
|
||||
from service.models.player import Player, PlayerId
|
||||
|
||||
from ..utils import wrap_exception, wrap_invalid_param, wrap_json
|
||||
|
||||
|
||||
class BiSView(View):
|
||||
|
||||
async def get(self) -> Response:
|
||||
try:
|
||||
nick = self.request.query.getone('nick', None)
|
||||
party: Iterable[Player] = [
|
||||
player
|
||||
for player in self.request.app['party'].party
|
||||
if nick is None or player.nick == nick
|
||||
]
|
||||
loot = list(sum([player.bis.pieces for player in party], []))
|
||||
|
||||
except Exception as e:
|
||||
self.request.app.logger.exception('could not get bis')
|
||||
return wrap_exception(e, self.request.query)
|
||||
|
||||
return wrap_json(loot, self.request.query)
|
||||
|
||||
async def post(self) -> Response:
|
||||
try:
|
||||
data = await self.request.json()
|
||||
except Exception:
|
||||
data = dict(await self.request.post())
|
||||
|
||||
required = ['action', 'job', 'nick']
|
||||
if any(param not in data for param in required):
|
||||
return wrap_invalid_param(required, data)
|
||||
player_id = PlayerId(Job[data['job']], data['nick'])
|
||||
|
||||
action = data.get('action')
|
||||
if action not in ('add', 'remove'):
|
||||
return wrap_invalid_param(['action'], data)
|
||||
|
||||
piece: Optional[Piece] = None
|
||||
try:
|
||||
if action == 'add':
|
||||
if 'is_tome' not in data or 'piece' not in data:
|
||||
return wrap_invalid_param(['is_tome', 'piece'], data)
|
||||
|
||||
piece = Piece.get(data) # type: ignore
|
||||
self.request.app['party'].set_item_bis(player_id, piece)
|
||||
|
||||
elif action == 'remove':
|
||||
if 'is_tome' not in data or 'piece' not in data:
|
||||
return wrap_invalid_param(['is_tome', 'piece'], data)
|
||||
|
||||
piece = Piece.get(data) # type: ignore
|
||||
self.request.app['party'].remove_item_bis(player_id, piece)
|
||||
|
||||
except Exception as e:
|
||||
self.request.app.logger.exception('could not add bis')
|
||||
return wrap_exception(e, data)
|
||||
|
||||
return wrap_json({'piece': piece, 'player_id': player_id}, data)
|
||||
|
||||
async def put(self) -> Response:
|
||||
try:
|
||||
data = await self.request.json()
|
||||
except Exception:
|
||||
data = dict(await self.request.post())
|
||||
|
||||
required = ['job', 'link', 'nick']
|
||||
if any(param not in data for param in required):
|
||||
return wrap_invalid_param(required, data)
|
||||
player_id = PlayerId(Job[data['job']], data['nick'])
|
||||
|
||||
try:
|
||||
parser = AriyalaParser(self.request.app['config'])
|
||||
items = parser.get(data['link'])
|
||||
for piece in items:
|
||||
self.request.app['party'].set_item_bis(player_id, piece)
|
||||
self.request.app['party'].set_bis_link(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': data['link']}, data)
|
76
src/service/api/views/loot.py
Normal file
76
src/service/api/views/loot.py
Normal file
@ -0,0 +1,76 @@
|
||||
from aiohttp.web import Response, View
|
||||
from typing import Iterable
|
||||
|
||||
from service.models.job import Job
|
||||
from service.models.piece import Piece
|
||||
from service.models.player import Player, PlayerId
|
||||
|
||||
from ..utils import wrap_exception, wrap_invalid_param, wrap_json
|
||||
|
||||
|
||||
class LootView(View):
|
||||
|
||||
async def get(self) -> Response:
|
||||
try:
|
||||
nick = self.request.query.getone('nick', None)
|
||||
party: Iterable[Player] = [
|
||||
player
|
||||
for player in self.request.app['party'].party
|
||||
if nick is None or player.nick == nick
|
||||
]
|
||||
loot = list(sum([player.loot for player in party], []))
|
||||
|
||||
except Exception as e:
|
||||
self.request.app.logger.exception('could not get loot')
|
||||
return wrap_exception(e, self.request.query)
|
||||
|
||||
return wrap_json(loot, self.request.query)
|
||||
|
||||
async def post(self) -> Response:
|
||||
try:
|
||||
data = await self.request.json()
|
||||
except Exception:
|
||||
data = dict(await self.request.post())
|
||||
|
||||
required = ['action', 'is_tome', 'job', 'nick', 'piece']
|
||||
if any(param not in data for param in required):
|
||||
return wrap_invalid_param(required, data)
|
||||
|
||||
action = data.get('action')
|
||||
if action not in ('add', 'remove'):
|
||||
return wrap_invalid_param(['action'], data)
|
||||
|
||||
try:
|
||||
player_id = PlayerId(Job[data['job']], data['nick'])
|
||||
piece = Piece.get(data)
|
||||
if action == 'add':
|
||||
self.request.app['party'].set_item(player_id, piece)
|
||||
|
||||
elif action == 'remove':
|
||||
self.request.app['party'].remove_item(player_id, piece)
|
||||
|
||||
except Exception as e:
|
||||
self.request.app.logger.exception('could not add loot')
|
||||
return wrap_exception(e, data)
|
||||
|
||||
return wrap_json({'piece': piece, 'player_id': player_id}, data)
|
||||
|
||||
async def put(self) -> Response:
|
||||
try:
|
||||
data = await self.request.json()
|
||||
except Exception:
|
||||
data = dict(await self.request.post())
|
||||
|
||||
required = ['is_tome', 'piece']
|
||||
if any(param not in data for param in required):
|
||||
return wrap_invalid_param(required, data)
|
||||
|
||||
try:
|
||||
piece = Piece.get(data)
|
||||
players = self.request.app['loot'].suggest(piece)
|
||||
|
||||
except Exception as e:
|
||||
self.request.app.logger.exception('could not suggest loot')
|
||||
return wrap_exception(e, data)
|
||||
|
||||
return wrap_json(players, data)
|
65
src/service/api/views/player.py
Normal file
65
src/service/api/views/player.py
Normal file
@ -0,0 +1,65 @@
|
||||
from aiohttp.web import Response, View
|
||||
from typing import Iterable
|
||||
|
||||
from service.core.ariyala_parser import AriyalaParser
|
||||
from service.models.bis import BiS
|
||||
from service.models.job import Job
|
||||
from service.models.player import Player, PlayerId
|
||||
|
||||
from ..utils import wrap_exception, wrap_invalid_param, wrap_json
|
||||
|
||||
|
||||
class PlayerView(View):
|
||||
|
||||
async def get(self) -> Response:
|
||||
try:
|
||||
nick = self.request.query.getone('nick', None)
|
||||
party: Iterable[Player] = [
|
||||
player
|
||||
for player in self.request.app['party'].party
|
||||
if nick is None or player.nick == nick
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
self.request.app.logger.exception('could not get loot')
|
||||
return wrap_exception(e, self.request.query)
|
||||
|
||||
return wrap_json(party, self.request.query)
|
||||
|
||||
async def post(self) -> Response:
|
||||
try:
|
||||
data = await self.request.json()
|
||||
except Exception:
|
||||
data = dict(await self.request.post())
|
||||
|
||||
required = ['action', 'job', 'nick']
|
||||
if any(param not in data for param in required):
|
||||
return wrap_invalid_param(required, data)
|
||||
priority = data.get('priority', 0)
|
||||
link = data.get('link', None)
|
||||
|
||||
action = data.get('action')
|
||||
if action not in ('add', 'remove'):
|
||||
return wrap_invalid_param(['action'], data)
|
||||
|
||||
try:
|
||||
if action == 'add':
|
||||
player = Player(Job[data['job']], data['nick'], BiS(), [], link, priority)
|
||||
player_id = player.player_id
|
||||
self.request.app['party'].set_player(player)
|
||||
|
||||
if link is not None:
|
||||
parser = AriyalaParser(self.request.app['config'])
|
||||
items = parser.get(link)
|
||||
for piece in items:
|
||||
self.request.app['party'].set_item_bis(player_id, piece)
|
||||
|
||||
elif action == 'remove':
|
||||
player_id = PlayerId(Job[data['job']], data['nick'])
|
||||
self.request.app['party'].remove_player(player_id)
|
||||
|
||||
except Exception as e:
|
||||
self.request.app.logger.exception('could not add loot')
|
||||
return wrap_exception(e, data)
|
||||
|
||||
return wrap_json(player_id, data)
|
46
src/service/api/web.py
Normal file
46
src/service/api/web.py
Normal file
@ -0,0 +1,46 @@
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from service.core.config import Configuration
|
||||
from service.core.database import Database
|
||||
from service.core.loot_selector import LootSelector
|
||||
from service.core.party import Party
|
||||
|
||||
from .routes import setup_routes
|
||||
|
||||
|
||||
async def on_shutdown(app: web.Application) -> None:
|
||||
app.logger.warning('server terminated')
|
||||
|
||||
|
||||
def run_server(app: web.Application) -> None:
|
||||
app.logger.info('start server')
|
||||
web.run_app(app,
|
||||
host=app['config'].get('web', 'host'),
|
||||
port=app['config'].getint('web', 'port'),
|
||||
handle_signals=False)
|
||||
|
||||
def setup_service(config: Configuration, database: Database, loot: LootSelector, party: Party) -> web.Application:
|
||||
app = web.Application(logger=logging.getLogger('http'))
|
||||
app.on_shutdown.append(on_shutdown)
|
||||
|
||||
app.middlewares.append(web.normalize_path_middleware(append_slash=False, remove_slash=True))
|
||||
|
||||
# routes
|
||||
app.logger.info('setup routes')
|
||||
setup_routes(app)
|
||||
|
||||
app.logger.info('setup configuration')
|
||||
app['config'] = config
|
||||
|
||||
app.logger.info('setup database')
|
||||
app['database'] = database
|
||||
|
||||
app.logger.info('setup loot selector')
|
||||
app['loot'] = loot
|
||||
|
||||
app.logger.info('setup party worker')
|
||||
app['party'] = party
|
||||
|
||||
return app
|
0
src/service/application/__init__.py
Normal file
0
src/service/application/__init__.py
Normal file
23
src/service/application/application.py
Normal file
23
src/service/application/application.py
Normal file
@ -0,0 +1,23 @@
|
||||
from service.core.config import Configuration
|
||||
|
||||
from .core import Application
|
||||
|
||||
|
||||
def get_config(config_path: str) -> Configuration:
|
||||
config = Configuration()
|
||||
config.load(config_path, {})
|
||||
config.load_logging()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Simple loot recorder for FFXIV')
|
||||
parser.add_argument('-c', '--config', help='configuration path', default='ffxivbis.ini')
|
||||
args = parser.parse_args()
|
||||
|
||||
config = get_config(args.config)
|
||||
app = Application(config)
|
||||
app.run()
|
25
src/service/application/core.py
Normal file
25
src/service/application/core.py
Normal file
@ -0,0 +1,25 @@
|
||||
import logging
|
||||
|
||||
from service.api.web import run_server, setup_service
|
||||
from service.core.config import Configuration
|
||||
from service.core.database import Database
|
||||
from service.core.loot_selector import LootSelector
|
||||
from service.core.party import Party
|
||||
|
||||
|
||||
class Application:
|
||||
|
||||
def __init__(self, config: Configuration) -> None:
|
||||
self.config = config
|
||||
self.logger = logging.getLogger('application')
|
||||
|
||||
def run(self) -> None:
|
||||
database = Database.get(self.config)
|
||||
database.migration()
|
||||
party = Party.get(database)
|
||||
|
||||
priority = self.config.get('settings', 'priority').split()
|
||||
loot_selector = LootSelector(party, priority)
|
||||
|
||||
web = setup_service(self.config, database, loot_selector, party)
|
||||
run_server(web)
|
0
src/service/core/__init__.py
Normal file
0
src/service/core/__init__.py
Normal file
63
src/service/core/ariyala_parser.py
Normal file
63
src/service/core/ariyala_parser.py
Normal file
@ -0,0 +1,63 @@
|
||||
import os
|
||||
import requests
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from service.models.piece import Piece
|
||||
|
||||
from .config import Configuration
|
||||
|
||||
|
||||
class AriyalaParser:
|
||||
|
||||
def __init__(self, config: Configuration) -> None:
|
||||
self.ariyala_url = config.get('ariyala', 'ariyala_url')
|
||||
self.xivapi_url = config.get('ariyala', 'xivapi_url')
|
||||
self.request_timeout = config.getfloat('ariyala', 'request_timeout')
|
||||
|
||||
def __remap_key(self, key: str) -> Optional[str]:
|
||||
if key == 'mainhand':
|
||||
return 'weapon'
|
||||
elif key == 'chest':
|
||||
return 'body'
|
||||
elif key == 'ringLeft':
|
||||
return 'left_ring'
|
||||
elif key == 'ringRight':
|
||||
return 'right_ring'
|
||||
elif key in ('head', 'hands', 'waist', 'legs', 'feet', 'ears', 'neck', 'wrist'):
|
||||
return key
|
||||
return None
|
||||
|
||||
def get(self, url: str) -> List[Piece]:
|
||||
items = self.get_ids(url)
|
||||
return [
|
||||
Piece.get({'piece': slot, 'is_tome': self.get_is_tome(item_id)}) # type: ignore
|
||||
for slot, item_id in items.items()
|
||||
]
|
||||
|
||||
def get_ids(self, url: str) -> Dict[str, int]:
|
||||
norm_path = os.path.normpath(url)
|
||||
set_id = os.path.basename(norm_path)
|
||||
response = requests.get('{}/store.app'.format(self.ariyala_url), params={'identifier': set_id})
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
job = data['content']
|
||||
bis = data['datasets'][job]['normal']['items']
|
||||
|
||||
result: Dict[str, int] = {}
|
||||
for original_key, value in bis.items():
|
||||
key = self.__remap_key(original_key)
|
||||
if key is None:
|
||||
continue
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
def get_is_tome(self, item_id: int) -> bool:
|
||||
response = requests.get('{}/item/{}'.format(self.xivapi_url, item_id),
|
||||
params={'columns': 'IsEquippable'},
|
||||
timeout=self.request_timeout)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
return data['IsEquippable'] == 0 # don't ask
|
55
src/service/core/config.py
Normal file
55
src/service/core/config.py
Normal file
@ -0,0 +1,55 @@
|
||||
import configparser
|
||||
import os
|
||||
|
||||
from logging.config import fileConfig
|
||||
from typing import Any, Dict, Mapping, Optional
|
||||
|
||||
from .exceptions import MissingConfiguration
|
||||
|
||||
|
||||
class Configuration(configparser.RawConfigParser):
|
||||
|
||||
def __init__(self) -> None:
|
||||
configparser.RawConfigParser.__init__(self, allow_no_value=True)
|
||||
self.path: Optional[str] = None
|
||||
self.root_path: Optional[str] = None
|
||||
|
||||
@property
|
||||
def include(self) -> str:
|
||||
return self.__with_root_path(self.get('settings', 'include'))
|
||||
|
||||
def __load_section(self, conf: str) -> None:
|
||||
self.read(os.path.join(self.include, conf))
|
||||
|
||||
def __with_root_path(self, path: str) -> str:
|
||||
return os.path.join(self.root_path, path)
|
||||
|
||||
def get_section(self, section: str) -> Dict[str, str]:
|
||||
if not self.has_section(section):
|
||||
raise MissingConfiguration(section)
|
||||
return dict(self[section])
|
||||
|
||||
def load(self, path: str, values: Mapping[str, Mapping[str, Any]]) -> None:
|
||||
self.path = path
|
||||
self.root_path = os.path.dirname(self.path)
|
||||
|
||||
self.read(self.path)
|
||||
self.load_includes()
|
||||
|
||||
# don't use direct ConfigParser.update here, it overrides whole section
|
||||
for section, options in values.items():
|
||||
if section not in self:
|
||||
self.add_section(section)
|
||||
for key, value in options.items():
|
||||
self.set(section, key, value)
|
||||
|
||||
def load_includes(self) -> None:
|
||||
try:
|
||||
include_dir = self.include
|
||||
for conf in filter(lambda p: p.endswith('.ini'), sorted(os.listdir(include_dir))):
|
||||
self.__load_section(conf)
|
||||
except (FileNotFoundError, configparser.NoOptionError, configparser.NoSectionError):
|
||||
pass
|
||||
|
||||
def load_logging(self) -> None:
|
||||
fileConfig(self.__with_root_path(self.get('settings', 'logging')))
|
81
src/service/core/database.py
Normal file
81
src/service/core/database.py
Normal file
@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from yoyo import get_backend, read_migrations
|
||||
from typing import List, Mapping, Optional, Type, Union
|
||||
|
||||
from service.models.loot import Loot
|
||||
from service.models.piece import Piece
|
||||
from service.models.player import Player, PlayerId
|
||||
from service.models.upgrade import Upgrade
|
||||
|
||||
from .config import Configuration
|
||||
from .exceptions import InvalidDatabase
|
||||
|
||||
|
||||
class Database:
|
||||
|
||||
def __init__(self, migrations_path: str) -> None:
|
||||
self.migrations_path = migrations_path
|
||||
self.logger = logging.getLogger('database')
|
||||
|
||||
@staticmethod
|
||||
def now() -> int:
|
||||
return int(datetime.datetime.now().timestamp())
|
||||
|
||||
@classmethod
|
||||
def get(cls: Type[Database], config: Configuration) -> Database:
|
||||
database_type = config.get('settings', 'database')
|
||||
database_settings = config.get_section(database_type)
|
||||
|
||||
if database_type == 'sqlite':
|
||||
from .sqlite import SQLiteDatabase
|
||||
obj: Type[Database] = SQLiteDatabase
|
||||
else:
|
||||
raise InvalidDatabase(database_type)
|
||||
|
||||
return obj(**database_settings)
|
||||
|
||||
@property
|
||||
def connection(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def delete_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def delete_piece_bis(self, player_id: PlayerId, piece: Piece) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def delete_player(self, player_id: PlayerId) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_party(self) -> List[Player]:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_player(self, player_id: PlayerId) -> Optional[int]:
|
||||
raise NotImplementedError
|
||||
|
||||
def insert_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def insert_piece_bis(self, player_id: PlayerId, piece: Piece) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def insert_player(self, player: Player) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def migration(self) -> None:
|
||||
self.logger.info('perform migrations at {}'.format(self.connection))
|
||||
backend = get_backend(self.connection)
|
||||
migrations = read_migrations(self.migrations_path)
|
||||
with backend.lock():
|
||||
backend.apply_migrations(backend.to_apply(migrations))
|
||||
|
||||
def set_loot(self, party: Mapping[int, Player], bis: List[Loot], loot: List[Loot]) -> List[Player]:
|
||||
for piece in bis:
|
||||
party[piece.player_id].bis.set_item(piece.piece)
|
||||
for piece in loot:
|
||||
party[piece.player_id].loot.append(piece.piece)
|
||||
return list(party.values())
|
19
src/service/core/exceptions.py
Normal file
19
src/service/core/exceptions.py
Normal file
@ -0,0 +1,19 @@
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
class InvalidDatabase(Exception):
|
||||
|
||||
def __init__(self, database_type: str) -> None:
|
||||
Exception.__init__(self, 'Unsupported database {}'.format(database_type))
|
||||
|
||||
|
||||
class InvalidDataRow(Exception):
|
||||
|
||||
def __init__(self, data: Mapping[str, Any]) -> None:
|
||||
Exception.__init__(self, 'Invalid data row `{}`'.format(data))
|
||||
|
||||
|
||||
class MissingConfiguration(Exception):
|
||||
|
||||
def __init__(self, section: str) -> None:
|
||||
Exception.__init__(self, 'Missing configuration section {}'.format(section))
|
24
src/service/core/loot_selector.py
Normal file
24
src/service/core/loot_selector.py
Normal file
@ -0,0 +1,24 @@
|
||||
from typing import Iterable, List, Tuple, Union
|
||||
|
||||
from service.models.player import Player, PlayerIdWithCounters
|
||||
from service.models.piece import Piece
|
||||
from service.models.upgrade import Upgrade
|
||||
|
||||
from .party import Party
|
||||
|
||||
|
||||
class LootSelector:
|
||||
|
||||
def __init__(self, party: Party, order_by: List[str] = None) -> None:
|
||||
self.party = party
|
||||
self.order_by = order_by or ['is_required', 'loot_count_bis', 'loot_count_total', 'loot_count', 'loot_priority']
|
||||
|
||||
def __order_by(self, player: Player, piece: Union[Piece, Upgrade]) -> Tuple:
|
||||
return tuple(map(lambda method: getattr(player, method)(piece), self.order_by))
|
||||
|
||||
def __sorted_by(self, piece: Union[Piece, Upgrade]) -> Iterable[Player]:
|
||||
# pycharm is lying, don't trust it
|
||||
return sorted(self.party.players.values(), key=lambda player: self.__order_by(player, piece), reverse=True)
|
||||
|
||||
def suggest(self, piece: Union[Piece, Upgrade]) -> List[PlayerIdWithCounters]:
|
||||
return [player.player_id_with_counters(piece) for player in self.__sorted_by(piece)]
|
72
src/service/core/party.py
Normal file
72
src/service/core/party.py
Normal file
@ -0,0 +1,72 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from threading import Lock
|
||||
from typing import Dict, List, Optional, Type, Union
|
||||
|
||||
from service.models.piece import Piece
|
||||
from service.models.player import Player, PlayerId
|
||||
from service.models.upgrade import Upgrade
|
||||
|
||||
from .database import Database
|
||||
|
||||
|
||||
class Party:
|
||||
|
||||
def __init__(self, database: Database) -> None:
|
||||
self.lock = Lock()
|
||||
self.players: Dict[PlayerId, Player] = {}
|
||||
self.database = database
|
||||
|
||||
@property
|
||||
def party(self) -> List[Player]:
|
||||
with self.lock:
|
||||
return list(self.players.values())
|
||||
|
||||
@classmethod
|
||||
def get(cls: Type[Party], database: Database) -> Party:
|
||||
obj = Party(database)
|
||||
for player in database.get_party():
|
||||
obj.set_player(player)
|
||||
return obj
|
||||
|
||||
def set_bis_link(self, player_id: PlayerId, link: str) -> None:
|
||||
with self.lock:
|
||||
player = self.players[player_id]
|
||||
player.link = link
|
||||
self.database.insert_player(player)
|
||||
|
||||
def remove_player(self, player_id: PlayerId) -> Optional[Player]:
|
||||
self.database.delete_player(player_id)
|
||||
with self.lock:
|
||||
player = self.players.pop(player_id, None)
|
||||
return player
|
||||
|
||||
def set_player(self, player: Player) -> PlayerId:
|
||||
player_id = player.player_id
|
||||
self.database.insert_player(player)
|
||||
with self.lock:
|
||||
self.players[player_id] = player
|
||||
return player_id
|
||||
|
||||
def set_item(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
||||
self.database.insert_piece(player_id, piece)
|
||||
with self.lock:
|
||||
self.players[player_id].loot.append(piece)
|
||||
|
||||
def remove_item(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
||||
self.database.delete_piece(player_id, piece)
|
||||
with self.lock:
|
||||
try:
|
||||
self.players[player_id].loot.remove(piece)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def set_item_bis(self, player_id: PlayerId, piece: Piece) -> None:
|
||||
self.database.insert_piece_bis(player_id, piece)
|
||||
with self.lock:
|
||||
self.players[player_id].bis.set_item(piece)
|
||||
|
||||
def remove_item_bis(self, player_id: PlayerId, piece: Piece) -> None:
|
||||
self.database.delete_piece_bis(player_id, piece)
|
||||
with self.lock:
|
||||
self.players[player_id].bis.remove_item(piece)
|
109
src/service/core/sqlite.py
Normal file
109
src/service/core/sqlite.py
Normal file
@ -0,0 +1,109 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from service.models.bis import BiS
|
||||
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.upgrade import Upgrade
|
||||
|
||||
from .database import Database
|
||||
from .sqlite_helper import SQLiteHelper
|
||||
|
||||
|
||||
class SQLiteDatabase(Database):
|
||||
|
||||
def __init__(self, database_path: str, migrations_path: str) -> None:
|
||||
Database.__init__(self, migrations_path)
|
||||
self.database_path = database_path
|
||||
|
||||
@property
|
||||
def connection(self) -> str:
|
||||
return 'sqlite:///{path}'.format(path=self.database_path)
|
||||
|
||||
def delete_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
||||
player = self.get_player(player_id)
|
||||
if player is None:
|
||||
return
|
||||
|
||||
with SQLiteHelper(self.database_path) as cursor:
|
||||
cursor.execute(
|
||||
'''delete from loot
|
||||
where loot_id in (
|
||||
select loot_id from loot
|
||||
where player_id = ? and piece = ? and is_tome = ? order by created desc limit 1
|
||||
)''',
|
||||
(player, piece.name, getattr(piece, 'is_tome', True)))
|
||||
|
||||
def delete_piece_bis(self, player_id: PlayerId, piece: Piece) -> None:
|
||||
player = self.get_player(player_id)
|
||||
if player is None:
|
||||
return
|
||||
|
||||
with SQLiteHelper(self.database_path) as cursor:
|
||||
cursor.execute(
|
||||
'''delete from bis where player_id = ? and piece = ?''',
|
||||
(player, piece.name))
|
||||
|
||||
def delete_player(self, player_id: PlayerId) -> None:
|
||||
with SQLiteHelper(self.database_path) as cursor:
|
||||
cursor.execute('''delete from players where nick = ? and job = ?''',
|
||||
(player_id.nick, player_id.job.name))
|
||||
|
||||
def get_party(self) -> List[Player]:
|
||||
with SQLiteHelper(self.database_path) as cursor:
|
||||
cursor.execute('''select * from bis''')
|
||||
bis_pieces = [Loot(row['player_id'], Piece.get(row)) for row in cursor.fetchall()]
|
||||
cursor.execute('''select * from loot''')
|
||||
loot_pieces = [Loot(row['player_id'], Piece.get(row)) for row in cursor.fetchall()]
|
||||
cursor.execute('''select * from players''')
|
||||
party = {
|
||||
row['player_id']: Player(Job[row['job']], row['nick'], BiS(), [], row['bis_link'], row['priority'])
|
||||
for row in cursor.fetchall()
|
||||
}
|
||||
return self.set_loot(party, bis_pieces, loot_pieces)
|
||||
|
||||
def get_player(self, player_id: PlayerId) -> Optional[int]:
|
||||
with SQLiteHelper(self.database_path) as cursor:
|
||||
cursor.execute('''select player_id from players where nick = ? and job = ?''',
|
||||
(player_id.nick, player_id.job.name))
|
||||
player = cursor.fetchone()
|
||||
return player['player_id'] if player is not None else None
|
||||
|
||||
def insert_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
||||
player = self.get_player(player_id)
|
||||
if player is None:
|
||||
return
|
||||
|
||||
with SQLiteHelper(self.database_path) as cursor:
|
||||
cursor.execute(
|
||||
'''insert into loot
|
||||
(created, piece, is_tome, player_id)
|
||||
values
|
||||
(?, ?, ?, ?)''',
|
||||
(Database.now(), piece.name, getattr(piece, 'is_tome', True), player)
|
||||
)
|
||||
|
||||
def insert_piece_bis(self, player_id: PlayerId, piece: Piece) -> None:
|
||||
player = self.get_player(player_id)
|
||||
if player is None:
|
||||
return
|
||||
|
||||
with SQLiteHelper(self.database_path) as cursor:
|
||||
cursor.execute(
|
||||
'''replace into bis
|
||||
(created, piece, is_tome, player_id)
|
||||
values
|
||||
(?, ?, ?, ?)''',
|
||||
(Database.now(), piece.name, piece.is_tome, player)
|
||||
)
|
||||
|
||||
def insert_player(self, player: Player) -> None:
|
||||
with SQLiteHelper(self.database_path) as cursor:
|
||||
cursor.execute(
|
||||
'''replace into players
|
||||
(created, nick, job, bis_link, priority)
|
||||
values
|
||||
(?, ?, ?, ?, ?)''',
|
||||
(Database.now(), player.nick, player.job.name, player.link, player.priority)
|
||||
)
|
28
src/service/core/sqlite_helper.py
Normal file
28
src/service/core/sqlite_helper.py
Normal file
@ -0,0 +1,28 @@
|
||||
# because sqlite3 does not support context management
|
||||
import sqlite3
|
||||
|
||||
from types import TracebackType
|
||||
from typing import Any, Dict, Optional, Type
|
||||
|
||||
|
||||
def dict_factory(cursor: sqlite3.Cursor, row: sqlite3.Row) -> Dict[str, Any]:
|
||||
return {
|
||||
key: value
|
||||
for key, value in zip([column[0] for column in cursor.description], row)
|
||||
}
|
||||
|
||||
|
||||
class SQLiteHelper():
|
||||
def __init__(self, database_path: str) -> None:
|
||||
self.database_path = database_path
|
||||
|
||||
def __enter__(self) -> sqlite3.Cursor:
|
||||
self.conn = sqlite3.connect(self.database_path)
|
||||
self.conn.row_factory = dict_factory
|
||||
self.conn.execute('''pragma foreign_keys = on''')
|
||||
return self.conn.cursor()
|
||||
|
||||
def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException],
|
||||
traceback: Optional[TracebackType]) -> None:
|
||||
self.conn.commit()
|
||||
self.conn.close()
|
1
src/service/core/version.py
Normal file
1
src/service/core/version.py
Normal file
@ -0,0 +1 @@
|
||||
__version__ = '0.1.0'
|
0
src/service/models/__init__.py
Normal file
0
src/service/models/__init__.py
Normal file
47
src/service/models/bis.py
Normal file
47
src/service/models/bis.py
Normal file
@ -0,0 +1,47 @@
|
||||
import itertools
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from .piece import Piece
|
||||
from .upgrade import Upgrade
|
||||
|
||||
|
||||
@dataclass
|
||||
class BiS:
|
||||
weapon: Optional[Piece] = None
|
||||
head: Optional[Piece] = None
|
||||
body: Optional[Piece] = None
|
||||
hands: Optional[Piece] = None
|
||||
waist: Optional[Piece] = None
|
||||
legs: Optional[Piece] = None
|
||||
feet: Optional[Piece] = None
|
||||
ears: Optional[Piece] = None
|
||||
neck: Optional[Piece] = None
|
||||
wrist: Optional[Piece] = None
|
||||
left_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
|
||||
def pieces(self) -> List[Piece]:
|
||||
return [piece for piece in self.__dict__.values() if isinstance(piece, Piece)]
|
||||
|
||||
@property
|
||||
def upgrades_required(self) -> Dict[Upgrade, int]:
|
||||
return {
|
||||
upgrade: len(list(pieces))
|
||||
for upgrade, pieces in itertools.groupby(self.pieces, lambda piece: piece.upgrade)
|
||||
}
|
||||
|
||||
def set_item(self, piece: Union[Piece, Upgrade]) -> None:
|
||||
setattr(self, piece.name, piece)
|
||||
|
||||
def remove_item(self, piece: Union[Piece, Upgrade]) -> None:
|
||||
setattr(self, piece.name, None)
|
78
src/service/models/job.py
Normal file
78
src/service/models/job.py
Normal file
@ -0,0 +1,78 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum, auto
|
||||
from typing import Tuple
|
||||
|
||||
from .piece import Piece, PieceAccessory, Weapon
|
||||
|
||||
|
||||
class Job(Enum):
|
||||
PLD = auto()
|
||||
WAR = auto()
|
||||
DRK = auto()
|
||||
GNB = auto()
|
||||
WHM = auto()
|
||||
SCH = auto()
|
||||
AST = auto()
|
||||
MNK = auto()
|
||||
DRG = auto()
|
||||
NIN = auto()
|
||||
SAM = auto()
|
||||
BRD = auto()
|
||||
MCH = auto()
|
||||
DNC = auto()
|
||||
BLM = auto()
|
||||
SMN = auto()
|
||||
RDM = auto()
|
||||
|
||||
@staticmethod
|
||||
def group_accs_dex() -> Tuple:
|
||||
return Job.group_ranges() + (Job.NIN,)
|
||||
|
||||
@staticmethod
|
||||
def group_accs_str() -> Tuple:
|
||||
return Job.group_mnk() + (Job.DRG,)
|
||||
|
||||
@staticmethod
|
||||
def group_casters() -> Tuple:
|
||||
return (Job.BLM, Job.SMN, Job.RDM)
|
||||
|
||||
@staticmethod
|
||||
def group_healers() -> Tuple:
|
||||
return (Job.WHM, Job.SCH, Job.AST)
|
||||
|
||||
@staticmethod
|
||||
def group_mnk() -> Tuple:
|
||||
return (Job.MNK, Job.SAM)
|
||||
|
||||
@staticmethod
|
||||
def group_ranges() -> Tuple:
|
||||
return (Job.BRD, Job.MCH, Job.DNC)
|
||||
|
||||
@staticmethod
|
||||
def group_tanks() -> Tuple:
|
||||
return (Job.PLD, Job.WAR, Job.DRK, Job.GNB)
|
||||
|
||||
@staticmethod
|
||||
def has_same_loot(left: Job, right: Job, piece: Piece) -> bool:
|
||||
# same jobs, alright
|
||||
if left == right:
|
||||
return True
|
||||
|
||||
# weapons are unique per class always
|
||||
if isinstance(piece, Weapon):
|
||||
return False
|
||||
|
||||
# group comparison
|
||||
for group in (Job.group_casters(), Job.group_healers(), Job.group_mnk(), Job.group_ranges(), Job.group_tanks()):
|
||||
if left in group and right in group:
|
||||
return True
|
||||
|
||||
# accessories group comparison
|
||||
if isinstance(Piece, PieceAccessory):
|
||||
for group in (Job.group_accs_dex(), Job.group_accs_str()):
|
||||
if left in group and right in group:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
11
src/service/models/loot.py
Normal file
11
src/service/models/loot.py
Normal file
@ -0,0 +1,11 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
from .piece import Piece
|
||||
from .upgrade import Upgrade
|
||||
|
||||
|
||||
@dataclass
|
||||
class Loot:
|
||||
player_id: int
|
||||
piece: Union[Piece, Upgrade]
|
131
src/service/models/piece.py
Normal file
131
src/service/models/piece.py
Normal file
@ -0,0 +1,131 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Mapping, Type, Union
|
||||
|
||||
from .upgrade import Upgrade
|
||||
|
||||
from service.core.exceptions import InvalidDataRow
|
||||
|
||||
|
||||
@dataclass
|
||||
class Piece:
|
||||
is_tome: bool
|
||||
name: str
|
||||
|
||||
@property
|
||||
def upgrade(self) -> Upgrade:
|
||||
if not self.is_tome:
|
||||
return Upgrade.NoUpgrade
|
||||
elif isinstance(self, Waist) or isinstance(self, PieceAccessory):
|
||||
return Upgrade.AccessoryUpgrade
|
||||
elif isinstance(self, Weapon):
|
||||
return Upgrade.WeaponUpgrade
|
||||
elif isinstance(self, PieceGear):
|
||||
return Upgrade.GearUpgrade
|
||||
return Upgrade.NoUpgrade
|
||||
|
||||
@classmethod
|
||||
def get(cls: Type[Piece], data: Mapping[str, Any]) -> Union[Piece, Upgrade]:
|
||||
try:
|
||||
piece_type = data['piece']
|
||||
is_tome = bool(data['is_tome'])
|
||||
except KeyError:
|
||||
raise InvalidDataRow(data)
|
||||
if piece_type.lower() == 'weapon':
|
||||
return Weapon(is_tome)
|
||||
elif piece_type.lower() == 'head':
|
||||
return Head(is_tome)
|
||||
elif piece_type.lower() == 'body':
|
||||
return Body(is_tome)
|
||||
elif piece_type.lower() == 'hands':
|
||||
return Hands(is_tome)
|
||||
elif piece_type.lower() == 'waist':
|
||||
return Waist(is_tome)
|
||||
elif piece_type.lower() == 'legs':
|
||||
return Legs(is_tome)
|
||||
elif piece_type.lower() == 'feet':
|
||||
return Feet(is_tome)
|
||||
elif piece_type.lower() == 'ears':
|
||||
return Ears(is_tome)
|
||||
elif piece_type.lower() == 'neck':
|
||||
return Neck(is_tome)
|
||||
elif piece_type.lower() == 'wrist':
|
||||
return Wrist(is_tome)
|
||||
elif piece_type.lower() in ('left_ring', 'right_ring', 'ring'):
|
||||
return Ring(is_tome, piece_type.lower())
|
||||
elif piece_type.lower() in Upgrade.dict_types():
|
||||
return Upgrade[piece_type]
|
||||
else:
|
||||
raise InvalidDataRow(data)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PieceAccessory(Piece):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class PieceGear(Piece):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Weapon(Piece):
|
||||
name: str = 'weapon'
|
||||
|
||||
|
||||
@dataclass
|
||||
class Head(PieceGear):
|
||||
name: str = 'head'
|
||||
|
||||
|
||||
@dataclass
|
||||
class Body(PieceGear):
|
||||
name: str = 'body'
|
||||
|
||||
|
||||
@dataclass
|
||||
class Hands(PieceGear):
|
||||
name: str = 'hands'
|
||||
|
||||
|
||||
@dataclass
|
||||
class Waist(PieceGear):
|
||||
name: str = 'waist'
|
||||
|
||||
|
||||
@dataclass
|
||||
class Legs(PieceGear):
|
||||
name: str = 'legs'
|
||||
|
||||
|
||||
@dataclass
|
||||
class Feet(PieceGear):
|
||||
name: str = 'feet'
|
||||
|
||||
|
||||
@dataclass
|
||||
class Ears(PieceAccessory):
|
||||
name: str = 'ears'
|
||||
|
||||
|
||||
@dataclass
|
||||
class Neck(PieceAccessory):
|
||||
name: str = 'neck'
|
||||
|
||||
|
||||
@dataclass
|
||||
class Wrist(PieceAccessory):
|
||||
name: str = 'wrist'
|
||||
|
||||
|
||||
@dataclass
|
||||
class Ring(PieceAccessory):
|
||||
name: str = 'ring'
|
||||
|
||||
# override __eq__method to be able to compare left/right rings
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if not isinstance(other, Ring):
|
||||
return False
|
||||
return self.is_tome == other.is_tome
|
73
src/service/models/player.py
Normal file
73
src/service/models/player.py
Normal file
@ -0,0 +1,73 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from .bis import BiS
|
||||
from .job import Job
|
||||
from .piece import Piece
|
||||
from .upgrade import Upgrade
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlayerId:
|
||||
job: Job
|
||||
nick: str
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(str(self))
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlayerIdWithCounters(PlayerId):
|
||||
loot_count: int
|
||||
loot_count_bis: int
|
||||
loot_count_total: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlayerIdFull:
|
||||
jobs: List[Job]
|
||||
nick: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Player:
|
||||
job: Job
|
||||
nick: str
|
||||
bis: BiS
|
||||
loot: List[Union[Piece, Upgrade]]
|
||||
link: Optional[str] = None
|
||||
priority: int = 0
|
||||
|
||||
@property
|
||||
def player_id(self) -> PlayerId:
|
||||
return PlayerId(self.job, self.nick)
|
||||
|
||||
def player_id_with_counters(self, piece: Union[Piece, Upgrade]) -> PlayerIdWithCounters:
|
||||
return PlayerIdWithCounters(self.job, self.nick, self.loot_count(piece),
|
||||
self.loot_count_bis(piece), self.loot_count_total(piece))
|
||||
|
||||
# ordering methods
|
||||
def is_required(self, piece: Union[Piece, Upgrade]) -> bool:
|
||||
# lets check if it is even in bis
|
||||
if not self.bis.has_piece(piece):
|
||||
return False
|
||||
|
||||
if isinstance(piece, Piece):
|
||||
# alright it is in is, lets check if he even got it
|
||||
return self.loot_count(piece) == 0
|
||||
elif isinstance(piece, Upgrade):
|
||||
# alright it lets check how much upgrades does they need
|
||||
return self.bis.upgrades_required[piece] > self.loot_count(piece)
|
||||
return False
|
||||
|
||||
def loot_count(self, piece: Union[Piece, Upgrade]) -> int:
|
||||
return self.loot.count(piece)
|
||||
|
||||
def loot_count_bis(self, _: Union[Piece, Upgrade]) -> int:
|
||||
return len([piece for piece in self.loot if self.bis.has_piece(piece)])
|
||||
|
||||
def loot_count_total(self, _: Union[Piece, Upgrade]) -> int:
|
||||
return len(self.loot)
|
||||
|
||||
def loot_priority(self, _: Union[Piece, Upgrade]) -> int:
|
||||
return self.priority
|
13
src/service/models/upgrade.py
Normal file
13
src/service/models/upgrade.py
Normal file
@ -0,0 +1,13 @@
|
||||
from enum import Enum, auto
|
||||
from typing import List
|
||||
|
||||
|
||||
class Upgrade(Enum):
|
||||
NoUpgrade = auto()
|
||||
AccessoryUpgrade = auto()
|
||||
GearUpgrade = auto()
|
||||
WeaponUpgrade = auto()
|
||||
|
||||
@staticmethod
|
||||
def dict_types() -> List[str]:
|
||||
return list(map(lambda t: t.name.lower(), Upgrade))
|
5
test/__init__.py
Normal file
5
test/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'src'))
|
128
test/conftest.py
Normal file
128
test/conftest.py
Normal file
@ -0,0 +1,128 @@
|
||||
import os
|
||||
import pytest
|
||||
import tempfile
|
||||
|
||||
from typing import Any, List
|
||||
|
||||
from service.api.web import setup_service
|
||||
from service.core.ariyala_parser import AriyalaParser
|
||||
from service.core.config import Configuration
|
||||
from service.core.database import Database
|
||||
from service.core.loot_selector import LootSelector
|
||||
from service.core.party import Party
|
||||
from service.core.sqlite import SQLiteDatabase
|
||||
from service.models.bis import BiS
|
||||
from service.models.job import Job
|
||||
from service.models.piece import Head, Piece, Weapon
|
||||
from service.models.player import Player
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def parser(config: Configuration) -> AriyalaParser:
|
||||
return AriyalaParser(config)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bis() -> BiS:
|
||||
return BiS()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bis2() -> BiS:
|
||||
return BiS()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bis_link() -> str:
|
||||
return 'https://ffxiv.ariyala.com/19V5R'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bis_set() -> List[Piece]:
|
||||
items: List[Piece] = []
|
||||
items.append(Piece.get({'piece': 'weapon', 'is_tome': False}))
|
||||
items.append(Piece.get({'piece': 'head', 'is_tome': False}))
|
||||
items.append(Piece.get({'piece': 'body', 'is_tome': False}))
|
||||
items.append(Piece.get({'piece': 'hands', 'is_tome': True}))
|
||||
items.append(Piece.get({'piece': 'waist', 'is_tome': True}))
|
||||
items.append(Piece.get({'piece': 'legs', 'is_tome': True}))
|
||||
items.append(Piece.get({'piece': 'feet', 'is_tome': False}))
|
||||
items.append(Piece.get({'piece': 'ears', 'is_tome': False}))
|
||||
items.append(Piece.get({'piece': 'neck', 'is_tome': True}))
|
||||
items.append(Piece.get({'piece': 'wrist', 'is_tome': False}))
|
||||
items.append(Piece.get({'piece': 'left_ring', 'is_tome': True}))
|
||||
items.append(Piece.get({'piece': 'right_ring', 'is_tome': True}))
|
||||
|
||||
return items
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config() -> Configuration:
|
||||
config = Configuration()
|
||||
config.load('/dev/null', {
|
||||
'ariyala': {
|
||||
'ariyala_url': 'https://ffxiv.ariyala.com',
|
||||
'request_timeout': 1,
|
||||
'xivapi_url': 'https://xivapi.com'
|
||||
},
|
||||
'settings': {
|
||||
'include': '/dev/null'
|
||||
}
|
||||
})
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def database() -> SQLiteDatabase:
|
||||
db = SQLiteDatabase(
|
||||
tempfile.mktemp('-ffxivbis.db'),
|
||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), 'migrations'))
|
||||
db.migration()
|
||||
return db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def head_with_upgrade() -> Piece:
|
||||
return Head(is_tome=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def party(database: Database) -> Party:
|
||||
return Party(database)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def player(bis: BiS) -> Player:
|
||||
return Player(Job.WHM, 'A nick', bis, [])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def player2(bis2: BiS) -> Player:
|
||||
return Player(Job.AST, 'Another nick', bis2, [], priority=0)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def selector(party: Party, player: Player, player2: Player,
|
||||
head_with_upgrade: Piece, weapon: Piece) -> LootSelector:
|
||||
obj = LootSelector(party)
|
||||
|
||||
obj.party.set_player(player)
|
||||
player.bis.set_item(weapon)
|
||||
|
||||
obj.party.set_player(player2)
|
||||
player2.bis.set_item(head_with_upgrade)
|
||||
player2.bis.set_item(weapon)
|
||||
|
||||
return LootSelector(party)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def server(loop: Any, aiohttp_client: Any,
|
||||
config: Configuration, database: Database, selector: LootSelector, party: Party) -> Any:
|
||||
app = setup_service(config, database, selector, party)
|
||||
return loop.run_until_complete(aiohttp_client(app))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def weapon() -> Piece:
|
||||
return Weapon(is_tome=False)
|
10
test/test_ariyala.py
Normal file
10
test/test_ariyala.py
Normal file
@ -0,0 +1,10 @@
|
||||
from typing import List
|
||||
|
||||
from service.core.ariyala_parser import AriyalaParser
|
||||
from service.models.piece import Piece
|
||||
|
||||
|
||||
def test_get(parser: AriyalaParser, bis_link: str, bis_set: List[Piece]) -> None:
|
||||
items = parser.get(bis_link)
|
||||
|
||||
assert items == bis_set
|
20
test/test_bis.py
Normal file
20
test/test_bis.py
Normal file
@ -0,0 +1,20 @@
|
||||
from service.models.bis import BiS
|
||||
from service.models.piece import Piece
|
||||
from service.models.upgrade import Upgrade
|
||||
|
||||
|
||||
def test_set_item(bis: BiS, weapon: Piece) -> None:
|
||||
bis.set_item(weapon)
|
||||
assert bis.has_piece(weapon)
|
||||
|
||||
|
||||
def test_remove_item(bis: BiS, weapon: Piece) -> None:
|
||||
test_set_item(bis, weapon)
|
||||
bis.remove_item(weapon)
|
||||
assert not bis.has_piece(weapon)
|
||||
|
||||
|
||||
def test_upgrades_required(bis: BiS, weapon: Piece, head_with_upgrade: Piece) -> None:
|
||||
bis.set_item(weapon)
|
||||
bis.set_item(head_with_upgrade)
|
||||
assert bis.upgrades_required == {Upgrade.NoUpgrade: 1, Upgrade.GearUpgrade: 1}
|
10
test/test_loot_selector.py
Normal file
10
test/test_loot_selector.py
Normal file
@ -0,0 +1,10 @@
|
||||
from service.core.loot_selector import LootSelector
|
||||
from service.models.piece import Piece
|
||||
from service.models.player import Player
|
||||
|
||||
|
||||
def test_suggest_by_need(selector: LootSelector, player: Player, player2: Player, head_with_upgrade: Piece) -> None:
|
||||
assert selector.suggest(head_with_upgrade) == \
|
||||
[player2.player_id_with_counters(head_with_upgrade), player.player_id_with_counters(head_with_upgrade)]
|
||||
|
||||
|
95
test/test_party.py
Normal file
95
test/test_party.py
Normal file
@ -0,0 +1,95 @@
|
||||
from service.core.database import Database
|
||||
from service.core.party import Party
|
||||
from service.models.piece import Piece
|
||||
from service.models.player import Player
|
||||
|
||||
|
||||
def test_set_player(party: Party, player: Player) -> None:
|
||||
assert len(party.players) == 0
|
||||
|
||||
party.set_player(player)
|
||||
assert len(party.players) == 1
|
||||
|
||||
|
||||
def test_remove_player(party: Party, player: Player) -> None:
|
||||
party.remove_player(player.player_id)
|
||||
assert len(party.players) == 0
|
||||
|
||||
party.set_player(player)
|
||||
party.remove_player(player.player_id)
|
||||
assert len(party.players) == 0
|
||||
|
||||
|
||||
def test_set_bis_link(party: Party, player: Player, bis_link: str) -> None:
|
||||
party.set_player(player)
|
||||
|
||||
party.set_bis_link(player.player_id, bis_link)
|
||||
assert player.link == bis_link
|
||||
|
||||
|
||||
def test_set_item(party: Party, player: Player, head_with_upgrade: Piece, weapon: Piece) -> None:
|
||||
party.set_player(player)
|
||||
|
||||
party.set_item(player.player_id, weapon)
|
||||
assert abs(player.loot_count(weapon)) == 1
|
||||
assert abs(player.loot_count(head_with_upgrade)) == 0
|
||||
|
||||
|
||||
def test_remove_item(party: Party, player: Player, head_with_upgrade: Piece, weapon: Piece) -> None:
|
||||
party.set_player(player)
|
||||
|
||||
party.remove_item(player.player_id, weapon)
|
||||
assert abs(player.loot_count(weapon)) == 0
|
||||
assert abs(player.loot_count(head_with_upgrade)) == 0
|
||||
|
||||
party.set_item(player.player_id, weapon)
|
||||
assert abs(player.loot_count(weapon)) == 1
|
||||
assert abs(player.loot_count(head_with_upgrade)) == 0
|
||||
|
||||
party.remove_item(player.player_id, weapon)
|
||||
assert abs(player.loot_count(weapon)) == 0
|
||||
assert abs(player.loot_count(head_with_upgrade)) == 0
|
||||
|
||||
|
||||
def test_set_item_bis(party: Party, player: Player, head_with_upgrade: Piece, weapon: Piece) -> None:
|
||||
party.set_player(player)
|
||||
|
||||
party.set_item_bis(player.player_id, head_with_upgrade)
|
||||
assert player.bis.has_piece(head_with_upgrade)
|
||||
assert not player.bis.has_piece(weapon)
|
||||
|
||||
|
||||
def test_remove_item_bis(party: Party, player: Player, head_with_upgrade: Piece, weapon: Piece) -> None:
|
||||
party.set_player(player)
|
||||
|
||||
party.remove_item_bis(player.player_id, head_with_upgrade)
|
||||
assert not player.bis.has_piece(head_with_upgrade)
|
||||
assert not player.bis.has_piece(weapon)
|
||||
|
||||
party.set_item_bis(player.player_id, head_with_upgrade)
|
||||
assert player.bis.has_piece(head_with_upgrade)
|
||||
assert not player.bis.has_piece(weapon)
|
||||
|
||||
party.set_item_bis(player.player_id, weapon)
|
||||
assert player.bis.has_piece(head_with_upgrade)
|
||||
assert player.bis.has_piece(weapon)
|
||||
|
||||
party.remove_item_bis(player.player_id, head_with_upgrade)
|
||||
assert not player.bis.has_piece(head_with_upgrade)
|
||||
assert player.bis.has_piece(weapon)
|
||||
|
||||
|
||||
def test_get(party: Party, database: Database, player: Player, head_with_upgrade: Piece, weapon: Piece,
|
||||
bis_link: str) -> None:
|
||||
party.set_player(player)
|
||||
party.set_bis_link(player.player_id, bis_link)
|
||||
party.set_item_bis(player.player_id, head_with_upgrade)
|
||||
party.set_item_bis(player.player_id, weapon)
|
||||
party.set_item(player.player_id, weapon)
|
||||
|
||||
new_party = Party.get(database)
|
||||
assert party.party == new_party.party
|
||||
|
||||
party.remove_player(player.player_id)
|
||||
new_party = Party.get(database)
|
||||
assert party.party == new_party.party
|
13
test/test_piece.py
Normal file
13
test/test_piece.py
Normal file
@ -0,0 +1,13 @@
|
||||
from service.models.piece import Piece
|
||||
|
||||
|
||||
def test_parse_head(head_with_upgrade: Piece) -> None:
|
||||
assert Piece.get({'piece': 'head', 'is_tome': True}) == head_with_upgrade
|
||||
|
||||
|
||||
def test_parse_weapon(weapon: Piece) -> None:
|
||||
assert Piece.get({'piece': 'weapon', 'is_tome': False}) == weapon
|
||||
|
||||
|
||||
def test_parse_upgrade(head_with_upgrade: Piece) -> None:
|
||||
assert Piece.get({'piece': head_with_upgrade.upgrade.name, 'is_tome': True}) == head_with_upgrade.upgrade
|
73
test/test_player.py
Normal file
73
test/test_player.py
Normal file
@ -0,0 +1,73 @@
|
||||
from service.models.piece import Piece
|
||||
from service.models.player import Player
|
||||
|
||||
|
||||
def test_loot_count(player: Player, head_with_upgrade: Piece, weapon: Piece) -> None:
|
||||
assert abs(player.loot_count(head_with_upgrade)) == 0
|
||||
assert abs(player.loot_count(weapon)) == 0
|
||||
|
||||
player.loot.append(head_with_upgrade)
|
||||
assert abs(player.loot_count(head_with_upgrade)) == 1
|
||||
assert abs(player.loot_count(weapon)) == 0
|
||||
|
||||
player.loot.append(weapon)
|
||||
assert abs(player.loot_count(head_with_upgrade)) == 1
|
||||
assert abs(player.loot_count(weapon)) == 1
|
||||
|
||||
|
||||
def test_loot_count_bis(player: Player, head_with_upgrade: Piece, weapon: Piece) -> None:
|
||||
assert abs(player.loot_count_bis(head_with_upgrade)) == 0
|
||||
assert abs(player.loot_count_bis(weapon)) == 0
|
||||
|
||||
player.bis.set_item(head_with_upgrade)
|
||||
player.loot.append(head_with_upgrade)
|
||||
assert abs(player.loot_count_bis(head_with_upgrade)) == 1
|
||||
assert abs(player.loot_count_bis(weapon)) == 1
|
||||
|
||||
player.bis.set_item(weapon)
|
||||
assert abs(player.loot_count_bis(head_with_upgrade)) == 1
|
||||
assert abs(player.loot_count_bis(weapon)) == 1
|
||||
|
||||
|
||||
def test_loot_count_total(player: Player, head_with_upgrade: Piece, weapon: Piece) -> None:
|
||||
assert abs(player.loot_count_total(head_with_upgrade)) == 0
|
||||
assert abs(player.loot_count_total(weapon)) == 0
|
||||
|
||||
player.loot.append(head_with_upgrade)
|
||||
assert abs(player.loot_count_total(head_with_upgrade)) == 1
|
||||
assert abs(player.loot_count_total(weapon)) == 1
|
||||
|
||||
player.loot.append(weapon)
|
||||
assert abs(player.loot_count_total(head_with_upgrade)) == 2
|
||||
assert abs(player.loot_count_total(weapon)) == 2
|
||||
|
||||
|
||||
def test_loot_priority(player: Player, head_with_upgrade: Piece, weapon: Piece) -> None:
|
||||
assert abs(player.priority) == abs(player.loot_priority(head_with_upgrade))
|
||||
assert abs(player.priority) == abs(player.loot_priority(weapon))
|
||||
|
||||
player.loot.append(head_with_upgrade)
|
||||
assert abs(player.priority) == abs(player.loot_priority(head_with_upgrade))
|
||||
assert abs(player.priority) == abs(player.loot_priority(weapon))
|
||||
|
||||
player.loot.append(weapon)
|
||||
assert abs(player.priority) == abs(player.loot_priority(head_with_upgrade))
|
||||
assert abs(player.priority) == abs(player.loot_priority(weapon))
|
||||
|
||||
|
||||
def test_is_required(player: Player, head_with_upgrade: Piece, weapon: Piece) -> None:
|
||||
assert not player.is_required(weapon)
|
||||
assert not player.is_required(head_with_upgrade)
|
||||
|
||||
player.bis.set_item(weapon)
|
||||
assert player.is_required(weapon)
|
||||
assert not player.is_required(head_with_upgrade)
|
||||
|
||||
player.loot.append(weapon)
|
||||
assert not player.is_required(weapon)
|
||||
assert not player.is_required(head_with_upgrade)
|
||||
|
||||
player.bis.set_item(head_with_upgrade)
|
||||
assert not player.is_required(weapon)
|
||||
assert player.is_required(head_with_upgrade)
|
||||
assert player.is_required(head_with_upgrade.upgrade)
|
67
test/test_view_bis.py
Normal file
67
test/test_view_bis.py
Normal file
@ -0,0 +1,67 @@
|
||||
from typing import Any, List
|
||||
|
||||
from service.api.utils import make_json
|
||||
from service.core.party import Party
|
||||
from service.models.piece import Piece
|
||||
from service.models.player import Player
|
||||
|
||||
|
||||
async def test_bis_get(server: Any, party: Party, player: Player, player2: Player,
|
||||
head_with_upgrade: Piece, weapon: Piece) -> None:
|
||||
party.set_item_bis(player.player_id, weapon)
|
||||
party.set_item_bis(player2.player_id, weapon)
|
||||
party.set_item_bis(player2.player_id, head_with_upgrade)
|
||||
|
||||
response = await server.get('/api/v1/party/bis')
|
||||
assert response.status == 200
|
||||
assert await response.text() == make_json([weapon, weapon, head_with_upgrade], {}, 200)
|
||||
|
||||
|
||||
async def test_bis_get_with_filter(server: Any, party: Party, player: Player, player2: Player,
|
||||
head_with_upgrade: Piece, weapon: Piece) -> None:
|
||||
party.set_item_bis(player.player_id, weapon)
|
||||
party.set_item_bis(player2.player_id, weapon)
|
||||
party.set_item_bis(player2.player_id, head_with_upgrade)
|
||||
|
||||
response = await server.get('/api/v1/party/bis', params={'nick': player.nick})
|
||||
assert response.status == 200
|
||||
assert await response.text() == make_json([weapon], {'nick': player.nick}, 200)
|
||||
|
||||
response = await server.get('/api/v1/party/bis', params={'nick': player2.nick})
|
||||
assert response.status == 200
|
||||
assert await response.text() == make_json([weapon, head_with_upgrade], {'nick': player2.nick}, 200)
|
||||
|
||||
|
||||
async def test_bis_post_add(server: Any, player: Player, head_with_upgrade: Piece) -> None:
|
||||
response = await server.post('/api/v1/party/bis', json={
|
||||
'action': 'add',
|
||||
'piece': head_with_upgrade.name,
|
||||
'is_tome': head_with_upgrade.is_tome,
|
||||
'job': player.job.name,
|
||||
'nick': player.nick
|
||||
})
|
||||
assert response.status == 200
|
||||
assert player.bis.has_piece(head_with_upgrade)
|
||||
|
||||
|
||||
async def test_bis_post_remove(server: Any, player: Player, player2: Player, weapon: Piece) -> None:
|
||||
response = await server.post('/api/v1/party/bis', json={
|
||||
'action': 'remove',
|
||||
'piece': weapon.name,
|
||||
'is_tome': weapon.is_tome,
|
||||
'job': player.job.name,
|
||||
'nick': player.nick
|
||||
})
|
||||
assert response.status == 200
|
||||
assert not player.bis.has_piece(weapon)
|
||||
assert player2.bis.has_piece(weapon)
|
||||
|
||||
|
||||
async def test_bis_put(server: Any, player: Player, bis_link: str, bis_set: List[Piece]) -> None:
|
||||
response = await server.put('/api/v1/party/bis', json={
|
||||
'job': player.job.name,
|
||||
'link': bis_link,
|
||||
'nick': player.nick
|
||||
})
|
||||
assert response.status == 200
|
||||
assert player.bis.pieces == bis_set
|
88
test/test_view_loot.py
Normal file
88
test/test_view_loot.py
Normal file
@ -0,0 +1,88 @@
|
||||
from typing import Any
|
||||
|
||||
from service.api.utils import make_json
|
||||
from service.core.party import Party
|
||||
from service.models.piece import Piece
|
||||
from service.models.player import Player
|
||||
|
||||
|
||||
async def test_loot_get(server: Any, party: Party, player: Player, player2: Player, weapon: Piece) -> None:
|
||||
party.set_item(player.player_id, weapon)
|
||||
party.set_item(player2.player_id, weapon)
|
||||
|
||||
response = await server.get('/api/v1/party/loot')
|
||||
assert response.status == 200
|
||||
assert await response.text() == make_json([weapon, weapon], {}, 200)
|
||||
|
||||
|
||||
async def test_loot_get_with_filter(server: Any, party: Party, player: Player, player2: Player, weapon: Piece) -> None:
|
||||
party.set_item(player.player_id, weapon)
|
||||
party.set_item(player2.player_id, weapon)
|
||||
|
||||
response = await server.get('/api/v1/party/loot', params={'nick': player.nick})
|
||||
assert response.status == 200
|
||||
assert await response.text() == make_json([weapon], {'nick': player.nick}, 200)
|
||||
|
||||
response = await server.get('/api/v1/party/loot', params={'nick': player2.nick})
|
||||
assert response.status == 200
|
||||
assert await response.text() == make_json([weapon], {'nick': player2.nick}, 200)
|
||||
|
||||
|
||||
async def test_loot_post_add(server: Any, player: Player, weapon: Piece) -> None:
|
||||
response = await server.get('/api/v1/party/loot')
|
||||
assert response.status == 200
|
||||
assert await response.text() == make_json([], {}, 200)
|
||||
assert weapon not in player.loot
|
||||
|
||||
response = await server.post('/api/v1/party/loot', json={
|
||||
'action': 'add',
|
||||
'piece': weapon.name,
|
||||
'is_tome': weapon.is_tome,
|
||||
'job': player.job.name,
|
||||
'nick': player.nick
|
||||
})
|
||||
assert response.status == 200
|
||||
assert weapon in player.loot
|
||||
|
||||
|
||||
async def test_loot_post_remove(server: Any, player: Player, head_with_upgrade: Piece, weapon: Piece) -> None:
|
||||
assert weapon not in player.loot
|
||||
player.loot.append(weapon)
|
||||
player.loot.append(weapon)
|
||||
assert player.loot.count(weapon) == 2
|
||||
|
||||
response = await server.post('/api/v1/party/loot', json={
|
||||
'action': 'remove',
|
||||
'piece': weapon.name,
|
||||
'is_tome': weapon.is_tome,
|
||||
'job': player.job.name,
|
||||
'nick': player.nick
|
||||
})
|
||||
assert response.status == 200
|
||||
assert player.loot.count(weapon) == 1
|
||||
|
||||
player.loot.append(head_with_upgrade)
|
||||
|
||||
response = await server.post('/api/v1/party/loot', json={
|
||||
'action': 'remove',
|
||||
'piece': weapon.name,
|
||||
'is_tome': weapon.is_tome,
|
||||
'job': player.job.name,
|
||||
'nick': player.nick
|
||||
})
|
||||
assert response.status == 200
|
||||
assert player.loot.count(weapon) == 0
|
||||
assert player.loot.count(head_with_upgrade) == 1
|
||||
|
||||
|
||||
async def test_loot_put(server: Any, player: Player, player2: Player, head_with_upgrade: Piece) -> None:
|
||||
response = await server.put('/api/v1/party/loot', json={
|
||||
'is_tome': head_with_upgrade.is_tome,
|
||||
'piece': head_with_upgrade.name
|
||||
})
|
||||
assert response.status == 200
|
||||
assert await response.text() == make_json(
|
||||
[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
|
||||
)
|
81
test/test_view_player.py
Normal file
81
test/test_view_player.py
Normal file
@ -0,0 +1,81 @@
|
||||
from typing import Any, List
|
||||
|
||||
from service.api.utils import make_json
|
||||
from service.core.party import Party
|
||||
from service.models.piece import Piece
|
||||
from service.models.player import Player
|
||||
|
||||
|
||||
async def test_players_get(server: Any, party: Party, player: Player) -> None:
|
||||
party.set_player(player)
|
||||
|
||||
response = await server.get('/api/v1/party')
|
||||
assert response.status == 200
|
||||
assert await response.text() == make_json(party.party, {}, 200)
|
||||
|
||||
|
||||
async def test_players_get_with_filter(server: Any, party: Party, player: Player, player2: Player) -> None:
|
||||
party.set_player(player)
|
||||
|
||||
response = await server.get('/api/v1/party', params={'nick': player.nick})
|
||||
assert response.status == 200
|
||||
assert await response.text() == make_json([player], {'nick': player.nick}, 200)
|
||||
|
||||
response = await server.get('/api/v1/party', params={'nick': player2.nick})
|
||||
assert response.status == 200
|
||||
assert await response.text() == make_json([player2], {'nick': player2.nick}, 200)
|
||||
|
||||
|
||||
async def test_players_post_add(server: Any, party: Party, player: Player) -> None:
|
||||
party.remove_player(player.player_id)
|
||||
|
||||
response = await server.get('/api/v1/party', params={'nick': player.nick})
|
||||
assert response.status == 200
|
||||
assert await response.text() == make_json([], {'nick': player.nick}, 200)
|
||||
|
||||
response = await server.post('/api/v1/party', json={
|
||||
'action': 'add',
|
||||
'job': player.job.name,
|
||||
'nick': player.nick
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
assert player.player_id in party.players
|
||||
|
||||
|
||||
async def test_players_post_remove(server: Any, party: Party, player: Player) -> None:
|
||||
response = await server.get('/api/v1/party', params={'nick': player.nick})
|
||||
assert response.status == 200
|
||||
assert await response.text() == make_json([player], {'nick': player.nick}, 200)
|
||||
|
||||
response = await server.post('/api/v1/party', json={
|
||||
'action': 'remove',
|
||||
'job': player.job.name,
|
||||
'nick': player.nick
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
response = await server.get('/api/v1/party', params={'nick': player.nick})
|
||||
assert response.status == 200
|
||||
assert await response.text() == make_json([], {'nick': player.nick}, 200)
|
||||
|
||||
assert player.player_id not in party.players
|
||||
|
||||
|
||||
async def test_players_post_add_with_link(server: Any, party: Party, player: Player,
|
||||
bis_link: str, bis_set: List[Piece]) -> None:
|
||||
party.remove_player(player.player_id)
|
||||
|
||||
response = await server.get('/api/v1/party', params={'nick': player.nick})
|
||||
assert response.status == 200
|
||||
assert await response.text() == make_json([], {'nick': player.nick}, 200)
|
||||
|
||||
response = await server.post('/api/v1/party', json={
|
||||
'action': 'add',
|
||||
'job': player.job.name,
|
||||
'nick': player.nick,
|
||||
'link': bis_link
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
assert party.players[player.player_id].bis.pieces == bis_set
|
Loading…
Reference in New Issue
Block a user