commit 855727cb0d8282ae1f695d1ff7c4754e8a9d96c2 Author: zevav Date: Thu Nov 14 11:03:19 2019 +0100 ... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4dd4eb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,127 @@ +# Created by .ignore support plugin (hsz.mobi) +### Python template +# 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/ +pip-wheel-metadata/ +share/python-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/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# 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/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..31217d9 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Microservices with Docker, Flask, and React + +[![Build Status](https://travis-ci.org/zevaverbach/testdriven.svg?branch=master)](https://travis-ci.org/zevaverbach/testdriven) diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml new file mode 100644 index 0000000..b19db9e --- /dev/null +++ b/docker-compose-prod.yml @@ -0,0 +1,34 @@ +version: '3.7' + +services: + users: + build: + context: ./services/users + dockerfile: Dockerfile-prod + expose: + - 5000 + environment: + - FLASK_ENV=production + - APP_SETTINGS=project.config.ProductionConfig + - DATABASE_URL=postgres://postgres:postgres@users-db:5432/users_dev + - DATABASE_TEST_URL=postgres://postgres:postgres@users-db:5432/users_test + depends_on: + - users-db + users-db: + build: + context: ./services/users/project/db + dockerfile: Dockerfile + expose: + - 5432 + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + nginx: + build: + context: ./services/nginx + dockerfile: Dockerfile-prod + restart: always + ports: + - "80:80" + depends_on: + - users diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cc0507c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.7' + +services: + users: + build: + context: ./services/users + dockerfile: Dockerfile + volumes: + - './services/users:/usr/src/app' + ports: + - "5001:5000" + environment: + - FLASK_ENV=development + - APP_SETTINGS=project.config.DevelopmentConfig + - DATABASE_URL=postgres://postgres:postgres@users-db:5432/users_dev + - DATABASE_TEST_URL=postgres://postgres:postgres@users-db:5432/users_test + depends_on: + - users-db + users-db: + build: + context: ./services/users/project/db + dockerfile: Dockerfile + ports: + - "5435:5432" + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + nginx: + build: + context: ./services/nginx + dockerfile: Dockerfile + restart: always + ports: + - "80:80" + depends_on: + - users + diff --git a/services/nginx/Dockerfile b/services/nginx/Dockerfile new file mode 100644 index 0000000..a583ce1 --- /dev/null +++ b/services/nginx/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx:1.15.9-alpine + +RUN rm /etc/nginx/conf.d/default.conf +COPY /dev.conf /etc/nginx/conf.d \ No newline at end of file diff --git a/services/nginx/Dockerfile-prod b/services/nginx/Dockerfile-prod new file mode 100644 index 0000000..4983b55 --- /dev/null +++ b/services/nginx/Dockerfile-prod @@ -0,0 +1,4 @@ +FROM nginx:1.15.9-alpine + +RUN rm /etc/nginx/conf.d/default.conf +COPY /prod.conf /etc/nginx/conf.d \ No newline at end of file diff --git a/services/nginx/__init__.py b/services/nginx/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/nginx/dev.conf b/services/nginx/dev.conf new file mode 100644 index 0000000..793807b --- /dev/null +++ b/services/nginx/dev.conf @@ -0,0 +1,14 @@ +server { + + listen 80; + + location / { + proxy_pass http://users:5000; + proxy_redirect default; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + } + +} diff --git a/services/nginx/prod.conf b/services/nginx/prod.conf new file mode 100644 index 0000000..793807b --- /dev/null +++ b/services/nginx/prod.conf @@ -0,0 +1,14 @@ +server { + + listen 80; + + location / { + proxy_pass http://users:5000; + proxy_redirect default; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + } + +} diff --git a/services/users/.dockerignore b/services/users/.dockerignore new file mode 100644 index 0000000..914fb96 --- /dev/null +++ b/services/users/.dockerignore @@ -0,0 +1,4 @@ +env +.dockerignore +Dockerfile +Dockerfile-prod diff --git a/services/users/.travis.yml b/services/users/.travis.yml new file mode 100644 index 0000000..fc3f7c4 --- /dev/null +++ b/services/users/.travis.yml @@ -0,0 +1,24 @@ +language: python +sudo: required + +services: + - docker + +env: + - DOCKER_COMPOSE_VERSION=1.23.2 + +before_install: + - sudo rm /usr/local/bin/docker-compose + - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose + - chmod +x docker-compose + - sudo mv docker-compose /usr/local/bin + +before_script: + - docker-compose up -d --build + +script: + - docker-compose exec users python manage.py test + - docker-compose exec users flake8 project + +after_script: + - docker-compose down diff --git a/services/users/Dockerfile b/services/users/Dockerfile new file mode 100644 index 0000000..3694d3a --- /dev/null +++ b/services/users/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.8.0-alpine + +RUN apk update && \ + apk add --virtual build-deps gcc python-dev musl-dev && \ + apk add postgresql-dev netcat-openbsd + +WORKDIR /usr/src/app + +COPY ./requirements.txt /usr/src/app/requirements.txt +RUN pip install -r requirements.txt + +COPY ./entrypoint.sh /usr/src/app/entrypoint.sh +RUN chmod +x /usr/src/app/entrypoint.sh + +COPY . /usr/src/app + +CMD ["/usr/src/app/entrypoint.sh"] diff --git a/services/users/Dockerfile-prod b/services/users/Dockerfile-prod new file mode 100644 index 0000000..749f801 --- /dev/null +++ b/services/users/Dockerfile-prod @@ -0,0 +1,17 @@ +FROM python:3.8.0-alpine + +RUN apk update && \ + apk add --virtual build-deps gcc python-dev musl-dev && \ + apk add postgresql-dev netcat-openbsd + +WORKDIR /usr/src/app + +COPY ./requirements.txt /usr/src/app/requirements.txt +RUN pip install -r requirements.txt + +COPY ./entrypoint-prod.sh /usr/src/app/entrypoint-prod.sh +RUN chmod +x /usr/src/app/entrypoint-prod.sh + +COPY . /usr/src/app + +CMD ["/usr/src/app/entrypoint-prod.sh"] diff --git a/services/users/Pipfile b/services/users/Pipfile new file mode 100644 index 0000000..378e84d --- /dev/null +++ b/services/users/Pipfile @@ -0,0 +1,18 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] +flask = "*" +flask-restful = "*" +flask-sqlalchemy = "*" +psycopg2-binary = "*" +flask-testing = "*" +coverage = "*" +flake8 = "*" + +[requires] +python_version = "3.8" diff --git a/services/users/Pipfile.lock b/services/users/Pipfile.lock new file mode 100644 index 0000000..4ce6c2f --- /dev/null +++ b/services/users/Pipfile.lock @@ -0,0 +1,252 @@ +{ + "_meta": { + "hash": { + "sha256": "77ca234305c2fbf685654a912edd73bd100774766adeb44a9cafa01deb77a205" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aniso8601": { + "hashes": [ + "sha256:529dcb1f5f26ee0df6c0a1ee84b7b27197c3c50fc3a6321d66c544689237d072", + "sha256:c033f63d028b9a58e3ab0c2c7d0532ab4bfa7452bfc788fbfe3ddabd327b181a" + ], + "version": "==8.0.0" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "coverage": { + "hashes": [ + "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", + "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", + "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", + "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", + "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", + "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", + "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", + "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", + "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", + "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", + "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", + "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", + "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", + "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", + "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", + "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", + "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", + "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", + "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", + "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", + "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", + "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", + "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", + "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", + "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", + "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", + "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", + "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", + "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", + "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", + "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", + "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025" + ], + "index": "pypi", + "version": "==4.5.4" + }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, + "flake8": { + "hashes": [ + "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", + "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" + ], + "index": "pypi", + "version": "==3.7.9" + }, + "flask": { + "hashes": [ + "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", + "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" + ], + "index": "pypi", + "version": "==1.1.1" + }, + "flask-restful": { + "hashes": [ + "sha256:ecd620c5cc29f663627f99e04f17d1f16d095c83dc1d618426e2ad68b03092f8", + "sha256:f8240ec12349afe8df1db168ea7c336c4e5b0271a36982bff7394f93275f2ca9" + ], + "index": "pypi", + "version": "==0.3.7" + }, + "flask-sqlalchemy": { + "hashes": [ + "sha256:0078d8663330dc05a74bc72b3b6ddc441b9a744e2f56fe60af1a5bfc81334327", + "sha256:6974785d913666587949f7c2946f7001e4fa2cb2d19f4e69ead02e4b8f50b33d" + ], + "index": "pypi", + "version": "==2.4.1" + }, + "flask-testing": { + "hashes": [ + "sha256:dc076623d7d850653a018cb64f500948334c8aeb6b10a5a842bf1bcfb98122bc" + ], + "index": "pypi", + "version": "==0.7.1" + }, + "itsdangerous": { + "hashes": [ + "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", + "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" + ], + "version": "==1.1.0" + }, + "jinja2": { + "hashes": [ + "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", + "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" + ], + "version": "==2.10.3" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + ], + "version": "==1.1.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "psycopg2-binary": { + "hashes": [ + "sha256:040234f8a4a8dfd692662a8308d78f63f31a97e1c42d2480e5e6810c48966a29", + "sha256:086f7e89ec85a6704db51f68f0dcae432eff9300809723a6e8782c41c2f48e03", + "sha256:18ca813fdb17bc1db73fe61b196b05dd1ca2165b884dd5ec5568877cabf9b039", + "sha256:19dc39616850342a2a6db70559af55b22955f86667b5f652f40c0e99253d9881", + "sha256:2166e770cb98f02ed5ee2b0b569d40db26788e0bf2ec3ae1a0d864ea6f1d8309", + "sha256:3a2522b1d9178575acee4adf8fd9f979f9c0449b00b4164bb63c3475ea6528ed", + "sha256:3aa773580f85a28ffdf6f862e59cb5a3cc7ef6885121f2de3fca8d6ada4dbf3b", + "sha256:3b5deaa3ee7180585a296af33e14c9b18c218d148e735c7accf78130765a47e3", + "sha256:407af6d7e46593415f216c7f56ba087a9a42bd6dc2ecb86028760aa45b802bd7", + "sha256:4c3c09fb674401f630626310bcaf6cd6285daf0d5e4c26d6e55ca26a2734e39b", + "sha256:4c6717962247445b4f9e21c962ea61d2e884fc17df5ddf5e35863b016f8a1f03", + "sha256:50446fae5681fc99f87e505d4e77c9407e683ab60c555ec302f9ac9bffa61103", + "sha256:5057669b6a66aa9ca118a2a860159f0ee3acf837eda937bdd2a64f3431361a2d", + "sha256:5dd90c5438b4f935c9d01fcbad3620253da89d19c1f5fca9158646407ed7df35", + "sha256:659c815b5b8e2a55193ede2795c1e2349b8011497310bb936da7d4745652823b", + "sha256:69b13fdf12878b10dc6003acc8d0abf3ad93e79813fd5f3812497c1c9fb9be49", + "sha256:7a1cb80e35e1ccea3e11a48afe65d38744a0e0bde88795cc56a4d05b6e4f9d70", + "sha256:7e6e3c52e6732c219c07bd97fff6c088f8df4dae3b79752ee3a817e6f32e177e", + "sha256:7f42a8490c4fe854325504ce7a6e4796b207960dabb2cbafe3c3959cb00d1d7e", + "sha256:84156313f258eafff716b2961644a4483a9be44a5d43551d554844d15d4d224e", + "sha256:8578d6b8192e4c805e85f187bc530d0f52ba86c39172e61cd51f68fddd648103", + "sha256:890167d5091279a27e2505ff0e1fb273f8c48c41d35c5b92adbf4af80e6b2ed6", + "sha256:98e10634792ac0e9e7a92a76b4991b44c2325d3e7798270a808407355e7bb0a1", + "sha256:9aadff9032e967865f9778485571e93908d27dab21d0fdfdec0ca779bb6f8ad9", + "sha256:9f24f383a298a0c0f9b3113b982e21751a8ecde6615494a3f1470eb4a9d70e9e", + "sha256:a73021b44813b5c84eda4a3af5826dd72356a900bac9bd9dd1f0f81ee1c22c2f", + "sha256:afd96845e12638d2c44d213d4810a08f4dc4a563f9a98204b7428e567014b1cd", + "sha256:b73ddf033d8cd4cc9dfed6324b1ad2a89ba52c410ef6877998422fcb9c23e3a8", + "sha256:b8f490f5fad1767a1331df1259763b3bad7d7af12a75b950c2843ba319b2415f", + "sha256:dbc5cd56fff1a6152ca59445178652756f4e509f672e49ccdf3d79c1043113a4", + "sha256:eac8a3499754790187bb00574ab980df13e754777d346f85e0ff6df929bcd964", + "sha256:eaed1c65f461a959284649e37b5051224f4db6ebdc84e40b5e65f2986f101a08" + ], + "index": "pypi", + "version": "==2.8.4" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pyflakes": { + "hashes": [ + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + ], + "version": "==2.1.1" + }, + "pytz": { + "hashes": [ + "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", + "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" + ], + "version": "==2019.3" + }, + "six": { + "hashes": [ + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + ], + "version": "==1.13.0" + }, + "sqlalchemy": { + "hashes": [ + "sha256:afa5541e9dea8ad0014251bc9d56171ca3d8b130c9627c6cb3681cff30be3f8a" + ], + "version": "==1.3.11" + }, + "werkzeug": { + "hashes": [ + "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", + "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4" + ], + "version": "==0.16.0" + } + }, + "develop": {} +} diff --git a/services/users/entrypoint-prod.sh b/services/users/entrypoint-prod.sh new file mode 100755 index 0000000..2a30ec0 --- /dev/null +++ b/services/users/entrypoint-prod.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +echo "Waiting for postgres..." + +while ! nc -z users-db 5432; do + sleep 0.1 +done + +echo "PostgreSQL started" + +gunicorn -b 0.0.0.0:5000 manage:app diff --git a/services/users/entrypoint.sh b/services/users/entrypoint.sh new file mode 100755 index 0000000..5202bad --- /dev/null +++ b/services/users/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +echo "Waiting for postgres..." + +while ! nc -z users-db 5432; do + sleep 0.1 +done + +echo "PostgreSQL started" + +python manage.py run -h 0.0.0.0 \ No newline at end of file diff --git a/services/users/manage.py b/services/users/manage.py new file mode 100644 index 0000000..27229b0 --- /dev/null +++ b/services/users/manage.py @@ -0,0 +1,66 @@ +import sys +import unittest + +import coverage +from flask.cli import FlaskGroup + +from project import create_app, db +from project.api.models import User + +COV = coverage.coverage( + branch=True, + include='project/*', + omit=[ + 'project/tests/*', + 'project/config.py', + ] +) +COV.start() + +app = create_app() +cli = FlaskGroup(create_app=create_app) + + +@cli.command('recreate_db') +def recreate_all(): + db.drop_all() + db.create_all() + db.session.commit() + + +@cli.command('seed_db') +def seed_db(): + db.drop_all() + db.create_all() + db.session.commit() + db.session.add(User(username='michael', email='hermanmu@gmail.com')) + db.session.add(User(username='michaelherman', email='michael@herman.org')) + db.session.commit() + + +@cli.command() +def test(): + tests = unittest.TestLoader().discover('project/tests', pattern='test*.py') + result = unittest.TextTestRunner(verbosity=2).run(tests) + if result.wasSuccessful(): + return 0 + sys.exit(result) + + +@cli.command() +def cov(): + tests = unittest.TestLoader().discover('project/tests') + result = unittest.TextTestRunner(verbosity=2).run(tests) + if result.wasSuccessful(): + COV.stop() + COV.save() + print('Coverage Summary:') + COV.report() + COV.html_report() + COV.erase() + return 0 + sys.exit(result) + + +if __name__ == "__main__": + cli() diff --git a/services/users/project/__init__.py b/services/users/project/__init__.py new file mode 100644 index 0000000..0143a5f --- /dev/null +++ b/services/users/project/__init__.py @@ -0,0 +1,23 @@ +import os + +from flask import Flask +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + + +def create_app(script_info=None): + app = Flask(__name__) + app_settings = os.getenv('APP_SETTINGS') + app.config.from_object(app_settings) + + db.init_app(app) + + from project.api.users import users_blueprint + app.register_blueprint(users_blueprint) + + @app.shell_context_processor + def ctx(): + return {'app': app, 'db': db} + + return app diff --git a/services/users/project/api/__init__.py b/services/users/project/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/users/project/api/models.py b/services/users/project/api/models.py new file mode 100644 index 0000000..a85c77c --- /dev/null +++ b/services/users/project/api/models.py @@ -0,0 +1,22 @@ +from sqlalchemy import func + +from project import db + + +class User(db.Model): + + __tablename__ = 'users' + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + username = db.Column(db.String, nullable=False) + email = db.Column(db.String, nullable=False) + active = db.Column(db.Boolean, default=True, nullable=False) + created_date = db.Column(db.DateTime, default=func.now(), nullable=False) + + def to_json(self): + return { + 'id': self.id, + 'username': self.username, + 'email': self.email, + 'active': self.active, + } diff --git a/services/users/project/api/templates/index.html b/services/users/project/api/templates/index.html new file mode 100644 index 0000000..1e2be89 --- /dev/null +++ b/services/users/project/api/templates/index.html @@ -0,0 +1,49 @@ + + + + + Flask on Docker + + + + + {% block css %}{% endblock %} + + +
+
+
+

All Users

+

+
+
+ +
+
+ +
+ +
+
+
+ {% if users %} +
    + {% for user in users %} +
  1. {{user.username}}
  2. + {% endfor %} +
+ {% else %} +

No users!

+ {% endif %} +
+
+ {% block js %}{% endblock %} + + \ No newline at end of file diff --git a/services/users/project/api/users.py b/services/users/project/api/users.py new file mode 100644 index 0000000..3a75152 --- /dev/null +++ b/services/users/project/api/users.py @@ -0,0 +1,89 @@ +from flask import Blueprint, request, render_template +from flask_restful import Resource, Api +from sqlalchemy import exc + +from project.api.models import User +from project import db + +users_blueprint = Blueprint('users', __name__, template_folder='./templates') +api = Api(users_blueprint) + + +class UsersPing(Resource): + def get(self): + return { + 'status': 'success', + 'message': 'pong!' + } + + +class UsersList(Resource): + def get(self): + response_object = {'status': 'success', + 'data': { + 'users': [user.to_json() + for user in User.query.all()] + }} + return response_object, 200 + + def post(self): + post_data = request.get_json() + response_object = { + 'status': 'fail', + 'message': 'Invalid payload.', + } + if not post_data: + return response_object, 400 + username = post_data.get('username') + email = post_data.get('email') + try: + user = User.query.filter_by(email=email).first() + if not user: + db.session.add(User(username=username, email=email)) + db.session.commit() + response_object['status'] = 'success' + response_object['message'] = f'{email} was added!' + return response_object, 201 + else: + response_object['message'] = 'Sorry. That email already exists.' + return response_object, 400 + except exc.IntegrityError: + db.session.rollback() + return response_object, 400 + + +class Users(Resource): + def get(self, user_id): + response_object = {'status': 'fail', + 'message': 'User does not exist'} + try: + user = User.query.filter_by(id=int(user_id)).first() + if not user: + return response_object, 404 + else: + response_object = {'status': 'success', + 'data': { + 'id': user.id, + 'username': user.username, + 'email': user.email, + 'active': user.active, + }} + return response_object, 200 + except ValueError: + return response_object, 404 + + +@users_blueprint.route('/', methods=['GET', 'POST']) +def index(): + if request.method == 'POST': + username = request.form['username'] + email = request.form['email'] + db.session.add(User(username=username, email=email)) + db.session.commit() + users = User.query.all() + return render_template('index.html', users=users) + + +api.add_resource(UsersPing, '/users/ping') +api.add_resource(UsersList, '/users') +api.add_resource(Users, '/users/') diff --git a/services/users/project/config.py b/services/users/project/config.py new file mode 100644 index 0000000..d2e25eb --- /dev/null +++ b/services/users/project/config.py @@ -0,0 +1,20 @@ +import os + + +class BaseConfig: + TESTING = False + SQLALCHEMY_TRACK_MODIFICATIONS = False + SECRET_KEY = 'my_precious' + + +class DevelopmentConfig(BaseConfig): + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') + + +class ProductionConfig(BaseConfig): + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') + + +class TestingConfig(BaseConfig): + TESTING = True + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_TEST_URL') diff --git a/services/users/project/db/Dockerfile b/services/users/project/db/Dockerfile new file mode 100644 index 0000000..efffac6 --- /dev/null +++ b/services/users/project/db/Dockerfile @@ -0,0 +1,3 @@ +FROM postgres:11.2-alpine + +ADD create.sql /docker-entrypoint-initdb.d diff --git a/services/users/project/db/create.sql b/services/users/project/db/create.sql new file mode 100644 index 0000000..dc1815a --- /dev/null +++ b/services/users/project/db/create.sql @@ -0,0 +1,3 @@ +CREATE DATABASE users_prod; +CREATE DATABASE users_dev; +CREATE DATABASE users_test; diff --git a/services/users/project/tests/__init__.py b/services/users/project/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/users/project/tests/base.py b/services/users/project/tests/base.py new file mode 100644 index 0000000..3fd8c07 --- /dev/null +++ b/services/users/project/tests/base.py @@ -0,0 +1,19 @@ +from flask_testing import TestCase + +from project import create_app, db + +app = create_app() + + +class BaseTestCase(TestCase): + def create_app(self): + app.config.from_object('project.config.TestingConfig') + return app + + def setUp(self): + db.create_all() + db.session.commit() + + def tearDown(self): + db.session.remove() + db.drop_all() diff --git a/services/users/project/tests/test_config.py b/services/users/project/tests/test_config.py new file mode 100644 index 0000000..1a352fb --- /dev/null +++ b/services/users/project/tests/test_config.py @@ -0,0 +1,52 @@ +import os +import unittest + +from flask import current_app +from flask_testing import TestCase + +from project import create_app + +app = create_app() + + +class TestDevelopmentConfig(TestCase): + def create_app(self): + app.config.from_object('project.config.DevelopmentConfig') + return app + + def test_app_is_development(self): + self.assertTrue(app.config['SECRET_KEY'] == 'my_precious') + self.assertFalse(current_app is None) + self.assertTrue( + app.config['SQLALCHEMY_DATABASE_URI'] == + os.environ.get('DATABASE_URL') + ) + + +class TestTestingConfig(TestCase): + def create_app(self): + app.config.from_object('project.config.TestingConfig') + return app + + def test_app_is_testing(self): + self.assertTrue(app.config['SECRET_KEY'] == 'my_precious') + self.assertTrue(app.config['TESTING']) + self.assertFalse(app.config['PRESERVE_CONTEXT_ON_EXCEPTION']) + self.assertTrue( + app.config['SQLALCHEMY_DATABASE_URI'] == + os.environ.get('DATABASE_TEST_URL') + ) + + +class TestProductionConfig(TestCase): + def create_app(self): + app.config.from_object('project.config.ProductionConfig') + return app + + def test_app_is_testing(self): + self.assertTrue(app.config['SECRET_KEY'] == 'my_precious') + self.assertFalse(app.config['TESTING']) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/users/project/tests/test_users.py b/services/users/project/tests/test_users.py new file mode 100644 index 0000000..934a448 --- /dev/null +++ b/services/users/project/tests/test_users.py @@ -0,0 +1,148 @@ +import json +import unittest + +from project import db +from project.api.models import User +from project.tests.base import BaseTestCase + + +def add_user(username, email): + user = User(username=username, email=email) + db.session.add(user) + db.session.commit() + return user + + +class TestUserService(BaseTestCase): + + def test_all_users(self): + add_user('michael', 'michael@mherman.org') + add_user('fletcher', 'fletcher@notreal.com') + with self.client: + response = self.client.get(f'/users') + data = json.loads(response.data.decode()) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(data['data']['users']), 2) + self.assertIn('michael', data['data']['users'][0]['username']) + self.assertIn('michael@mherman.org', data['data']['users'][0]['email']) + self.assertIn('fletcher', data['data']['users'][1]['username']) + self.assertIn('fletcher@notreal.com', data['data']['users'][1]['email']) + self.assertIn('success', data['status']) + + def test_main_users(self): + response = self.client.get('/') + self.assertEqual(response.status_code, 200) + self.assertIn(b'All Users', response.data) + self.assertIn(b'

No users!

', response.data) + + def test_main_with_users(self): + add_user('michael', 'michael@mherman.org') + add_user('fletcher', 'fletcher@notreal.com') + with self.client: + response = self.client.get('/') + self.assertEqual(response.status_code, 200) + self.assertIn(b'All Users', response.data) + self.assertNotIn(b'

No users!

', response.data) + self.assertIn(b'michael', response.data) + self.assertIn(b'fletcher', response.data) + + def test_main_add_user(self): + with self.client: + response = self.client.post('/', + data={ + 'username': 'michael', + 'email': 'michael@sonotreal.com', + }, + follow_redirects=True) + self.assertEqual(response.status_code, 200) + self.assertIn(b'All Users', response.data) + self.assertNotIn(b'

No users!

', response.data) + self.assertIn(b'michael', response.data) + + def test_users(self): + response = self.client.get('/users/ping') + data = json.loads(response.data.decode()) + self.assertEqual(response.status_code, 200) + self.assertIn('pong!', data['message']) + self.assertIn('success', data['status']) + + def test_single_user(self): + user = add_user('michael', 'michael@mherman.org') + with self.client: + response = self.client.get(f'/users/{user.id}') + data = json.loads(response.data.decode()) + self.assertEqual(response.status_code, 200) + self.assertIn('michael', data['data']['username']) + self.assertIn('michael@mherman.org', data['data']['email']) + self.assertIn('success', data['status']) + + def test_single_user_no_id(self): + with self.client: + response = self.client.get(f'/users/blah') + data = json.loads(response.data.decode()) + self.assertEqual(response.status_code, 404) + self.assertIn('User does not exist', data['message']) + self.assertIn('fail', data['status']) + + def test_single_user_incorrect_id(self): + with self.client: + response = self.client.get(f'/users/999') + data = json.loads(response.data.decode()) + self.assertEqual(response.status_code, 404) + self.assertIn('User does not exist', data['message']) + self.assertIn('fail', data['status']) + + def test_add_user(self): + with self.client: + response = self.client.post('/users', + data=json.dumps({ + 'username': 'michael', + 'email': 'michael@mherman.org', + }), + content_type='application/json') + data = json.loads(response.data.decode()) + self.assertEqual(response.status_code, 201) + self.assertIn('michael@mherman.org was added!', data['message']) + self.assertIn('success', data['status']) + + def test_add_user_invalid_json(self): + with self.client: + response = self.client.post('/users', + data=json.dumps({}), + content_type='application/json') + data = json.loads(response.data.decode()) + self.assertEqual(response.status_code, 400) + self.assertIn('Invalid payload.', data['message']) + self.assertIn('fail', data['status']) + + def test_add_user_invalid_json_keys(self): + with self.client: + response = self.client.post('/users', + data=json.dumps({'email': 'michael@mherman.org'}), + content_type='application/json') + data = json.loads(response.data.decode()) + self.assertEqual(response.status_code, 400) + self.assertIn('Invalid payload.', data['message']) + self.assertIn('fail', data['status']) + + def test_add_user_duplicate_email(self): + with self.client: + self.client.post('/users', + data=json.dumps({'username': 'michael', + 'email': 'michael@mherman.org', + }), + content_type='application/json') + response = self.client.post('/users', + data=json.dumps({ + 'username': 'michael', + 'email': 'michael@mherman.org', + }), + content_type='application/json') + data = json.loads(response.data.decode()) + self.assertEqual(response.status_code, 400) + self.assertIn('Sorry. That email already exists.', data['message']) + self.assertIn('fail', data['status']) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/users/requirements.txt b/services/users/requirements.txt new file mode 100644 index 0000000..d41ece1 --- /dev/null +++ b/services/users/requirements.txt @@ -0,0 +1,21 @@ +-i https://pypi.org/simple +aniso8601==8.0.0 +click==7.0 +coverage==4.5.4 +entrypoints==0.3 +flake8==3.7.9 +flask-restful==0.3.7 +flask-sqlalchemy==2.4.1 +flask-testing==0.7.1 +flask==1.1.1 +itsdangerous==1.1.0 +jinja2==2.10.3 +markupsafe==1.1.1 +mccabe==0.6.1 +psycopg2-binary==2.8.4 +pycodestyle==2.5.0 +pyflakes==2.1.1 +pytz==2019.3 +six==1.13.0 +sqlalchemy==1.3.11 +werkzeug==0.16.0 diff --git a/services/users/setup.cfg b/services/users/setup.cfg new file mode 100644 index 0000000..049c871 --- /dev/null +++ b/services/users/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +max-line-length=99 \ No newline at end of file