1
0
mirror of https://github.com/ijaric/voice_assistant.git synced 2025-05-24 14:33:26 +00:00

Merge pull request #34 from ijaric/test/try_postgres

Пример: Handler, Service & Repository
This commit is contained in:
Artem Litvinov 2023-10-03 15:41:12 +01:00 committed by GitHub
commit eab9177c00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 251 additions and 77 deletions

43
package-lock.json generated
View File

@ -11,7 +11,7 @@
"husky": "^8.0.1", "husky": "^8.0.1",
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"prettier": "2.8.3", "prettier": "2.8.3",
"pyright": "^1.1.299" "pyright": "^1.1.329"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@ -1063,6 +1063,20 @@
"node": ">=14.14" "node": ">=14.14"
} }
}, },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@ -2215,9 +2229,9 @@
} }
}, },
"node_modules/pyright": { "node_modules/pyright": {
"version": "1.1.299", "version": "1.1.329",
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.299.tgz", "resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.329.tgz",
"integrity": "sha512-37agqu0oNRsftFHNyGtaN3SBJJ+Qe2pembdgIpif9iL0VNqCU7j3wauSm8j0peXg1uNHFcS8UMrft4hT7w2XvQ==", "integrity": "sha512-5AT98Mi0OYcDiQ5lD1nPJ3cq8gX/HHaXrQ5WjJ/QZkaJtGqnEdrUp5Gq5wBPipWgOnv/l5e50YScaaNDMjoy9Q==",
"dev": true, "dev": true,
"bin": { "bin": {
"pyright": "index.js", "pyright": "index.js",
@ -2225,6 +2239,9 @@
}, },
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
} }
}, },
"node_modules/q": { "node_modules/q": {
@ -3883,6 +3900,13 @@
"universalify": "^2.0.0" "universalify": "^2.0.0"
} }
}, },
"fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"optional": true
},
"function-bind": { "function-bind": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@ -4702,10 +4726,13 @@
"dev": true "dev": true
}, },
"pyright": { "pyright": {
"version": "1.1.299", "version": "1.1.329",
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.299.tgz", "resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.329.tgz",
"integrity": "sha512-37agqu0oNRsftFHNyGtaN3SBJJ+Qe2pembdgIpif9iL0VNqCU7j3wauSm8j0peXg1uNHFcS8UMrft4hT7w2XvQ==", "integrity": "sha512-5AT98Mi0OYcDiQ5lD1nPJ3cq8gX/HHaXrQ5WjJ/QZkaJtGqnEdrUp5Gq5wBPipWgOnv/l5e50YScaaNDMjoy9Q==",
"dev": true "dev": true,
"requires": {
"fsevents": "~2.3.2"
}
}, },
"q": { "q": {
"version": "1.5.1", "version": "1.5.1",

View File

@ -7,7 +7,7 @@
"husky": "^8.0.1", "husky": "^8.0.1",
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"prettier": "2.8.3", "prettier": "2.8.3",
"pyright": "^1.1.299" "pyright": "^1.1.329"
}, },
"hooks": { "hooks": {
"commit-msg": "commitlint" "commit-msg": "commitlint"

View File

@ -0,0 +1,37 @@
"""Added initial table
Revision ID: 9749b063b095
Revises:
Create Date: 2023-10-02 19:46:05.078494
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "9749b063b095"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"joke",
sa.Column("type", sa.String(), nullable=False),
sa.Column("setup", sa.String(), nullable=False),
sa.Column("punchline", sa.String(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("joke")
# ### end Alembic commands ###

View File

@ -1,5 +1,4 @@
from .health import * from .health import basic_router
from .joke import JokeHandler
__all__ = [ __all__ = ["JokeHandler", "basic_router"]
"basic_router",
]

View File

@ -0,0 +1,25 @@
import fastapi
import lib.api.v1.schemas as api_shemas
import lib.joke.services as joke_services
class JokeHandler:
def __init__(self, joke_service: joke_services.JokeService):
self.joke_service = joke_service
self.router = fastapi.APIRouter()
self.router.add_api_route(
"/",
self.get_joke,
methods=["GET"],
summary="Статус работоспособности",
description="Проверяет доступность сервиса FastAPI.",
)
async def get_joke(self):
joke = await self.joke_service.get_joke()
if joke:
return api_shemas.JokeResponse(
joke=f"{joke.setup}\n{joke.punchline}", id=joke.id_field, category=joke.type_field
)
return api_shemas.JokeResponse(joke="No joke for you!", id=0, category="No category")

View File

@ -1,5 +1,4 @@
from .base import HealthResponseModel from .base import HealthResponseModel
from .joke import JokeResponse
__all__ = [ __all__ = ["HealthResponseModel", "JokeResponse"]
"HealthResponseModel",
]

View File

@ -0,0 +1,7 @@
import pydantic
class JokeResponse(pydantic.BaseModel):
id_field: int = pydantic.Field(alias="id")
joke: str
category: str

View File

@ -11,6 +11,8 @@ import lib.app.errors as app_errors
import lib.app.settings as app_settings import lib.app.settings as app_settings
import lib.app.split_settings as app_split_settings import lib.app.split_settings as app_split_settings
import lib.clients as clients import lib.clients as clients
import lib.joke.repository as joke_repository
import lib.joke.services as joke_services
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -45,7 +47,6 @@ class Application:
logger.info("Initializing global clients") logger.info("Initializing global clients")
postgres_client = clients.AsyncPostgresClient(settings=settings) postgres_client = clients.AsyncPostgresClient(settings=settings)
http_client = clients.get_async_http_session()
disposable_resources.append( disposable_resources.append(
DisposableResource( DisposableResource(
@ -61,6 +62,7 @@ class Application:
# Repositories # Repositories
logger.info("Initializing repositories") logger.info("Initializing repositories")
jk_repository = joke_repository.JokeRepository(async_session=postgres_client.get_async_session())
# Caches # Caches
@ -69,12 +71,13 @@ class Application:
# Services # Services
logger.info("Initializing services") logger.info("Initializing services")
jk_serivces = joke_services.JokeService(jk_repository)
# Handlers # Handlers
logger.info("Initializing handlers") logger.info("Initializing handlers")
liveness_probe_handler = api_v1_handlers.basic_router liveness_probe_handler = api_v1_handlers.basic_router
joke_handler = api_v1_handlers.JokeHandler(joke_service=jk_serivces).router
logger.info("Creating application") logger.info("Creating application")
@ -88,6 +91,7 @@ class Application:
# Routes # Routes
fastapi_app.include_router(liveness_probe_handler, prefix="/api/v1/health", tags=["health"]) fastapi_app.include_router(liveness_probe_handler, prefix="/api/v1/health", tags=["health"])
fastapi_app.include_router(joke_handler, prefix="/test", tags=["some"])
application = Application( application = Application(
settings=settings, settings=settings,

View File

@ -1,4 +1,3 @@
from .httpx import get_async_http_session
from .postgres import AsyncPostgresClient from .postgres import AsyncPostgresClient
__all__ = ["AsyncPostgresClient", "get_async_http_session"] __all__ = ["AsyncPostgresClient"]

View File

@ -1,16 +0,0 @@
import contextlib
import typing
import httpx
@contextlib.asynccontextmanager
async def get_async_http_session(
settings: dict[str, typing.Any] | None = None
) -> typing.AsyncGenerator[httpx.AsyncClient, None]:
"""Async http client."""
if settings is None:
settings = {}
client = httpx.AsyncClient(**settings) # Insert your own settings here
async with client as ac:
yield ac

View File

@ -0,0 +1,4 @@
from .repository import JokeRepository
from .services import JokeService
__all__ = ["JokeRepository", "JokeService"]

View File

@ -0,0 +1,30 @@
import logging
import sqlalchemy.exc
import sqlalchemy.ext.asyncio as sa_asyncio
import lib.models as models
class JokeRepository:
def __init__(self, async_session: sa_asyncio.async_sessionmaker[sa_asyncio.AsyncSession]):
self.async_session = async_session
self.logger = logging.getLogger(__name__)
async def get_joke_by(self, id: int) -> models.JokeORM | None:
try:
async with self.async_session() as session:
joke = await session.get(models.JokeORM, id)
return joke
except sqlalchemy.exc.SQLAlchemyError as error:
self.logger.exception("Error: %s", error)
async def add_joke(self, joke: models.JokeORM) -> models.JokeORM | None:
try:
async with self.async_session() as session:
session.add(joke)
await session.commit()
await session.refresh(joke)
return joke
except sqlalchemy.exc.SQLAlchemyError as error:
self.logger.exception("Error: %s", error)

View File

@ -0,0 +1,30 @@
import logging
import httpx
import pydantic
import lib.joke.repository as joke_repository
import lib.models as models
class JokeService:
def __init__(self, repository: joke_repository.JokeRepository):
self.repository = repository
self.logger = logging.getLogger(__name__)
async def get_joke(self) -> models.Joke | None:
try:
async with httpx.AsyncClient() as client:
response = await client.get("https://official-joke-api.appspot.com/random_joke")
content = response.json()
self.logger.info("Joke retrieved from API")
formatted_joke = models.JokeORM(
type_field=content["type"], setup=content["setup"], punchline=content["punchline"]
)
await self.repository.add_joke(formatted_joke)
self.logger.info("Joke added to database")
return models.Joke(**content)
except pydantic.ValidationError as error:
self.logger.exception("Validation Error: %s", error)
except httpx.HTTPError as error:
self.logger.exception("HTTP Error: %s", error)

View File

@ -1,4 +1,5 @@
from .base_sqlalchemy import Base, IdCreatedUpdatedBaseMixin from .joke import Joke
from .orm import Base, IdCreatedUpdatedBaseMixin, JokeORM
from .token import Token from .token import Token
__all__ = ["Base", "IdCreatedUpdatedBaseMixin", "Token"] __all__ = ["Base", "IdCreatedUpdatedBaseMixin", "Joke", "JokeORM", "Token"]

View File

@ -1,36 +0,0 @@
import uuid
import sqlalchemy
import sqlalchemy.dialects.postgresql
import sqlalchemy.ext.declarative
import sqlalchemy.orm
class Base(sqlalchemy.orm.DeclarativeBase):
"""Base class for all models."""
pass
class IdCreatedUpdatedBaseMixin(Base):
@sqlalchemy.ext.declarative.declared_attr
def uuid(cls):
return sqlalchemy.Column(
sqlalchemy.dialects.postgresql.UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
unique=True,
nullable=False,
)
@sqlalchemy.ext.declarative.declared_attr
def created_at(cls):
return sqlalchemy.Column(sqlalchemy.DateTime, server_default=sqlalchemy.sql.func.now())
@sqlalchemy.ext.declarative.declared_attr
def updated_at(cls):
return sqlalchemy.Column(sqlalchemy.DateTime, server_default=sqlalchemy.sql.func.now())
@sqlalchemy.ext.declarative.declared_attr.directive
def __tablename__(cls) -> str:
return cls.__name__.lower()

View File

@ -0,0 +1,10 @@
import pydantic
class Joke(pydantic.BaseModel):
"""Joke model."""
id_field: int = pydantic.Field(alias="id")
type_field: str = pydantic.Field(alias="type")
setup: str
punchline: str

View File

@ -0,0 +1,4 @@
from .base import Base, IdCreatedUpdatedBaseMixin
from .joke import JokeORM
__all__ = ["Base", "IdCreatedUpdatedBaseMixin", "JokeORM"]

View File

@ -0,0 +1,35 @@
import datetime
import uuid
import sqlalchemy
import sqlalchemy.dialects.postgresql
import sqlalchemy.ext.declarative
import sqlalchemy.orm as sa_orm
import sqlalchemy.sql as sa_sql
class Base(sa_orm.DeclarativeBase):
"""Base class for all models."""
@sqlalchemy.ext.declarative.declared_attr.directive
def __tablename__(cls):
return cls.__name__.lower()
__mapper_args__ = {"eager_defaults": True}
id: sa_orm.Mapped[uuid.UUID] = sa_orm.mapped_column(primary_key=True, default=uuid.uuid4)
class IdCreatedUpdatedBaseMixin:
# id: sa_orm.Mapped[int] = sa_orm.mapped_column(primary_key=True)
# id_field: sa_orm.Mapped[uuid.UUID] = sa_orm.mapped_column(name="uuid", primary_key=True, unique=True, default=uuid.uuid4, nullable=False)
created: sa_orm.Mapped[datetime.datetime] = sa_orm.mapped_column(server_default=sa_sql.func.now())
updated: sa_orm.Mapped[datetime.datetime] = sa_orm.mapped_column(
server_default=sa_sql.func.now(), onupdate=sa_sql.func.now()
)
# __mapper_args__ = {"eager_defaults": True}
# @sqlalchemy.ext.declarative.declared_attr.directive
# def __tablename__(cls) -> str:
# return cls.__name__.lower()

View File

@ -0,0 +1,13 @@
import sqlalchemy.orm as sa_orm
import lib.models.orm.base as base
Base = base.Base
class JokeORM(Base):
__tablename__ = "joke" # type: ignore
type_field: sa_orm.Mapped[str] = sa_orm.mapped_column(name="type", nullable=False)
setup: sa_orm.Mapped[str] = sa_orm.mapped_column()
punchline: sa_orm.Mapped[str] = sa_orm.mapped_column()

View File

@ -988,13 +988,13 @@ pytest = ">=4.6"
[[package]] [[package]]
name = "pyright" name = "pyright"
version = "1.1.327" version = "1.1.329"
description = "Command line wrapper for pyright" description = "Command line wrapper for pyright"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "pyright-1.1.327-py3-none-any.whl", hash = "sha256:3462cda239e9140276238bbdbd0b59d77406f1c2e14d8cb8c20c8e25639c6b3c"}, {file = "pyright-1.1.329-py3-none-any.whl", hash = "sha256:c16f88a7ac14ddd0513e62fec56d69c37e3c6b412161ad16aa23a9c7e3dabaf4"},
{file = "pyright-1.1.327.tar.gz", hash = "sha256:ba74148ad64f22020dbbed6781c4bdb38ecb8a7ca90dc3c87a4f08d1c0e11592"}, {file = "pyright-1.1.329.tar.gz", hash = "sha256:5baf82ff5ecb8c8b3ac400e8536348efbde0b94a09d83d5b440c0d143fd151a8"},
] ]
[package.dependencies] [package.dependencies]

View File

@ -129,6 +129,8 @@ ignore = [
"D415", "D415",
# Type-checkers interpret redundant `as` as exporting an item # Type-checkers interpret redundant `as` as exporting an item
"PLC0414", "PLC0414",
# Permit using alias for 'import'
"PLR0402",
# Causes churn and awful looking import blocks for little gain # Causes churn and awful looking import blocks for little gain
"TCH" "TCH"
] ]