First go at it
Some checks failed
Build image - Testing / build-api-testing (push) Failing after 24s
SonarQube Scan / SonarQube Trigger (push) Failing after 37s

This commit is contained in:
Evan 2025-01-20 23:15:24 -05:00
parent 13af3b78eb
commit 9ae5ee3ef5
8 changed files with 223 additions and 11 deletions

View file

@ -0,0 +1,28 @@
name: Build image - Stable
on:
push:
tags:
- "v*"
jobs:
build-app-stable:
runs-on: ubuntu-latest
env:
PACKAGE_SERVER: git.bigun.dev
PACKAGE_USER: evan
PACKAGE_REPO: evan/AtmoAssistant
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build & Deploy
run: |
version=$(echo ${{ github.ref }} | awk -F "v" '{print $2}')
cd app
echo ${{ secrets.PACKAGE_TOKEN }} | docker login -u ${{ env.PACKAGE_USER }} --password-stdin ${{ env.PACKAGE_SERVER }}
docker build -t ${{ env.PACKAGE_SERVER }}/${{ env.PACKAGE_REPO }}:stable .
docker build -t ${{ env.PACKAGE_SERVER }}/${{ env.PACKAGE_REPO }}:$version .
docker push ${{ env.PACKAGE_SERVER }}/${{ env.PACKAGE_REPO }}:stable
docker push ${{ env.PACKAGE_SERVER }}/${{ env.PACKAGE_REPO }}:$version

View file

@ -0,0 +1,25 @@
name: Build image - Testing
on:
push:
paths:
- 'api/**'
- '.gitea/workflows/build-api-testing.yml'
jobs:
build-api-testing:
runs-on: ubuntu-latest
env:
PACKAGE_SERVER: git.bigun.dev
PACKAGE_USER: evan
PACKAGE_REPO: evan/AtmoAssistant
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build & Deploy
run: |
cd api
echo ${{ secrets.PACKAGE_TOKEN }} | docker login -u ${{ env.PACKAGE_USER }} --password-stdin ${{ env.PACKAGE_SERVER }}
docker build -t ${{ env.PACKAGE_SERVER }}/${{ env.PACKAGE_REPO }}:testing .
docker push ${{ env.PACKAGE_SERVER }}/${{ env.PACKAGE_REPO }}:testing

View file

@ -8,7 +8,7 @@ ENV PROMETHEUS_MULTIPROC_DIR=/dev/shm
ENV PAPERSIZE=letter
# Defining working directory and adding source code
WORKDIR /template
WORKDIR /AtmoAssistant
COPY . .
# Install requirements

View file

@ -4,6 +4,9 @@ import tempfile
env_DEBUG = os.environ.get("DEBUG", "").lower() == "true"
env_SECURE = os.environ.get("SECURE", "").lower() == "true"
env_OWM_KEY = os.environ.get("OWM_API_KEY", "")
env_OWM_UNITS = os.environ.get("OWM_UNITS", "")
env_AUTHORIZED_CALLERS = list(os.environ.get("AUTHORIZED_CALLERS", ""))
env_SECRET_KEY = os.environ.get("SECRET_KEY", os.urandom(24))
if not env_SECRET_KEY:
env_SECRET_KEY = os.urandom(24)

View file

@ -1,15 +1,18 @@
services:
template:
container_name: template
image: git.bigun.dev/evan/template:stable
AtmoAssistant:
container_name: AtmoAssistant
image: git.bigun.dev/evan/AtmoAssistant:stable
ports:
- 80:5000 # API
- 9200:9200 # Prometheus
restart: unless-stopped
volumes:
- /etc/localtime:/etc/localtime
- ./database:/template/instance
- ./database:/AtmoAssistant/instance
environment:
- DEBUG=FALSE # Enables debug route and Flask's debug mode
- SECRET_KEY="" # Should be a long random value, randomly regenerated every launch if not specified
- SECRET_KEY= # Should be a long random value, randomly regenerated every launch if not specified
- SECURE=FALSE # Set to True when using HTTPS
- OWM_API_KEY= # API key from OpenWeatherMap (One Call 3.0 and Geocoding)
- OWM_UNITS= # Units for OpenWeatherMap (Standard, Metric, Imperial)
- AUTHORIZED_CALLERS= # Comma seperated list of authorized phone numbers, eg +13365550916,+13365553721

View file

@ -1,4 +1,5 @@
Flask==3.1.0
flask_sqlalchemy==3.1.1
gunicorn==23.0.0
prometheus-flask-exporter==0.23.1
prometheus-flask-exporter==0.23.1
twilio==9.4.3

View file

@ -1,10 +1,99 @@
from flask import jsonify, request
from twilio.twiml.voice_response import VoiceResponse, Gather
from utils import (
logger,
validate_data_presence,
)
from utils import logger, validate_data_presence, _get_weather, _get_cords
# from config import
from . import routes as app
from . import by_path_counter
@app.route("/voice", methods=["GET", "POST"])
@by_path_counter
def voice():
"""Respond to incoming phone calls with a menu of options"""
# Start our TwiML response
resp = VoiceResponse()
from_number = request.form["From"]
if from_number not in env_AUTHORIZED_CALLERS:
resp.say("You are calling from an unauthorized number. Goodbye.")
return str(resp)
resp.say("Welcome to Atmo Assistant.")
# Start our <Gather> verb
gather = Gather(num_digits=1, action="/gather")
gather.say(
"Main menu. Enter one for Asheboro. Two for Lynchburg. Three for Cullowhee. Zero for another zipcode."
)
resp.append(gather)
# If the user doesn't select an option, redirect them into a loop
resp.redirect("/voice")
return str(resp)
@app.route("/gather", methods=["GET", "POST"])
@by_path_counter
def gather():
"""Processes results from the <Gather> prompt in /voice"""
# Start our TwiML response
resp = VoiceResponse()
# If Twilio's request to our app included already gathered digits,
# process them
if "Digits" in request.values:
# Get which digit the caller chose
choice = request.values["Digits"]
# <Say> a different message depending on the caller's choice
if choice == "1":
weather = _get_weather(35.6396, -79.8509)
resp.say(weather)
elif choice == "2":
weather = _get_weather(37.3490, -79.1787)
resp.say(weather)
elif choice == "3":
weather = _get_weather(35.3087, -83.1861)
resp.say(weather)
elif choice == "0":
custom = Gather(num_digits=5, action="/custom")
custom.say("Enter a zipcode for its weather.")
resp.append(custom)
else:
# If the caller didn't choose 1 or 2, apologize and ask them again
resp.say("Sorry, I don't understand that choice.")
# If the user didn't choose 1 or 2 (or anything), send them back to /voice
resp.redirect("/voice")
return str(resp)
@app.route("/custom", methods=["GET", "POST"])
@by_path_counter
def custom():
"""Processes results from the <Gather> prompt in /gather"""
# Start our TwiML response
resp = VoiceResponse()
# If Twilio's request to our app included already gathered digits,
# process them
if "Digits" in request.values:
# Get which digit the caller chose
choice = request.values["Digits"]
# <Say> a different message depending on the caller's choice
if len(choice) == 5:
weather = _get_weather(_get_cords(choice))
resp.say(weather)
return str(resp)
else:
# If the caller didn't choose 1 or 2, apologize and ask them again
resp.say("Sorry, I don't understand that choice.")
# If the user didn't choose 1 or 2 (or anything), send them back to /voice
resp.redirect("/voice")
return str(resp)

View file

@ -4,10 +4,12 @@ from urllib import parse
import logging
import re
import typing as t
import requests
import models
logger = logging.getLogger("gunicorn.error")
weather_template = "The current temperature is {0} and feels like {1}. The high today is {2} with a low of {3}. The current humidity is {4} percent. The summary for today is: {5}."
def str_none(x):
@ -47,3 +49,64 @@ def validate_data_presence(data: t.Dict[str, t.Any], keys: list[str]) -> bool:
if key not in data:
return False
return True
def _get_weather(lat, long):
try:
if lat is None or long is None:
return "An error has occured and the provided zipcode could not be understood."
weather_json = _get_weather_json(lat, long)
if weather_json is None:
return "An error has occured and the weather could not be retrieved."
weather = weather_template.format(
weather_json.current.temp,
weather_json.current.feels_like,
weather_json.daily[0].temp.max,
weather_json.daily[0].temp.min,
weather_json.current.humidity,
weather_json.daily[0].summary,
)
return weather
except Exception as e:
logger.error("Error in _get_weather: " + e)
return "An error has occured and the weather could not be retrieved."
def _get_weather_json(lat, long):
url = "https://api.openweathermap.org/data/3.0/onecall?lat={0}&lon={1}&exclude=alerts,minutely,hourly&units={2}&appid={3}".format(
lat, long, env_OWM_UNITS, env_OWM_KEY
)
try:
response = requests.get(url)
if response.status_code == 200:
weather = response.json()
return weather
else:
logger.error("Error in _get_weather_json: " + str(response.status_code))
return None
except requests.exceptions.RequestException as e:
logger.error("Error in _get_weather_json: " + e)
return None
def _get_cords(zipcode):
url = "http://api.openweathermap.org/geo/1.0/zip?zip={0},US&appid={1}".format(
zipcode, env_OWM_KEY
)
try:
response = requests.get(url)
if response.status_code == 200:
zipcode = response.json()
return zipcode.lat, zipcode.long
else:
logger.error("Error in _get_cords: " + str(response.status_code))
return None, None
except requests.exceptions.RequestException as e:
logger.error("Error in _get_cords: " + e)
return None, None