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:
commit
eab9177c00
43
package-lock.json
generated
43
package-lock.json
generated
|
@ -11,7 +11,7 @@
|
|||
"husky": "^8.0.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"prettier": "2.8.3",
|
||||
"pyright": "^1.1.299"
|
||||
"pyright": "^1.1.329"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
|
@ -1063,6 +1063,20 @@
|
|||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
|
@ -2215,9 +2229,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/pyright": {
|
||||
"version": "1.1.299",
|
||||
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.299.tgz",
|
||||
"integrity": "sha512-37agqu0oNRsftFHNyGtaN3SBJJ+Qe2pembdgIpif9iL0VNqCU7j3wauSm8j0peXg1uNHFcS8UMrft4hT7w2XvQ==",
|
||||
"version": "1.1.329",
|
||||
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.329.tgz",
|
||||
"integrity": "sha512-5AT98Mi0OYcDiQ5lD1nPJ3cq8gX/HHaXrQ5WjJ/QZkaJtGqnEdrUp5Gq5wBPipWgOnv/l5e50YScaaNDMjoy9Q==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"pyright": "index.js",
|
||||
|
@ -2225,6 +2239,9 @@
|
|||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/q": {
|
||||
|
@ -3883,6 +3900,13 @@
|
|||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
|
@ -4702,10 +4726,13 @@
|
|||
"dev": true
|
||||
},
|
||||
"pyright": {
|
||||
"version": "1.1.299",
|
||||
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.299.tgz",
|
||||
"integrity": "sha512-37agqu0oNRsftFHNyGtaN3SBJJ+Qe2pembdgIpif9iL0VNqCU7j3wauSm8j0peXg1uNHFcS8UMrft4hT7w2XvQ==",
|
||||
"dev": true
|
||||
"version": "1.1.329",
|
||||
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.329.tgz",
|
||||
"integrity": "sha512-5AT98Mi0OYcDiQ5lD1nPJ3cq8gX/HHaXrQ5WjJ/QZkaJtGqnEdrUp5Gq5wBPipWgOnv/l5e50YScaaNDMjoy9Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"q": {
|
||||
"version": "1.5.1",
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"husky": "^8.0.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"prettier": "2.8.3",
|
||||
"pyright": "^1.1.299"
|
||||
"pyright": "^1.1.329"
|
||||
},
|
||||
"hooks": {
|
||||
"commit-msg": "commitlint"
|
||||
|
|
|
@ -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 ###
|
|
@ -1,5 +1,4 @@
|
|||
from .health import *
|
||||
from .health import basic_router
|
||||
from .joke import JokeHandler
|
||||
|
||||
__all__ = [
|
||||
"basic_router",
|
||||
]
|
||||
__all__ = ["JokeHandler", "basic_router"]
|
||||
|
|
25
src/fastapi_app/lib/api/v1/handlers/joke.py
Normal file
25
src/fastapi_app/lib/api/v1/handlers/joke.py
Normal 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")
|
|
@ -1,5 +1,4 @@
|
|||
from .base import HealthResponseModel
|
||||
from .joke import JokeResponse
|
||||
|
||||
__all__ = [
|
||||
"HealthResponseModel",
|
||||
]
|
||||
__all__ = ["HealthResponseModel", "JokeResponse"]
|
||||
|
|
7
src/fastapi_app/lib/api/v1/schemas/joke.py
Normal file
7
src/fastapi_app/lib/api/v1/schemas/joke.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
import pydantic
|
||||
|
||||
|
||||
class JokeResponse(pydantic.BaseModel):
|
||||
id_field: int = pydantic.Field(alias="id")
|
||||
joke: str
|
||||
category: str
|
|
@ -11,6 +11,8 @@ import lib.app.errors as app_errors
|
|||
import lib.app.settings as app_settings
|
||||
import lib.app.split_settings as app_split_settings
|
||||
import lib.clients as clients
|
||||
import lib.joke.repository as joke_repository
|
||||
import lib.joke.services as joke_services
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -45,7 +47,6 @@ class Application:
|
|||
|
||||
logger.info("Initializing global clients")
|
||||
postgres_client = clients.AsyncPostgresClient(settings=settings)
|
||||
http_client = clients.get_async_http_session()
|
||||
|
||||
disposable_resources.append(
|
||||
DisposableResource(
|
||||
|
@ -61,6 +62,7 @@ class Application:
|
|||
# Repositories
|
||||
|
||||
logger.info("Initializing repositories")
|
||||
jk_repository = joke_repository.JokeRepository(async_session=postgres_client.get_async_session())
|
||||
|
||||
# Caches
|
||||
|
||||
|
@ -69,12 +71,13 @@ class Application:
|
|||
# Services
|
||||
|
||||
logger.info("Initializing services")
|
||||
jk_serivces = joke_services.JokeService(jk_repository)
|
||||
|
||||
# Handlers
|
||||
|
||||
logger.info("Initializing handlers")
|
||||
liveness_probe_handler = api_v1_handlers.basic_router
|
||||
|
||||
joke_handler = api_v1_handlers.JokeHandler(joke_service=jk_serivces).router
|
||||
|
||||
logger.info("Creating application")
|
||||
|
||||
|
@ -88,6 +91,7 @@ class Application:
|
|||
|
||||
# Routes
|
||||
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(
|
||||
settings=settings,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from .httpx import get_async_http_session
|
||||
from .postgres import AsyncPostgresClient
|
||||
|
||||
__all__ = ["AsyncPostgresClient", "get_async_http_session"]
|
||||
__all__ = ["AsyncPostgresClient"]
|
||||
|
|
|
@ -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
|
4
src/fastapi_app/lib/joke/__init__.py
Normal file
4
src/fastapi_app/lib/joke/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from .repository import JokeRepository
|
||||
from .services import JokeService
|
||||
|
||||
__all__ = ["JokeRepository", "JokeService"]
|
30
src/fastapi_app/lib/joke/repository.py
Normal file
30
src/fastapi_app/lib/joke/repository.py
Normal 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)
|
30
src/fastapi_app/lib/joke/services.py
Normal file
30
src/fastapi_app/lib/joke/services.py
Normal 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)
|
|
@ -1,4 +1,5 @@
|
|||
from .base_sqlalchemy import Base, IdCreatedUpdatedBaseMixin
|
||||
from .joke import Joke
|
||||
from .orm import Base, IdCreatedUpdatedBaseMixin, JokeORM
|
||||
from .token import Token
|
||||
|
||||
__all__ = ["Base", "IdCreatedUpdatedBaseMixin", "Token"]
|
||||
__all__ = ["Base", "IdCreatedUpdatedBaseMixin", "Joke", "JokeORM", "Token"]
|
||||
|
|
|
@ -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()
|
10
src/fastapi_app/lib/models/joke.py
Normal file
10
src/fastapi_app/lib/models/joke.py
Normal 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
|
4
src/fastapi_app/lib/models/orm/__init__.py
Normal file
4
src/fastapi_app/lib/models/orm/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from .base import Base, IdCreatedUpdatedBaseMixin
|
||||
from .joke import JokeORM
|
||||
|
||||
__all__ = ["Base", "IdCreatedUpdatedBaseMixin", "JokeORM"]
|
35
src/fastapi_app/lib/models/orm/base.py
Normal file
35
src/fastapi_app/lib/models/orm/base.py
Normal 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()
|
13
src/fastapi_app/lib/models/orm/joke.py
Normal file
13
src/fastapi_app/lib/models/orm/joke.py
Normal 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()
|
6
src/fastapi_app/poetry.lock
generated
6
src/fastapi_app/poetry.lock
generated
|
@ -988,13 +988,13 @@ pytest = ">=4.6"
|
|||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.327"
|
||||
version = "1.1.329"
|
||||
description = "Command line wrapper for pyright"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pyright-1.1.327-py3-none-any.whl", hash = "sha256:3462cda239e9140276238bbdbd0b59d77406f1c2e14d8cb8c20c8e25639c6b3c"},
|
||||
{file = "pyright-1.1.327.tar.gz", hash = "sha256:ba74148ad64f22020dbbed6781c4bdb38ecb8a7ca90dc3c87a4f08d1c0e11592"},
|
||||
{file = "pyright-1.1.329-py3-none-any.whl", hash = "sha256:c16f88a7ac14ddd0513e62fec56d69c37e3c6b412161ad16aa23a9c7e3dabaf4"},
|
||||
{file = "pyright-1.1.329.tar.gz", hash = "sha256:5baf82ff5ecb8c8b3ac400e8536348efbde0b94a09d83d5b440c0d143fd151a8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
|
@ -129,6 +129,8 @@ ignore = [
|
|||
"D415",
|
||||
# Type-checkers interpret redundant `as` as exporting an item
|
||||
"PLC0414",
|
||||
# Permit using alias for 'import'
|
||||
"PLR0402",
|
||||
# Causes churn and awful looking import blocks for little gain
|
||||
"TCH"
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue
Block a user