[add] Init from base

This commit is contained in:
Pablo Aramburo 2022-05-08 16:06:16 -06:00
parent 69f8b5a79f
commit 70560bb34e
32 changed files with 2828 additions and 0 deletions

111
.gitignore vendored Normal file
View File

@ -0,0 +1,111 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
.static_storage/
.media/
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
# jetbrains
.idea/*
# Mac stuff
.DS_Store

43
Dockerfile Normal file
View File

@ -0,0 +1,43 @@
FROM python:3.9-alpine3.15
ENV PYTHONUNBUFFERED 1
ENV POETRY_VERSION 1.1.4
WORKDIR /usr/src/app
COPY poetry.lock pyproject.toml /usr/src/app/
RUN \
apk add --no-cache --virtual .build-deps g++ musl-dev libffi-dev openssl-dev python3-dev rust cargo && \
# System deps
apk add --no-cache tzdata postgresql-dev && \
# Dependency manager for python
pip install --no-cache-dir poetry==$POETRY_VERSION && \
# Project initialization:
poetry config virtualenvs.create false && \
poetry install --no-interaction --no-ansi && \
apk --purge del .build-deps
RUN addgroup -S slothgroup && adduser -S container_sloth -G slothgroup
RUN chown container_sloth:slothgroup -R /usr/src/app
RUN mkdir -p /etc/paraphrasing_bot/templates && chown -R container_sloth /etc/paraphrasing_bot/templates
COPY paraphrasing_bot/templates /etc/paraphrasing_bot/templates
VOLUME /etc/paraphrasing_bot/templates
RUN mkdir -p /var/log/paraphrasing_bot && chown -R container_sloth /var/log/paraphrasing_bot
VOLUME /var/log/paraphrasing_bot
RUN mkdir -p /etc/paraphrasing_bot/static && chown -R container_sloth /etc/paraphrasing_bot/static
COPY paraphrasing_bot/static /etc/paraphrasing_bot/static
VOLUME /etc/paraphrasing_bot/static
COPY . .
RUN chmod 755 /usr/src/app
USER container_sloth
HEALTHCHECK --interval=10s --timeout=2s --start-period=15s \
CMD wget --quiet --tries=1 --spider http://localhost:5000/health-check || exit 1
CMD ["gunicorn", "-b", "0.0.0.0:5000", "--reload", "app:app", "w", "2", "--threads", "3"]

70
app.py Normal file
View File

@ -0,0 +1,70 @@
import logging
import sys
import traceback
from dotenv import load_dotenv
from flask import redirect
from flask import request, jsonify, send_file, make_response
from marshmallow import ValidationError
from paraphrasing_bot.app import app
from paraphrasing_bot.src.domain.exceptions.HandledWithMessageException import HandledWithMessageException
from paraphrasing_bot.src.services import Config
from paraphrasing_bot.src.routes.generic import generic_blueprint
# Load the .env file in case that it has been used to set envs
# instead of injecting them through docker
load_dotenv('.env')
app_config = Config.Config()
# Applications
custom_logger = logging.getLogger("paraphrasing_bot")
@app.before_request
def log_request_info():
request_data = {
"headers": dict(request.headers),
"body": request.get_data()
}
custom_logger.debug(request_data)
# Loading blueprints
app.register_blueprint(generic_blueprint)
# Catch all exceptions
# Any exception not caught by the routes above will be handled here
@app.errorhandler(Exception)
def all_exception_handler(error: Exception):
custom_logger.error(error)
error_type = error.__class__.__name__
status_code = 500
message = "Ha ocurrido un error inesperado, por favor intente de nuevo. Si el problema persiste contacte al administrador del sistema."
if isinstance(error, HandledWithMessageException):
message = str(error)
if isinstance(error, ValidationError):
message = str(error)
status_code = 400
response = {
"message": message
}
if app_config.DEV_MODE:
cosa = traceback.format_exc()
custom_logger.debug(cosa)
response["error"] = str(error)
response["stacktrace"] = cosa
return jsonify(response), status_code
if __name__ == "__main__":
app.run(host=app_config.APP_HOST, port=app_config.APP_PORT, debug=app_config.DEV_MODE)

1
database/.dockerignore Normal file
View File

@ -0,0 +1 @@
Dockerfile

7
database/Dockerfile Normal file
View File

@ -0,0 +1,7 @@
FROM postgres:12-alpine
# So that the timezone can be sent with the TZ environment
RUN apk add --no-cache tzdata
# Starting scripts
ADD . /docker-entrypoint-initdb.d

9
database/docker-init.sh Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
cd /docker-entrypoint-initdb.d/
find "$PWD" -type f -name "*.sql" | sort | \
while read l; do
# The env variables are set by docker-compose and taken from the .env file
psql -d $POSTGRES_DB -U $POSTGRES_USER -f "$d/$l";
done

50
docker-compose.yml Normal file
View File

@ -0,0 +1,50 @@
version: '3.7'
services:
api:
container_name: paraphrasing_bot_api
restart: always
build: .
ports:
- "24708:5000"
volumes:
- ./logs:/var/log/paraphrasing_bot
- .:/usr/src/app
- ./paraphrasing_bot/templates:/etc/paraphrasing_bot/templates
env_file:
- .env
depends_on:
- postgres
- redis
postgres:
container_name: paraphrasing_bot_pg
restart: always
build: database
environment:
- TZ=${TZ}
- POSTGRES_PORT=${POSTGRES_PORT}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
# volumes:
# - ~/.volumes/paraphrasing_bot/postgres:/var/lib/postgresql/data:rw
pgadmin:
image: dpage/pgadmin4:latest
container_name: paraphrasing_bot_pgadmin
restart: always
ports:
- "45707:80"
volumes:
- ~/.volumes/paraphrasing_bot/pgadmin:/var/lib/pgadmin:rw
depends_on:
- postgres
environment:
PGADMIN_DEFAULT_EMAIL: 'tools@example.com'
PGADMIN_DEFAULT_PASSWORD: 'admin'
redis:
container_name: paraphrasing_bot_redis
image: redis:6-alpine
restart: always
command: redis-server "/usr/local/etc/redis/redis.conf" --requirepass ${REDIS_PASSWORD} --port ${REDIS_PORT} --bind 0.0.0.0
volumes:
# - ~/.volumes/paraphrasing_bot/redis:/data
- ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf

1865
docker/redis/redis.conf Normal file

File diff suppressed because it is too large Load Diff

1
migrations/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration.

50
migrations/alembic.ini Normal file
View File

@ -0,0 +1,50 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

90
migrations/env.py Normal file
View File

@ -0,0 +1,90 @@
from __future__ import with_statement
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option(
'sqlalchemy.url',
str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
connectable = current_app.extensions['migrate'].db.engine
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Normal file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1 @@
__version__ = '0.1.0'

82
paraphrasing_bot/app.py Normal file
View File

@ -0,0 +1,82 @@
import logging.config
from datetime import timedelta
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_cors import CORS
from flask_jwt_extended import JWTManager
from flask_log_request_id import RequestID, RequestIDLogFilter
from logging.handlers import RotatingFileHandler
from paraphrasing_bot.src.services import Config
app_config = Config.Config()
app = Flask(__name__, static_folder='static')
cors = CORS(app, expose_headers=["Content-Disposition"])
app.config['CORS_HEADERS'] = 'Content-Type'
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://%s:%s@%s:%i/%s' % (
app_config.POSTGRES_USER,
app_config.POSTGRES_PASSWORD,
app_config.POSTGRES_HOST,
app_config.POSTGRES_PORT,
app_config.POSTGRES_DB
)
# Print queries in dev mode
app.config['SQLALCHEMY_ECHO'] = app_config.DEV_MODE
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
'pool_recycle': app_config.SQLALCHEMY_POOL_RECYCLE,
'pool_timeout': app_config.SQLALCHEMY_POOL_TIMEOUT,
'pool_size': app_config.SQLALCHEMY_POOL_SIZE,
'max_overflow': app_config.SQLALCHEMY_POOL_MAX_OVERFLOW,
}
db = SQLAlchemy(app, session_options={'autocommit': True})
migrate = Migrate(app, db, compare_type=True)
# Will add a unique identifier to each request
RequestID(app)
# Initializing the logger object
logger = logging.getLogger()
logger.setLevel(logging.DEBUG if app_config.DEV_MODE else logging.INFO)
# Configuring the log to stream to stdout
stream_handler = logging.StreamHandler()
stream_formatter = logging.Formatter(
'%(asctime)-15s %(request_id)-36s %(levelname)-8s %(message)s')
stream_handler.setFormatter(stream_formatter)
stream_handler.addFilter(RequestIDLogFilter())
logger.addHandler(stream_handler)
# Configuring the log output to a file in json format
file_handler = RotatingFileHandler('/var/log/paraphrasing_bot/app.json', maxBytes=100 * 1024 * 1024, backupCount=10, mode='a',
encoding='utf-8')
file_formatter = logging.Formatter(
'{"timestamp":"%(created)f", '
'"time":"%(asctime)s", '
'"name": "%(name)s", '
'"request_id":"%(request_id)s", '
'"level": "%(levelname)s", '
'"caller_function": "%(funcName)s", '
'"script_path": "%(pathname)s", '
'"message": "%(message)s"}')
file_handler.setFormatter(file_formatter)
file_handler.addFilter(RequestIDLogFilter())
logger.addHandler(file_handler)
# JWT config
app.config["JWT_SECRET_KEY"] = app_config.JWT_SECRET
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=app_config.JWT_ACCESS_HOURS_DURATION)
app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(hours=app_config.JWT_REFRESH_HOURS_DURATION)
app.config["JWT_TOKEN_LOCATION"] = ["headers", "query_string"]
app.config["JWT_QUERY_STRING_NAME"] = "token"
app.config["JWT_ALGORITHM"] = app_config.JWT_ALGORITHM
app.config["JWT_DECODE_ALGORITHMS"] = app_config.JWT_ALGORITHM
jwt = JWTManager(app)

View File

@ -0,0 +1 @@
TEMPLATES_DISK_PATH = '/etc/paraphrasing_bot/templates'

View File

@ -0,0 +1,2 @@
class HandledWithMessageException(Exception):
pass

View File

@ -0,0 +1,14 @@
from sqlalchemy_mixins.repr import ReprMixin
from sqlalchemy_mixins.smartquery import SmartQueryMixin
from sqlalchemy_mixins.activerecord import ActiveRecordMixin
from paraphrasing_bot.app import db
from paraphrasing_bot.src.repositories.mixin.TimestampsMixin import TimestampsMixin
from paraphrasing_bot.src.repositories.mixin.SerializeMixin import SerializeMixin
class BaseModel(db.Model, ReprMixin, SmartQueryMixin, ActiveRecordMixin, TimestampsMixin, SerializeMixin):
__abstract__ = True
BaseModel.set_session(db.session)

View File

@ -0,0 +1,47 @@
import enum
from collections.abc import Iterable
from sqlalchemy_mixins.inspection import InspectionMixin
class SerializeMixin(InspectionMixin):
"""Mixin to make model serializable."""
__abstract__ = True
def to_dict(self, nested=False, hybrid_attributes=False, exclude=None):
"""Return dict object with model's data.
:param nested: flag to return nested relationships' data if true
:type: bool
:param hybrid_attributes: flag to include hybrid attributes if true
:type: bool
:return: dict
"""
result = dict()
if exclude is None:
view_cols = self.columns
else:
view_cols = filter(lambda e: e not in exclude, self.columns)
for key in view_cols:
value = getattr(self, key)
result[key] = getattr(self, key) if not isinstance(value, enum.Enum) else value.value
if hybrid_attributes:
for key in self.hybrid_properties:
result[key] = getattr(self, key)
if nested:
for key in self.relations:
obj = getattr(self, key)
if isinstance(obj, SerializeMixin):
result[key] = obj.to_dict(hybrid_attributes=hybrid_attributes)
elif isinstance(obj, Iterable):
result[key] = [
o.to_dict(hybrid_attributes=hybrid_attributes) for o in obj
if isinstance(o, SerializeMixin)
]
return result

View File

@ -0,0 +1,22 @@
import sqlalchemy as sa
class TimestampsMixin:
"""Mixin that define timestamp columns."""
__abstract__ = True
__created_at_name__ = 'created_at'
__updated_at_name__ = 'updated_at'
__datetime_func__ = sa.func.now()
created_at = sa.Column(__created_at_name__,
sa.TIMESTAMP(timezone=True),
default=__datetime_func__,
nullable=False)
updated_at = sa.Column(__updated_at_name__,
sa.TIMESTAMP(timezone=True),
default=__datetime_func__,
onupdate=__datetime_func__,
nullable=False)

View File

@ -0,0 +1,8 @@
from flask import Blueprint, jsonify
generic_blueprint = Blueprint('generic_blueprint', __name__)
@generic_blueprint.route('/health-check', methods=['GET'])
def health_check():
return jsonify({"message": "Process running!"})

View File

@ -0,0 +1,27 @@
import os
class Config:
TZ = os.getenv("TZ", "America/Mexico_City")
DEV_MODE = bool(os.getenv("DEV_MODE") != "false")
APP_PORT = int(os.getenv("APP_PORT", 5000))
APP_HOST = os.getenv("HOST", "0.0.0.0")
BACKEND_ADDRESS = os.environ["BACKEND_ADDRESS"]
POSTGRES_HOST = os.environ["POSTGRES_HOST"]
POSTGRES_PORT = int(os.environ["POSTGRES_PORT"])
POSTGRES_USER = os.environ["POSTGRES_USER"]
POSTGRES_PASSWORD = os.environ["POSTGRES_PASSWORD"]
POSTGRES_DB = os.environ["POSTGRES_DB"]
SQLALCHEMY_POOL_RECYCLE = int(os.getenv("SQLALCHEMY_POOL_RECYCLE", 90))
SQLALCHEMY_POOL_TIMEOUT = int(os.getenv("SQLALCHEMY_POOL_TIMEOUT", 900))
SQLALCHEMY_POOL_SIZE = int(os.getenv("SQLALCHEMY_POOL_SIZE", 200))
SQLALCHEMY_POOL_MAX_OVERFLOW = int(os.getenv("SQLALCHEMY_POOL_MAX_OVERFLOW", 50))
REDIS_HOST = os.environ["REDIS_HOST"]
REDIS_PASSWORD = os.environ["REDIS_PASSWORD"]
REDIS_PORT = int(os.environ["REDIS_PORT"])
REDIS_DATABASE = os.environ["REDIS_DATABASE"]
REDIS_DEFAULT_TTL = int(os.environ["REDIS_DEFAULT_TTL"])

View File

@ -0,0 +1,10 @@
import queue
def queue_to_list(items_queue: queue) -> list:
items = []
while items_queue.qsize():
current = items_queue.get()
items.append(current)
return items

View File

@ -0,0 +1,41 @@
import logging
import redis
from datetime import timedelta
from paraphrasing_bot.src.services.Config import Config
class Redis:
def __init__(self):
self.app_config = Config()
self.logging = logging.getLogger("redis-service")
self.connection = redis.Redis(
host=self.app_config.REDIS_HOST,
port=self.app_config.REDIS_PORT,
password=self.app_config.REDIS_PASSWORD,
db=self.app_config.REDIS_DATABASE,
decode_responses=True,
health_check_interval=10
)
def save(self, key: str, value: str, ttl: timedelta = None, raise_exception: bool = False):
if not ttl or not isinstance(ttl, timedelta):
ttl = timedelta(seconds=self.app_config.REDIS_DEFAULT_TTL)
try:
self.connection.set(name=key, value=value, ex=ttl)
except redis.ConnectionError as e:
self.logging.error(e)
if raise_exception:
raise e
def get(self, key: str, raise_exception: bool = False):
try:
self.connection.get(name=key)
except redis.ConnectionError as e:
self.logging.error(e)
if raise_exception:
raise e

View File

@ -0,0 +1,10 @@
import requests
class RequestsBearerAuth(requests.auth.AuthBase):
def __init__(self, token):
self.token = token
def __call__(self, r):
r.headers["authorization"] = "Bearer " + self.token
return r

View File

View File

View File

@ -0,0 +1,208 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<style type="text/css">
@media screen {
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
src: local('Lato Regular'), local('Lato-Regular'), url(https://fonts.gstatic.com/s/lato/v11/qIIYRU-oROkIk8vfvxw6QvesZW2xOQ-xsNqO47m55DA.woff) format('woff');
}
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: local('Lato Bold'), local('Lato-Bold'), url(https://fonts.gstatic.com/s/lato/v11/qdgUG4U09HnJwhYI-uK18wLUuEpTyoUstqEm5AMlJo4.woff) format('woff');
}
@font-face {
font-family: 'Lato';
font-style: italic;
font-weight: 400;
src: local('Lato Italic'), local('Lato-Italic'), url(https://fonts.gstatic.com/s/lato/v11/RYyZNoeFgb0l7W3Vu1aSWOvvDin1pK8aKteLpeZ5c0A.woff) format('woff');
}
@font-face {
font-family: 'Lato';
font-style: italic;
font-weight: 700;
src: local('Lato Bold Italic'), local('Lato-BoldItalic'), url(https://fonts.gstatic.com/s/lato/v11/HkF_qI1x_noxlxhrhMQYELO3LdcAZYWl9Si6vvxL-qU.woff) format('woff');
}
}
/* CLIENT-SPECIFIC STYLES */
body,
table,
td,
a {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
/* RESET STYLES */
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
}
table {
border-collapse: collapse !important;
}
body {
height: 100% !important;
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
}
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* MOBILE STYLES */
@media screen and (max-width:600px) {
h1 {
font-size: 32px !important;
line-height: 32px !important;
}
}
/* ANDROID CENTER FIX */
div[style*="margin: 16px 0;"] {
margin: 0 !important;
}
</style>
</head>
<body style="background-color: #f4f4f4; margin: 0 !important; padding: 0 !important;">
<!-- HIDDEN PREHEADER TEXT -->
<div style="display: none; font-size: 1px; color: #fefefe; line-height: 1px; font-family: 'Lato', Helvetica, Arial, sans-serif; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden;"> We're thrilled to have you here! Get ready to dive into your new account. </div>
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<!-- LOGO -->
<tr>
<td bgcolor="#FFA73B" align="center">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
<tr>
<td align="center" valign="top" style="padding: 40px 10px 40px 10px;"></td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor="#FFA73B" align="center" style="padding: 0px 10px 0px 10px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 2;">Welcome {{first_name}}!</h1>
<img src=" https://img.icons8.com/clouds/100/000000/handshake.png" width="125" height="120" style="display: block; border: 0px;" />
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;">We're excited to have you get started. First, you need to confirm your account. Just press the button below.</p>
</td>
</tr>
<tr>
<td bgcolor="#ffffff" align="left">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 3px;" bgcolor="#FFA73B">
<a href="{{verification_url}}" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #FFA73B; display: inline-block;">Confirm Account</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- COPY -->
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 0px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;">If that doesn't work, copy and paste the following link in your browser:</p>
</td>
</tr>
<!-- COPY -->
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 20px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;">
<a href="#" target="_blank" style="color: #FFA73B;">{{verification_url}}</a>
</p>
</td>
</tr>
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 0px 30px 20px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;">If you have any questions, just reply to this email—we're always happy to help out.</p>
</td>
</tr>
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 0px 30px 40px 30px; border-radius: 0px 0px 4px 4px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;">Cheers, <br>BBB Team </p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor="#f4f4f4" align="center" style="padding: 30px 10px 0px 10px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
<tr>
<td bgcolor="#FFECD1" align="center" style="padding: 30px 30px 30px 30px; border-radius: 4px 4px 4px 4px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<h2 style="font-size: 20px; font-weight: 400; color: #111111; margin: 0;">Need more help?</h2>
<p style="margin: 0;">
<a href="#" target="_blank" style="color: #FFA73B;">We&rsquo;re here to help you out</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
<tr>
<td bgcolor="#f4f4f4" align="left" style="padding: 0px 30px 30px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;">
<br>
<p style="margin: 0;">If these emails get annoying, please feel free to <a href="#" target="_blank" style="color: #111111; font-weight: 700;">unsubscribe</a>. </p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

29
pyproject.toml Normal file
View File

@ -0,0 +1,29 @@
[tool.poetry]
name = "paraphrasing-bot"
version = "0.1.0"
description = ""
authors = ["Pablo Aramburo <josepablo.aramburo@laziness.rocks>"]
[tool.poetry.dependencies]
python = "^3.7"
Flask = "^1.1.2"
gunicorn = "^20.0.4"
Flask-Log-Request-ID = "^0.10.1"
Flask-SQLAlchemy = "^2.5.1"
Flask-Migrate = "^2.6.0"
python-dotenv = "^0.15.0"
psycopg2 = "^2.8.6"
marshmallow = "^3.10.0"
Flask-Cors = "^3.0.10"
pendulum = "2.0.5"
requests = "^2.25.1"
redis = "^3.5.3"
python-dateutil = "^2.8.1"
sqlalchemy-mixins = "^1.5"
[tool.poetry.dev-dependencies]
pytest = "^5.2"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

0
tests/__init__.py Normal file
View File

View File

@ -0,0 +1,5 @@
from paraphrasing_bot import __version__
def test_version():
assert __version__ == '0.1.0'