diff --git a/.gitignore b/.gitignore
index 4f68177..7351200 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,113 @@
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### PyCharm Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+# Sonarlint plugin
+# https://plugins.jetbrains.com/plugin/7973-sonarlint
+.idea/**/sonarlint/
+
+# SonarQube Plugin
+# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
+.idea/**/sonarIssues.xml
+
+# Markdown Navigator plugin
+# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
+.idea/**/markdown-navigator.xml
+.idea/**/markdown-navigator-enh.xml
+.idea/**/markdown-navigator/
+
+# Cache file creation bug
+# See https://youtrack.jetbrains.com/issue/JBR-2257
+.idea/$CACHE_FILE$
+
+# CodeStream plugin
+# https://plugins.jetbrains.com/plugin/12206-codestream
+.idea/codestream.xml
+
+# Azure Toolkit for IntelliJ plugin
+# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
+.idea/**/azureSettings.xml
+
+### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -8,7 +118,7 @@ __pycache__/
# Distribution / packaging
.Python
-# build/
+build/
develop-eggs/
dist/
downloads/
@@ -46,8 +156,10 @@ htmlcov/
nosetests.xml
coverage.xml
*.cover
+*.py,cover
.hypothesis/
.pytest_cache/
+cover/
# Translations
*.mo
@@ -57,6 +169,7 @@ coverage.xml
*.log
local_settings.py
db.sqlite3
+db.sqlite3-journal
# Flask stuff:
instance/
@@ -69,6 +182,7 @@ instance/
docs/_build/
# PyBuilder
+.pybuilder/
target/
# Jupyter Notebook
@@ -79,10 +193,38 @@ profile_default/
ipython_config.py
# pyenv
-.python-version
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
-# celery beat schedule file
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
celerybeat-schedule
+celerybeat.pid
# SageMath parsed files
*.sage.py
@@ -114,11 +256,34 @@ dmypy.json
# Pyre type checker
.pyre/
-# IDE
-.idea
-.vscode
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+### Python Patch ###
+# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
+poetry.toml
+
+# ruff
+.ruff_cache/
-# Custom
-rpmtools
-local
-.DS_Store
+### venv ###
+# Virtualenv
+# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
+[Bb]in
+[Ii]nclude
+[Ll]ib
+[Ll]ib64
+[Ll]ocal
+[Ss]cripts
+pyvenv.cfg
+pip-selfcheck.json
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 0000000..d9a0ab1
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ postgresql
+ true
+ org.postgresql.Driver
+ jdbc:postgresql://localhost:5432/postgres
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..2db4903
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kts_project_template.iml b/.idea/kts_project_template.iml
new file mode 100644
index 0000000..5195124
--- /dev/null
+++ b/.idea/kts_project_template.iml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..f0e9982
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..24c3a8c
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/READ_INSTRUCTION.txt b/READ_INSTRUCTION.txt
new file mode 100644
index 0000000..8fafab8
--- /dev/null
+++ b/READ_INSTRUCTION.txt
@@ -0,0 +1,13 @@
+Текущие функции такие:
+1) Регистрация! - регистрирует всех пользователей в чате
+2) Загрузить фотографии! - человек пишет данную фун-ию и затем присылает фотографии
+3) Начать игру! - начинается игра
+3) Остановить игру! - игра останавливается, выводятся оставшиеся пользователи
+4) Последняя игра! - выводится победитель из последней игры, если он есть
+
+Механизм игры такой:
+РЕЖИМ: ВСЕ ПРОТИВ ВСЕХ! - все сражаются против друг друга, пока не останется 1 или 0 пользователей. В случае если остался 1 - выводится победитель,
+в случае, если никто не остался - выводится соответствующее сообщение
+
+
+
diff --git a/alembic.ini b/alembic.ini
new file mode 100644
index 0000000..46e74ba
--- /dev/null
+++ b/alembic.ini
@@ -0,0 +1,103 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+script_location = alembic
+
+# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
+# Uncomment the line below if you want the files to be prepended with date and time
+# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory.
+prepend_sys_path = .
+
+# timezone to use when rendering the date within the migration file
+# as well as the filename.
+# If specified, requires the python-dateutil library that can be
+# installed by adding `alembic[tz]` to the pip requirements
+# string value is passed to dateutil.tz.gettz()
+# leave blank for localtime
+# timezone =
+
+# max length of characters to apply to the
+# "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; This defaults
+# to alembic/versions. When using multiple version
+# directories, initial revisions must be specified with --version-path.
+# The path separator used here should be the separator specified by "version_path_separator" below.
+# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
+
+# version path separator; As mentioned above, this is the character used to split
+# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
+# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
+# Valid values for version_path_separator are:
+#
+# version_path_separator = :
+# version_path_separator = ;
+# version_path_separator = space
+version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
+
+# the output encoding used when revision files
+# are written from script.py.mako
+# output_encoding = utf-8
+
+sqlalchemy.url = postgresql+asyncpg://kts_user:kts_pass@localhost:5432/kts
+
+
+[post_write_hooks]
+# post_write_hooks defines scripts or Python functions that are run
+# on newly generated revision scripts. See the documentation for further
+# detail and examples
+
+# format using "black" - use the console_scripts runner, against the "black" entrypoint
+# hooks = black
+# black.type = console_scripts
+# black.entrypoint = black
+# black.options = -l 79 REVISION_SCRIPT_FILENAME
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/alembic/README b/alembic/README
new file mode 100644
index 0000000..e0d0858
--- /dev/null
+++ b/alembic/README
@@ -0,0 +1 @@
+Generic single-database configuration with an async dbapi.
\ No newline at end of file
diff --git a/alembic/env.py b/alembic/env.py
new file mode 100644
index 0000000..3def7dd
--- /dev/null
+++ b/alembic/env.py
@@ -0,0 +1,91 @@
+import asyncio
+from logging.config import fileConfig
+
+from sqlalchemy import engine_from_config
+from sqlalchemy import pool
+from sqlalchemy.engine import Connection
+from sqlalchemy.ext.asyncio import AsyncEngine
+from app.store.models.model import ParticipantsModel
+
+from alembic import context
+
+from app.store.database.sqlalchemy_base import db
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+if config.config_file_name is not None:
+ fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+target_metadata = db.metadata
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline() -> None:
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url,
+ target_metadata=target_metadata,
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def do_run_migrations(connection: Connection) -> None:
+ context.configure(connection=connection, target_metadata=target_metadata)
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+async def run_migrations_online() -> None:
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ connectable = AsyncEngine(
+ engine_from_config(
+ config.get_section(config.config_ini_section),
+ prefix="sqlalchemy.",
+ poolclass=pool.NullPool,
+ future=True,
+ )
+ )
+
+ async with connectable.connect() as connection:
+ await connection.run_sync(do_run_migrations)
+
+ await connectable.dispose()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ asyncio.run(run_migrations_online())
diff --git a/alembic/script.py.mako b/alembic/script.py.mako
new file mode 100644
index 0000000..55df286
--- /dev/null
+++ b/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade() -> None:
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade() -> None:
+ ${downgrades if downgrades else "pass"}
diff --git a/alembic/versions/1d6d7e9045d3_participants.py b/alembic/versions/1d6d7e9045d3_participants.py
new file mode 100644
index 0000000..c61490e
--- /dev/null
+++ b/alembic/versions/1d6d7e9045d3_participants.py
@@ -0,0 +1,37 @@
+"""Participants
+
+Revision ID: 1d6d7e9045d3
+Revises:
+Create Date: 2023-02-27 20:20:14.025321
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '1d6d7e9045d3'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('participants',
+ sa.Column('id', sa.BigInteger(), nullable=False),
+ sa.Column('name', sa.Text(), nullable=False),
+ sa.Column('wins', sa.BigInteger(), nullable=True),
+ sa.Column('chat_id', sa.BigInteger(), nullable=False),
+ sa.Column('owner_id', sa.BigInteger(), nullable=True),
+ sa.Column('photo_id', sa.BigInteger(), nullable=True),
+ sa.Column('access_key', sa.Text(), nullable=True),
+ sa.PrimaryKeyConstraint('id')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('participants')
+ # ### end Alembic commands ###
diff --git a/kts_backend/users/__init__.py b/app/__init__.py
similarity index 100%
rename from kts_backend/users/__init__.py
rename to app/__init__.py
diff --git a/kts_backend/web/__init__.py b/app/base/__init__.py
similarity index 100%
rename from kts_backend/web/__init__.py
rename to app/base/__init__.py
diff --git a/app/base/base_accessor.py b/app/base/base_accessor.py
new file mode 100644
index 0000000..aa64bb5
--- /dev/null
+++ b/app/base/base_accessor.py
@@ -0,0 +1,19 @@
+import typing
+from logging import getLogger
+
+if typing.TYPE_CHECKING:
+ from app.web.app import Application
+
+
+class BaseAccessor:
+ def __init__(self, app: "Application", *args, **kwargs):
+ self.app = app
+ self.logger = getLogger("accessor")
+ app.on_startup.append(self.connect)
+ app.on_cleanup.append(self.disconnect)
+
+ async def connect(self, app: "Application"):
+ return
+
+ async def disconnect(self, app: "Application"):
+ return
diff --git a/app/store/__init__.py b/app/store/__init__.py
new file mode 100644
index 0000000..97b1fb3
--- /dev/null
+++ b/app/store/__init__.py
@@ -0,0 +1,22 @@
+import typing
+
+from app.store.database.database import Database
+
+if typing.TYPE_CHECKING:
+ from app.web.app import Application
+
+
+class Store:
+ def __init__(self, app: "Application"):
+ from app.store.bot.manager import BotManager
+ from app.store.vk_api.accessor import VkApiAccessor
+
+ self.vk_api = VkApiAccessor(app)
+ self.bots_manager = BotManager(app)
+
+
+def setup_store(app: "Application"):
+ app.database = Database(app)
+ app.on_startup.append(app.database.connect)
+ app.on_cleanup.append(app.database.disconnect)
+ app.store = Store(app)
diff --git a/app/store/bot/__init__.py b/app/store/bot/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/store/bot/dataclassess.py b/app/store/bot/dataclassess.py
new file mode 100644
index 0000000..61f6abd
--- /dev/null
+++ b/app/store/bot/dataclassess.py
@@ -0,0 +1,20 @@
+from dataclasses import dataclass
+
+
+@dataclass
+class UpdateObject:
+ id: int
+ user_id: int
+ body: str
+
+
+@dataclass
+class Update:
+ type: str
+ object: UpdateObject
+
+
+@dataclass
+class Message:
+ user_id: int
+ text: str
diff --git a/app/store/bot/manager.py b/app/store/bot/manager.py
new file mode 100644
index 0000000..c85b2b2
--- /dev/null
+++ b/app/store/bot/manager.py
@@ -0,0 +1,332 @@
+import typing
+from logging import getLogger
+from time import sleep, time
+from random import choice
+from sqlalchemy.sql import select, update as refresh
+
+from app.store.bot.services import make_grid, check_winner
+from app.store.vk_api.dataclasses import Message, Update, Attachment, UpdateObject
+from app.web.app import app
+from app.store.models.model import ParticipantsModel
+
+if typing.TYPE_CHECKING:
+ from app.web.app import Application
+
+
+class SM:
+ def __init__(self):
+ self.state_photo = False
+ self.state_in_game = False
+ self.state_wait_votes = False
+ self.users = None
+ self.new_pair = None
+ self.voters_dict = {}
+ self.voters = []
+ self.state_send_photo = False
+ self.amount_users = None
+ self.last_winner = None
+
+ def reset_values(self):
+ self.state_photo = False
+ self.state_in_game = False
+ self.state_wait_votes = False
+ self.users = None
+ self.new_pair = None
+ self.voters_dict = {}
+ self.voters = []
+ self.state_send_photo = False
+ self.amount_users = None
+
+
+class BotManager:
+ def __init__(self, app: "Application"):
+ self.app = app
+ self.bot = None
+ self.logger = getLogger("handler")
+ self.active_chats = {}
+ self.time_end = {}
+ self.storage = {}
+
+ async def handle_updates(self, updates: list[Update]):
+ for i in self.storage.keys():
+ if time() - self.storage[i][0] > 30 and self.storage[i][1]:
+ updates.append(
+ Update(
+ type="time_out",
+ object=UpdateObject(
+ chat_id=i,
+ id=-1,
+ body="time_out",
+ ),
+ )
+ )
+ for update in updates:
+ if update.object.chat_id not in self.active_chats.keys():
+ temp = SM()
+ self.active_chats[update.object.chat_id] = temp
+ this_chat = self.active_chats[update.object.chat_id]
+ else:
+ this_chat = self.active_chats[update.object.chat_id]
+ if update.object.body == "Регистрация!":
+ await self.command_registery(update)
+ if update.object.body == "Загрузить фотографии!" or this_chat.state_photo:
+ if not this_chat.state_in_game:
+ this_chat.state_photo = True
+ await self.command_download_photo(update, this_chat)
+ else:
+ await self.app.store.vk_api.send_message(
+ Message(
+ chat_id=update.object.chat_id,
+ text=f"Нельзя загружать фотографии во время игры!",
+ )
+ )
+ if update.object.body == "Начать игру!":
+ if not this_chat.state_in_game:
+ this_chat.reset_values()
+ await self.command_start_game(update, this_chat)
+ else:
+ await self.app.store.vk_api.send_message(
+ Message(chat_id=update.object.chat_id, text=f"Игра уже идет!")
+ )
+ if update.object.body == "Остановить игру!":
+ if this_chat.state_in_game:
+ await self.command_stop_game(this_chat, update)
+ else:
+ await self.app.store.vk_api.send_message(
+ Message(
+ chat_id=update.object.chat_id,
+ text=f"Игровая сессия не запущена!",
+ )
+ )
+ if update.object.body == "Последняя игра!":
+ if not this_chat.state_in_game:
+ if this_chat.last_winner is not None:
+ await self.app.store.vk_api.send_message(
+ Message(
+ chat_id=update.object.chat_id,
+ text=f"Последний победитель: {this_chat.last_winner}",
+ )
+ )
+ else:
+ await self.app.store.vk_api.send_message(
+ Message(
+ chat_id=update.object.chat_id, text="Игр еще не было!"
+ )
+ )
+ else:
+ await self.app.store.vk_api.send_message(
+ Message(
+ chat_id=update.object.chat_id,
+ text="Данная команда недоступна во время игры!",
+ )
+ )
+ if this_chat.state_send_photo:
+ await self.command_send_photo(update, this_chat)
+ if this_chat.state_wait_votes:
+ await self.command_write_answers(update, this_chat)
+ if this_chat.state_in_game and (
+ (not this_chat.state_wait_votes)
+ or time() - self.storage[update.object.chat_id][0] > 30
+ ):
+ await self.command_send_preresult(update, this_chat)
+ if self.check_users(this_chat):
+ if len(this_chat.users) == 1:
+ this_chat.last_winner = this_chat.users[-1][0]
+ await self.app.store.vk_api.send_message(
+ Message(
+ chat_id=update.object.chat_id,
+ text=f"Победил {this_chat.users[0][0]}!",
+ )
+ )
+ else:
+ await self.app.store.vk_api.send_message(
+ Message(
+ chat_id=update.object.chat_id, text=f"Никто не победил!"
+ )
+ )
+ elif len(this_chat.users) > 1:
+ await self.command_send_photo(update, this_chat)
+ this_chat.state_send_photo = False
+
+ async def command_registery(self, update):
+ await self.app.database.connect()
+ async with self.app.database.session.begin() as session:
+ result = await app.store.vk_api.make_userlist(update.object.chat_id)
+ for k, v in result:
+ users_exists_select = select(
+ ParticipantsModel.__table__.c.chat_id,
+ ParticipantsModel.__table__.c.name,
+ ).where(
+ ParticipantsModel.__table__.columns.chat_id
+ == update.object.chat_id,
+ ParticipantsModel.__table__.c.name == k,
+ )
+ result = await session.execute(users_exists_select)
+ if not ((update.object.chat_id, k) in result.fetchall()):
+ new_user = ParticipantsModel(
+ name=k,
+ wins=0,
+ chat_id=update.object.chat_id,
+ owner_id=v,
+ photo_id=None,
+ access_key=None,
+ )
+ session.add(new_user)
+ await session.commit()
+ await self.app.store.vk_api.send_message(
+ Message(
+ chat_id=update.object.chat_id, text="Регистрация прошла успешно!"
+ )
+ )
+
+ def check_users(self, this_chat):
+ if len(this_chat.users) <= 1:
+ this_chat.state_send_photo = False
+ this_chat.state_wait_votes = False
+ this_chat.state_in_game = False
+ return 1
+ return 0
+
+ async def command_download_photo(self, update, this_chat):
+ if hasattr(update.object, "type") and update.object.type == "photo":
+ await self.app.database.connect()
+ async with self.app.database.session.begin() as session:
+ users_add_photos = (
+ refresh(ParticipantsModel.__table__)
+ .where(
+ ParticipantsModel.__table__.c.owner_id
+ == update.object.owner_id,
+ ParticipantsModel.__table__.c.chat_id == update.object.chat_id,
+ )
+ .values(
+ photo_id=update.object.photo_id,
+ access_key=update.object.access_key,
+ )
+ )
+ await session.execute(users_add_photos)
+ await session.commit()
+ this_chat.state_photo = False
+ await self.app.store.vk_api.send_message(
+ Message(
+ chat_id=update.object.chat_id,
+ text="Фотографии успешно загружены!",
+ )
+ )
+
+ async def command_start_game(self, update, this_chat):
+ this_chat.users = await app.store.vk_api.proccess_start_game(
+ update.object.chat_id
+ )
+ this_chat.amount_users = len(this_chat.users)
+ if len(this_chat.users) == 0:
+ await self.app.store.vk_api.send_message(
+ Message(chat_id=update.object.chat_id, text="Вы не прошли регистрацию!")
+ )
+ else:
+ for i in range(3, 0, -1):
+ await self.app.store.vk_api.send_message(
+ Message(
+ chat_id=update.object.chat_id,
+ text=f"Игра начинается через {i}.",
+ )
+ )
+ sleep(1)
+ await self.app.store.vk_api.send_message(
+ Message(chat_id=update.object.chat_id, text=f"Поехали!")
+ )
+ this_chat.state_send_photo = True
+
+ async def command_send_photo(self, update, this_chat):
+ this_chat.new_pair = make_grid(this_chat.users)
+ attach_pair = [i[1:] for i in this_chat.new_pair]
+ await self.app.store.vk_api.send_photo(
+ Attachment(
+ chat_id=update.object.chat_id, attachment=attach_pair, text="Выбирай!"
+ )
+ )
+ this_chat.state_in_game = True
+ (
+ this_chat.voters_dict[this_chat.new_pair[0]],
+ this_chat.voters_dict[this_chat.new_pair[1]],
+ ) = (0, 0)
+ this_chat.state_wait_votes = True
+ self.storage[update.object.chat_id] = [time(), this_chat.state_wait_votes]
+
+ async def command_write_answers(self, update, this_chat):
+ if update.object.id not in this_chat.voters:
+ this_chat.state_send_photo = False
+ if update.object.body == "1":
+ this_chat.voters_dict[this_chat.new_pair[0]] += 1
+ this_chat.voters.append(update.object.id)
+ if len(this_chat.voters) == this_chat.amount_users:
+ this_chat.state_wait_votes = False
+ self.storage[update.object.chat_id][1] = this_chat.state_wait_votes
+ this_chat.voters = []
+ elif update.object.body == "2":
+ this_chat.voters_dict[this_chat.new_pair[1]] += 1
+ this_chat.voters.append(update.object.id)
+ if len(this_chat.voters) == this_chat.amount_users:
+ this_chat.state_wait_votes = False
+ self.storage[update.object.chat_id][1] = this_chat.state_wait_votes
+ this_chat.voters = []
+ elif update.object.id == -1:
+ this_chat.state_wait_votes = False
+ self.storage[update.object.chat_id][1] = this_chat.state_wait_votes
+ this_chat.voters = []
+ elif update.object.id in this_chat.voters and update.object.body in ("1", "2"):
+ await self.app.store.vk_api.send_message(
+ Message(
+ chat_id=update.object.chat_id, text=f"Вы уже отдали свой голос!"
+ )
+ )
+
+ async def command_send_preresult(self, update, this_chat):
+ check = check_winner(this_chat.voters_dict, this_chat.new_pair)
+ if check == 1:
+ this_chat.users.remove(this_chat.new_pair[1])
+ await self.app.store.vk_api.send_message(
+ Message(
+ chat_id=update.object.chat_id,
+ text=f"И в текущем сражении победителем стал обладатель первой картинки",
+ )
+ )
+ elif check == 2:
+ this_chat.users.remove(this_chat.new_pair[0])
+ await self.app.store.vk_api.send_message(
+ Message(
+ chat_id=update.object.chat_id,
+ text=f"И в текущем сражении победителем стал обладатель второй картинки",
+ )
+ )
+ elif not check:
+ this_chat.users.remove(this_chat.new_pair[0])
+ this_chat.users.remove(this_chat.new_pair[1])
+ await self.app.store.vk_api.send_message(
+ Message(
+ chat_id=update.object.chat_id,
+ text=f"Никто не победил - следовательно оба вылетают.",
+ )
+ )
+ elif update.object.id == -1:
+ this_chat.users.remove(choice(this_chat.new_pair))
+ await self.app.store.vk_api.send_message(
+ Message(
+ chat_id=update.object.chat_id,
+ text=f"Никто не проголосовал, поэтому победитель определяется случайным образом.",
+ )
+ )
+ this_chat.state_send_photo = True
+
+ async def command_stop_game(self, this_chat, update):
+ await self.app.store.vk_api.send_message(
+ Message(chat_id=update.object.chat_id, text="Оставшиеся пользователи:")
+ )
+ for i in this_chat.users:
+ await self.app.store.vk_api.send_message(
+ Message(chat_id=update.object.chat_id, text=f"{i[0]}")
+ )
+ this_chat.reset_values()
+ self.storage[update.object.chat_id][1] = False
+ await self.app.store.vk_api.send_message(
+ Message(chat_id=update.object.chat_id, text=f"Игра остановлена!")
+ )
diff --git a/app/store/bot/poller.py b/app/store/bot/poller.py
new file mode 100644
index 0000000..94ba589
--- /dev/null
+++ b/app/store/bot/poller.py
@@ -0,0 +1,25 @@
+import asyncio
+from asyncio import Task
+from typing import Optional
+
+from app.store import Store
+
+
+class Poller:
+ def __init__(self, store: Store):
+ self.store = store
+ self.is_running = False
+ self.poll_task: Optional[Task] = None
+
+ async def start(self):
+ self.is_running = True
+ self.poll_task = asyncio.create_task(self.poll())
+
+ async def stop(self):
+ self.is_running = False
+ await self.poll_task
+
+ async def poll(self):
+ while self.is_running:
+ updates = await self.store.vk_api.poll()
+ await self.store.bots_manager.handle_updates(updates)
diff --git a/app/store/bot/services.py b/app/store/bot/services.py
new file mode 100644
index 0000000..88399da
--- /dev/null
+++ b/app/store/bot/services.py
@@ -0,0 +1,18 @@
+from random import choice
+
+
+def make_grid(data_users: list):
+ participant_1 = None
+ participant_2 = None
+ while participant_1 == participant_2:
+ participant_1 = choice(data_users)
+ participant_2 = choice(data_users)
+ return [participant_1, participant_2]
+
+
+def check_winner(game: dict, pair: list):
+ if game[pair[0]] > game[pair[1]]:
+ return 1
+ if game[pair[0]] < game[pair[1]]:
+ return 2
+ return 0
diff --git a/app/store/database/__init__.py b/app/store/database/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/store/database/database.py b/app/store/database/database.py
new file mode 100644
index 0000000..8161d11
--- /dev/null
+++ b/app/store/database/database.py
@@ -0,0 +1,27 @@
+from typing import Optional, TYPE_CHECKING, Any
+from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
+from sqlalchemy.orm import declarative_base, sessionmaker
+
+from app.store.database.sqlalchemy_base import db
+
+if TYPE_CHECKING:
+ from app.web.app import Application
+
+
+class Database:
+ def __init__(self, app: "Application"):
+ self.app = app
+ self._engine: Optional[AsyncEngine] = None
+ self._db: Optional[declarative_base] = None
+ self.session: Optional[AsyncSession] = None
+
+ async def connect(self, *_: list, **__: dict) -> None:
+ self._db = db
+ self._engine = create_async_engine("postgresql+asyncpg://kts_user:kts_pass@localhost:5432/kts", echo=True)
+ self.session = sessionmaker(self._engine, expire_on_commit=False, future=True, class_=AsyncSession)
+
+
+
+ async def disconnect(self, *_: list, **__: dict) -> None:
+ if self._engine:
+ await self._engine.dispose()
\ No newline at end of file
diff --git a/app/store/database/sqlalchemy_base.py b/app/store/database/sqlalchemy_base.py
new file mode 100644
index 0000000..7e8b0a8
--- /dev/null
+++ b/app/store/database/sqlalchemy_base.py
@@ -0,0 +1,3 @@
+from sqlalchemy.orm import declarative_base
+
+db = declarative_base()
diff --git a/app/store/models/__init__.py b/app/store/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/store/models/model.py b/app/store/models/model.py
new file mode 100644
index 0000000..1996d61
--- /dev/null
+++ b/app/store/models/model.py
@@ -0,0 +1,18 @@
+from app.store.database.sqlalchemy_base import db
+
+from sqlalchemy import (
+ Column,
+ Text,
+ BigInteger,
+)
+
+
+class ParticipantsModel(db):
+ __tablename__ = "participants"
+ id = Column(BigInteger, primary_key=True)
+ name = Column(Text, nullable=False)
+ wins = Column(BigInteger)
+ chat_id = Column(BigInteger, nullable=False)
+ owner_id = Column(BigInteger)
+ photo_id = Column(BigInteger)
+ access_key = Column(Text)
diff --git a/app/store/vk_api/__init__.py b/app/store/vk_api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/store/vk_api/accessor.py b/app/store/vk_api/accessor.py
new file mode 100644
index 0000000..7b840cd
--- /dev/null
+++ b/app/store/vk_api/accessor.py
@@ -0,0 +1,200 @@
+import random
+import typing
+from typing import Optional
+from aiohttp import TCPConnector
+from sqlalchemy import select
+from aiohttp.client import ClientSession
+from app.store.models.model import ParticipantsModel
+from app.base.base_accessor import BaseAccessor
+from app.store.vk_api.dataclasses import (
+ Message,
+ Update,
+ UpdateObject,
+ UpdatePhoto,
+ Attachment,
+)
+from app.store.vk_api.poller import Poller
+
+if typing.TYPE_CHECKING:
+ from app.web.app import Application
+
+API_PATH = "https://api.vk.com/method/"
+
+
+class VkApiAccessor(BaseAccessor):
+ def __init__(self, app: "Application", *args, **kwargs):
+ super().__init__(app, *args, **kwargs)
+ self.session: Optional[ClientSession] = None
+ self.key: Optional[str] = None
+ self.server: Optional[str] = None
+ self.poller: Optional[Poller] = None
+ self.ts: Optional[int] = None
+
+ async def connect(self, app: "Application"):
+ self.session = ClientSession(connector=TCPConnector(verify_ssl=False))
+ try:
+ await self._get_long_poll_service()
+ except Exception as e:
+ self.logger.error("Exception", exc_info=e)
+ self.poller = Poller(app.store)
+ self.logger.info("start polling")
+ await self.poller.start()
+
+ async def disconnect(self, app: "Application"):
+ if self.session:
+ await self.session.close()
+ if self.poller:
+ await self.poller.stop()
+
+ @staticmethod
+ def _build_query(host: str, method: str, params: dict) -> str:
+ url = host + method + "?"
+ if "v" not in params:
+ params["v"] = "5.132"
+ url += "&".join([f"{k}={v}" for k, v in params.items()])
+ print(url)
+ return url
+
+ async def _get_long_poll_service(self):
+ async with self.session.get(
+ self._build_query(
+ host=API_PATH,
+ method="groups.getLongPollServer",
+ params={
+ "group_id": self.app.config.bot.group_id,
+ "access_token": self.app.config.bot.token,
+ },
+ )
+ ) as resp:
+ data = (await resp.json())["response"]
+
+ self.logger.info(data)
+ self.key = data["key"]
+ self.server = data["server"]
+ self.ts = data["ts"]
+ self.logger.info(self.server)
+
+ async def poll(self):
+ async with self.session.get(
+ self._build_query(
+ host=self.server,
+ method="",
+ params={
+ "act": "a_check",
+ "key": self.key,
+ "ts": self.ts,
+ "wait": 1,
+ },
+ )
+ ) as resp:
+ data = await resp.json()
+ self.logger.info(data)
+ self.ts = data["ts"]
+ raw_updates = data.get("updates", [])
+ updates = []
+ for update in raw_updates:
+ updt = update["object"]["message"]["attachments"]
+ if len(updt) != 0:
+ for i in updt:
+ if i["type"] == "photo":
+ updates.append(
+ Update(
+ type=update["type"],
+ object=UpdatePhoto(
+ chat_id=update["object"]["message"]["peer_id"],
+ id=update["object"]["message"]["id"],
+ body=update["object"]["message"]["text"],
+ type=i["type"],
+ owner_id=i["photo"]["owner_id"],
+ photo_id=i["photo"]["id"],
+ access_key=i["photo"]["access_key"],
+ ),
+ )
+ )
+ else:
+ updates.append(
+ Update(
+ type=update["type"],
+ object=UpdateObject(
+ chat_id=update["object"]["message"]["peer_id"],
+ id=update["object"]["message"]["from_id"],
+ body=update["object"]["message"]["text"],
+ ),
+ )
+ )
+
+ await self.app.store.bots_manager.handle_updates(updates)
+
+ async def send_message(self, message: Message) -> None:
+ async with self.session.get(
+ self._build_query(
+ API_PATH,
+ "messages.send",
+ params={
+ "random_id": random.randint(1, 2**32),
+ "peer_id": message.chat_id,
+ "message": message.text,
+ "access_token": self.app.config.bot.token,
+ },
+ )
+ ) as resp:
+ data = await resp.json()
+ self.logger.info(data)
+
+ @staticmethod
+ def _build_attachment(attach_mass: list[str]):
+ spisok = []
+ for i in attach_mass:
+ stroka = f"photo{i[0]}_{i[1]}_{i[2]}"
+ spisok.append(stroka)
+ return ",".join(spisok)
+
+ async def send_photo(self, attachment: Attachment) -> None:
+ attachments = self._build_attachment(attachment.attachment)
+ print(attachments)
+ async with self.session.get(
+ self._build_query(
+ API_PATH,
+ "messages.send",
+ params={
+ "random_id": random.randint(1, 2**32),
+ "peer_id": attachment.chat_id,
+ "attachment": attachments,
+ "message": attachment.text,
+ "access_token": self.app.config.bot.token,
+ },
+ )
+ ) as resp:
+ data = await resp.json()
+ self.logger.info(data)
+
+ async def make_userlist(self, chat_id):
+ async with self.session.get(
+ self._build_query(
+ API_PATH,
+ "messages.getConversationMembers",
+ params={
+ "peer_id": chat_id,
+ "access_token": self.app.config.bot.token,
+ },
+ )
+ ) as resp:
+ data = await resp.json()
+ participants = []
+ for i in range(data["response"]["count"] - 1):
+ full_name = f'@{data["response"]["profiles"][i]["screen_name"]}'
+ id_profile = data["response"]["profiles"][i]["id"]
+ participants.append((full_name, id_profile))
+ return participants
+
+ async def proccess_start_game(self, chat_id):
+ await self.app.database.connect()
+ async with self.app.database.session.begin() as session:
+ users_exists_select = select(
+ ParticipantsModel.__table__.c.name,
+ ParticipantsModel.__table__.c.owner_id,
+ ParticipantsModel.__table__.c.photo_id,
+ ParticipantsModel.__table__.c.access_key,
+ ).where(ParticipantsModel.__table__.c.chat_id == chat_id)
+ result = await session.execute(users_exists_select)
+ return result.fetchall()
diff --git a/app/store/vk_api/dataclasses.py b/app/store/vk_api/dataclasses.py
new file mode 100644
index 0000000..a6f544f
--- /dev/null
+++ b/app/store/vk_api/dataclasses.py
@@ -0,0 +1,38 @@
+from dataclasses import dataclass
+
+
+@dataclass
+class UpdateObject:
+ chat_id: int
+ id: int
+ body: str
+
+
+@dataclass
+class UpdatePhoto(UpdateObject):
+ type: str
+ owner_id: int
+ photo_id: int
+ access_key: str
+
+
+@dataclass
+class Update:
+ type: str
+ object: UpdateObject | UpdatePhoto
+
+
+@dataclass
+class Message:
+ chat_id: int
+ text: str
+
+
+@dataclass
+class Attachment(Message):
+ attachment: list[str]
+
+
+@dataclass
+class MessageAttachment:
+ pass
diff --git a/app/store/vk_api/poller.py b/app/store/vk_api/poller.py
new file mode 100644
index 0000000..c97f3c1
--- /dev/null
+++ b/app/store/vk_api/poller.py
@@ -0,0 +1,26 @@
+import asyncio
+from asyncio import Task
+from typing import Optional
+from app.store import Store
+
+
+class Poller:
+ def __init__(self, store: Store):
+ self.store = store
+ self.is_running = False
+ self.poll_task: Optional[Task] = None
+
+ async def start(self):
+ self.is_running = True
+ self.poll_task = asyncio.create_task(self.poll())
+ await asyncio.gather(self.poll_task)
+
+ async def stop(self):
+ self.is_running = False
+ await self.poll_task
+
+ async def poll(self):
+ while self.is_running:
+ updates = await self.store.vk_api.poll()
+ if not (updates is None):
+ await self.store.bots_manager.handle_updates(updates)
diff --git a/app/web/__init__.py b/app/web/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/web/app.py b/app/web/app.py
new file mode 100644
index 0000000..e6f5fb0
--- /dev/null
+++ b/app/web/app.py
@@ -0,0 +1,28 @@
+from typing import Optional
+
+from aiohttp.web import (
+ Application as AiohttpApplication,
+ Request as AiohttpRequest,
+ View as AiohttpView,
+)
+
+from app.store import Store, setup_store
+from app.store.database.database import Database
+from app.web.config import Config, setup_config
+from app.web.logger import setup_logging
+
+
+class Application(AiohttpApplication):
+ config: Optional[Config] = None
+ store: Optional[Store] = None
+ database: Optional[Database] = None
+
+
+app = Application()
+
+
+def setup_app(config_path: str) -> Application:
+ setup_logging(app)
+ setup_config(app, config_path)
+ setup_store(app)
+ return app
diff --git a/app/web/config.py b/app/web/config.py
new file mode 100644
index 0000000..a885702
--- /dev/null
+++ b/app/web/config.py
@@ -0,0 +1,30 @@
+import typing
+from dataclasses import dataclass
+
+import yaml
+
+if typing.TYPE_CHECKING:
+ from app.web.app import Application
+
+
+@dataclass
+class BotConfig:
+ token: str
+ group_id: int
+
+
+@dataclass
+class Config:
+ bot: BotConfig = None
+
+
+def setup_config(app: "Application", config_path: str):
+ with open(config_path, "r") as f:
+ raw_config = yaml.safe_load(f)
+
+ app.config = Config(
+ bot=BotConfig(
+ token=raw_config["bot"]["token"],
+ group_id=raw_config["bot"]["group_id"],
+ ),
+ )
diff --git a/app/web/logger.py b/app/web/logger.py
new file mode 100644
index 0000000..95e6bd8
--- /dev/null
+++ b/app/web/logger.py
@@ -0,0 +1,9 @@
+import logging
+import typing
+
+if typing.TYPE_CHECKING:
+ from app.web.app import Application
+
+
+def setup_logging(_: "Application") -> None:
+ logging.basicConfig(level=logging.INFO)
diff --git a/kts_backend/web/mw.py b/app/web/mw.py
similarity index 100%
rename from kts_backend/web/mw.py
rename to app/web/mw.py
diff --git a/kts_backend/users/urls.py b/app/web/urls.py
similarity index 68%
rename from kts_backend/users/urls.py
rename to app/web/urls.py
index ba6dd07..b21877e 100644
--- a/kts_backend/users/urls.py
+++ b/app/web/urls.py
@@ -1,9 +1,10 @@
from aiohttp.web_app import Application
from aiohttp_cors import CorsConfig
-
__all__ = ("register_urls",)
def register_urls(application: Application, cors: CorsConfig):
- pass
+ import app.users.urls
+
+ app.users.urls.register_urls(application, cors)
diff --git a/config.yml b/config.yml
new file mode 100644
index 0000000..2c86ac7
--- /dev/null
+++ b/config.yml
@@ -0,0 +1,6 @@
+
+bot:
+ token: vk1.a.2QX30vzXv86Yge4oS5MeibmPBlT0gSFRkZ2_AUIIOTUjDxbJys6JL9b_1zuX-1QOs6njHGYz-j9UXXf4rxFkQFqOl5jRuNl5hAx2CAMS4Fcbep0eIU1gnfOot91wYbUkc1wiw6vl90T71gx0NzUVqRS3UEPTy_pQgqSfxcFXsOtykgeYDYJTTC03zzKmYcrr_qcAz_YauFYw0jWVS30FZA
+ group_id: 207946988
+
+
diff --git a/kts_backend/__init__.py b/kts_backend/__init__.py
deleted file mode 100644
index 9f2c7c5..0000000
--- a/kts_backend/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-import os
-
-
-def read_version():
- current_dir = os.path.dirname(os.path.realpath(__file__))
- with open(os.path.join(current_dir, "..", "VERSION")) as f:
- return f.read().strip()
-
-
-__appname__ = "kts_backend"
-__version__ = read_version()
diff --git a/kts_backend/store/__init__.py b/kts_backend/store/__init__.py
deleted file mode 100644
index b1d7c6a..0000000
--- a/kts_backend/store/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-class Store:
- def __init__(self, *args, **kwargs):
- from kts_backend.users.accessor import UserAccessor
-
- self.user = UserAccessor(self)
diff --git a/kts_backend/users/accessor.py b/kts_backend/users/accessor.py
deleted file mode 100644
index b65cb4d..0000000
--- a/kts_backend/users/accessor.py
+++ /dev/null
@@ -1,2 +0,0 @@
-class UserAccessor:
- pass
diff --git a/kts_backend/users/schema.py b/kts_backend/users/schema.py
deleted file mode 100644
index 42163d1..0000000
--- a/kts_backend/users/schema.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from marshmallow import Schema
-
-
-class UserSchema(Schema):
- pass
diff --git a/kts_backend/users/views/__init__.py b/kts_backend/users/views/__init__.py
deleted file mode 100644
index 0899133..0000000
--- a/kts_backend/users/views/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .get import *
diff --git a/kts_backend/web/app.py b/kts_backend/web/app.py
deleted file mode 100644
index 8cbab44..0000000
--- a/kts_backend/web/app.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from typing import Sequence, Callable
-
-from aiohttp.web import (
- Application as AiohttpApplication,
- View as AiohttpView,
- Request as AiohttpRequest,
-)
-from pyparsing import Optional
-
-
-from kts_backend import __appname__, __version__
-from .urls import register_urls
-
-
-__all__ = ("ApiApplication",)
-
-
-class Application(AiohttpApplication):
- config = None
- store = None
- database = None
diff --git a/kts_backend/web/urls.py b/kts_backend/web/urls.py
deleted file mode 100644
index 7f84462..0000000
--- a/kts_backend/web/urls.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from aiohttp.web_app import Application
-from aiohttp_cors import CorsConfig
-
-__all__ = ("register_urls",)
-
-
-def register_urls(application: Application, cors: CorsConfig):
- import kts_backend.users.urls
-
- kts_backend.users.urls.register_urls(application, cors)
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..8f1f171
--- /dev/null
+++ b/main.py
@@ -0,0 +1,13 @@
+import os
+
+from app.web.app import setup_app
+from aiohttp.web import run_app
+
+if __name__ == "__main__":
+ run_app(
+ setup_app(
+ config_path=os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), "config.yml"
+ )
+ )
+ )
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 00c3664..c3ef3b3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,39 @@
-aiohttp==3.8.1
-black==22.6.0
-pytest==7.1.2
+aiohttp==3.8.3
+aiohttp-apispec==2.2.3
+aiohttp-session==2.12.0
+aiosignal==1.3.1
+alembic==1.9.3
+apispec==3.3.2
+async-timeout==4.0.2
+asyncpg==0.27.0
+attrs==22.2.0
+cffi==1.15.1
+charset-normalizer==2.1.1
+coverage==7.1.0
+cryptography==39.0.1
+frozenlist==1.3.3
+greenlet==2.0.2
+idna==3.4
+iniconfig==2.0.0
+Jinja2==3.1.2
+Mako==1.2.4
+MarkupSafe==2.1.2
+multidict==6.0.4
+packaging==23.0
+pluggy==1.0.0
+pycparser==2.21
+pyparsing==3.0.9
+pytest==7.2.1
pytest-aiohttp==1.0.4
-pytest-asyncio==0.19.0
-pytest-env==0.6.2
+pytest-asyncio==0.20.3
+pytest-cov==4.0.0
+pytest-mock==3.10.0
+python-dateutil==2.8.2
+python-dotenv==0.21.1
+PyYAML==6.0
+simplejson==3.18.3
+six==1.16.0
+SQLAlchemy==2.0.3
+typing_extensions==4.4.0
+webargs==5.5.3
+yarl==1.8.2