Skip to content

Commit

Permalink
docs: enforce required docstrings
Browse files Browse the repository at this point in the history
  • Loading branch information
MikaelSiidorow committed Apr 17, 2023
1 parent 66a735c commit 9cd740f
Show file tree
Hide file tree
Showing 9 changed files with 78 additions and 4 deletions.
6 changes: 6 additions & 0 deletions kipubot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Kipubot - A Telegram bot for graphing friday raffles."""

import logging
import os

Expand All @@ -11,11 +13,15 @@

# ENV CONFIG
class Settings(BaseSettings):
"""Configuration for the bot."""

BOT_TOKEN: str
DATABASE_URL: str
DEVELOPER_CHAT_ID: str | None = None

class Config:
"""Environment variables to load from."""

env_file = ".env"
env_file_encoding = "utf-8"

Expand Down
1 change: 1 addition & 0 deletions kipubot/__main__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Entrypoint for the kipubot package."""
from kipubot.bot import main

main()
3 changes: 3 additions & 0 deletions kipubot/bot.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/usr/bin/env python3

"""Main kipubot file for running the bot."""

from telegram.ext import ApplicationBuilder, PicklePersistence

from kipubot import config
Expand All @@ -17,6 +19,7 @@


def main() -> None:
"""Run the bot with all handlers."""
persistence = PicklePersistence(filepath="data/.pkl")
app = ApplicationBuilder().token(config.BOT_TOKEN).persistence(persistence).build()

Expand Down
7 changes: 6 additions & 1 deletion kipubot/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# CONSTANTS
"""Constants used in the bot.
- EXCEL_MIME: The MIME type of the Excel file sent by MobilePay.
- STRINGS: A dictionary of strings used in the bot.
"""

EXCEL_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"

STRINGS = {
Expand Down
24 changes: 23 additions & 1 deletion kipubot/db.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Database connection and queries for Kipubot."""

import logging
from contextlib import contextmanager, suppress

Expand All @@ -17,7 +19,7 @@

@contextmanager
def logging_connection():
"""Log errors while connecting to the database"""
"""Log errors while connecting to the database."""
try:
_logger.info("Getting db connection...")
with _pool.connection() as conn:
Expand Down Expand Up @@ -69,6 +71,7 @@ def logging_connection():


def get_registered_member_ids(chat_id: int) -> list[int]:
"""Get a list of all registered user IDs in a chat."""
with logging_connection() as conn:
return [
row[0]
Expand All @@ -82,6 +85,7 @@ def get_registered_member_ids(chat_id: int) -> list[int]:


def get_admin_ids(chat_id: int) -> list[int]:
"""Get a list of all admin IDs in a chat."""
with logging_connection() as conn:
data = conn.execute(
"SELECT admins FROM chat WHERE chat_id = %s", (chat_id,)
Expand All @@ -90,6 +94,7 @@ def get_admin_ids(chat_id: int) -> list[int]:


def get_prev_winner_ids(chat_id: int) -> list[int]:
"""Get a list of all previous winner IDs in a chat."""
with logging_connection() as conn:
data = conn.execute(
"SELECT prev_winners FROM chat WHERE chat_id = %s", (chat_id,)
Expand All @@ -98,6 +103,7 @@ def get_prev_winner_ids(chat_id: int) -> list[int]:


def get_winner_id(chat_id: int) -> int:
"""Get the current winner ID in a chat."""
with logging_connection() as conn:
data = conn.execute(
"SELECT cur_winner FROM chat WHERE chat_id = %s", (chat_id,)
Expand All @@ -106,6 +112,7 @@ def get_winner_id(chat_id: int) -> int:


def get_chats_where_winner(user_id: int) -> list[tuple[int, str]]:
"""Get a list of all chats where a user is the current winner."""
with logging_connection() as conn:
return conn.execute(
"""SELECT c.chat_id, c.title
Expand All @@ -120,6 +127,7 @@ def get_chats_where_winner(user_id: int) -> list[tuple[int, str]]:
def get_raffle_data(
chat_id: int,
) -> tuple[int, Timestamp, Timestamp, int, list[Timestamp], list[str], list[int]]:
"""Get the raffle data for a chat."""
with logging_connection() as conn:
return conn.execute(
"SELECT * FROM raffle WHERE chat_id = %s", [chat_id]
Expand All @@ -133,6 +141,7 @@ def save_raffle_data(
entry_fee: int,
df: DataFrame,
) -> None:
"""Save the raffle data for a chat."""
dates = df["date"].tolist()
entries = df["name"].tolist()
amounts = df["amount"].tolist()
Expand All @@ -156,6 +165,7 @@ def save_raffle_data(


def save_user_or_ignore(user_id: int) -> None:
"""Save a user to the database, or ignore if already exists."""
with logging_connection() as conn:
conn.execute(
"""INSERT INTO chat_user
Expand All @@ -167,6 +177,7 @@ def save_user_or_ignore(user_id: int) -> None:


def save_chat_or_ignore(chat_id: int, title: str, admin_ids: list[int]) -> None:
"""Save a chat to the database, or ignore if already exists."""
with logging_connection() as conn:
conn.execute(
"""INSERT INTO chat (chat_id, title, admins)
Expand All @@ -178,6 +189,7 @@ def save_chat_or_ignore(chat_id: int, title: str, admin_ids: list[int]) -> None:


def register_user(chat_id: int, user_id: int) -> None:
"""Register a user in a chat."""
save_user_or_ignore(user_id)

with logging_connection() as conn:
Expand All @@ -195,11 +207,16 @@ def register_user(chat_id: int, user_id: int) -> None:


def register_user_or_ignore(chat_id: int, user_id: int) -> None:
"""Register a user in a chat, or ignore if already registered."""
with suppress(AlreadyRegisteredError):
register_user(chat_id, user_id)


def admin_cycle_winners(winner_id: int, chat_id: int) -> None:
"""Admin set current winner to a specific user.
Move the previous winner to the list of previous winners.
"""
with logging_connection() as conn:
conn.execute(
"""UPDATE chat
Expand All @@ -212,6 +229,7 @@ def admin_cycle_winners(winner_id: int, chat_id: int) -> None:


def replace_cur_winner(winner_id: int, chat_id: int) -> None:
"""Replace the current winner in a chat."""
with logging_connection() as conn:
conn.execute(
"""UPDATE chat
Expand All @@ -222,6 +240,10 @@ def replace_cur_winner(winner_id: int, chat_id: int) -> None:


def cycle_winners(user_id: int, winner_id: int, chat_id: int) -> None:
"""Set the current winner in a chat.
Move the previous winner to the list of previous winners.
"""
with logging_connection() as conn:
conn.execute(
"""UPDATE chat
Expand Down
9 changes: 9 additions & 0 deletions kipubot/errors.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
"""Custom exceptions for Kipubot."""


class NoEntriesError(Exception):
"""Raised when there are no entries in a raffle."""

pass


class NoRaffleError(Exception):
"""Raised when there is no raffle in a chat."""

pass


class AlreadyRegisteredError(Exception):
"""Raised when a user tries to register twice."""

pass
2 changes: 2 additions & 0 deletions kipubot/handlers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Handlers for Kipubot."""

__all__ = (
"start_handler",
"moro_handler",
Expand Down
27 changes: 26 additions & 1 deletion kipubot/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Utility functions for Kipubot."""

import os
import re
from typing import NamedTuple
Expand All @@ -20,19 +22,24 @@


class RaffleStatsData(NamedTuple):
"""Data for raffle stats without entry data."""

start_date: pd.Timestamp
end_date: pd.Timestamp
entry_fee: int


class RaffleData(NamedTuple):
"""Data for raffle stats with entry data."""

start_date: pd.Timestamp
end_date: pd.Timestamp
entry_fee: int
df: pd.DataFrame


def is_int(x: str) -> bool:
"""Safely check if a string is an integer."""
try:
int(x)
except ValueError:
Expand All @@ -42,6 +49,7 @@ def is_int(x: str) -> bool:


def is_float(x: str) -> bool:
"""Safely check if a string is a float."""
try:
float(x)
except ValueError:
Expand All @@ -51,6 +59,7 @@ def is_float(x: str) -> bool:


def int_price_to_str(num: int) -> str:
"""Format an integer price to a string."""
float_num = num / 100.0

str_num: str = (
Expand All @@ -69,6 +78,7 @@ def int_price_to_str(num: int) -> str:


async def get_chat_member_opt(chat: Chat, member_id: int) -> ChatMember | None:
"""Get a chat member, or None if the user is not in the chat."""
try:
return await chat.get_member(member_id)
except BadRequest as e:
Expand All @@ -78,6 +88,7 @@ async def get_chat_member_opt(chat: Chat, member_id: int) -> ChatMember | None:


def preband(x, xd, yd, p, func):
"""Calculate the prediction band for a curve fit."""
conf = 0.95
alpha = 1.0 - conf
quantile = stats.t.ppf(1.0 - alpha / 2.0, xd.size - len(p))
Expand All @@ -95,6 +106,7 @@ def preband(x, xd, yd, p, func):


def fit_timedata(x_series, y_series):
"""Fit a curve to the data."""
# ignore the end date in curve fitting
x = x_series.values[:-1]
y = y_series.values[:-1]
Expand Down Expand Up @@ -126,6 +138,7 @@ def f(x, slope, intercept):


def remove_emojis(text: str) -> str:
"""Remove emojis from a string."""
emojis = re.compile(
pattern="["
"\U0001F600-\U0001F64F" # emoticons
Expand All @@ -139,6 +152,7 @@ def remove_emojis(text: str) -> str:


def validate_excel(excel_path: str) -> bool:
"""Validate that the submitted excel file is in the correct (MP) format."""
df = pd.read_excel(
excel_path,
usecols="A,B,D",
Expand All @@ -157,6 +171,7 @@ def validate_excel(excel_path: str) -> bool:
def read_excel_to_df(
excel_path: str, start_date: pd.Timestamp, end_date: pd.Timestamp
) -> pd.DataFrame:
"""Read the excel file to a dataframe."""
df = pd.read_excel(
excel_path,
usecols="A,B,D",
Expand All @@ -172,18 +187,20 @@ def read_excel_to_df(


def get_raffle_stats(chat_id: int) -> RaffleStatsData:
"""Get the stats of a raffle, not including dataframe."""
query_result = db.get_raffle_data(chat_id)

if query_result is None:
error_text = f"No raffle found for chat {chat_id}"
raise NoRaffleError(error_text)

_, start_date, end_date, entry_fee, dates, entries, amounts = query_result
_, start_date, end_date, entry_fee, _, _, _ = query_result

return RaffleStatsData(start_date, end_date, entry_fee)


def get_raffle(chat_id: int) -> RaffleData:
"""Get the data of a raffle, including dataframe."""
query_result = db.get_raffle_data(chat_id)

if query_result is None:
Expand All @@ -198,6 +215,7 @@ def get_raffle(chat_id: int) -> RaffleData:


def get_cur_time_hel() -> pd.Timestamp:
"""Get the current time in Helsinki as a Timestamp."""
# take current time in helsinki and convert it to naive time,
# as mobilepay times are naive (naive = no timezone specified).
helsinki_tz = pytz.timezone("Europe/Helsinki")
Expand All @@ -213,10 +231,12 @@ def save_raffle(
entry_fee: int,
df: pd.DataFrame,
) -> None:
"""Save a raffle to the database."""
db.save_raffle_data(chat_id, start_date, end_date, entry_fee, df)


def parse_df_essentials(raffle_data: RaffleData) -> RaffleData:
"""Parse the essentials of a raffle dataframe."""
start_date, end_date, fee, df = raffle_data

df.at[start_date, "amount"] = 0
Expand All @@ -230,6 +250,7 @@ def parse_df_essentials(raffle_data: RaffleData) -> RaffleData:


def parse_expected(raffle_data: RaffleData) -> RaffleData:
"""Parse the expected values of a raffle dataframe."""
start_date, end_date, entry_fee, df = parse_df_essentials(raffle_data)

df["win_odds"] = 1.0 / df["unique"]
Expand All @@ -247,6 +268,7 @@ def parse_expected(raffle_data: RaffleData) -> RaffleData:


def parse_graph(raffle_data: RaffleData) -> RaffleData:
"""Parse the graph values of a raffle dataframe."""
df = raffle_data.df

df.at[get_cur_time_hel(), "amount"] = 0
Expand All @@ -258,6 +280,7 @@ def parse_graph(raffle_data: RaffleData) -> RaffleData:


def configure_and_save_plot(out_img_path: str) -> None:
"""Configure and save the plot to an image file."""
ax = plt.gca()

# toggle legend
Expand All @@ -280,6 +303,7 @@ def configure_and_save_plot(out_img_path: str) -> None:


def generate_graph(out_img_path: str, chat_id: int, chat_title: str) -> None:
"""Generate a graph of raffle progress."""
# -- get raffle data --
raffle_data = get_raffle(chat_id)

Expand Down Expand Up @@ -320,6 +344,7 @@ def generate_graph(out_img_path: str, chat_id: int, chat_title: str) -> None:


def generate_expected(out_img_path: str, chat_id: int, chat_title: str) -> None:
"""Generate a graph of expected values."""
# -- get raffle data --
raffle_data = get_raffle(chat_id)

Expand Down
Loading

0 comments on commit 9cd740f

Please sign in to comment.