Get the MRE api working #1
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -1 +1 @@
|
|||||||
/app/secret_key filter=git-crypt diff=git-crypt
|
secrets filter=git-crypt diff=git-crypt
|
||||||
|
|||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "app/mre_module"]
|
||||||
|
path = app/mre_module
|
||||||
|
url = http://10.0.50.3:3002/Quarter/MRE-module.git
|
||||||
@ -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
39
app/MRE/api.py
Normal 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}
|
||||||
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
from flask import Blueprint
|
|
||||||
|
|
||||||
api_blueprint = Blueprint('api_blueprint',
|
|
||||||
__name__,
|
|
||||||
url_prefix='api')
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
12
app/api.py
Normal 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
1
app/mre_module
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit ce94312099a10d3cf273e42d4b315ac17a2f187f
|
||||||
@ -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
3
docker/entrypoint.sh
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#! /usr/bin/bash
|
||||||
|
|
||||||
|
uwsgi --ini /var/www/run.py
|
||||||
27
docker/mre.Dockerfile
Normal file
27
docker/mre.Dockerfile
Normal 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"]
|
||||||
@ -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
2
run.py
@ -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
102
scripts/render_templates.py
Normal 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()}')
|
||||||
Loading…
Reference in New Issue
Block a user