init: Initial project setup

This commit is contained in:
ITQ
2024-03-17 18:50:03 +03:00
parent 9b652d063b
commit aa99051c7b
21 changed files with 703 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
[flake8]
exclude =
migrations,
__pycache__,
venv
import-order-style=google
inline-quotes = double
max-line-lenght = 79
application_import_names = bot
+160
View File
@@ -0,0 +1,160 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# 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/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# 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
# 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
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# 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/
+17
View File
@@ -0,0 +1,17 @@
FROM python:3.12-slim
WORKDIR /app
# Copy requirements file
COPY requirements/prod.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r prod.txt
# Copy the rest of the application files
COPY . .
# Apply migrations
# RUN alembic -c app/alembic.ini upgrade head
CMD ["python", "-m", "app"]
+137
View File
@@ -0,0 +1,137 @@
# ✈️ Travel agent bot
This bot will help you organise and plan your travels.
## ⚙️ Technologies
### Python
Python is used successfully in thousands of real-world business applications around the world, including many large and mission critical systems.
### aiogram
aiogram is a modern and fully asynchronous framework for Telegram Bot API using asyncio and aiohttp.
### SQLAlchemy
SQLAlchemy is the Python SQL toolkit and Object Relational Mapper that gives application developers the full power and flexibility of SQL.
It provides a full suite of well known enterprise-level persistence patterns, designed for efficient and high-performing database access, adapted into a simple and Pythonic domain language.
### PostgreSQL
PostgreSQL is a powerful, open source object-relational database system with over 35 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance.
## 🛠️ Local development & testing
### Clone repository with git
```bash
git clone https://github.com/Central-University-IT-prod/backend-devitq.git
```
### Navigate to the project directory
```bash
cd backend-devitq
```
### Create virtual enviroment & activate it
#### Windows
```cmd
python -m venv venv
venv\Scripts\activate
```
#### Linux
```bash
python -m venv venv
source venv/bin/activate
```
### Install dev requirements
```bash
pip install -r requirements/dev.txt
```
### Setup .env file
#### Windows
```cmd
copy .env.template .env
```
#### Linux
```bash
cp .env.template .env
```
And replace all default values with actual values
### Apply migrations with alembic
```bash
alembic -c app/alembic.ini upgrade head
```
### Run bot in development mode
```bash
python -m app
```
## 🚀 Deploying
Our app uses docker compose for production deployment.
### Clone repository with git
```bash
git clone https://github.com/Central-University-IT-prod/backend-devitq.git
```
### Navigate to the project directory
```bash
cd backend-devitq
```
### Setup .env file
#### Windows
```cmd
copy .env.template .env
```
#### Linux
```bash
cp .env.template .env
```
And replace all default values with actual values
### Pull actual docker images
```bash
docker compose pull
```
### Start containers (in detached mode)
```bash
docker compose up -d
```
## Notes
Bot Token: 6943803094:AAEHG-vOP2pNEuxb9rDIhisiQuGLuBIjx1Q
I thought up the architecture of the project myself, I won't mind feedback on it)
View File
+9
View File
@@ -0,0 +1,9 @@
import asyncio
import logging
import sys
from app.bot import main
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
asyncio.run(main())
+116
View File
@@ -0,0 +1,116 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = app/migrations
# 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
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# 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>=3.9 or backports.zoneinfo library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# 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 migrations/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:migrations/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.
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = none
[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
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix 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
+35
View File
@@ -0,0 +1,35 @@
__all__ = ("main",)
from typing import Optional
from aiogram import Bot, Dispatcher, types
from aiogram.enums import ParseMode
from aiogram.filters import CommandStart
from aiogram.types import Message
from aiogram.utils.markdown import hbold
from app.config import Config
dp = Dispatcher()
@dp.message(CommandStart())
async def command_start_handler(message: Message) -> None:
await message.answer(f"Hello, {hbold(message.from_user.full_name)}!")
@dp.message()
async def echo_handler(message: types.Message) -> None:
try:
await message.send_copy(chat_id=message.chat.id)
except TypeError:
await message.answer("Nice try!")
async def main() -> None:
bot_token: Optional[str] = Config.BOT_TOKEN
if bot_token is not None:
bot = Bot(bot_token, parse_mode=ParseMode.HTML)
await dp.start_polling(bot)
else:
exit("BOT_TOKEN is not set")
+12
View File
@@ -0,0 +1,12 @@
__all__ = ("Config",)
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
BOT_TOKEN = os.getenv("BOT_TOKEN")
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI")
+1
View File
@@ -0,0 +1 @@
Generic single-database configuration.
+79
View File
@@ -0,0 +1,79 @@
__all__ = ()
from logging.config import fileConfig
import os
from alembic import context
from app.models import Base
from dotenv import load_dotenv
from sqlalchemy import engine_from_config
from sqlalchemy import pool
load_dotenv()
# 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)
target_metadata = Base.metadata
config.set_main_option("sqlalchemy.url", os.getenv("SQLALCHEMY_DATABASE_URI"))
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 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 = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+26
View File
@@ -0,0 +1,26 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
+19
View File
@@ -0,0 +1,19 @@
__all__ = ("User",)
from typing import Any
from sqlalchemy import Column, Integer, SmallInteger, String
from sqlalchemy.ext.declarative import declarative_base
Base: Any = declarative_base()
class User(Base):
__tablename__ = "users"
telegram_id: Column[int] = Column(Integer, primary_key=True)
age: Column[int] = Column(SmallInteger, nullable=False)
bio: Column[str] = Column(String(100), nullable=True)
country: Column[str] = Column(String(100), nullable=False)
city: Column[str] = Column(String(100), nullable=False)
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
GREEN='\033[1;32m'
NC='\033[0m'
sort-requirements requirements/dev.txt
sort-requirements requirements/prod.txt
sort-requirements requirements/test.txt
sort-requirements requirements/lints.txt
printf "${GREEN}Requirements sorted${NC}\n"
black .
flake8 .
mypy .
printf "${GREEN}Linters runned${NC}\n"
# alembic -c app/alembic.ini upgrade head
printf "${GREEN}Tests runned${NC}\n"
+22
View File
@@ -0,0 +1,22 @@
name: travel_agent_bot
version: '3'
services:
app:
build: .
container_name: bot
depends_on:
- postgres
postgres:
image: postgres:latest
container_name: db
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: wTAb5KoZ4dBtscg
ports:
- "5432:5432"
volumes:
travel_agent_bot_data:
+9
View File
@@ -0,0 +1,9 @@
[tool.black]
line-length = 79
skip-string-normalization=true
[tool.isort]
order_by_type = false
[tool.mypy]
exclude = "migrations"
+7
View File
@@ -0,0 +1,7 @@
black
mypy
sort-requirements
-r prod.txt
-r tests.txt
-r lints.txt
+19
View File
@@ -0,0 +1,19 @@
flake8-absolute-import
flake8-bugbear
flake8-builtins
flake8-clean-block
flake8-commas
flake8-comments
flake8-comprehensions
flake8-dunder-all
flake8-eradicate
flake8-expression-complexity
flake8-fixme
flake8-import-order
flake8-print
flake8-quotes
flake8-return
flake8-type-ignore
flake8-use-pathlib
flake8_implicit_str_concat
pep8-naming
+5
View File
@@ -0,0 +1,5 @@
aiogram==3.4.1
alembic==1.13.1
psycopg2-binary==2.9.9
python-dotenv==1.0.1
sqlalchemy==2.0.28
View File
+2
View File
@@ -0,0 +1,2 @@
BOT_TOKEN = <your_bot_token>
SQLALCHEMY_DATABASE_URI = <database_uri>