Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jobs:
BOT_TOKEN: "fake"
SIGNING_SECRET: "also_fake"
TZ: "US/Eastern"
TESTING: "True"

runs-on: ubuntu-latest
steps:
Expand All @@ -20,23 +21,25 @@ jobs:
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: '3.11.5'
python-version: '3.13.0'
cache: 'pip'

- name: Install Python dependencies
run: pip install .[test]

- name: Black
run: python -m black --check src/

- name: isort
run: python -m isort --check src/

- name: ssort
run: python -m ssort --check src/

- name: Pylint
run: python -m pylint src/
- name: Lint and Format src/
run: |
python -m black --check src/
python -m isort --check src/
python -m ssort --check src/
python -m pylint --fail-under=10 src/

- name: Lint and Format tests/
run: |
python -m black --check tests/
python -m isort --check tests/
python -m ssort --check tests/
python -m pylint --fail-under=10 tests/

- name: Pytest
run: python -m pytest tests/
52 changes: 52 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/pylint-dev/pylint
rev: v3.2.5
hooks:
- id: pylint
args: ["--fail-under=10"]
additional_dependencies: [
"aiohttp==3.11.10",
"fastapi==0.115.6",
"python-dateutil==2.9.0.post0",
"pytz==2024.2",
"slack_bolt==1.21.3",
"slack_sdk==3.34.0",
"uvicorn==0.32.1",
"black==24.10.0",
"httpx==0.28.0",
"isort==5.13.2",
"pytest==8.3.4",
"pytest-asyncio==0.24.0",
"ssort==0.14.0",
"pre-commit==3.7.1"
]
- repo: local
hooks:
- id: ssort
name: ssort
entry: python -m ssort
language: system
types: [python]
args: ["--check"]
- id: pytest
name: pytest
entry: bash -c 'source .envrc && pytest'
language: system
types: [python]
pass_filenames: false
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ test = [
"pylint==3.3.2",
"pytest==8.3.4",
"pytest-asyncio==0.24.0",
"ssort==0.14.0"
"ssort==0.14.0",
"pre-commit==3.7.1"
]

[project.urls]
Expand Down
8 changes: 4 additions & 4 deletions src/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ async def periodically_delete_old_messages():
while True:
try:
await database.delete_old_messages()
except Exception: # pylint: disable=broad-except
print(traceback.format_exc())
except sqlite3.Error:
logging.error("An unexpected error occurred: %s", traceback.format_exc())
os._exit(1)
await asyncio.sleep(60 * 60 * 24) # 24 hours

Expand All @@ -225,8 +225,8 @@ async def periodically_check_api():
while True:
try:
await check_api()
except Exception: # pylint: disable=broad-except
print(traceback.format_exc())
except sqlite3.Error:
logging.error("An unexpected error occurred: %s", traceback.format_exc())
os._exit(1)
await asyncio.sleep(60 * 60) # 60 minutes x 60 seconds

Expand Down
74 changes: 49 additions & 25 deletions src/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,47 @@
import datetime
import os
import sqlite3
from typing import Generator, Union
from typing import Union

DB_PATH = os.path.abspath(os.environ.get("DB_PATH", "./slack-events-bot.db"))
DB_PATH = os.path.abspath(
os.environ.get(
"DB_PATH",
":memory:" if os.environ.get("TESTING") == "True" else "./slack-events-bot.db",
)
)


def get_connection(commit: bool = False) -> Generator:
"""
Yields a SQLite connection to another method.
class Connection:
"""Context manager for SQLite connection."""

Once the other method has finished,
the transaction if committed if the commit parameter is true,
and then the connection is always closed.
"""
conn = sqlite3.connect(DB_PATH)
def __init__(self, commit: bool = False):
self.commit = commit
self.conn = None

yield conn
def __enter__(self):
self.conn = sqlite3.connect(DB_PATH)
return self.conn

if commit:
conn.commit()
def __exit__(self, exc_type, value, traceback):
if self.commit:
self.conn.commit()
self.conn.close()

conn.close()

def get_connection(commit: bool = False) -> Connection:
"""
Returns a context manager for a SQLite connection.

Usage:
with get_connection(commit=True) as conn:
# do something with conn
"""
return Connection(commit)


def create_tables():
"""Create database tables needed for slack events bot"""
for conn in get_connection(commit=True):
with get_connection(commit=True) as conn:
cur = conn.cursor()
cur.executescript(
"""
Expand Down Expand Up @@ -77,7 +92,7 @@ async def create_message(
week, message, message_timestamp, slack_channel_id, sequence_position: int
):
"""Create a record of a message sent in slack for a week"""
for conn in get_connection(commit=True):
with get_connection(commit=True) as conn:
cur = conn.cursor()
# get database's channel id for slack channel id
cur.execute(
Expand All @@ -96,7 +111,7 @@ async def create_message(

async def update_message(week, message, message_timestamp, slack_channel_id):
"""Updates a record of a message sent in slack for a week"""
for conn in get_connection(commit=True):
with get_connection(commit=True) as conn:
cur = conn.cursor()
# get database's channel id for slack channel id
cur.execute(
Expand All @@ -114,7 +129,7 @@ async def update_message(week, message, message_timestamp, slack_channel_id):

async def get_messages(week) -> list:
"""Get all messages sent in slack for a week"""
for conn in get_connection():
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
"""SELECT m.message, m.message_timestamp, c.slack_channel_id, m.sequence_position
Expand All @@ -139,7 +154,7 @@ async def get_messages(week) -> list:

async def get_most_recent_message_for_channel(slack_channel_id) -> dict:
"""Get the most recently posted message for a subscribed Slack channel"""
for conn in get_connection():
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
"""SELECT m.week, m.message, m.message_timestamp
Expand Down Expand Up @@ -168,7 +183,7 @@ async def get_most_recent_message_for_channel(slack_channel_id) -> dict:

async def get_slack_channel_ids() -> list:
"""Get all slack channels that the bot is configured for"""
for conn in get_connection():
with get_connection() as conn:
cur = conn.cursor()
cur.execute("SELECT slack_channel_id FROM channels")
return [x[0] for x in cur.fetchall()]
Expand All @@ -178,7 +193,7 @@ async def get_slack_channel_ids() -> list:

async def add_channel(slack_channel_id):
"""Add a slack channel to post in for the bot"""
for conn in get_connection(commit=True):
with get_connection(commit=True) as conn:
cur = conn.cursor()
cur.execute(
"INSERT INTO channels (slack_channel_id) VALUES (?)", [slack_channel_id]
Expand All @@ -187,14 +202,14 @@ async def add_channel(slack_channel_id):

async def remove_channel(channel_id):
"""Remove a slack channel to post in from the bot"""
for conn in get_connection(commit=True):
with get_connection(commit=True) as conn:
cur = conn.cursor()
cur.execute("DELETE FROM channels WHERE slack_channel_id = ?", [channel_id])


async def delete_old_messages(days_back=90):
"""delete all messages and cooldowns with timestamp older than current timestamp - days_back"""
for conn in get_connection(commit=True):
with get_connection(commit=True) as conn:
cur = conn.cursor()
cur.execute(
"DELETE FROM messages where cast(message_timestamp as decimal) < ?",
Expand All @@ -216,12 +231,21 @@ async def delete_old_messages(days_back=90):
)


def clear_db():
"""Clear all data from the database tables."""
with get_connection(commit=True) as conn:
cur = conn.cursor()
cur.execute("DELETE FROM channels")
cur.execute("DELETE FROM messages")
cur.execute("DELETE FROM cooldowns")


async def create_cooldown(accessor: str, resource: str, cooldown_minutes: int) -> None:
"""
Upserts a cooldown record for an entity which will let the system know when to make the resource
available to them once again.
"""
for conn in get_connection(commit=True):
with get_connection(commit=True) as conn:
cur = conn.cursor()
cur.execute(
"""INSERT INTO cooldowns (accessor, resource, expires_at)
Expand All @@ -247,7 +271,7 @@ async def get_cooldown_expiry_time(accessor: str, resource: str) -> Union[str, N
Returns the time at which an accessor is able to access a resource
or None if no restriction has ever been put in place.
"""
for conn in get_connection():
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
"""SELECT expires_at FROM cooldowns
Expand Down
Loading