diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..54e6276 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +# Ignore .git +.git + +# Ignore Test and Coverage Outputs +.htmlcov +.coverage +tests/.pytest* + +# Ignore Configuration and JS/CSS source Files +config/ + +# Ignore Databases +*.db + +# Ignore node_packages +node_modules/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..83634bc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +FROM node:lts-alpine AS builder +WORKDIR /app + +COPY sigl sigl +COPY src src +COPY Makefile ./ +COPY package.json ./ +COPY tailwind.config.js ./ + +RUN apk add build-base \ + && npm install \ + && make css + +FROM python:3.8-slim-buster + +RUN groupadd -g 5151 sigl \ + && adduser --disabled-password --uid 5151 --gid 5151 sigl + +WORKDIR /home/sigl + +RUN apt-get update \ + && apt-get -y upgrade \ + && apt-get -y install --no-install-recommends build-essential \ + libpq-dev libmariadbclient-dev + +COPY requirements.txt ./ +RUN python -m venv venv \ + && venv/bin/pip install -r requirements.txt \ + && venv/bin/pip install gunicorn + +COPY sigl sigl +COPY migrations migrations +COPY docker/* ./ +COPY pyproject.toml ./ + +COPY --from=builder /app/static ./static + +RUN mkdir -p /var/lib/sigl \ + && chown -R sigl:sigl /var/lib/sigl ./ \ + && chmod +x docker-entry.sh + +USER sigl +EXPOSE 5151 +ENTRYPOINT [ "/home/sigl/docker-entry.sh" ] +CMD [ "sigl" ] diff --git a/Makefile b/Makefile index bd48c82..e246aa2 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,12 @@ db-downgrade : lint : poetry run flake8 +requirements.txt : poetry.lock + poetry export -f requirements.txt --without-hashes -o requirements.txt + +requirements-dev.txt : poetry.lock + poetry export --dev -f requirements.txt --without-hashes -o requirements-dev.txt + serve : poetry run python -m sigl @@ -42,6 +48,8 @@ test-x : test-wip : poetry run python -m pytest tests -m wip -.PHONY : db-init db-migrate db-upgrad db-downgrade \ +.PHONY : css \ + db-init db-migrate db-upgrad db-downgrade \ lint shell serve \ - test test-wip test-x \ No newline at end of file + requirements.txt requirements-dev.txt \ + test test-wip test-x diff --git a/docker/config.py b/docker/config.py new file mode 100644 index 0000000..ee8d225 --- /dev/null +++ b/docker/config.py @@ -0,0 +1,24 @@ +"""Sigl Docker Default Configuration. + +Simple Grocery List (Sigl) | sigl.app +Copyright (c) 2022 Asymworks, LLC. All Rights Reserved. +""" + +# Session and Token Keys +APP_SESSION_KEY = 'sigl-docker-session-key' +APP_TOKEN_KEY = 'sigl-docker-token-key' + +# Database Settings +DB_DRIVER = 'sqlite' +DB_FILE = '/var/lib/sigl/sigl-test.db' + +# Mail Configuration +MAIL_ENABLED = False + +# Logging Configuration +LOGGING_DEST = 'wsgi' +LOGGING_LEVEL = 'debug' +LOGGING_FORMAT = '[%(asctime)s] [%(remote_addr)s - %(url)s] %(levelname)s in %(module)s: %(message)s' +LOGGING_BACKTRACE = False +SOCKETIO_LOGGING = True +ENGINEIO_LOGGING = True diff --git a/docker/docker-entry.sh b/docker/docker-entry.sh new file mode 100644 index 0000000..2445193 --- /dev/null +++ b/docker/docker-entry.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +export FLASK_APP=sigl.factory +export FLASK_ENV=production +export SIGL_CONFIG=${SIGL_CONFIG:-/home/sigl/config.py} +export SIGL_PORT=${SIGL_PORT:-5151} + +source venv/bin/activate + +if [ "$1" = 'sigl' ]; then + exec gunicorn -k eventlet -b :${SIGL_PORT} --access-logfile - --error-logfile - sigl.wsgi:app +fi + +if [ "$1" = 'db' ]; then + if [ "$2" = 'downgrade' ]; then + exec flask db downgrade + else + exec flask db upgrade + fi + + exit 0 +fi + +exec "$@" diff --git a/poetry.lock b/poetry.lock index 6d67de6..58bfc72 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,6 @@ [[package]] name = "alembic" -version = "1.8.0" +version = "1.8.1" description = "A database migration tool for SQLAlchemy." category = "main" optional = false @@ -84,7 +84,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.4.1" +version = "6.4.2" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -95,30 +95,26 @@ toml = ["tomli"] [[package]] name = "dnspython" -version = "2.2.1" +version = "1.16.0" description = "DNS toolkit" category = "main" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] -dnssec = ["cryptography (>=2.6,<37.0)"] -curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] -doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"] -idna = ["idna (>=2.1,<4.0)"] -trio = ["trio (>=0.14,<0.20)"] -wmi = ["wmi (>=1.5.1,<2.0.0)"] +DNSSEC = ["pycryptodome", "ecdsa (>=0.13)"] +IDNA = ["idna (>=2.1)"] [[package]] name = "eventlet" -version = "0.33.1" +version = "0.30.2" description = "Highly concurrent networking library" category = "main" optional = false python-versions = "*" [package.dependencies] -dnspython = ">=1.15.0" +dnspython = ">=1.15.0,<2.0.0" greenlet = ">=0.3" six = ">=1.10.0" @@ -137,7 +133,7 @@ pyflakes = ">=2.4.0,<2.5.0" [[package]] name = "flask" -version = "2.1.2" +version = "2.1.3" description = "A simple framework for building complex web applications." category = "main" optional = false @@ -154,18 +150,6 @@ Werkzeug = ">=2.0" async = ["asgiref (>=3.2)"] dotenv = ["python-dotenv"] -[[package]] -name = "flask-cors" -version = "3.0.10" -description = "A Flask extension adding a decorator for CORS support" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -Flask = ">=0.9" -Six = "*" - [[package]] name = "flask-mail" version = "0.9.1" @@ -310,23 +294,6 @@ category = "main" optional = false python-versions = ">=3.7" -[[package]] -name = "marshmallow" -version = "3.17.0" -description = "A lightweight library for converting complex datatypes to and from native Python datatypes." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -packaging = ">=17.0" - -[package.extras] -dev = ["pytest", "pytz", "simplejson", "mypy (==0.961)", "flake8 (==4.0.1)", "flake8-bugbear (==22.6.22)", "pre-commit (>=2.4,<3.0)", "tox"] -docs = ["sphinx (==4.5.0)", "sphinx-issues (==3.0.1)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.8)"] -lint = ["mypy (==0.961)", "flake8 (==4.0.1)", "flake8-bugbear (==22.6.22)", "pre-commit (>=2.4,<3.0)"] -tests = ["pytest", "pytz", "simplejson"] - [[package]] name = "mccabe" version = "0.6.1" @@ -339,7 +306,7 @@ python-versions = "*" name = "packaging" version = "21.3" description = "Core utilities for Python packages" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -386,7 +353,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "pyparsing" version = "3.0.9" description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" +category = "dev" optional = false python-versions = ">=3.6.8" @@ -503,20 +470,20 @@ watchdog = ["watchdog"] [[package]] name = "zipp" -version = "3.8.0" +version = "3.8.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "9a061ae1f8fa92a76f3d49706e062795362cdeb6f9b6067105f7dc6227e66018" +content-hash = "3679bc9fc39e1e9f7160045f491258dfb6fce1b57b2b7963f11a636ee26106d9" [metadata.files] alembic = [] @@ -533,17 +500,16 @@ codespell = [ ] colorama = [] coverage = [] -dnspython = [] +dnspython = [ + {file = "dnspython-1.16.0-py2.py3-none-any.whl", hash = "sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"}, + {file = "dnspython-1.16.0.zip", hash = "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01"}, +] eventlet = [] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, ] flask = [] -flask-cors = [ - {file = "Flask-Cors-3.0.10.tar.gz", hash = "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"}, - {file = "Flask_Cors-3.0.10-py2.py3-none-any.whl", hash = "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438"}, -] flask-mail = [ {file = "Flask-Mail-0.9.1.tar.gz", hash = "sha256:22e5eb9a940bf407bcf30410ecc3708f3c56cc44b29c34e1726fe85006935f41"}, ] @@ -561,7 +527,6 @@ itsdangerous = [] jinja2 = [] mako = [] markupsafe = [] -marshmallow = [] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, diff --git a/pyproject.toml b/pyproject.toml index 4c847eb..b2f11c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,23 +7,21 @@ license = "BSD-3-Clause" [tool.poetry.dependencies] python = "^3.9" -Flask = "^2.1.2" -Flask-SQLAlchemy = "^2.5.1" -Flask-Mail = "^0.9.1" -Flask-SocketIO = "^5.2.0" -eventlet = "^0.33.1" -Flask-Cors = "^3.0.10" alembic = "^1.8.0" -SQLAlchemy = "^1.4.39" +eventlet = "0.30.2" +Flask = "^2.1.2" +Flask-Mail = "^0.9.1" Flask-Migrate = "^3.1.0" -marshmallow = "^3.17.0" +Flask-SocketIO = "^5.2.0" +Flask-SQLAlchemy = "^2.5.1" +SQLAlchemy = "^1.4.39" [tool.poetry.dev-dependencies] +codespell = "^2.1.0" coverage = "^6.4.1" flake8 = "^4.0.1" -pytest = "^7.1.2" isort = "^5.10.1" -codespell = "^2.1.0" +pytest = "^7.1.2" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..4cd8e71 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,40 @@ +alembic==1.8.1; python_version >= "3.7" +atomicwrites==1.4.1; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.4.0" +attrs==21.4.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" +bidict==0.22.0; python_version >= "3.7" +blinker==1.4 +click==8.1.3; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" +codespell==2.1.0; python_version >= "3.5" +colorama==0.4.5; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" and platform_system == "Windows" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0" and platform_system == "Windows" +coverage==6.4.2; python_version >= "3.7" +dnspython==1.16.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" +eventlet==0.30.2 +flake8==4.0.1; python_version >= "3.6" +flask-mail==0.9.1 +flask-migrate==3.1.0; python_version >= "3.6" +flask-socketio==5.2.0; python_version >= "3.6" +flask-sqlalchemy==2.5.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") +flask==2.1.3; python_version >= "3.7" +greenlet==1.1.2; python_version >= "3" and python_full_version < "3.0.0" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") or python_version >= "3" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") and python_full_version >= "3.5.0" +importlib-metadata==4.12.0; python_version < "3.10" and python_version >= "3.7" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7") +iniconfig==1.1.1; python_version >= "3.7" +isort==5.10.1; python_full_version >= "3.6.1" and python_version < "4.0" +itsdangerous==2.1.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" +jinja2==3.1.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" +mako==1.2.1; python_version >= "3.7" +markupsafe==2.1.1; python_version >= "3.7" +mccabe==0.6.1; python_version >= "3.6" +packaging==21.3; python_version >= "3.7" +pluggy==1.0.0; python_version >= "3.7" +py==1.11.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" +pycodestyle==2.8.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +pyflakes==2.4.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +pyparsing==3.0.9; python_full_version >= "3.6.8" and python_version >= "3.7" +pytest==7.1.2; python_version >= "3.7" +python-engineio==4.3.3; python_version >= "3.6" +python-socketio==5.7.0; python_version >= "3.6" +six==1.16.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" +sqlalchemy==1.4.39; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +tomli==2.0.1; python_version >= "3.7" +werkzeug==2.1.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" +zipp==3.8.1; python_version < "3.10" and python_version >= "3.7" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..753862b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,24 @@ +alembic==1.8.1; python_version >= "3.7" +bidict==0.22.0; python_version >= "3.7" +blinker==1.4 +click==8.1.3; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" +colorama==0.4.5; python_version >= "3.7" and python_full_version < "3.0.0" and platform_system == "Windows" or platform_system == "Windows" and python_version >= "3.7" and python_full_version >= "3.5.0" +dnspython==1.16.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" +eventlet==0.30.2 +flask-mail==0.9.1 +flask-migrate==3.1.0; python_version >= "3.6" +flask-socketio==5.2.0; python_version >= "3.6" +flask-sqlalchemy==2.5.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") +flask==2.1.3; python_version >= "3.7" +greenlet==1.1.2; python_version >= "3" and python_full_version < "3.0.0" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") or python_version >= "3" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") and python_full_version >= "3.5.0" +importlib-metadata==4.12.0; python_version < "3.10" and python_version >= "3.7" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7") +itsdangerous==2.1.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" +jinja2==3.1.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" +mako==1.2.1; python_version >= "3.7" +markupsafe==2.1.1; python_version >= "3.7" +python-engineio==4.3.3; python_version >= "3.6" +python-socketio==5.7.0; python_version >= "3.6" +six==1.16.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" +sqlalchemy==1.4.39; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +werkzeug==2.1.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" +zipp==3.8.1; python_version < "3.10" and python_version >= "3.7" diff --git a/sigl/templates/base.html.j2 b/sigl/templates/base.html.j2 index dc615be..215b40e 100644 --- a/sigl/templates/base.html.j2 +++ b/sigl/templates/base.html.j2 @@ -8,7 +8,7 @@ {% block head_scripts %}{% endblock %} {% if config['ENV'] == 'production' %} - + {% else %} {% endif %} @@ -52,6 +52,10 @@
{% block main %}{% endblock %}
+ {% block body_scripts %}{% endblock %}
{% for e in list.entries %} - {{ listEntry(e, last=list.last) }} + {{ listEntry(e, last=loop.last) }} {% endfor %} {% elif sortBy == 'category' %} diff --git a/sigl/wsgi.py b/sigl/wsgi.py new file mode 100644 index 0000000..f6b31b8 --- /dev/null +++ b/sigl/wsgi.py @@ -0,0 +1,10 @@ +"""Sigl WSGI Entry Point. + +Simple Grocery List (Sigl) | sigl.app +Copyright (c) 2022 Asymworks, LLC. All Rights Reserved. +""" + +from .factory import create_app + + +app = create_app()