From c11103a5ba4220c824b3aa6d356ca88ed7a52ca8 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Wed, 11 Sep 2019 11:56:27 +0300 Subject: [PATCH] postgres demo support --- README.md | 13 ++- TODO.md | 5 +- setup.py | 3 +- src/service/application/core.py | 2 +- src/service/core/database.py | 12 ++- src/service/core/postgres.py | 166 ++++++++++++++++++++++++++++++++ 6 files changed, 194 insertions(+), 7 deletions(-) create mode 100644 src/service/core/postgres.py diff --git a/README.md b/README.md index c117637..f2feea9 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Service which allows to manage savage loot distribution easy. * `include`: path to include configuration directory, string, optional. * `logging`: path to logging configuration, see `logging.ini` for reference, string, optional. - * `database`: database provide name, string, required. Allowed values: `sqlite`. + * `database`: database provide name, string, required. Allowed values: `sqlite`, `postgres`. * `priority`: methods of `Player` class which will be called to sort players for loot priority, space separated list of strings, required. * `ariyala` section @@ -129,6 +129,17 @@ Service which allows to manage savage loot distribution easy. * `root_username`: username of administrator, string, required. * `root_password`: md5 hashed password of administrator, string, required. +* `postgres` section + + Database settings for `postgres` provider. + + * `database`: database name, string, required. + * `host`: database host, string, required. + * `password`: database password, string, required. + * `port`: database port, int, required. + * `username`: database username, string, required. + * `migrations_path`: path to database migrations, string, required. + * `sqlite` section Database settings for `sqlite` provider. diff --git a/TODO.md b/TODO.md index 7360943..4838ed9 100644 --- a/TODO.md +++ b/TODO.md @@ -1,2 +1,3 @@ -[ ] postgres support -[ ] pretty UI \ No newline at end of file +* [ ] items improvements +* [ ] multiple parties support +* [ ] pretty UI \ No newline at end of file diff --git a/setup.py b/setup.py index 202ddf3..56d7967 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,6 @@ setup( 'aiohttp', 'aiohttp_jinja2', 'aiohttp_security', - 'aiosqlite', 'Jinja2', 'passlib', 'requests', @@ -44,6 +43,8 @@ setup( include_package_data=True, extras_require={ + 'Postgresql': ['aiopg'], + 'SQLite': ['aiosqlite'], 'test': ['coverage', 'pytest'], }, ) diff --git a/src/service/application/core.py b/src/service/application/core.py index 3ca4578..120c5f2 100644 --- a/src/service/application/core.py +++ b/src/service/application/core.py @@ -26,7 +26,7 @@ class Application: def run(self) -> None: loop = asyncio.get_event_loop() - database = Database.get(self.config) + database = loop.run_until_complete(Database.get(self.config)) database.migration() party = loop.run_until_complete(Party.get(database)) diff --git a/src/service/core/database.py b/src/service/core/database.py index 3eae21a..dcdddb2 100644 --- a/src/service/core/database.py +++ b/src/service/core/database.py @@ -35,22 +35,30 @@ class Database: return int(datetime.datetime.now().timestamp()) @classmethod - def get(cls: Type[Database], config: Configuration) -> Database: + async 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 + elif database_type == 'postgres': + from .postgres import PostgresDatabase + obj = PostgresDatabase else: raise InvalidDatabase(database_type) - return obj(**database_settings) + database = obj(**database_settings) + await database.init() + return database @property def connection(self) -> str: raise NotImplementedError + async def init(self) -> None: + pass + async def delete_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None: raise NotImplementedError diff --git a/src/service/core/postgres.py b/src/service/core/postgres.py new file mode 100644 index 0000000..c3bbce9 --- /dev/null +++ b/src/service/core/postgres.py @@ -0,0 +1,166 @@ +# +# Copyright (c) 2019 Evgeniy Alekseev. +# +# This file is part of ffxivbis +# (see https://github.com/arcan1s/ffxivbis). +# +# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause +# +import asyncpg + +from passlib.hash import md5_crypt +from psycopg2.extras import DictCursor +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 service.models.user import User + +from .database import Database + + +class PostgresDatabase(Database): + + def __init__(self, host: str, port: int, username: str, password: str, database: str, migrations_path: str) -> None: + Database.__init__(self, migrations_path) + self.host = host + self.port = int(port) + self.username = username + self.password = password + self.database = database + self.pool: asyncpg.pool.Pool = None # type: ignore + + @property + def connection(self) -> str: + return 'postgresql://{username}:{password}@{host}:{port}/{database}'.format( + username=self.username, password=self.password, host=self.host, port=self.port, database=self.database + ) + + async def init(self) -> None: + self.pool = await asyncpg.create_pool(host=self.host, port=self.port, username=self.username, + password=self.password, database=self.database) + + async def delete_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None: + player = await self.get_player(player_id) + if player is None: + return + + async with self.pool.acquire() as conn: + await conn.execute( + '''delete from loot + where loot_id in ( + select loot_id from loot + where player_id = $1 and piece = $2 and is_tome = $3 order by created desc limit 1 + )''', + player, piece.name, getattr(piece, 'is_tome', True) + ) + + async def delete_piece_bis(self, player_id: PlayerId, piece: Piece) -> None: + player = await self.get_player(player_id) + if player is None: + return + + async with self.pool.acquire() as conn: + await conn.execute( + '''delete from bis where player_id = $1 and piece = $2''', + player, piece.name) + + async def delete_player(self, player_id: PlayerId) -> None: + async with self.pool.acquire() as conn: + await conn.execute('''delete from players where nick = $1 and job = $2''', + player_id.nick, player_id.job.name) + + async def delete_user(self, username: str) -> None: + async with self.pool.acquire() as conn: + await conn.execute('''delete from users where username = $1''', username) + + async def get_party(self) -> List[Player]: + async with self.pool.acquire() as conn: + rows = await conn.fetch('''select * from bis''') + bis_pieces = [Loot(row['player_id'], Piece.get(row)) for row in rows] + + rows = await conn.fetch('''select * from loot''') + loot_pieces = [Loot(row['player_id'], Piece.get(row)) for row in rows] + + rows = await conn.fetch('''select * from players''') + party = { + row['player_id']: Player(Job[row['job']], row['nick'], BiS(), [], row['bis_link'], row['priority']) + for row in rows + } + + return self.set_loot(party, bis_pieces, loot_pieces) + + async def get_player(self, player_id: PlayerId) -> Optional[int]: + async with self.pool.acquire() as conn: + player = await conn.fetchrow('''select player_id from players where nick = $1 and job = $2''', + player_id.nick, player_id.job.name) + return player['player_id'] if player is not None else None + + async def get_user(self, username: str) -> Optional[User]: + async with self.pool.acquire() as conn: + user = await conn.fetchrow('''select * from users where username = $1''', username) + return User(user['username'], user['password'], user['permission']) if user is not None else None + + async def get_users(self) -> List[User]: + async with self.pool.acquire() as conn: + users = await conn.fetch('''select * from users''') + return [User(user['username'], user['password'], user['permission']) for user in users] + + async def insert_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None: + player = await self.get_player(player_id) + if player is None: + return + + async with self.pool.acquire() as conn: + await conn.execute( + '''insert into loot + (created, piece, is_tome, player_id) + values + ($1, $2, $3, $4)''', + Database.now(), piece.name, getattr(piece, 'is_tome', True), player + ) + + async def insert_piece_bis(self, player_id: PlayerId, piece: Piece) -> None: + player = await self.get_player(player_id) + if player is None: + return + + async with self.pool.acquire() as conn: + await conn.execute( + '''insert into bis + (created, piece, is_tome, player_id) + values + ($1, $2, $3, $4) + on conflict on constraint bis_piece_player_id_idx do update set + created = $1, is_tome = $3''', + Database.now(), piece.name, piece.is_tome, player + ) + + async def insert_player(self, player: Player) -> None: + async with self.pool.acquire() as conn: + await conn.execute( + '''insert into players + (created, nick, job, bis_link, priority) + values + ($1, $2, $3, $4, $5) + on conflict on constraint players_nick_job_idx do update set + created = $1, bis_link = $4, priority = $5''', + Database.now(), player.nick, player.job.name, player.link, player.priority + ) + + async def insert_user(self, user: User, hashed_password: bool) -> None: + password = user.password if hashed_password else md5_crypt.hash(user.password) + async with self.pool.acquire() as conn: + await conn.execute( + '''insert into users + (username, password, permission) + values + ($1, $2, $3) + on conflict on constraint users_username_idx do update set + password = $2, permission = $3''', + user.username, password, user.permission + ) \ No newline at end of file