This commit is contained in:
2019-11-14 11:03:19 +01:00
commit 855727cb0d
32 changed files with 1108 additions and 0 deletions

127
.gitignore vendored Normal file
View File

@@ -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/

3
README.md Normal file
View File

@@ -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)

34
docker-compose-prod.yml Normal file
View File

@@ -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

37
docker-compose.yml Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

14
services/nginx/dev.conf Normal file
View File

@@ -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;
}
}

14
services/nginx/prod.conf Normal file
View File

@@ -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;
}
}

View File

@@ -0,0 +1,4 @@
env
.dockerignore
Dockerfile
Dockerfile-prod

View File

@@ -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

17
services/users/Dockerfile Normal file
View File

@@ -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"]

View File

@@ -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"]

18
services/users/Pipfile Normal file
View File

@@ -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"

252
services/users/Pipfile.lock generated Normal file
View File

@@ -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": {}
}

View File

@@ -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

11
services/users/entrypoint.sh Executable file
View File

@@ -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

66
services/users/manage.py Normal file
View File

@@ -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()

View File

@@ -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

View File

View File

@@ -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,
}

View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Flask on Docker</title>
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="//cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css"
rel="stylesheet"
>
{% block css %}{% endblock %}
</head>
<body>
<div class="container">
<div class="column is-one-third">
<br>
<h1 class="title">All Users</h1>
<hr><br>
<form action="/" method="POST">
<div class="field">
<input
name="username" class="input" type="text"
placeholder="Enter a username" required>
</div>
<div class="field">
<input
name="email" class="input" type="email"
placeholder="Enter an email address" required>
</div>
<input
type="submit" class="button is-primary is-fullwidth" value="Submit">
</form>
<br>
<hr>
{% if users %}
<ol>
{% for user in users %}
<li>{{user.username}}</li>
{% endfor %}
</ol>
{% else %}
<p>No users!</p>
{% endif %}
</div>
</div>
{% block js %}{% endblock %}
</body>
</html>

View File

@@ -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/<user_id>')

View File

@@ -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')

View File

@@ -0,0 +1,3 @@
FROM postgres:11.2-alpine
ADD create.sql /docker-entrypoint-initdb.d

View File

@@ -0,0 +1,3 @@
CREATE DATABASE users_prod;
CREATE DATABASE users_dev;
CREATE DATABASE users_test;

View File

View File

@@ -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()

View File

@@ -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()

View File

@@ -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'<p>No users!</p>', 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'<p>No users!</p>', 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'<p>No users!</p>', 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()

View File

@@ -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

2
services/users/setup.cfg Normal file
View File

@@ -0,0 +1,2 @@
[flake8]
max-line-length=99