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 logging
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
from arel import HotReload, Path
|
||||||
from database import engine, Base
|
from database import engine, Base
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from fastapi import FastAPI, Request, status
|
from fastapi import FastAPI, Request, status, Header
|
||||||
from fastapi.responses import JSONResponse
|
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 loggerino import timestamp_log_config
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from schemas import SMSMessage
|
from schemas import SMSMessage
|
||||||
|
|
@ -13,26 +17,77 @@ from service import (
|
||||||
retrieve_sms_messages_by_phone_number_handler,
|
retrieve_sms_messages_by_phone_number_handler,
|
||||||
delete_sms_message_by_uuid_handler
|
delete_sms_message_by_uuid_handler
|
||||||
)
|
)
|
||||||
|
from typing import Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from uvicorn.config import LOGGING_CONFIG
|
from uvicorn.config import LOGGING_CONFIG
|
||||||
|
|
||||||
|
|
||||||
# Create instance of FastAPI
|
# --------------------------------------------------------------------------------
|
||||||
app = FastAPI()
|
# Bootstrap
|
||||||
|
# --------------------------------------------------------------------------------
|
||||||
|
|
||||||
# Load dotenv file (.env)
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# Load custom logger format
|
|
||||||
|
# --------------------------------------------------------------------------------
|
||||||
|
# App Creation
|
||||||
|
# --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------------
|
||||||
|
# Logger
|
||||||
|
# --------------------------------------------------------------------------------
|
||||||
|
|
||||||
logger = logging.getLogger("uvicorn.error")
|
logger = logging.getLogger("uvicorn.error")
|
||||||
|
|
||||||
# Create tables (if they don't exist)
|
|
||||||
|
# --------------------------------------------------------------------------------
|
||||||
|
# Database
|
||||||
|
# --------------------------------------------------------------------------------
|
||||||
|
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
# Load whitelisted IPs
|
|
||||||
|
# --------------------------------------------------------------------------------
|
||||||
|
# Application Configurations
|
||||||
|
# --------------------------------------------------------------------------------
|
||||||
|
|
||||||
WHITELISTED_IPS = getenv("WHITELISTED_IPS").split(',')
|
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")
|
@app.middleware("http")
|
||||||
async def validate_ip(request: Request, call_next):
|
async def validate_ip(request: Request, call_next):
|
||||||
ip = str(request.headers.get("x-forwarded-for", str(request.client.host)))
|
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)
|
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)
|
@app.post("/sms-message", status_code=status.HTTP_200_OK)
|
||||||
async def post_sms_message(message: SMSMessage):
|
async def post_sms_message(message: SMSMessage):
|
||||||
receive_sms_messages_handler(message)
|
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)
|
@app.delete('/sms-message/{uuid}', status_code=status.HTTP_202_ACCEPTED)
|
||||||
def delete_sms_message_by_uuid(uuid: UUID):
|
def delete_sms_message_by_uuid(uuid: UUID):
|
||||||
delete_sms_message_by_uuid_handler(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
|
fastapi
|
||||||
|
jinja2
|
||||||
python-dotenv
|
python-dotenv
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
uvicorn
|
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