Skip to content

Commit

Permalink
feat: convert to libsql database and use raw SQL, update docker build
Browse files Browse the repository at this point in the history
  • Loading branch information
kahlstrm committed Dec 8, 2024
1 parent cf787ae commit 57261a5
Show file tree
Hide file tree
Showing 12 changed files with 705 additions and 759 deletions.
19 changes: 0 additions & 19 deletions .github/workflows/build-pipeline.yml

This file was deleted.

40 changes: 40 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Deploy

on:
push:
branches:
- main
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=true
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.env
sqlite.db*

1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
56 changes: 31 additions & 25 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
# syntax=docker/dockerfile:1
FROM python:3.11-slim as build

# Setup ENV variables here (if needed in the future)
# Install pipenv
RUN pip3 install pipenv
# Install python dependencies in /.venv
COPY Pipfile .
COPY Pipfile.lock .
RUN PIPENV_VENV_IN_PROJECT=1 pipenv install --deploy
# Install app into container
FROM python:3.11-slim as runtime
WORKDIR /bot
COPY --from=build /.venv /.venv
COPY --from=build root/.cache /home/botuser/.cache
COPY . .
ENV PATH="/.venv/bin:$PATH"
# Create a non-root user and add permission to access /bot folder
RUN useradd botuser && \
chown -R botuser /bot && chown -R botuser /home/botuser && chown -R botuser /.venv
USER botuser
RUN prisma generate && prisma py fetch

# Run the app
CMD ["python3", "bot.py" ]
# Use a Python image with uv pre-installed
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim

# Install the project into `/app`
WORKDIR /app

# Enable bytecode compilation
ENV UV_COMPILE_BYTECODE=1

# Copy from the cache instead of linking since it's a mounted volume
ENV UV_LINK_MODE=copy

# Install the project's dependencies using the lockfile and settings
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project --no-dev

# Then, add the rest of the project source code and install it
# Installing separately from its dependencies allows optimal layer caching
COPY pyproject.toml uv.lock bot.py /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev

# Place executables in the environment at the front of the path
ENV PATH="/app/.venv/bin:$PATH"

# Reset the entrypoint, don't invoke `uv`
ENTRYPOINT []

CMD ["python3", "bot.py"]
23 changes: 0 additions & 23 deletions Pipfile

This file was deleted.

602 changes: 0 additions & 602 deletions Pipfile.lock

This file was deleted.

22 changes: 4 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,16 @@
A simple telegram bot that describes current weather (currently for Otaniemi, Espoo) and predicts the "beautifulness" of the day using previously learnt data.

## Development
Currently only works on x86 only because of Prisma binaries (tested on Linux)

For development you need:
- [pipenv](https://pipenv.pypa.io/en/latest/)
- Python 3.11 is required in the Pipfile, highly recommended
- [uv](https://docs.astral.sh/uv/)
- Python 3.13
- A free API key from [OpenWeatherMap](https://openweathermap.org/current)
- A Postgres database (local/remote)
- A libSQL database (local/remote)
- A TG bot API key

1. Install dependencies:

pipenv install --dev
2. setup environment variables inside `.env`
3. generate Prisma binaries:

pipenv run generate
4. setup database:

pipenv run db push
5. to start dev environment run:

pipenv run dev

## Production
## Production (Docker deployment)

For production you need (or is recommended at least):
- [Docker](https://docs.docker.com/get-docker/)
Expand Down
119 changes: 67 additions & 52 deletions bot.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#!/usr/bin/env python
from datetime import datetime, timedelta
import os
import time
from typing import Tuple
import asyncio
import re
import requests
from dotenv import load_dotenv
import libsql_client
from telegram import Update
from telegram.ext import (
ApplicationBuilder,
Expand All @@ -15,17 +15,28 @@
PollHandler,
filters,
)
from prisma import Prisma
from prisma.models import WeatherIcon

LOOP = asyncio.get_event_loop()
load_dotenv()
POLL_TIME = timedelta(minutes=10)
BOT_TOKEN = os.getenv("BOT_TOKEN")
WEATHER_API = os.getenv("WEATHER_API")
BOT_TOKEN = os.getenv("BOT_TOKEN", "")
WEATHER_API = os.getenv("WEATHER_API", "")
DB_URL = os.getenv("DATABASE_URL", "http://127.0.0.1:8080")
DB_TOKEN = os.getenv("DATABASE_TOKEN")
if not BOT_TOKEN or not WEATHER_API:
print("env variables missing")
weather_code_dict: dict[str, Tuple[WeatherIcon, datetime]] = {}


class WeatherStatus:
def __init__(
self, code: int, temperature: int, votes_yes: int, votes_no: int
) -> None:
self.code = code
self.temperature = temperature
self.votes_yes = votes_yes
self.votes_no = votes_no


weather_code_dict: dict[str, Tuple[WeatherStatus, datetime]] = {}
poll_created_dict: dict[int, Tuple[int, datetime]] = {}

word_filter = [
Expand Down Expand Up @@ -123,7 +134,8 @@ async def day(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not res:
res, timestamp = await add_data_to_db(code, temp_rounded, 1, 1)
if not res:
await update.effective_chat.send_message(f"Error occurred, send help.")
await update.effective_chat.send_message("Error occurred, send help.")
return
weather_code_dict[code] = (res, timestamp)
beautiful_pct = res.votes_yes / (res.votes_no + res.votes_yes) * 100
beautiness = "Kaunis" if beautiful_pct > 50 else "Ei kaunis"
Expand Down Expand Up @@ -192,55 +204,58 @@ async def handle_poll_ended(update: Update, context: ContextTypes.DEFAULT_TYPE):


async def add_data_to_db(code: int, temp_rounded: int, yes_amount: int, no_amount: int):
db = Prisma()
try:
await db.connect()
res = await db.weathericon.find_first(
where={"code": code, "temperature": temp_rounded}
)
if not res:
res = await db.weathericon.create(
data={
"code": code,
"temperature": temp_rounded,
"votes_yes": yes_amount,
"votes_no": no_amount,
}
)
else:
await db.weathericon.update_many(
where={
"code": code,
"temperature": temp_rounded,
},
data={
"votes_yes": {"increment": yes_amount},
"votes_no": {"increment": no_amount},
},
async with libsql_client.create_client(
url=DB_URL, auth_token=DB_TOKEN
) as db_client:
try:
db_res = await db_client.execute(
"select code, temperature, votes_yes, votes_no from weather_status where code=? and temperature =?",
[code, temp_rounded],
)
except Exception as e:
print(e)
return None, datetime.now() - timedelta(minutes=10)
finally:
await db.disconnect()
return res, datetime.now()
if not db_res.rows:
db_res = await db_client.execute(
"INSERT INTO weather_status(code,temperature,votes_yes,votes_no) VALUES(?,?,?,?) returning *",
[code, temp_rounded, yes_amount, no_amount],
)
else:
db_res = await db_client.execute(
"UPDATE weather_status SET votes_yes = votes_yes + ?, votes_no = votes_no + ? WHERE code =? AND temperature=? returning *",
[yes_amount, no_amount, code, temp_rounded],
)
except Exception as e:
print(e)
return None, datetime.now() - timedelta(minutes=10)
row = db_res.rows[0]
res = WeatherStatus(
int(row["code"]),
int(row["temperature"]),
int(row["votes_yes"]),
int(row["votes_no"]),
)
return res, datetime.now()


async def fetch_from_db(code: str, temp_rounded: int):
db = Prisma()
try:
await db.connect()
db_res = await db.weathericon.find_first(
where={"code": code, "temperature": temp_rounded}
async with libsql_client.create_client(
url=DB_URL, auth_token=DB_TOKEN
) as db_client:
db_res = await db_client.execute(
"select code, temperature, votes_yes, votes_no from weather_status where code=? and temperature=?",
[code, temp_rounded],
)
if not db_res:
db_res = await db.weathericon.create(
{"code": code, "temperature": temp_rounded}
if not db_res.rows:
db_res = await db_client.execute(
"INSERT INTO weather_status(code,temperature,votes_yes,votes_no) VALUES(?,?,1,1) returning *",
[code, temp_rounded],
)
finally:
if db.is_connected():
await db.disconnect()
return db_res, datetime.now()
row = db_res[0]
res = WeatherStatus(
int(row["code"]),
int(row["temperature"]),
int(row["votes_yes"]),
int(row["votes_no"]),
)
return res, datetime.now()


def main():
Expand Down
20 changes: 0 additions & 20 deletions prisma/schema.prisma

This file was deleted.

11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[project]
name = "paivabot"
version = "0.1.0"
description = "bot to tell weather + give an educated guess if the day is beautiful or not"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"libsql-client>=0.3.1",
"python-telegram-bot>=21.9",
"requests>=2.32.3",
]
Loading

0 comments on commit 57261a5

Please sign in to comment.