[add] Init from base
This commit is contained in:
parent
69f8b5a79f
commit
70560bb34e
111
.gitignore
vendored
Normal file
111
.gitignore
vendored
Normal 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
43
Dockerfile
Normal 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
70
app.py
Normal 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
1
database/.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
Dockerfile
|
7
database/Dockerfile
Normal file
7
database/Dockerfile
Normal 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
9
database/docker-init.sh
Executable 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
50
docker-compose.yml
Normal 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
1865
docker/redis/redis.conf
Normal file
File diff suppressed because it is too large
Load Diff
1
migrations/README
Normal file
1
migrations/README
Normal file
@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
50
migrations/alembic.ini
Normal file
50
migrations/alembic.ini
Normal 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
90
migrations/env.py
Normal 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
24
migrations/script.py.mako
Normal 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"}
|
1
paraphrasing_bot/__init__.py
Normal file
1
paraphrasing_bot/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
__version__ = '0.1.0'
|
82
paraphrasing_bot/app.py
Normal file
82
paraphrasing_bot/app.py
Normal 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)
|
0
paraphrasing_bot/src/applications/__init__.py
Normal file
0
paraphrasing_bot/src/applications/__init__.py
Normal file
1
paraphrasing_bot/src/domain/constants/App.py
Normal file
1
paraphrasing_bot/src/domain/constants/App.py
Normal file
@ -0,0 +1 @@
|
||||
TEMPLATES_DISK_PATH = '/etc/paraphrasing_bot/templates'
|
@ -0,0 +1,2 @@
|
||||
class HandledWithMessageException(Exception):
|
||||
pass
|
14
paraphrasing_bot/src/repositories/BaseModel.py
Normal file
14
paraphrasing_bot/src/repositories/BaseModel.py
Normal 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)
|
47
paraphrasing_bot/src/repositories/mixin/SerializeMixin.py
Normal file
47
paraphrasing_bot/src/repositories/mixin/SerializeMixin.py
Normal 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
|
22
paraphrasing_bot/src/repositories/mixin/TimestampsMixin.py
Normal file
22
paraphrasing_bot/src/repositories/mixin/TimestampsMixin.py
Normal 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)
|
8
paraphrasing_bot/src/routes/generic.py
Normal file
8
paraphrasing_bot/src/routes/generic.py
Normal 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!"})
|
27
paraphrasing_bot/src/services/Config.py
Normal file
27
paraphrasing_bot/src/services/Config.py
Normal 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"])
|
10
paraphrasing_bot/src/services/Queue.py
Normal file
10
paraphrasing_bot/src/services/Queue.py
Normal 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
|
41
paraphrasing_bot/src/services/Redis.py
Normal file
41
paraphrasing_bot/src/services/Redis.py
Normal 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
|
10
paraphrasing_bot/src/services/RequestsBearerAuth.py
Normal file
10
paraphrasing_bot/src/services/RequestsBearerAuth.py
Normal 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
|
0
paraphrasing_bot/src/services/__init__.py
Normal file
0
paraphrasing_bot/src/services/__init__.py
Normal file
0
paraphrasing_bot/static/.gitkeep
Normal file
0
paraphrasing_bot/static/.gitkeep
Normal file
0
paraphrasing_bot/templates/.gitkeep
Normal file
0
paraphrasing_bot/templates/.gitkeep
Normal file
208
paraphrasing_bot/templates/verify_account.html
Normal file
208
paraphrasing_bot/templates/verify_account.html
Normal 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’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
29
pyproject.toml
Normal 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
0
tests/__init__.py
Normal file
5
tests/test_paraphrasing_bot.py
Normal file
5
tests/test_paraphrasing_bot.py
Normal file
@ -0,0 +1,5 @@
|
||||
from paraphrasing_bot import __version__
|
||||
|
||||
|
||||
def test_version():
|
||||
assert __version__ == '0.1.0'
|
Loading…
Reference in New Issue
Block a user