Compare commits

...

6 commits

14 changed files with 980 additions and 688 deletions

View file

@ -1 +1 @@
3.11 3.13

View file

@ -1,6 +1,6 @@
apiVersion: v2 apiVersion: v2
appVersion: 0.3.3 appVersion: 0.3.5
description: A Helm chart for Kubernetes description: A Helm chart for Kubernetes
name: huesoporro name: huesoporro
type: application type: application
version: 0.3.3 version: 0.3.5

View file

@ -8,7 +8,7 @@ fullnameOverride: ''
image: image:
pullPolicy: Always pullPolicy: Always
repository: git.roboces.dev/catalin/huesoporro repository: git.roboces.dev/catalin/huesoporro
tag: 0.3.3 tag: 0.3.5
imagePullSecrets: [] imagePullSecrets: []
ingress: ingress:
annotations: {} annotations: {}

View file

@ -19,10 +19,10 @@
"flake-compat": { "flake-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1733328505, "lastModified": 1747046372,
"owner": "edolstra", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -46,10 +46,31 @@
"type": "github" "type": "github"
} }
}, },
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1747372754,
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": { "gitignore": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
"pre-commit-hooks", "git-hooks",
"nixpkgs" "nixpkgs"
] ]
}, },
@ -83,7 +104,7 @@
}, },
"nixpkgs-python": { "nixpkgs-python": {
"inputs": { "inputs": {
"flake-compat": "flake-compat", "flake-compat": "flake-compat_2",
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
] ]
@ -101,33 +122,15 @@
"type": "github" "type": "github"
} }
}, },
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat_2",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1737465171,
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "9364dc02281ce2d37a1f55b6e51f7c0f65a75f17",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"devenv": "devenv", "devenv": "devenv",
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"nixpkgs-python": "nixpkgs-python", "nixpkgs-python": "nixpkgs-python",
"pre-commit-hooks": "pre-commit-hooks" "pre-commit-hooks": [
"git-hooks"
]
} }
} }
}, },

View file

@ -1,6 +1,6 @@
[project] [project]
name = "huesoporro" name = "huesoporro"
version = "0.3.3" version = "0.3.5"
description = "Misc Twitch bot" description = "Misc Twitch bot"
readme = "README.md" readme = "README.md"
authors = [ authors = [
@ -24,6 +24,7 @@ dependencies = [
"pytz>=2024.2", "pytz>=2024.2",
"discord-py>=2.4.0", "discord-py>=2.4.0",
"tenacity>=9.0.0", "tenacity>=9.0.0",
"uvicorn>=0.34.0",
] ]
[project.scripts] [project.scripts]

6
renovate.json5 Normal file
View file

@ -0,0 +1,6 @@
{
$schema: "https://docs.renovatebot.com/renovate-schema.json",
lockFileMaintenance: {
enabled: true,
},
}

View file

@ -13,7 +13,7 @@ app = Typer()
@app.command() @app.command()
def import_vod_cc(channel_name: str, youtube_url: str, db_path: Path | None = None): def import_vod(channel_name: str, youtube_url: str, db_path: Path | None = None):
logger.info(f"Importing VOD closed captions for {channel_name} from {youtube_url}") logger.info(f"Importing VOD closed captions for {channel_name} from {youtube_url}")
s = Settings.get(db_filepath=db_path) s = Settings.get(db_filepath=db_path)
import_from_vod_action = ImportFromVODAction( import_from_vod_action = ImportFromVODAction(

View file

@ -7,6 +7,7 @@ from huesoporro.actions.chatbot.create_or_update_chatbot import (
from huesoporro.actions.chatbot.get_chatbot_by_user_id import GetChatbotByUserIdAction from huesoporro.actions.chatbot.get_chatbot_by_user_id import GetChatbotByUserIdAction
from huesoporro.actions.users.authenticate_user import AuthenticateUserAction from huesoporro.actions.users.authenticate_user import AuthenticateUserAction
from huesoporro.actions.users.get_user_by_jwt import GetUserByJWTAction from huesoporro.actions.users.get_user_by_jwt import GetUserByJWTAction
from huesoporro.bot import BotsManager
from huesoporro.infra.authenticator import TwitchAuthenticator from huesoporro.infra.authenticator import TwitchAuthenticator
from huesoporro.infra.repos import ChatbotRepo, UserRepo from huesoporro.infra.repos import ChatbotRepo, UserRepo
from huesoporro.libs.db import MarkovDatabase from huesoporro.libs.db import MarkovDatabase
@ -28,29 +29,29 @@ from huesoporro.svc.users_svcs import (
) )
def get_settings() -> Settings: async def get_settings() -> Settings:
return Settings.get() return Settings.get()
def get_authenticator(s: Settings) -> TwitchAuthenticator: async def get_authenticator(s: Settings) -> TwitchAuthenticator:
return TwitchAuthenticator(s=s) return TwitchAuthenticator(s=s)
def get_chatbot_repo(s: Settings): async def get_chatbot_repo(s: Settings):
return ChatbotRepo(s=s) return ChatbotRepo(s=s)
def get_get_chatbot_by_user_id_svc(chatbot_repo: ChatbotRepo): async def get_get_chatbot_by_user_id_svc(chatbot_repo: ChatbotRepo):
return GetChatbotByUserIdSvc(repo=chatbot_repo) return GetChatbotByUserIdSvc(repo=chatbot_repo)
def get_get_tokens_by_auth_code_svc( async def get_get_tokens_by_auth_code_svc(
twitch_authenticator: TwitchAuthenticator, s: Settings twitch_authenticator: TwitchAuthenticator, s: Settings
): ):
return GetTwitchAuthByAuthCodeSvc(s=s, authenticator=twitch_authenticator) return GetTwitchAuthByAuthCodeSvc(s=s, authenticator=twitch_authenticator)
def get_create_chatbot_svc(chatbot_repo: ChatbotRepo): async def get_create_chatbot_svc(chatbot_repo: ChatbotRepo):
return CreateChatbotSvc(repo=chatbot_repo) return CreateChatbotSvc(repo=chatbot_repo)
@ -58,19 +59,19 @@ async def get_user_repo(s: Settings):
return UserRepo(s=s) return UserRepo(s=s)
def get_create_user_svc(user_repo: UserRepo): async def get_create_user_svc(user_repo: UserRepo):
return CreateUserSvc(user_repo=user_repo) return CreateUserSvc(user_repo=user_repo)
def get_update_user_svc(user_repo: UserRepo): async def get_update_user_svc(user_repo: UserRepo):
return UpdateUserSvc(user_repo=user_repo) return UpdateUserSvc(user_repo=user_repo)
def get_refresh_token_svc(twitch_authenticator: TwitchAuthenticator): async def get_refresh_token_svc(twitch_authenticator: TwitchAuthenticator):
return RefreshTokenSvc(twitch_authenticator=twitch_authenticator) return RefreshTokenSvc(twitch_authenticator=twitch_authenticator)
def get_is_valid_token_svc(twitch_authenticator: TwitchAuthenticator): async def get_is_valid_token_svc(twitch_authenticator: TwitchAuthenticator):
return IsValidTokenSvc(authenticator=twitch_authenticator) return IsValidTokenSvc(authenticator=twitch_authenticator)
@ -118,11 +119,11 @@ async def get_sentences_storer_svc(db: MarkovDatabase):
return SentenceStorerSvc(db=db) return SentenceStorerSvc(db=db)
def get_update_chatbot_svc(chatbot_repo: ChatbotRepo): async def get_update_chatbot_svc(chatbot_repo: ChatbotRepo):
return UpdateChatbotSvc(repo=chatbot_repo) return UpdateChatbotSvc(repo=chatbot_repo)
def get_create_or_update_chatbot_action( async def get_create_or_update_chatbot_action(
create_chatbot_svc: CreateChatbotSvc, create_chatbot_svc: CreateChatbotSvc,
update_chatbot_svc: UpdateChatbotSvc, update_chatbot_svc: UpdateChatbotSvc,
get_chatbot_by_user_id_svc: GetChatbotByUserIdSvc, get_chatbot_by_user_id_svc: GetChatbotByUserIdSvc,
@ -134,7 +135,7 @@ def get_create_or_update_chatbot_action(
) )
def get_get_chatbot_by_user_id_action( async def get_get_chatbot_by_user_id_action(
get_chatbot_by_user_id_svc: GetChatbotByUserIdSvc, get_chatbot_by_user_id_svc: GetChatbotByUserIdSvc,
): ):
return GetChatbotByUserIdAction( return GetChatbotByUserIdAction(
@ -158,6 +159,10 @@ async def get_authenticate_action(
) )
async def get_bot_manager(s: Settings):
return BotsManager(s=s)
async def chatbot( async def chatbot(
get_chatbot_by_user_id_action: GetChatbotByUserIdAction, get_chatbot_by_user_id_action: GetChatbotByUserIdAction,
create_or_update_chatbot_action: CreateOrUpdateChatbotAction, create_or_update_chatbot_action: CreateOrUpdateChatbotAction,

View file

@ -11,6 +11,7 @@ from apps.httpapi.litestar.dependencies import (
authenticate, authenticate,
get_authenticate_action, get_authenticate_action,
get_authenticator, get_authenticator,
get_bot_manager,
get_chatbot_repo, get_chatbot_repo,
get_create_chatbot_svc, get_create_chatbot_svc,
get_create_or_update_chatbot_action, get_create_or_update_chatbot_action,
@ -43,12 +44,11 @@ from apps.httpapi.litestar.routes.api import (
save_bot_settings, save_bot_settings,
) )
from apps.httpapi.litestar.routes.auth import get_code, login from apps.httpapi.litestar.routes.auth import get_code, login
from huesoporro.bot import BotsManager
from huesoporro.settings import Settings from huesoporro.settings import Settings
@get("/healthz") @get("/healthz")
def get_health() -> dict: async def get_health() -> dict:
return {"status": "ok"} return {"status": "ok"}
@ -89,7 +89,7 @@ def create_app():
"s": Provide(get_settings, use_cache=True), "s": Provide(get_settings, use_cache=True),
"a": Provide(get_authenticator, use_cache=True), "a": Provide(get_authenticator, use_cache=True),
"user": Provide(authenticate), "user": Provide(authenticate),
"bm": Provide(BotsManager, use_cache=True), "bm": Provide(get_bot_manager, use_cache=True),
"sss": Provide(get_sentences_storer_svc), "sss": Provide(get_sentences_storer_svc),
"twitch_authenticator": Provide(get_authenticator), "twitch_authenticator": Provide(get_authenticator),
"authenticate_action": Provide(get_authenticate_action), "authenticate_action": Provide(get_authenticate_action),
@ -115,9 +115,8 @@ def create_app():
app = create_app() app = create_app()
if __name__ == "__main__": if __name__ == "__main__":
s = Settings.get() s = Settings.get()
config = uvicorn.Config("main:app", host=s.port, port=s.port, log_level="info") config = uvicorn.Config("main:app", host=s.host, port=s.port, log_level="info")
server = uvicorn.Server(config) server = uvicorn.Server(config)
server.run() server.run()

View file

@ -151,6 +151,7 @@ class MessageType(StrEnum):
YES = "YES" YES = "YES"
WHAT = "WHAT" WHAT = "WHAT"
LAUGH = "LAUGH" LAUGH = "LAUGH"
ANO_SUFFIX = "ANO_SUFFIX"
OTHER = "OTHER" OTHER = "OTHER"
@ -168,6 +169,8 @@ class MessageHandler:
"keking", "keking",
"KEKW", "KEKW",
"OMEGADANCEBUTFAST", "OMEGADANCEBUTFAST",
"xdd",
"xdding",
] ]
self.send = channel_send_func self.send = channel_send_func
@ -175,16 +178,17 @@ class MessageHandler:
"""Determines the type of message based on its content""" """Determines the type of message based on its content"""
if content.startswith("!"): if content.startswith("!"):
return MessageType.COMMAND return MessageType.COMMAND
if content == "Yes": if content in ["Yes", "yes"]:
return MessageType.YES return MessageType.YES
if content.startswith("WHAT"): if content.startswith("WHAT"):
return MessageType.WHAT return MessageType.WHAT
if content.endswith("ano") and len(content) > 3: # noqa: PLR2004
return MessageType.ANO_SUFFIX
if content in self.laugh_patterns: if content in self.laugh_patterns:
return MessageType.LAUGH return MessageType.LAUGH
return MessageType.OTHER return MessageType.OTHER
async def handle_laugh(self) -> str: async def handle_laugh(self) -> str:
"""Handles laugh messages"""
return random.choice(self.laugh_patterns) # noqa: S311 return random.choice(self.laugh_patterns) # noqa: S311
@ -200,6 +204,7 @@ class SaveMessagesCog(commands.Cog):
MessageType.YES: self._create_typed_send("yes"), MessageType.YES: self._create_typed_send("yes"),
MessageType.WHAT: self._create_typed_send("what"), MessageType.WHAT: self._create_typed_send("what"),
MessageType.LAUGH: self._create_typed_send("laugh"), MessageType.LAUGH: self._create_typed_send("laugh"),
MessageType.ANO_SUFFIX: self._create_typed_send("ano_suffix"),
} }
for func in self.send_functions.values(): for func in self.send_functions.values():
@ -227,13 +232,10 @@ class SaveMessagesCog(commands.Cog):
if not message.author: if not message.author:
return return
# Store reference to current message for send functions
self.current_message = message self.current_message = message
# Store the message content
await self.store_svc.run(message.content) await self.store_svc.run(message.content)
# Determine message type and handle accordingly
msg_type = self.message_handler.get_message_type(message.content) msg_type = self.message_handler.get_message_type(message.content)
response = None response = None
@ -247,11 +249,14 @@ class SaveMessagesCog(commands.Cog):
response = "WHAT Ramon" response = "WHAT Ramon"
case MessageType.LAUGH: case MessageType.LAUGH:
response = await self.message_handler.handle_laugh() response = await self.message_handler.handle_laugh()
case MessageType.ANO_SUFFIX:
response = (
f"@{message.author.name} me la agarras con la mano. venga, tira"
)
case MessageType.OTHER: case MessageType.OTHER:
return return
if response and msg_type in self.send_functions: if response and msg_type in self.send_functions:
# Use the type-specific send function
await self.backoff_svc.call_async(self.send_functions[msg_type], response) await self.backoff_svc.call_async(self.send_functions[msg_type], response)

View file

@ -42,9 +42,7 @@ class IRepo(BaseModel, ABC, Generic[T]):
pass # pragma: no cover pass # pragma: no cover
@abstractmethod @abstractmethod
async def list( async def list(self, offset: int = 0, limit: int = 10, auto_commit=True) -> list[T]:
self, obj: T, offset: int = 0, limit: int = 10, auto_commit=True
) -> list[T]:
pass # pragma: no cover pass # pragma: no cover
@ -153,7 +151,7 @@ class UserRepo(IRepo[User]):
) )
async def list( # type: ignore[empty-body] async def list( # type: ignore[empty-body]
self, obj: User, offset: int = 0, limit: int = 10, auto_commit=True self, offset: int = 0, limit: int = 10, auto_commit=True
) -> list[User]: ) -> list[User]:
pass # pragma: no cover pass # pragma: no cover
@ -174,6 +172,8 @@ class QuoteRepo(IRepo[Quote]):
) )
async def create(self, obj: Quote, auto_commit=True) -> Quote: async def create(self, obj: Quote, auto_commit=True) -> Quote:
if await self.get_by_quote(obj.quote):
raise ValueError(f"Quote {obj.quote} already exists")
async with ( async with (
self.get_client(auto_commit=auto_commit) as db, self.get_client(auto_commit=auto_commit) as db,
await db.execute( await db.execute(
@ -196,18 +196,81 @@ class QuoteRepo(IRepo[Quote]):
return self._deserialize(data) return self._deserialize(data)
async def update(self, obj: Quote, auto_commit=True) -> Quote: # type: ignore[empty-body] async def update(self, obj: Quote, auto_commit=True) -> Quote: # type: ignore[empty-body]
pass # pragma: no cover if not await self.get_by_id(obj.id):
raise ValueError(f"Quote {obj.id} does not exist")
async with (
self.get_client(auto_commit=auto_commit) as db,
await db.execute(
"""
UPDATE quotes
SET quote = ?,
author = ?,
channel = ?,
last_updated_at = ?
WHERE id = ?
RETURNING *
""",
(
obj.quote,
obj.author,
obj.channel_name,
utils.get_utc_now(),
obj.id.hex,
),
) as cursor,
):
data = await cursor.fetchone()
return self._deserialize(data)
async def delete(self, obj: Quote, auto_commit=True): async def delete(self, obj: Quote, auto_commit=True):
pass # pragma: no cover async with self.get_client(auto_commit=auto_commit) as db:
await db.execute(
"""
DELETE FROM quotes WHERE id = ?
""",
(obj.id.hex,),
)
async def get_by_id(self, obj_id: UUID, auto_commit=True) -> Quote | None: # type: ignore[empty-body] async def get_by_id(self, obj_id: UUID, auto_commit=True) -> Quote | None: # type: ignore[empty-body]
pass # pragma: no cover async with (
self.get_client(auto_commit=auto_commit) as db,
db.execute(
"""
SELECT * FROM quotes WHERE id = ?
""",
(obj_id.hex,),
) as cursor,
):
data = await cursor.fetchone()
if not data:
return None
return self._deserialize(data)
async def get_by_quote(self, quote: str, auto_commit=True) -> Quote | None:
async with (
self.get_client(auto_commit=auto_commit) as db,
db.execute(
"""
SELECT * FROM quotes WHERE quote = ?
""",
(quote,),
) as cursor,
):
data = await cursor.fetchone()
if not data:
return None
return self._deserialize(data)
async def list( # type: ignore[empty-body] async def list( # type: ignore[empty-body]
self, obj: T, offset: int = 0, limit: int = 10, auto_commit=True self, offset: int = 0, limit: int = 10, auto_commit=True
) -> list[T]: ) -> list[Quote]:
pass # pragma: no cover async with self.get_client() as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"SELECT * FROM quotes LIMIT ? OFFSET ?", (limit, offset)
) as cursor:
results = await cursor.fetchall()
return [self._deserialize(result) for result in results]
async def get_random(self, channel_name: str, auto_commit=True) -> Quote | None: async def get_random(self, channel_name: str, auto_commit=True) -> Quote | None:
async with ( async with (
@ -298,11 +361,22 @@ class ChatbotRepo(IRepo[Chatbot]):
data = await cursor.fetchone() data = await cursor.fetchone()
return self._deserialize(data) return self._deserialize(data)
async def delete(self, obj: T, auto_commit=True): async def delete(self, obj: Chatbot, auto_commit=True):
pass # pragma: no cover if not await self.get_by_id(obj.id):
raise ValueError(f"Chatbot {obj.id} does not exist")
async with self.get_client() as db:
await db.execute("DELETE FROM chatbot WHERE id = ?", (obj.id.hex,))
async def get_by_id(self, obj_id: UUID, auto_commit=True) -> Chatbot | None: # type: ignore[empty-body] async def get_by_id(self, obj_id: UUID, auto_commit=True) -> Chatbot | None: # type: ignore[empty-body]
pass # pragma: no cover async with self.get_client() as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"SELECT * FROM chatbot WHERE id = ?", (obj_id.hex,)
) as cursor:
result = await cursor.fetchone()
if not result:
return None
return self._deserialize(result)
async def get_by_user_id(self, user_id: UUID) -> Chatbot | None: async def get_by_user_id(self, user_id: UUID) -> Chatbot | None:
async with self.get_client() as db: async with self.get_client() as db:
@ -316,6 +390,12 @@ class ChatbotRepo(IRepo[Chatbot]):
return self._deserialize(result) return self._deserialize(result)
async def list( # type: ignore[empty-body] async def list( # type: ignore[empty-body]
self, obj: T, offset: int = 0, limit: int = 10, auto_commit=True self, offset: int = 0, limit: int = 10, auto_commit=True
) -> list[T]: ) -> list[Chatbot]:
pass # pragma: no cover async with self.get_client() as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"SELECT * FROM chatbot LIMIT ? OFFSET ?", (limit, offset)
) as cursor:
results = await cursor.fetchall()
return [self._deserialize(result) for result in results]

View file

@ -65,7 +65,7 @@ def db(s, cdb):
pass pass
async def list( # type: ignore[empty-body] async def list( # type: ignore[empty-body]
self, obj: BaseModel, offset: int = 0, limit: int = 10, auto_commit=True self, offset: int = 0, limit: int = 10, auto_commit=True
) -> list[BaseModel]: ) -> list[BaseModel]:
pass pass
@ -169,6 +169,16 @@ async def chatbot(chatbot_factory, user):
return chatbot_factory.build(user_id=user.id) return chatbot_factory.build(user_id=user.id)
@pytest.fixture
async def five_chatbots(chatbot_factory, user):
return [chatbot_factory.build() for _ in range(5)]
@pytest.fixture
async def persisted_five_chatbots(five_chatbots, chatbot_repo):
return [await chatbot_repo.create(chatbot) for chat in five_chatbots]
@pytest.fixture @pytest.fixture
async def persisted_chatbot(chatbot_repo, chatbot, persisted_user): async def persisted_chatbot(chatbot_repo, chatbot, persisted_user):
return await chatbot_repo.create(chatbot) return await chatbot_repo.create(chatbot)
@ -204,11 +214,21 @@ async def quote(quote_factory):
return quote_factory.build() return quote_factory.build()
@pytest.fixture
async def five_quotes(quote_factory):
return [quote_factory.build() for _ in range(5)]
@pytest.fixture @pytest.fixture
async def persisted_quote(quote_repo, quote): async def persisted_quote(quote_repo, quote):
return await quote_repo.create(quote) return await quote_repo.create(quote)
@pytest.fixture
async def persisted_five_quotes(five_quotes, quote_repo):
return [await quote_repo.create(quote) for quote in five_quotes]
@pytest.fixture @pytest.fixture
async def create_quote_svc(quote_repo): async def create_quote_svc(quote_repo):
return CreateQuoteSvc(repo=quote_repo) return CreateQuoteSvc(repo=quote_repo)

View file

@ -88,8 +88,79 @@ async def test_update_chatbot_raises_value_error_on_non_existing_chatbot(
await chatbot_repo.update(chatbot) await chatbot_repo.update(chatbot)
async def test_delete_chatbot_raises_value_error_on_non_existing_chatbot(
chatbot_repo, chatbot
):
with pytest.raises(ValueError, match=f"Chatbot {chatbot.id} does not exist"):
await chatbot_repo.delete(chatbot)
async def test_delete_chatbot(chatbot_repo, persisted_chatbot):
assert await chatbot_repo.delete(persisted_chatbot) is None
assert await chatbot_repo.get_by_id(persisted_chatbot.id) is None
async def test_get_by_id(chatbot_repo, persisted_chatbot):
chatbot = await chatbot_repo.get_by_id(persisted_chatbot.id)
assert chatbot == persisted_chatbot
async def test_list(chatbot_repo, persisted_five_chatbots):
chatbots = await chatbot_repo.list()
assert len(chatbots) == 5 # noqa: PLR2004
async def test_list_offset_limit(chatbot_repo, persisted_five_chatbots):
chatbots = await chatbot_repo.list(offset=1, limit=2)
assert len(chatbots) == 2 # noqa: PLR2004
async def test_get_random_quote(quote_repo: QuoteRepo, persisted_quote): async def test_get_random_quote(quote_repo: QuoteRepo, persisted_quote):
quote = await quote_repo.get_random(persisted_quote.channel_name) quote = await quote_repo.get_random(persisted_quote.channel_name)
assert quote assert quote
assert quote.author == persisted_quote.author assert quote.author == persisted_quote.author
assert quote.channel_name == persisted_quote.channel_name assert quote.channel_name == persisted_quote.channel_name
async def test_create_quote_raises_value_error_for_existing_quote(
quote_repo: QuoteRepo, persisted_quote
):
with pytest.raises(
ValueError, match=f"Quote {persisted_quote.quote} already exists"
):
await quote_repo.create(persisted_quote)
async def test_create_quote(quote_repo: QuoteRepo, quote_factory):
quote = quote_factory.build()
created_quote = await quote_repo.create(quote)
assert created_quote == quote
async def test_update_quote_raises_value_error_on_non_existing_quote(
quote_repo: QuoteRepo, quote
):
with pytest.raises(ValueError, match=f"Quote {quote.id} does not exist"):
await quote_repo.update(quote)
async def test_update_quote(quote_repo: QuoteRepo, persisted_quote):
persisted_quote.quote = "new quote"
updated_quote = await quote_repo.update(persisted_quote)
persisted_quote.last_updated_at = updated_quote.last_updated_at
assert updated_quote == persisted_quote
async def test_delete_quote(quote_repo: QuoteRepo, persisted_quote):
assert await quote_repo.delete(persisted_quote) is None
assert await quote_repo.get_by_id(persisted_quote.id) is None
async def test_list_quotes(quote_repo, persisted_five_quotes):
quotes = await quote_repo.list()
assert len(quotes) == 5 # noqa: PLR2004
async def test_list_quotes_offset_limit(quote_repo, persisted_five_quotes):
quotes = await quote_repo.list(offset=1, limit=2)
assert len(quotes) == 2 # noqa: PLR2004

1296
uv.lock generated

File diff suppressed because it is too large Load diff