Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Ignore IDE config.
.idea
.vscode

# MacOS
.DS_Store

# dotenv environment variables file
.env
18 changes: 18 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM python:3.10.5-slim

LABEL maintainer="Alexey Kudimov"

ENV PYTHONUNBUFFERED=1
ENV TZ='Europe/Moscow'

RUN apt update && apt install -y vim
RUN mkdir /etc/app

WORKDIR /app
RUN pip install --no-cache-dir poetry

COPY pyproject.toml poetry.lock /app/
RUN poetry config virtualenvs.create false &&\
poetry install --no-interaction --no-ansi

COPY . .
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## :rocket: Websocket engine for Tic Tac Toe Game
### :zap: Stack:
* :dart: Python
* :dart: FastAPI
* :dart: PostgreSQL
* :dart: Redis

834 changes: 834 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[tool.poetry]
name = "tic-tac-toe-python"
version = "0.1.0"
description = "Websocket engine for Tic Tac Toe Game"
authors = ["alexeykudimov <work.alexeykudimov@gmail.com>"]
license = "MIT"

[tool.poetry.dependencies]
python = "^3.10"
fastapi = "^0.79.0"
uvicorn = "^0.18.2"
SQLAlchemy = "^1.4.40"
psycopg2-binary = "^2.9.3"
PyJWT = "^2.4.0"
aioredis = "^2.0.1"
python-dotenv = "^0.20.0"
aiohttp = "^3.8.1"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
Empty file added src/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions src/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import logging

from fastapi import FastAPI

from src.settings import settings
from src.tic_tac_toe.endpoints import router

logger = logging.getLogger(__name__)


def init_app() -> FastAPI:
app = FastAPI(docs_url=f'/docs', redoc_url=f'/redoc')
app.include_router(router)
return app

Empty file added src/bin/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions src/bin/start.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import os
import sys

sys.path.insert(1, os.path.join(sys.path[0], '../..'))

import logging
from src.logger import configure_logging

configure_logging()

import uvicorn

from src.app import init_app
from src.settings import settings

logger = logging.getLogger(__name__)

app = init_app()

if __name__ == '__main__':
uvicorn.run('src.bin.start:app', host='0.0.0.0',
port=settings.SERVER_PORT,
workers=settings.SERVER_WORKER_NUMBER,
reload=settings.ENABLE_WEB_SERVER_AUTORELOAD)
90 changes: 90 additions & 0 deletions src/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import logging.config

from src.settings import settings


def configure_logging():
logging.config.dictConfig(LOGGING_CONFIG)


class PackagePathFilter(logging.Filter):
def _patch_pathname(self, record, piece: str):
pathname = record.pathname
if piece in pathname:
index = pathname.index(piece)
record.pathname = 'app->' + pathname[index + len(piece):]
return record

def filter(self, record):
record = self._patch_pathname(record, '/src/tic_tac_toe/')
record = self._patch_pathname(record, '/src/bin/')
record = self._patch_pathname(record, '/site-packages/')
return record


LOGGING_CONFIG: dict = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "[%(asctime)s] [%(levelname)-5s] | "
"%(pathname)s@%(lineno)d | %(message)s"
},
"access": {
"()": "uvicorn.logging.AccessFormatter",
"fmt": '[%(asctime)s] [%(levelname)-5s] | '
'%(name)s | %(client_addr)s - '
'"%(request_line)s" %(status_code)s',
},
},
"handlers": {
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
"filters": ['special']
},
"access": {
"formatter": "access",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
"filters": ['special']
},
"null": {
"class": "logging.NullHandler"
},
},
'filters': {
'special': {
'()': PackagePathFilter
},
},
"loggers": {
"uvicorn": {
"handlers": ["null", ],
"propagate": False,
},
"asyncio": {
"handlers": ["null", ],
"propagate": False,
},
'alembic': {
'handlers': ['default', ],
'propagate': False,
},
"sqlalchemy.engine": {
"handlers": ["null", ],
"propagate": False,
},
# "uvicorn.error": {"handlers": ["default"], "level": "INFO"},
"uvicorn.access": {
"handlers": ["access", ],
"propagate": False,
},
"root": {
"handlers": ["default", ],
"level": settings.LOGGING_LEVEL,
},
},
}

Empty file added src/misc/__init__.py
Empty file.
21 changes: 21 additions & 0 deletions src/misc/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import logging

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base

from src.settings import settings

logging = logging.getLogger(__name__)

engine = create_engine(settings.DATABASE_URL,
echo=settings.ENABLE_SQL_ECHO,
echo_pool=settings.ENABLE_SQL_ECHO,
pool_pre_ping=True,
pool_timeout=1,
pool_size=10, max_overflow=20,
connect_args={"options": "-c timezone=utc"})

SessionLocal = sessionmaker(autocommit=False, autoflush=False,
future=True, bind=engine)

ModelBase = declarative_base()
37 changes: 37 additions & 0 deletions src/misc/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import logging
from contextlib import contextmanager

import aioredis
from sqlalchemy.orm import Session

from src.settings import settings
from src.misc.database import SessionLocal

logger = logging.getLogger(__name__)


def db() -> Session:
session = SessionLocal()
try:
yield session
finally:
session.close()


@contextmanager
def db_context(expire_on_commit: bool = True) -> Session:
session = SessionLocal(expire_on_commit=expire_on_commit)
try:
yield session
finally:
session.close()


redis_pool = aioredis.ConnectionPool.from_url(
settings.REDIS_URL, decode_responses=True)


def redis() -> aioredis.Redis:
return aioredis.Redis(connection_pool=redis_pool)


27 changes: 27 additions & 0 deletions src/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import os
from typing import Optional

from pydantic import BaseSettings, PostgresDsn, RedisDsn


class Settings(BaseSettings):
API_PREFIX: str = "/"
SECRET_KEY: str
JWT_SECRET: str
LOGGING_LEVEL: str = 'INFO'

SERVER_PORT: int = 8000
SERVER_WORKER_NUMBER: int = 4

DATABASE_URL: Optional[PostgresDsn] = None
REDIS_URL: Optional[RedisDsn] = None

ENABLE_SQL_ECHO: Optional[bool] = True
ENABLE_WEB_SERVER_AUTORELOAD: Optional[bool] = True

class Config:
case_sensitive = True
env_file = os.path.join(os.getcwd(), '..', '.env')


settings = Settings()
Empty file added src/tic_tac_toe/__init__.py
Empty file.
8 changes: 8 additions & 0 deletions src/tic_tac_toe/endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import logging
from fastapi import APIRouter

from src.settings import settings

logger = logging.getLogger(__name__)

router = APIRouter()
1 change: 1 addition & 0 deletions src/tic_tac_toe/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from pydantic import BaseModel
3 changes: 3 additions & 0 deletions src/tic_tac_toe/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import logging

logger = logging.getLogger(__name__)