MVP 1.0
This commit is contained in:
parent
3aea98c315
commit
20f069dd67
53
.forgejo/workflows/build-python-app.yml
Normal file
53
.forgejo/workflows/build-python-app.yml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
name: Build Python App
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install requirements
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
|
||||
- name: Lint with Ruff
|
||||
run: |
|
||||
pip install ruff
|
||||
ruff format . --target-version=py311
|
||||
continue-on-error: true
|
||||
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pip install coverage
|
||||
coverage run -m pytest -v -s
|
||||
|
||||
- name: Generate Coverage Report
|
||||
run: |
|
||||
coverage report -m
|
||||
|
||||
- name: Login to Digital Hippo Labs Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DIPPOLABS_USERNAME }}
|
||||
password: ${{ secrets.DIPPOLABS_TOKEN }}
|
||||
registry: hub.digitalhippo.tech
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
tags: hub.digitalhippo.tech/flowsms:latest
|
||||
27
Dockerfile
Normal file
27
Dockerfile
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
FROM python:3.11-alpine
|
||||
|
||||
WORKDIR /deployment
|
||||
|
||||
# Setup virtual environment
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
RUN python3 -m venv $VIRTUAL_ENV
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
# Install dependencies:
|
||||
COPY requirements.txt .
|
||||
RUN ["pip", "install", "-r", "requirements.txt"]
|
||||
|
||||
# Copy the application:
|
||||
ADD app.py .
|
||||
ADD database.py .
|
||||
ADD dtos.py .
|
||||
ADD loggerino.py .
|
||||
ADD models.py .
|
||||
ADD schemas.py .
|
||||
ADD service.py .
|
||||
|
||||
# Expose the port
|
||||
EXPOSE 8000
|
||||
|
||||
# Run the application:
|
||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--reload"]
|
||||
96
app.py
96
app.py
|
|
@ -1,10 +1,14 @@
|
|||
import logging
|
||||
import uvicorn
|
||||
|
||||
from arel import HotReload, Path
|
||||
from database import engine, Base
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI, Request, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import FastAPI, Request, status, Header
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import JSONResponse, HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from loggerino import timestamp_log_config
|
||||
from os import getenv
|
||||
from schemas import SMSMessage
|
||||
|
|
@ -13,26 +17,77 @@ from service import (
|
|||
retrieve_sms_messages_by_phone_number_handler,
|
||||
delete_sms_message_by_uuid_handler
|
||||
)
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
from uvicorn.config import LOGGING_CONFIG
|
||||
|
||||
|
||||
# Create instance of FastAPI
|
||||
app = FastAPI()
|
||||
# --------------------------------------------------------------------------------
|
||||
# Bootstrap
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
# Load dotenv file (.env)
|
||||
load_dotenv()
|
||||
|
||||
# Load custom logger format
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# App Creation
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Logger
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
logger = logging.getLogger("uvicorn.error")
|
||||
|
||||
# Create tables (if they don't exist)
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Database
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# Load whitelisted IPs
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Application Configurations
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
WHITELISTED_IPS = getenv("WHITELISTED_IPS").split(',')
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Static Files
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Templates
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Hot Reload
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
if _debug := getenv("DEBUG"):
|
||||
hot_reload = HotReload(paths=[Path(".")])
|
||||
app.add_websocket_route("/hot-reload", route=hot_reload, name="hot-reload")
|
||||
app.add_event_handler("startup", hot_reload.startup)
|
||||
app.add_event_handler("shutdown", hot_reload.shutdown)
|
||||
templates.env.globals["DEBUG"] = _debug
|
||||
templates.env.globals["hot_reload"] = hot_reload
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Middleware
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
@app.middleware("http")
|
||||
async def validate_ip(request: Request, call_next):
|
||||
ip = str(request.headers.get("x-forwarded-for", str(request.client.host)))
|
||||
|
|
@ -44,17 +99,30 @@ async def validate_ip(request: Request, call_next):
|
|||
return await call_next(request)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Routes
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
@app.get("/", summary="Main page that redirects to application", response_class=HTMLResponse)
|
||||
async def index(request: Request):
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
|
||||
@app.get('/sms-message/{number}', response_class=HTMLResponse)
|
||||
def get_sms_messages_by_phone_number(request: Request, number: str, hx_request: Optional[str] = Header(None), cost: bool = False, metadata: bool = False):
|
||||
contact = retrieve_sms_messages_by_phone_number_handler(number, cost, metadata)
|
||||
if hx_request:
|
||||
return templates.TemplateResponse(
|
||||
request=request, name="messages.html", context={"messages": contact}
|
||||
)
|
||||
return JSONResponse(content=jsonable_encoder({'status': 'success', 'results': len(contact), 'response': contact}))
|
||||
|
||||
|
||||
@app.post("/sms-message", status_code=status.HTTP_200_OK)
|
||||
async def post_sms_message(message: SMSMessage):
|
||||
receive_sms_messages_handler(message)
|
||||
|
||||
|
||||
@app.get('/sms-message/{number}', status_code=status.HTTP_200_OK)
|
||||
def get_sms_messages_by_phone_number(number: str, cost: bool = False, metadata: bool = False):
|
||||
contact = retrieve_sms_messages_by_phone_number_handler(number, cost, metadata)
|
||||
return {'status': 'success', 'results': len(contact), 'response': contact}
|
||||
|
||||
|
||||
@app.delete('/sms-message/{uuid}', status_code=status.HTTP_202_ACCEPTED)
|
||||
def delete_sms_message_by_uuid(uuid: UUID):
|
||||
delete_sms_message_by_uuid_handler(uuid)
|
||||
|
|
|
|||
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
services:
|
||||
|
||||
flosms:
|
||||
container_name: 'flosms'
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./Dockerfile
|
||||
tags:
|
||||
- "hub.digitalhippo.tech/flosms"
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: nginx-network
|
||||
BIN
flowsms.db
BIN
flowsms.db
Binary file not shown.
|
|
@ -1,4 +1,6 @@
|
|||
arel
|
||||
fastapi
|
||||
jinja2
|
||||
python-dotenv
|
||||
sqlalchemy
|
||||
uvicorn
|
||||
|
|
|
|||
92
static/css/style.css
Normal file
92
static/css/style.css
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: url('../images/background.jpg') center / cover;
|
||||
}
|
||||
|
||||
main.table {
|
||||
width: 82vw;
|
||||
height: 90vh;
|
||||
background-color: #fff5;
|
||||
backdrop-filter: blur(7px);
|
||||
box-shadow: 0 .4rem .8rem #0005;
|
||||
border-radius: .8rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table__header {
|
||||
width: 100%;
|
||||
height: 10%;
|
||||
background-color: #fff4;
|
||||
padding: .8rem 1rem;
|
||||
}
|
||||
|
||||
.table__content {
|
||||
width: 95%;
|
||||
max-height: calc(89% - .8rem);
|
||||
background-color: #fffb;
|
||||
margin: .8rem auto;
|
||||
border-radius: .6rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.table__content::-webkit-scrollbar-thumb {
|
||||
border-radius: .5rem;
|
||||
background-color: #0004;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.table__content:hover::-webkit-scrollbar-thumb {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
padding: 1rem;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #d5d1defe;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: #0000000b;
|
||||
}
|
||||
|
||||
td img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-right: .5rem;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
button {
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
color: white;
|
||||
background-color: red;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
@media (max-width: 1920px) {
|
||||
td:not(:first-of-type) {
|
||||
min-width: 22rem;
|
||||
}
|
||||
}
|
||||
BIN
static/images/background.jpg
Normal file
BIN
static/images/background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 MiB |
BIN
static/images/favicon.png
Normal file
BIN
static/images/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
static/images/user_3177440.png
Normal file
BIN
static/images/user_3177440.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
0
static/js/script.js
Normal file
0
static/js/script.js
Normal file
36
templates/index.html
Normal file
36
templates/index.html
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Table Demo</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
|
||||
<!-- HTMX -->
|
||||
<script src="https://unpkg.com/htmx.org@2.0.2"></script>
|
||||
|
||||
<!-- Favicons -->
|
||||
<link href="/static/images/favicon.png" rel="icon">
|
||||
</head>
|
||||
<body>
|
||||
<main class="table">
|
||||
<section class="table__header">
|
||||
<h1>Digital Hippo Labs - FlowSMS</h1>
|
||||
</section>
|
||||
<section class="table__content">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>From Number</th>
|
||||
<th>Message</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="messages" hx-get="/sms-message/17072064097" hx-swap="innerHTML" hx-trigger="load, every 2s"></tbody>
|
||||
</table>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
<script src="https://kit.fontawesome.com/b031afcd74.js" crossorigin="anonymous"></script>
|
||||
</html>
|
||||
12
templates/messages.html
Normal file
12
templates/messages.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{% for message in messages %}
|
||||
<tr>
|
||||
<td>{{message.from_number}}</td>
|
||||
<td>{{message.message}}</td>
|
||||
<td>{{message.timestamp}}</td>
|
||||
<td>
|
||||
<button hx-delete="/sms-message/{{message.id}}">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
Loading…
Reference in a new issue