Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ env:
REDIS_DSN: redis://localhost:6379/0
POSTGRES_DSN: postgresql://postgres:postgres@localhost:5432/postgres
SMARTLOG_DEBUG_HUIDS: 7d8eac87-8fc8-452c-9d57-34f988475368
poetry_version: "1.5.0"
poetry_version: "2.4.1"
project_name: "bot-example"
PROD_SERVER_HOST: "prod.example.com"
DEV_SERVER_HOST: "dev.example.com"
Expand Down Expand Up @@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: [ "3.8", "3.9", "3.10", "3.11"]
python-version: [ "3.10", "3.11", "3.12", "3.13" ]

services:
postgres:
Expand Down Expand Up @@ -113,7 +113,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: [ "3.10", "3.11", "3.12", "3.13" ]

steps:
- name: Checkout
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ ENV GUNICORN_CMD_ARGS ""
ARG CI_COMMIT_SHA=""
ENV GIT_COMMIT_SHA=${CI_COMMIT_SHA}

RUN pip install --user --no-cache-dir poetry==1.4.2 && \
RUN pip install --user --no-cache-dir poetry==2.4.1 && \
poetry config virtualenvs.in-project true

COPY poetry.lock pyproject.toml ./
Expand Down
5 changes: 0 additions & 5 deletions app/bot/commands/{% if CI %}test.py{% endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,7 @@ async def test_redis(message: IncomingMessage, bot: Bot) -> None:
"""Testing redis."""
# This test just for coverage
# Better to assert bot answers instead of using direct DB/Redis access

redis_repo = bot.state.redis_repo

await redis_repo.set("test_key", "test_value")


Expand All @@ -143,12 +141,9 @@ async def test_db(message: IncomingMessage, bot: Bot) -> None:
# add text to history
# example of using database
record_repo = RecordRepo(message.state.db_session)

await record_repo.create(record_data="test 1")
await record_repo.update(record_id=1, record_data="test 1 (updated)")

await record_repo.create(record_data="test 2")
await record_repo.delete(record_id=2)

await record_repo.create(record_data="test not unique data")
await record_repo.create(record_data="test not unique data")
16 changes: 10 additions & 6 deletions app/caching/redis_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import hashlib
import pickle # noqa: S403
from typing import Any, Hashable, Optional
from typing import Any, Hashable, Optional, cast

from redis import asyncio as aioredis

Expand Down Expand Up @@ -32,7 +32,7 @@ async def get(self, key: Hashable, default: Any = None) -> Any:
if cached_data is None:
return default

return pickle.loads(cached_data) # noqa: S301
return pickle.loads(cast(bytes, cached_data)) # noqa: S301

async def set(
self, key: Hashable, storage_value: Any, expire: Optional[int] = None
Expand All @@ -52,9 +52,13 @@ async def rget(self, key: Hashable, default: Any = None) -> Any:
return storage_value

def _key(self, arg: Hashable) -> str:
if self._prefix is not None:
prefix = self._prefix + self._delimiter
else:
if self._prefix is None:
prefix = ""
else:
prefix = self._prefix + self._delimiter

return prefix + hashlib.md5(pickle.dumps(arg)).hexdigest() # noqa: S303
hashed_key = hashlib.md5( # noqa: S324
pickle.dumps(arg),
usedforsecurity=False,
).hexdigest()
return prefix + hashed_key
4 changes: 1 addition & 3 deletions app/db/record/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ class RecordModel(Base):

__tablename__ = "records"

id: Mapped[int] = mapped_column(
primary_key=True, autoincrement=True
) # noqa: WPS125
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
record_data: Mapped[str]

def __repr__(self) -> str:
Expand Down
23 changes: 15 additions & 8 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Application with configuration for events, routers and middleware."""

import asyncio
from functools import partial
from contextlib import asynccontextmanager
from typing import AsyncIterator

from fastapi import FastAPI
from pybotx import Bot
Expand Down Expand Up @@ -57,7 +58,7 @@ async def shutdown(application: FastAPI) -> None:

# -- Redis --
redis_client: aioredis.Redis = application.state.redis
await redis_client.close()
await redis_client.aclose()

# -- Database --
await close_db_connections()
Expand All @@ -66,13 +67,19 @@ async def shutdown(application: FastAPI) -> None:
def get_application(raise_bot_exceptions: bool = False) -> FastAPI:
"""Create configured server application instance."""

application = FastAPI(title=strings.BOT_PROJECT_NAME, openapi_url=None)

application.add_event_handler(
"startup", partial(startup, application, raise_bot_exceptions)
@asynccontextmanager
async def lifespan(application: FastAPI) -> AsyncIterator[None]: # noqa: WPS430
await startup(application, raise_bot_exceptions)
try:
yield
finally:
await shutdown(application)

application = FastAPI(
title=strings.BOT_PROJECT_NAME,
openapi_url=None,
lifespan=lifespan,
)
application.add_event_handler("shutdown", partial(shutdown, application))

application.include_router(router)

return application
1 change: 1 addition & 0 deletions app/resources/strings.py.jinja
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Text and templates for messages and api responses."""

from typing import Any, Protocol, cast

from mako.lookup import TemplateLookup
Expand Down
93 changes: 48 additions & 45 deletions app/settings.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,67 @@
"""Application settings."""

from typing import Any, List
from typing import Annotated, Any, List
from uuid import UUID

from pybotx import BotAccountWithSecret
from pydantic import BaseSettings
from pydantic import AnyHttpUrl, field_validator
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict


class AppSettings(BaseSettings):
class Config: # noqa: WPS431
env_file = ".env"

@classmethod
def parse_env_var(cls, field_name: str, raw_val: str) -> Any:
if field_name == "BOT_CREDENTIALS":
if not raw_val:
return []

return [
cls._build_credentials_from_string(credentials_str)
for credentials_str in raw_val.replace(",", " ").split()
]
elif field_name == "SMARTLOG_DEBUG_HUIDS":
return cls.parse_smartlog_debug_huids(raw_val)

return cls.json_loads(raw_val) # type: ignore

@classmethod
def parse_smartlog_debug_huids(cls, raw_huids: Any) -> List[UUID]:
"""Parse debug huids separated by comma."""
if not raw_huids:
return []

return [UUID(huid) for huid in raw_huids.split(",")]

@classmethod
def _build_credentials_from_string(
cls, credentials_str: str
) -> BotAccountWithSecret:
credentials_str = credentials_str.replace("|", "@")
assert credentials_str.count("@") == 2, "Have you forgot to add `bot_id`?"

cts_url, secret_key, bot_id = [
str_value.strip() for str_value in credentials_str.split("@")
model_config = SettingsConfigDict(env_file=".env", extra="ignore")

@field_validator("BOT_CREDENTIALS", mode="before")
@classmethod
def parse_bot_credentials(cls, raw_val: Any) -> List[BotAccountWithSecret]:
if not raw_val:
return []

if isinstance(raw_val, str):
return [
cls._build_credentials_from_string(credentials_str)
for credentials_str in raw_val.replace(",", " ").split()
]

if "://" not in cts_url:
cts_url = f"https://{cts_url}"
return raw_val

return BotAccountWithSecret(
id=UUID(bot_id), cts_url=cts_url, secret_key=secret_key
)
@field_validator("SMARTLOG_DEBUG_HUIDS", mode="before")
@classmethod
def parse_smartlog_debug_huids(cls, raw_huids: Any) -> List[UUID]:
"""Parse debug huids separated by comma."""
if not raw_huids:
return []

BOT_CREDENTIALS: List[BotAccountWithSecret]
if isinstance(raw_huids, str):
return [UUID(huid.strip()) for huid in raw_huids.split(",")]

return raw_huids

@classmethod
def _build_credentials_from_string(
cls, credentials_str: str
) -> BotAccountWithSecret:
credentials_str = credentials_str.replace("|", "@")
assert credentials_str.count("@") == 2, "Have you forgot to add `bot_id`?"

cts_url, secret_key, bot_id = [
str_value.strip() for str_value in credentials_str.split("@")
]

if "://" not in cts_url:
cts_url = f"https://{cts_url}"

return BotAccountWithSecret(
id=UUID(bot_id), cts_url=AnyHttpUrl(cts_url), secret_key=secret_key
)

BOT_CREDENTIALS: Annotated[List[BotAccountWithSecret], NoDecode]

# base kwargs
DEBUG: bool = False

# User huids for debug
SMARTLOG_DEBUG_HUIDS: List[UUID]
SMARTLOG_DEBUG_HUIDS: Annotated[List[UUID], NoDecode]

# database
POSTGRES_DSN: str
Expand All @@ -72,4 +75,4 @@ def _build_credentials_from_string(
WORKER_TIMEOUT_SEC: float = 4


settings = AppSettings()
settings = AppSettings() # type: ignore[call-arg]
5 changes: 1 addition & 4 deletions app/{% if add_worker %}worker{% endif %}/worker.py.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@ async def startup(ctx: SaqCtx) -> None:

callback_repo = CallbackRedisRepo(aioredis.from_url(app_settings.REDIS_DSN))
bot = get_bot(callback_repo, raise_exceptions=False)

await bot.startup(fetch_tokens=False)

ctx["bot"] = bot

logger.info("Worker started")


Expand All @@ -39,7 +36,7 @@ async def healthcheck(_: SaqCtx) -> Literal[True]:
return True


queue = Queue(aioredis.from_url(app_settings.REDIS_DSN), name="{{bot_project_name}}")
queue = Queue.from_url(app_settings.REDIS_DSN, name="{{bot_project_name}}")

settings = {
"queue": queue,
Expand Down
2 changes: 1 addition & 1 deletion example.env
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ POSTGRES_PASSWORD="postgres"
POSTGRES_USER="postgres"
POSTGRES_DB="postgres"

POSTGRES_DSN=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:5432/${DEV_DB_NAME}
POSTGRES_DSN=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:5432/${POSTGRES_DB}
SQL_DEBUG=false

# Redis
Expand Down
Loading
Loading