Get the MRE api working #1

Merged
chris merged 50 commits from add_mre_api into main 2025-04-06 23:58:50 +00:00
19 changed files with 257 additions and 49 deletions

2
.gitattributes vendored
View File

@ -1 +1 @@
/app/secret_key filter=git-crypt diff=git-crypt secrets filter=git-crypt diff=git-crypt

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "app/mre_module"]
path = app/mre_module
url = http://10.0.50.3:3002/Quarter/MRE-module.git

View File

@ -1,7 +1,3 @@
from flask import Blueprint from flask import Blueprint
from .api import api_blueprint
mre_blueprint = Blueprint('mre_blueprint', blueprint = Blueprint('mre', __name__)
__name__,
url_prefix='mre')
mre_blueprint.register_blueprint(api_blueprint)

39
app/MRE/api.py Normal file
View File

@ -0,0 +1,39 @@
from flask import current_app
from flask_restx import Api, fields, apidoc, Model, Namespace, Resource
from flask_restx.reqparse import RequestParser
from app.mre_module import compute_mre
namespace = Namespace(
'mre',
description='API endpoints for MRE calculations.'
)
MRE_request_parser = RequestParser()
MRE_request_parser.add_argument('consumer_fico',
required=True,
type=int,
help='The consumer\'s fico score (int between [300, 850])')
MRE_request_parser.add_argument('home_price',
required=True,
type=float,
help='The price of the home the consumer wants to buy (float)')
MRE_request_parser.add_argument('down_payment',
required=True,
type=float,
help='The down payment (as a percentage) for purchasing the home (float between [0, 1]).')
@namespace.route('/compute_mre')
@namespace.doc(params={
'consumer_fico': 'The consumer\'s fico score (int).',
'home_price': 'The price of the home (float).',
'down_payment': 'The down_payment percentage (float).'
})
class ComputeMRE(Resource):
@namespace.doc(description='Computes and returns the MRE.')
def get(self):
args = MRE_request_parser.parse_args()
mre = compute_mre(args.home_price, args.down_payment, args.consumer_fico)
return {'consumer_mre': mre}

View File

@ -1,5 +0,0 @@
from flask import Blueprint
api_blueprint = Blueprint('api_blueprint',
__name__,
url_prefix='api')

View File

@ -1,7 +0,0 @@
import cmath
'''
TODO: Actually implement this
'''
def compute_mre(consumer_fico: int, consumer_income: float, home_price: float) -> float:
return None

View File

@ -1,17 +0,0 @@
from flask import current_app
from flask_restx import Api, fields, apidoc, Model
from . import api_blueprint
api = Api(api_blueprint)
mre_from_home_price_model = Model(
{
'consumer_income': fields.Float(attribute='consumer_income'),
'home_price': fields.Float(attribute='home_price'),
'consumer_fico': fields.Integer(attribute='consumer_fico')
}
)
@api.route('compute_mre_from_home_price')
def compute_mre_from_home_price():
pass

View File

@ -1,6 +1,8 @@
from flask import Flask from flask import Flask, g, request, url_for
import time
import os import os
from app.MRE import mre_blueprint from app.MRE import blueprint as mre_blueprint
from .api import blueprint as api_blueprint
from logging import Logger, Formatter, getLogger, DEBUG, INFO, FileHandler from logging import Logger, Formatter, getLogger, DEBUG, INFO, FileHandler
@ -19,8 +21,9 @@ def init_logger(app: Flask, log_level=INFO) -> Flask:
to_file = FileHandler(os.environ.get('MRE_LOG_PATH', f'./{app.name}.log')) to_file = FileHandler(os.environ.get('MRE_LOG_PATH', f'./{app.name}.log'))
to_file.setFormatter(formatter) to_file.setFormatter(formatter)
logger.addHandler(to_file) logger.addHandler(to_file)
app.logger = logger app.logger = logger
return app return app
@ -30,5 +33,31 @@ def create_app(app_name: str='MRE') -> Flask:
app = init_logger(app) app = init_logger(app)
app.register_blueprint(mre_blueprint) app.register_blueprint(mre_blueprint)
app.register_blueprint(api_blueprint, url_prefix='/api')
@app.before_request
def before_request():
g.start = time.time()
@app.after_request
def after_request(response):
request_time = time.time() - g.start
app.logger.info(f'HTTP request completed (method={request.method}, path={request.path}, request_time={request_time}, status_code={response.status_code}).')
return response
# def has_no_empty_params(rule):
# defaults = rule.defaults if rule.defaults is not None else ()
# arguments = rule.arguments if rule.arguments is not None else ()
# return len(defaults) >= len(arguments)
#
# @app.route('/ping')
# def ping():
# links = []
# for rule in app.url_map.iter_rules():
# if "GET" in rule.methods and has_no_empty_params(rule):
# url = url_for(rule.endpoint, **(rule.defaults or {}))
# links.append(url)
# app.logger.info(f'Endpoint: {url}')
# return {'links': links}, 200
return app return app

12
app/api.py Normal file
View File

@ -0,0 +1,12 @@
from flask_restx import Api
from flask import Blueprint
from .MRE.api import namespace as mre_namespace
blueprint = Blueprint('api', __name__)
api = Api(
blueprint,
doc='/doc/',
title='Quarter API documentatoin'
)
api.add_namespace(mre_namespace, '/mre')

1
app/mre_module Submodule

@ -0,0 +1 @@
Subproject commit ce94312099a10d3cf273e42d4b315ac17a2f187f

View File

@ -2,6 +2,8 @@ version: "3.8"
services: services:
nginx: nginx:
networks:
- frontend
image: nginx:latest image: nginx:latest
ports: ports:
- "80:80" - "80:80"
@ -10,12 +12,17 @@ services:
- ./nginx/conf.d:/etc/nginx/conf.d # Mount Nginx configuration - ./nginx/conf.d:/etc/nginx/conf.d # Mount Nginx configuration
- ./static:/var/www/static # Serve static files - ./static:/var/www/static # Serve static files
depends_on: depends_on:
- uwsgi - mre_api
uwsgi: mre_api:
image: python:3.9-slim-buster # Use a slim Python image build:
context: .
dockerfile: docker/mre.Dockerfile
networks:
- frontend
- backend
volumes: volumes:
- ./app:/app # Mount your Flask app directory - ./app:/var/www/app # Mount your Flask app directory
- ./uwsgi.ini:/etc/uwsgi.ini # UWSGI configuration - ./uwsgi.ini:/etc/uwsgi.ini # UWSGI configuration
command: uwsgi --ini /etc/uwsgi.ini command: uwsgi --ini /etc/uwsgi.ini
expose: expose:
@ -23,27 +30,43 @@ services:
environment: environment:
- FLASK_APP=run.py # Adjust this based on your Flask app's entry point - FLASK_APP=run.py # Adjust this based on your Flask app's entry point
- FLASK_ENV=dev # Dev environment - FLASK_ENV=dev # Dev environment
- MRE_POSTGRES_PASSWORD={{ POSTGRES_PASSWD }}
- MRE_POSTGRES_USER={{ POSTGRES_USER }}
- MRE_REDIS_PASSWORD={{ REDIS_PASSWD }}
depends_on: depends_on:
- redis # Ensure Redis is running before UWSGI starts - redis
- postgres
redis: redis:
networks:
- backend
image: redis:latest image: redis:latest
ports: ports:
- "6379:6379" # Expose Redis port (for debugging/accessing from outside) - "6379:6379" # Expose Redis port (for debugging/accessing from outside)
volumes: volumes:
- redis_data:/data # Persist Redis data - redis_data:/data # Persist Redis data
command: redis-server --save 20 1 --loglevel {{ LOG_LEGEL }} --requirepass {{ REDIS_PASSWD }}
postgres: postgres:
networks:
- backend
image: postgres:latest image: postgres:latest
ports: ports:
- "5432:5432" # Expose PostgreSQL port (for debugging/admin) - "5432:5432" # Expose PostgreSQL port (for debugging/admin)
environment: environment:
- POSTGRES_USER=youruser # Replace with your desired username - POSTGRES_USER={{ POSTGRES_USER }} # Replace with your desired username
- POSTGRES_PASSWORD=yourpassword # Replace with your desired password - POSTGRES_PASSWORD={{ POSTGRES_PASSWD }} # Replace with your desired password
- POSTGRES_DB=yourdb # Replace with your desired database name - POSTGRES_DB=yourdb # Replace with your desired database name
volumes: volumes:
- postgres_data:/var/lib/postgresql/data # Persist PostgreSQL data - postgres_data:/var/lib/postgresql/data # Persist PostgreSQL data
volumes: volumes:
postgres_data: postgres_data:
redis_data: redis_data:
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true

3
docker/entrypoint.sh Normal file
View File

@ -0,0 +1,3 @@
#! /usr/bin/bash
uwsgi --ini /var/www/run.py

27
docker/mre.Dockerfile Normal file
View File

@ -0,0 +1,27 @@
FROM python:3.13.2-bullseye
LABEL MAINTAINER="Chris Diesch <chris@quarterhomes.com>"
WORKDIR /var/www
# Add the entrypoint and make it executable
ADD ./docker/entrypoint.sh /var/docker-entrypoint.sh
RUN chmod +x /var/docker-entrypoint.sh
# add the requirements and install them
ADD ./requirements.txt /var/www/requirements.txt
RUN pip install -Ur /var/www/requirements.txt
# add the uwsgi runner
ADD ./run.py /var/www/run.py
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# Copy the app directory
COPY ./app /var/www/app
# define the entrypoint
ENTRYPOINT ["/var/docker-entrypoint.sh"]

View File

@ -1,4 +1,6 @@
flask==3.1.0 flask==3.1.0
flask-restx==1.3.0 flask-restx==1.3.0
pyopenssl==25.0.0 pyopenssl==25.0.0
werkzeug==3.1.3 werkzeug==3.1.3
jinja2==3.1.6
pytest==8.3.5

2
run.py
View File

@ -3,4 +3,4 @@ from app import create_app
app = create_app() app = create_app()
if __name__ == '__main__': if __name__ == '__main__':
app.run(ssl_context='adhoc') app.run(port=8080, host='0.0.0.0')

102
scripts/render_templates.py Normal file
View File

@ -0,0 +1,102 @@
import os
import jinja2
import argparse
import traceback
def render_template(template_file: str,
out_path: str,
**kwargs) -> None:
folder, file = os.path.split(template_file)
template_path = os.path.abspath(folder)
env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_path),
trim_blocks=True,
lstrip_blocks=True)
template = env.get_template(file)
with open(out_path, 'w+') as save:
save.write(template.render(**kwargs))
def load_template_vars_file(var_file_path: str) -> dict:
result = {}
with open(var_file_path) as reader:
for line in reader:
line = line.strip()
if line.startswith('#') or '=' not in line:
continue
key, value = line.split('=', 1)
result[key] = value
return result
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='render_templates.py',
description='A script to render the docker jinja templates.',
add_help=True)
parser.add_argument('template_path',
help='The path to the template(s) to render (Can be either a directory or a file).')
parser.add_argument('var_file_path',
help='The path to the file containing the variables to use for rendering the tempalte(s).')
parser.add_argument('out_path',
help='The path to write the rendered template (MUST be a directory).')
parser.add_argument('-k',
'--kwargs',
required=False,
type=str,
default='',
help='Additional kwargs to include if needed (format: key1=value1,key2=value2...).')
parser.add_argument('-o',
'--overwrite',
action='store_true',
help='If set the previous template will be overwritten.')
args = parser.parse_args()
# make sure we have a template file and a variable file.
if not os.path.exists(args.template_path):
print(f'The given template path {args.template_path} does not exist.')
exit(1)
if not os.path.exists(args.var_file_path):
print(f'The given template path {args.var_file_path} does not exist.')
exit(1)
run_on_dir = False
# if the template path is a directory, make sure output path exists
if os.path.isdir(args.template_path):
run_on_dir = True
if not os.path.exists(args.out_path):
os.makedirs(args.out_path)
else:
print('The output path must be a directory since the template path is.')
exit(1)
args.template_path = [os.path.join(args.template_path, p)
for p in filter(lambda x: x.ends_with('.tmpl'),
os.listdir(args.template_path))]
# if the tempalte path is a file
else:
# wrap it in a list
args.template_path = [args.template_path]
# load the variables from the variable file
kwargs = load_template_vars_file(args.var_file_path)
args.kwargs = {a.split('=')[0]: a.split('=')[1] for a in args.kwargs.split(',')} if args.kwargs else {}
for k in kwargs:
# The assumption made here is that anything passed on the commandline should be prioritized
# over values that appear in the template variables file.
if k not in args.kwargs:
args.kwargs[k] = kwargs[k]
for template in args.template_path:
out_file_name = os.path.join(args.out_path,
os.path.split(template.replace('.tmpl', ''))[1])
# if the output exists and we aren't overwritting files, then move on.
if os.path.exists(out_file_name) and not args.overwrite:
continue
try:
render_template(template, out_file_name, **args.kwargs)
except Exception as ex:
print(f'Failed to render template "{template}":')
print(f'Error: {str(ex)}\n{traceback.format_exec()}')

BIN
secrets

Binary file not shown.