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 .api import api_blueprint
|
||||
|
||||
mre_blueprint = Blueprint('mre_blueprint',
|
||||
__name__,
|
||||
url_prefix='mre')
|
||||
mre_blueprint.register_blueprint(api_blueprint)
|
||||
blueprint = Blueprint('mre', __name__)
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@ -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.setFormatter(formatter)
|
||||
logger.addHandler(to_file)
|
||||
|
||||
|
||||
app.logger = logger
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@ -30,5 +33,31 @@ def create_app(app_name: str='MRE') -> Flask:
|
||||
app = init_logger(app)
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
nginx:
|
||||
networks:
|
||||
- frontend
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
@ -10,12 +12,17 @@ services:
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d # Mount Nginx configuration
|
||||
- ./static:/var/www/static # Serve static files
|
||||
depends_on:
|
||||
- uwsgi
|
||||
- mre_api
|
||||
|
||||
uwsgi:
|
||||
image: python:3.9-slim-buster # Use a slim Python image
|
||||
mre_api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/mre.Dockerfile
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
volumes:
|
||||
- ./app:/app # Mount your Flask app directory
|
||||
- ./app:/var/www/app # Mount your Flask app directory
|
||||
- ./uwsgi.ini:/etc/uwsgi.ini # UWSGI configuration
|
||||
command: uwsgi --ini /etc/uwsgi.ini
|
||||
expose:
|
||||
@ -23,27 +30,43 @@ services:
|
||||
environment:
|
||||
- FLASK_APP=run.py # Adjust this based on your Flask app's entry point
|
||||
- FLASK_ENV=dev # Dev environment
|
||||
- MRE_POSTGRES_PASSWORD={{ POSTGRES_PASSWD }}
|
||||
- MRE_POSTGRES_USER={{ POSTGRES_USER }}
|
||||
- MRE_REDIS_PASSWORD={{ REDIS_PASSWD }}
|
||||
depends_on:
|
||||
- redis # Ensure Redis is running before UWSGI starts
|
||||
|
||||
- redis
|
||||
- postgres
|
||||
|
||||
redis:
|
||||
networks:
|
||||
- backend
|
||||
image: redis:latest
|
||||
ports:
|
||||
- "6379:6379" # Expose Redis port (for debugging/accessing from outside)
|
||||
volumes:
|
||||
- redis_data:/data # Persist Redis data
|
||||
command: redis-server --save 20 1 --loglevel {{ LOG_LEGEL }} --requirepass {{ REDIS_PASSWD }}
|
||||
|
||||
postgres:
|
||||
networks:
|
||||
- backend
|
||||
image: postgres:latest
|
||||
ports:
|
||||
- "5432:5432" # Expose PostgreSQL port (for debugging/admin)
|
||||
environment:
|
||||
- POSTGRES_USER=youruser # Replace with your desired username
|
||||
- POSTGRES_PASSWORD=yourpassword # Replace with your desired password
|
||||
- POSTGRES_USER={{ POSTGRES_USER }} # Replace with your desired username
|
||||
- POSTGRES_PASSWORD={{ POSTGRES_PASSWD }} # Replace with your desired password
|
||||
- POSTGRES_DB=yourdb # Replace with your desired database name
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data # Persist PostgreSQL data
|
||||
|
||||
volumes:
|
||||
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-restx==1.3.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()
|
||||
|
||||
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