Compare commits

..

No commits in common. "v0.3.5" and "v0.3.3" have entirely different histories.

14 changed files with 688 additions and 980 deletions

View file

@ -1 +1 @@
3.13
3.11

View file

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

View file

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

View file

@ -19,10 +19,10 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1747046372,
"lastModified": 1733328505,
"owner": "edolstra",
"repo": "flake-compat",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"type": "github"
},
"original": {
@ -46,31 +46,10 @@
"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": {
"inputs": {
"nixpkgs": [
"git-hooks",
"pre-commit-hooks",
"nixpkgs"
]
},
@ -104,7 +83,7 @@
},
"nixpkgs-python": {
"inputs": {
"flake-compat": "flake-compat_2",
"flake-compat": "flake-compat",
"nixpkgs": [
"nixpkgs"
]
@ -122,15 +101,33 @@
"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": {
"inputs": {
"devenv": "devenv",
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs",
"nixpkgs-python": "nixpkgs-python",
"pre-commit-hooks": [
"git-hooks"
]
"pre-commit-hooks": "pre-commit-hooks"
}
}
},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -151,7 +151,6 @@ class MessageType(StrEnum):
YES = "YES"
WHAT = "WHAT"
LAUGH = "LAUGH"
ANO_SUFFIX = "ANO_SUFFIX"
OTHER = "OTHER"
@ -169,8 +168,6 @@ class MessageHandler:
"keking",
"KEKW",
"OMEGADANCEBUTFAST",
"xdd",
"xdding",
]
self.send = channel_send_func
@ -178,17 +175,16 @@ class MessageHandler:
"""Determines the type of message based on its content"""
if content.startswith("!"):
return MessageType.COMMAND
if content in ["Yes", "yes"]:
if content == "Yes":
return MessageType.YES
if content.startswith("WHAT"):
return MessageType.WHAT
if content.endswith("ano") and len(content) > 3: # noqa: PLR2004
return MessageType.ANO_SUFFIX
if content in self.laugh_patterns:
return MessageType.LAUGH
return MessageType.OTHER
async def handle_laugh(self) -> str:
"""Handles laugh messages"""
return random.choice(self.laugh_patterns) # noqa: S311
@ -204,7 +200,6 @@ class SaveMessagesCog(commands.Cog):
MessageType.YES: self._create_typed_send("yes"),
MessageType.WHAT: self._create_typed_send("what"),
MessageType.LAUGH: self._create_typed_send("laugh"),
MessageType.ANO_SUFFIX: self._create_typed_send("ano_suffix"),
}
for func in self.send_functions.values():
@ -232,10 +227,13 @@ class SaveMessagesCog(commands.Cog):
if not message.author:
return
# Store reference to current message for send functions
self.current_message = message
# Store the 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)
response = None
@ -249,14 +247,11 @@ class SaveMessagesCog(commands.Cog):
response = "WHAT Ramon"
case MessageType.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:
return
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)

View file

@ -42,7 +42,9 @@ class IRepo(BaseModel, ABC, Generic[T]):
pass # pragma: no cover
@abstractmethod
async def list(self, offset: int = 0, limit: int = 10, auto_commit=True) -> list[T]:
async def list(
self, obj: T, offset: int = 0, limit: int = 10, auto_commit=True
) -> list[T]:
pass # pragma: no cover
@ -151,7 +153,7 @@ class UserRepo(IRepo[User]):
)
async def list( # type: ignore[empty-body]
self, offset: int = 0, limit: int = 10, auto_commit=True
self, obj: User, offset: int = 0, limit: int = 10, auto_commit=True
) -> list[User]:
pass # pragma: no cover
@ -172,8 +174,6 @@ class QuoteRepo(IRepo[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 (
self.get_client(auto_commit=auto_commit) as db,
await db.execute(
@ -196,81 +196,18 @@ class QuoteRepo(IRepo[Quote]):
return self._deserialize(data)
async def update(self, obj: Quote, auto_commit=True) -> Quote: # type: ignore[empty-body]
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)
pass # pragma: no cover
async def delete(self, obj: Quote, auto_commit=True):
async with self.get_client(auto_commit=auto_commit) as db:
await db.execute(
"""
DELETE FROM quotes WHERE id = ?
""",
(obj.id.hex,),
)
pass # pragma: no cover
async def get_by_id(self, obj_id: UUID, auto_commit=True) -> Quote | None: # type: ignore[empty-body]
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)
pass # pragma: no cover
async def list( # type: ignore[empty-body]
self, offset: int = 0, limit: int = 10, auto_commit=True
) -> list[Quote]:
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]
self, obj: T, offset: int = 0, limit: int = 10, auto_commit=True
) -> list[T]:
pass # pragma: no cover
async def get_random(self, channel_name: str, auto_commit=True) -> Quote | None:
async with (
@ -361,22 +298,11 @@ class ChatbotRepo(IRepo[Chatbot]):
data = await cursor.fetchone()
return self._deserialize(data)
async def delete(self, obj: Chatbot, auto_commit=True):
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 delete(self, obj: T, auto_commit=True):
pass # pragma: no cover
async def get_by_id(self, obj_id: UUID, auto_commit=True) -> Chatbot | None: # type: ignore[empty-body]
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)
pass # pragma: no cover
async def get_by_user_id(self, user_id: UUID) -> Chatbot | None:
async with self.get_client() as db:
@ -390,12 +316,6 @@ class ChatbotRepo(IRepo[Chatbot]):
return self._deserialize(result)
async def list( # type: ignore[empty-body]
self, offset: int = 0, limit: int = 10, auto_commit=True
) -> list[Chatbot]:
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]
self, obj: T, offset: int = 0, limit: int = 10, auto_commit=True
) -> list[T]:
pass # pragma: no cover

View file

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

View file

@ -88,79 +88,8 @@ async def test_update_chatbot_raises_value_error_on_non_existing_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):
quote = await quote_repo.get_random(persisted_quote.channel_name)
assert quote
assert quote.author == persisted_quote.author
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