diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..25ec6a0a3e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,26 @@ +// See https://aka.ms/vscode-remote/devcontainer.json for format details. +{ + "name": "electioncal", + "context": "..", + "service": "fastapi", + "dockerComposeFile": [ + "docker-compose.yml" + ], + "workspaceFolder": "/code", + "extensions": [ + "ms-python.python", + "bungcip.better-toml", + "waderyan.gitblame", + "oderwat.indent-rainbow", + "mechatroner.rainbow-csv", + "redhat.vscode-yaml", + "ms-python.vscode-pylance", + "visualstudioexptteam.vscodeintellicode", + "docsmsft.docs-markdown", + ], + "shutdownAction": "stopCompose", + "postCreateCommand": "cd /code; pipenv sync --dev", + // uncomment the following line if you are developing on linux + // due to the way docker on linux handles file perms with dev containers + // "remoteUser": "code" +} \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000000..e386a103b6 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,66 @@ +version: "3.3" + +volumes: + local_postgres_data: {} + +services: + traefik: + image: traefik:v1.7 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ../docker/local/traefik/traefik.toml:/traefik.toml + ports: + - "80:80" + - "8080:8080" + + mailhog: + image: mailhog/mailhog + labels: + traefik.enable: "True" + traefik.backend: "mailhog" + traefik.mailhog.port: 8025 + traefik.mailhog.frontend.rule: "Host:mailhog.docker" + + postgres: + image: postgres:12-alpine + volumes: + - local_postgres_data:/var/lib/postgresql/data + env_file: + - ../envs/local/postgres + + pgadmin: + image: dpage/pgadmin4 + depends_on: + - postgres + env_file: + - ../envs/local/pgadmin + labels: + traefik.enable: "True" + traefik.backend: "pgadmin" + traefik.pgadmin.port: 5050 + traefik.pgadmin.frontend.rule: "Host:pgadmin.docker" + + fastapi: + build: + context: . + dockerfile: ../docker/local/fastapi/Dockerfile + image: electioncal_local_fastapi + depends_on: + - postgres + - traefik + volumes: + - ..:/code + - ../docker/local/fastapi/entrypoint:/entrypoint + - ../docker/local/fastapi/start:/start + - ../.history:/commandhistory + env_file: + - ../envs/local/fastapi + - ../envs/local/postgres + labels: + traefik.enable: "True" + traefik.backend: "fastapi" + traefik.fastapi.port: 8000 + traefik.fastapi.frontend.rule: "Host:api.electioncal.docker" + traefik.static.port: 4242 + traefik.static.frontend.rule: "Host:electioncal.docker" + command: sleep infinity diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..5dc46e6b38 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto eol=lf +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1c5a79ed57..b13f6a7d82 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,85 @@ +# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig + +# Created by https://www.toptal.com/developers/gitignore/api/python,windows +# Edit at https://www.toptal.com/developers/gitignore?templates=python,windows + +### Python ### +# 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 +*.py,cover +.hypothesis/ +.pytest_cache/ +pytestdebug.log + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# pytype static type analyzer +.pytype/ + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# End of https://www.toptal.com/developers/gitignore/api/python,windows + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) + site/* !site/CNAME !site/static diff --git a/.history/.gitignore b/.history/.gitignore new file mode 100644 index 0000000000..86d0cb2726 --- /dev/null +++ b/.history/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..17e15f27ec --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..43a4a47c0e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "python.pythonPath": ".venv/bin/python", + "python.terminal.activateEnvironment": true, + "python.formatting.provider": "black", + "editor.formatOnSave": true, + "python.formatting.blackPath": "black", + "python.sortImports.path": "isort", + "[yaml]": { + "editor.tabSize": 4 + }, + "[toml]": { + "editor.tabSize": 4 + }, + "[dockerfile]": { + "editor.formatOnSave": false + }, + "python.languageServer": "Pylance", + "python.testing.promptToConfigure": false, + "python.testing.pytestEnabled": false, +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000..1f5110f229 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,59 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Run Uvicorn", + "type": "shell", + "command": [ + "pipenv", + "run", + "/start" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Build Static Site", + "type": "shell", + "command": [ + "pipenv", + "run", + "python", + "scripts/build_site.py" + ], + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Run Static Site", + "type": "shell", + "command": [ + "pipenv", + "run", + "python", + "-m", + "http.server", + "--directory", + "site/", + "4242" + ], + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000000..d16eea0c09 --- /dev/null +++ b/Pipfile @@ -0,0 +1,34 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +pytest = "*" +ipython = "*" +# sqlalchemy-stubs = "*" +# mypy = "*" +# black = "==19.10b0" +# isort = "*" + +[packages] +fastapi = "~=0.60.2" +uvicorn = "~=0.11.8" +pydantic = {version = "~=1.6.1", extras = ['email']} +python-multipart = "~=0.0.5" +sqlalchemy = "~=1.3.18" +databases = {extras = ["postgresql"], version = "~=0.3.2"} +requests = "~=2.24.0" +alembic = "~=1.4.2" +twilio = "~=6.44.2" +tenacity = "~=6.2.0" +# static site stuff +mistune= "==2.0.0a4" +Pillow = "*" +tomlkit = "*" +jinja2 = "~=2.11.2" +icalendar = "*" +pendulum = "*" + +[requires] +python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000000..6eb4a75312 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,703 @@ +{ + "_meta": { + "hash": { + "sha256": "ddd56ccbd1ba5820c6247605ae4cada6be851b5ef4cf3815170065af797df9f4" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "alembic": { + "hashes": [ + "sha256:035ab00497217628bf5d0be82d664d8713ab13d37b630084da8e1f98facf4dbf" + ], + "index": "pypi", + "version": "==1.4.2" + }, + "asyncpg": { + "hashes": [ + "sha256:09badce47a4645cfe523cc8a182bd047d5d62af0caaea77935e6a3c9e77dc364", + "sha256:22d161618b59e4b56fb2a5cc956aa9eeb336d07cae924a5b90c9aa1c2d137f15", + "sha256:28584783dd0d21b2a0db3bfe54fb12f21425a4cc015e4419083ea99e6de0de9b", + "sha256:308b8ba32c42ea1ed84c034320678ec307296bb4faf3fbbeb9f9e20b46db99a5", + "sha256:3ade59cef35bffae6dbc6f5f3ef56e1d53c67f0a7adc3cc4c714f07568d2d717", + "sha256:4421407b07b4e22291a226d9de0bf6f3ea8158aa1c12d83bfedbf5c22e13cd55", + "sha256:53cb2a0eb326f61e34ef4da2db01d87ce9c0ebe396f65a295829df334e31863f", + "sha256:615c7e3adb46e1f2e3aff45e4ee9401b4f24f9f7153e5530a0753369be72a5c6", + "sha256:68f7981f65317a5d5f497ec76919b488dbe0e838f8b924e7517a680bdca0f308", + "sha256:6b7807bfedd24dd15cfb2c17c60977ce01410615ecc285268b5144a944ec97ff", + "sha256:7e51d1a012b779e0ebf0195f80d004f65d3c60cc06f0fa1cef9d3e536262abbd", + "sha256:7ee29c4707eb8fb3d3a0348ac4495e06f4afaca3ee38c3bebedc9c8b239125ff", + "sha256:823eca36108bd64a8600efe7bbf1230aa00f2defa3be42852f3b61ab40cf1226", + "sha256:8587e206d78e739ca83a40c9982e03b28f8904c95a54dc782da99e86cf768f73", + "sha256:888593b6688faa7ec1c97ff7f2ca3b5a5b8abb15478fe2a13c5012b607a28737", + "sha256:915cebc8a7693c8a5e89804fa106678dbedcc50d0270ebab0b75f16e668bd59b", + "sha256:a4c1feb285ec3807ecd5b54ab718a3d065bb55c93ebaf800670eadde31484be8", + "sha256:aa2e0cb14c01a2f58caeeca7196681b30aa22dd22c82845560b401df5e98e171", + "sha256:b1b10916c006e5c2c0dcd5dadeb38cbf61ecd20d66c50164e82f31c22c7e329d", + "sha256:dddf4d4c5e781310a36529c3c87c1746837c2d2c7ec0f2ec4e4f06450d83c50a", + "sha256:dfd491e9865e64a3e91f1587b1d88d71dde1cfb850429253a73d4d44b98c3a0f", + "sha256:e7bfb9269aeb11d78d50accf1be46823683ced99209b7199e307cdf7da849522", + "sha256:ea26604932719b3612541e606508d9d604211f56a65806ccf8c92c64104f4f8a", + "sha256:ecd5232cf64f58caac3b85103f1223fdf20e9eb43bfa053c56ef9e5dd76ab099", + "sha256:f2d1aa890ffd1ad062a38b7ff7488764b3da4b0a24e0c83d7bbb1d1a6609df15" + ], + "version": "==0.21.0" + }, + "certifi": { + "hashes": [ + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + ], + "version": "==2020.6.20" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==7.1.2" + }, + "databases": { + "extras": [ + "postgresql" + ], + "hashes": [ + "sha256:78b758884ca585b81272af1de697e0c8a3034de92bdd08e9ac47436ef0cdca56", + "sha256:ee8dcece15a86359ef06414a6afcc15da15f5d078dc09af2e3a5f9dbfee4dce9" + ], + "index": "pypi", + "version": "==0.3.2" + }, + "dnspython": { + "hashes": [ + "sha256:044af09374469c3a39eeea1a146e8cac27daec951f1f1f157b1962fc7cb9d1b7", + "sha256:40bb3c24b9d4ec12500f0124288a65df232a3aa749bb0c39734b782873a2544d" + ], + "markers": "python_version >= '3.6'", + "version": "==2.0.0" + }, + "email-validator": { + "hashes": [ + "sha256:5f246ae8d81ce3000eade06595b7bb55a4cf350d559e890182a1466a21f25067", + "sha256:63094045c3e802c3d3d575b18b004a531c36243ca8d1cec785ff6bfcb04185bb" + ], + "version": "==1.1.1" + }, + "fastapi": { + "hashes": [ + "sha256:579c194f78ed3cff1a4e62bbaea79fc1bef05e35294147e41a807f4105a77885", + "sha256:7dd1e4380976741a71dec2a7f9035b69268881ca25001cc99f25ca89d2d38ad9" + ], + "index": "pypi", + "version": "==0.60.2" + }, + "h11": { + "hashes": [ + "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1", + "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1" + ], + "version": "==0.9.0" + }, + "httptools": { + "hashes": [ + "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be", + "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d", + "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce", + "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2", + "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6", + "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f", + "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009", + "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce", + "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a", + "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c", + "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4", + "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437" + ], + "markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'", + "version": "==0.1.1" + }, + "icalendar": { + "hashes": [ + "sha256:2f400b37353160af259019938e4397bf823f06a67ce9c50a58b95eb563720f00", + "sha256:7e6fe7232622abe32d8f54d0936ffcd5a9087198a4c2f1ec1803a7dd9fdd979f" + ], + "index": "pypi", + "version": "==4.0.6" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, + "jinja2": { + "hashes": [ + "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", + "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" + ], + "index": "pypi", + "version": "==2.11.2" + }, + "mako": { + "hashes": [ + "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27", + "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.1.3" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", + "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:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.1.1" + }, + "mistune": { + "hashes": [ + "sha256:1309e30a1846f3c1dd384e17e2dd957b024d0305b507a1546d13152a2b5dcc44", + "sha256:ebbed6e25e342705ec30f3184bdc148229dfbae0da6136c2a6892333c9f8bc6b" + ], + "index": "pypi", + "version": "==2.0.0a4" + }, + "pendulum": { + "hashes": [ + "sha256:0731f0c661a3cb779d398803655494893c9f581f6488048b3fb629c2342b5394", + "sha256:1245cd0075a3c6d889f581f6325dd8404aca5884dea7223a5566c38aab94642b", + "sha256:29c40a6f2942376185728c9a0347d7c0f07905638c83007e1d262781f1e6953a", + "sha256:2d1619a721df661e506eff8db8614016f0720ac171fe80dda1333ee44e684087", + "sha256:318f72f62e8e23cd6660dbafe1e346950281a9aed144b5c596b2ddabc1d19739", + "sha256:33fb61601083f3eb1d15edeb45274f73c63b3c44a8524703dc143f4212bf3269", + "sha256:3481fad1dc3f6f6738bd575a951d3c15d4b4ce7c82dce37cf8ac1483fde6e8b0", + "sha256:4c9c689747f39d0d02a9f94fcee737b34a5773803a64a5fdb046ee9cac7442c5", + "sha256:7c5ec650cb4bec4c63a89a0242cc8c3cebcec92fcfe937c417ba18277d8560be", + "sha256:94b1fc947bfe38579b28e1cccb36f7e28a15e841f30384b5ad6c5e31055c85d7", + "sha256:9702069c694306297ed362ce7e3c1ef8404ac8ede39f9b28b7c1a7ad8c3959e3", + "sha256:b06a0ca1bfe41c990bbf0c029f0b6501a7f2ec4e38bfec730712015e8860f207", + "sha256:b6c352f4bd32dff1ea7066bd31ad0f71f8d8100b9ff709fb343f3b86cee43efe", + "sha256:c501749fdd3d6f9e726086bf0cd4437281ed47e7bca132ddb522f86a1645d360", + "sha256:c807a578a532eeb226150d5006f156632df2cc8c5693d778324b43ff8c515dd0", + "sha256:db0a40d8bcd27b4fb46676e8eb3c732c67a5a5e6bfab8927028224fbced0b40b", + "sha256:de42ea3e2943171a9e95141f2eecf972480636e8e484ccffaf1e833929e9e052", + "sha256:e95d329384717c7bf627bf27e204bc3b15c8238fa8d9d9781d93712776c14002", + "sha256:f5e236e7730cab1644e1b87aca3d2ff3e375a608542e90fe25685dae46310116", + "sha256:f888f2d2909a414680a29ae74d0592758f2b9fcdee3549887779cd4055e975db", + "sha256:fb53ffa0085002ddd43b6ca61a7b34f2d4d7c3ed66f931fe599e1a531b42af9b" + ], + "index": "pypi", + "version": "==2.1.2" + }, + "pillow": { + "hashes": [ + "sha256:0295442429645fa16d05bd567ef5cff178482439c9aad0411d3f0ce9b88b3a6f", + "sha256:06aba4169e78c439d528fdeb34762c3b61a70813527a2c57f0540541e9f433a8", + "sha256:09d7f9e64289cb40c2c8d7ad674b2ed6105f55dc3b09aa8e4918e20a0311e7ad", + "sha256:0a80dd307a5d8440b0a08bd7b81617e04d870e40a3e46a32d9c246e54705e86f", + "sha256:1ca594126d3c4def54babee699c055a913efb01e106c309fa6b04405d474d5ae", + "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d", + "sha256:431b15cffbf949e89df2f7b48528be18b78bfa5177cb3036284a5508159492b5", + "sha256:52125833b070791fcb5710fabc640fc1df07d087fc0c0f02d3661f76c23c5b8b", + "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8", + "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233", + "sha256:6d7741e65835716ceea0fd13a7d0192961212fd59e741a46bbed7a473c634ed6", + "sha256:6edb5446f44d901e8683ffb25ebdfc26988ee813da3bf91e12252b57ac163727", + "sha256:725aa6cfc66ce2857d585f06e9519a1cc0ef6d13f186ff3447ab6dff0a09bc7f", + "sha256:8dad18b69f710bf3a001d2bf3afab7c432785d94fcf819c16b5207b1cfd17d38", + "sha256:94cf49723928eb6070a892cb39d6c156f7b5a2db4e8971cb958f7b6b104fb4c4", + "sha256:97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626", + "sha256:9ad7f865eebde135d526bb3163d0b23ffff365cf87e767c649550964ad72785d", + "sha256:9c87ef410a58dd54b92424ffd7e28fd2ec65d2f7fc02b76f5e9b2067e355ebf6", + "sha256:a060cf8aa332052df2158e5a119303965be92c3da6f2d93b6878f0ebca80b2f6", + "sha256:c79f9c5fb846285f943aafeafda3358992d64f0ef58566e23484132ecd8d7d63", + "sha256:c92302a33138409e8f1ad16731568c55c9053eee71bb05b6b744067e1b62380f", + "sha256:d08b23fdb388c0715990cbc06866db554e1822c4bdcf6d4166cf30ac82df8c41", + "sha256:d350f0f2c2421e65fbc62690f26b59b0bcda1b614beb318c81e38647e0f673a1", + "sha256:e901964262a56d9ea3c2693df68bc9860b8bdda2b04768821e4c44ae797de117", + "sha256:ec29604081f10f16a7aea809ad42e27764188fc258b02259a03a8ff7ded3808d", + "sha256:edf31f1150778abd4322444c393ab9c7bd2af271dd4dafb4208fb613b1f3cdc9", + "sha256:f7e30c27477dffc3e85c2463b3e649f751789e0f6c8456099eea7ddd53be4a8a", + "sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce" + ], + "index": "pypi", + "version": "==7.2.0" + }, + "psycopg2-binary": { + "hashes": [ + "sha256:008da3ab51adc70a5f1cfbbe5db3a22607ab030eb44bcecf517ad11a0c2b3cac", + "sha256:07cf82c870ec2d2ce94d18e70c13323c89f2f2a2628cbf1feee700630be2519a", + "sha256:08507efbe532029adee21b8d4c999170a83760d38249936038bd0602327029b5", + "sha256:107d9be3b614e52a192719c6bf32e8813030020ea1d1215daa86ded9a24d8b04", + "sha256:17a0ea0b0eabf07035e5e0d520dabc7950aeb15a17c6d36128ba99b2721b25b1", + "sha256:3286541b9d85a340ee4ed42732d15fc1bb441dc500c97243a768154ab8505bb5", + "sha256:3939cf75fc89c5e9ed836e228c4a63604dff95ad19aed2bbf71d5d04c15ed5ce", + "sha256:40abc319f7f26c042a11658bf3dd3b0b3bceccf883ec1c565d5c909a90204434", + "sha256:51f7823f1b087d2020d8e8c9e6687473d3d239ba9afc162d9b2ab6e80b53f9f9", + "sha256:6bb2dd006a46a4a4ce95201f836194eb6a1e863f69ee5bab506673e0ca767057", + "sha256:702f09d8f77dc4794651f650828791af82f7c2efd8c91ae79e3d9fe4bb7d4c98", + "sha256:7036ccf715925251fac969f4da9ad37e4b7e211b1e920860148a10c0de963522", + "sha256:7b832d76cc65c092abd9505cc670c4e3421fd136fb6ea5b94efbe4c146572505", + "sha256:8f74e631b67482d504d7e9cf364071fc5d54c28e79a093ff402d5f8f81e23bfa", + "sha256:930315ac53dc65cbf52ab6b6d27422611f5fb461d763c531db229c7e1af6c0b3", + "sha256:96d3038f5bd061401996614f65d27a4ecb62d843eb4f48e212e6d129171a721f", + "sha256:a20299ee0ea2f9cca494396ac472d6e636745652a64a418b39522c120fd0a0a4", + "sha256:a34826d6465c2e2bbe9d0605f944f19d2480589f89863ed5f091943be27c9de4", + "sha256:a69970ee896e21db4c57e398646af9edc71c003bc52a3cc77fb150240fefd266", + "sha256:b9a8b391c2b0321e0cd7ec6b4cfcc3dd6349347bd1207d48bcb752aa6c553a66", + "sha256:ba13346ff6d3eb2dca0b6fa0d8a9d999eff3dcd9b55f3a890f12b0b6362b2b38", + "sha256:bb0608694a91db1e230b4a314e8ed00ad07ed0c518f9a69b83af2717e31291a3", + "sha256:c8830b7d5f16fd79d39b21e3d94f247219036b29b30c8270314c46bf8b732389", + "sha256:cac918cd7c4c498a60f5d2a61d4f0a6091c2c9490d81bc805c963444032d0dab", + "sha256:cc30cb900f42c8a246e2cb76539d9726f407330bc244ca7729c41a44e8d807fb", + "sha256:ccdc6a87f32b491129ada4b87a43b1895cf2c20fdb7f98ad979647506ffc41b6", + "sha256:d1a8b01f6a964fec702d6b6dac1f91f2b9f9fe41b310cbb16c7ef1fac82df06d", + "sha256:e004db88e5a75e5fdab1620fb9f90c9598c2a195a594225ac4ed2a6f1c23e162", + "sha256:eb2f43ae3037f1ef5e19339c41cf56947021ac892f668765cd65f8ab9814192e", + "sha256:fa466306fcf6b39b8a61d003123d442b23707d635a5cb05ac4e1b62cc79105cd" + ], + "version": "==2.8.5" + }, + "pydantic": { + "extras": [ + "email" + ], + "hashes": [ + "sha256:1783c1d927f9e1366e0e0609ae324039b2479a1a282a98ed6a6836c9ed02002c", + "sha256:2dc946b07cf24bee4737ced0ae77e2ea6bc97489ba5a035b603bd1b40ad81f7e", + "sha256:2de562a456c4ecdc80cf1a8c3e70c666625f7d02d89a6174ecf63754c734592e", + "sha256:36dbf6f1be212ab37b5fda07667461a9219c956181aa5570a00edfb0acdfe4a1", + "sha256:3fa799f3cfff3e5f536cbd389368fc96a44bb30308f258c94ee76b73bd60531d", + "sha256:40d765fa2d31d5be8e29c1794657ad46f5ee583a565c83cea56630d3ae5878b9", + "sha256:418b84654b60e44c0cdd5384294b0e4bc1ebf42d6e873819424f3b78b8690614", + "sha256:4900b8820b687c9a3ed753684337979574df20e6ebe4227381d04b3c3c628f99", + "sha256:530d7222a2786a97bc59ee0e0ebbe23728f82974b1f1ad9a11cd966143410633", + "sha256:54122a8ed6b75fe1dd80797f8251ad2063ea348a03b77218d73ea9fe19bd4e73", + "sha256:6c3f162ba175678218629f446a947e3356415b6b09122dcb364e58c442c645a7", + "sha256:b49c86aecde15cde33835d5d6360e55f5e0067bb7143a8303bf03b872935c75b", + "sha256:b5b3489cb303d0f41ad4a7390cf606a5f2c7a94dcba20c051cd1c653694cb14d", + "sha256:cf3933c98cb5e808b62fae509f74f209730b180b1e3c3954ee3f7949e083a7df", + "sha256:eb75dc1809875d5738df14b6566ccf9fd9c0bcde4f36b72870f318f16b9f5c20", + "sha256:f769141ab0abfadf3305d4fcf36660e5cf568a666dd3efab7c3d4782f70946b1", + "sha256:f8af9b840a9074e08c0e6dc93101de84ba95df89b267bf7151d74c553d66833b" + ], + "index": "pypi", + "version": "==1.6.1" + }, + "pyjwt": { + "hashes": [ + "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", + "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" + ], + "version": "==1.7.1" + }, + "python-dateutil": { + "hashes": [ + "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", + "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.1" + }, + "python-editor": { + "hashes": [ + "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", + "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", + "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8", + "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77", + "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522" + ], + "version": "==1.0.4" + }, + "python-multipart": { + "hashes": [ + "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43" + ], + "index": "pypi", + "version": "==0.0.5" + }, + "pytz": { + "hashes": [ + "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", + "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" + ], + "version": "==2020.1" + }, + "pytzdata": { + "hashes": [ + "sha256:3efa13b335a00a8de1d345ae41ec78dd11c9f8807f522d39850f2dd828681540", + "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2020.1" + }, + "requests": { + "hashes": [ + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + ], + "index": "pypi", + "version": "==2.24.0" + }, + "six": { + "hashes": [ + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" + }, + "sqlalchemy": { + "hashes": [ + "sha256:072766c3bd09294d716b2d114d46ffc5ccf8ea0b714a4e1c48253014b771c6bb", + "sha256:107d4af989831d7b091e382d192955679ec07a9209996bf8090f1f539ffc5804", + "sha256:15c0bcd3c14f4086701c33a9e87e2c7ceb3bcb4a246cd88ec54a49cf2a5bd1a6", + "sha256:276936d41111a501cf4a1a0543e25449108d87e9f8c94714f7660eaea89ae5fe", + "sha256:3292a28344922415f939ee7f4fc0c186f3d5a0bf02192ceabd4f1129d71b08de", + "sha256:33d29ae8f1dc7c75b191bb6833f55a19c932514b9b5ce8c3ab9bc3047da5db36", + "sha256:3bba2e9fbedb0511769780fe1d63007081008c5c2d7d715e91858c94dbaa260e", + "sha256:5a49e8473b1ab1228302ed27365ea0fadd4bf44bc0f9e73fe38e10fdd3d6b4fc", + "sha256:618db68745682f64cedc96ca93707805d1f3a031747b5a0d8e150cfd5055ae4d", + "sha256:6547b27698b5b3bbfc5210233bd9523de849b2bb8a0329cd754c9308fc8a05ce", + "sha256:6557af9e0d23f46b8cd56f8af08eaac72d2e3c632ac8d5cf4e20215a8dca7cea", + "sha256:73a40d4fcd35fdedce07b5885905753d5d4edf413fbe53544dd871f27d48bd4f", + "sha256:8280f9dae4adb5889ce0bb3ec6a541bf05434db5f9ab7673078c00713d148365", + "sha256:83469ad15262402b0e0974e612546bc0b05f379b5aa9072ebf66d0f8fef16bea", + "sha256:860d0fe234922fd5552b7f807fbb039e3e7ca58c18c8d38aa0d0a95ddf4f6c23", + "sha256:883c9fb62cebd1e7126dd683222b3b919657590c3e2db33bdc50ebbad53e0338", + "sha256:8afcb6f4064d234a43fea108859942d9795c4060ed0fbd9082b0f280181a15c1", + "sha256:96f51489ac187f4bab588cf51f9ff2d40b6d170ac9a4270ffaed535c8404256b", + "sha256:9e865835e36dfbb1873b65e722ea627c096c11b05f796831e3a9b542926e979e", + "sha256:aa0554495fe06172b550098909be8db79b5accdf6ffb59611900bea345df5eba", + "sha256:b595e71c51657f9ee3235db8b53d0b57c09eee74dfb5b77edff0e46d2218dc02", + "sha256:b70bad2f1a5bd3460746c3fb3ab69e4e0eb5f59d977a23f9b66e5bdc74d97b86", + "sha256:c7adb1f69a80573698c2def5ead584138ca00fff4ad9785a4b0b2bf927ba308d", + "sha256:c898b3ebcc9eae7b36bd0b4bbbafce2d8076680f6868bcbacee2d39a7a9726a7", + "sha256:e49947d583fe4d29af528677e4f0aa21f5e535ca2ae69c48270ebebd0d8843c0", + "sha256:eb1d71643e4154398b02e88a42fc8b29db8c44ce4134cf0f4474bfc5cb5d4dac", + "sha256:f2e8a9c0c8813a468aa659a01af6592f71cd30237ec27c4cc0683f089f90dcfc", + "sha256:fe7fe11019fc3e6600819775a7d55abc5446dda07e9795f5954fdbf8a49e1c37" + ], + "index": "pypi", + "version": "==1.3.19" + }, + "starlette": { + "hashes": [ + "sha256:bd2ffe5e37fb75d014728511f8e68ebf2c80b0fa3d04ca1479f4dc752ae31ac9", + "sha256:ebe8ee08d9be96a3c9f31b2cb2a24dbdf845247b745664bd8a3f9bd0c977fdbc" + ], + "markers": "python_version >= '3.6'", + "version": "==0.13.6" + }, + "tenacity": { + "hashes": [ + "sha256:29ae90e7faf488a8628432154bb34ace1cca58244c6ea399fd33f066ac71339a", + "sha256:5a5d3dcd46381abe8b4f82b5736b8726fd3160c6c7161f53f8af7f1eb9b82173" + ], + "index": "pypi", + "version": "==6.2.0" + }, + "tomlkit": { + "hashes": [ + "sha256:6babbd33b17d5c9691896b0e68159215a9387ebfa938aa3ac42f4a4beeb2b831", + "sha256:ac57f29693fab3e309ea789252fcce3061e19110085aa31af5446ca749325618" + ], + "index": "pypi", + "version": "==0.7.0" + }, + "twilio": { + "hashes": [ + "sha256:db0c1ed249ad672007cbe7109e8a2cbe30c4ed660beba740976baa32f0a8574a" + ], + "index": "pypi", + "version": "==6.44.2" + }, + "urllib3": { + "hashes": [ + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.25.10" + }, + "uvicorn": { + "hashes": [ + "sha256:46a83e371f37ea7ff29577d00015f02c942410288fb57def6440f2653fff1d26", + "sha256:4b70ddb4c1946e39db9f3082d53e323dfd50634b95fd83625d778729ef1730ef" + ], + "index": "pypi", + "version": "==0.11.8" + }, + "uvloop": { + "hashes": [ + "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", + "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e", + "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09", + "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726", + "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891", + "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7", + "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5", + "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", + "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" + ], + "markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'", + "version": "==0.14.0" + }, + "websockets": { + "hashes": [ + "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", + "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", + "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", + "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", + "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", + "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", + "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", + "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", + "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", + "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", + "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", + "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", + "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", + "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", + "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", + "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", + "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", + "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", + "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", + "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", + "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", + "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==8.1" + } + }, + "develop": { + "attrs": { + "hashes": [ + "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", + "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.1.0" + }, + "backcall": { + "hashes": [ + "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", + "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" + ], + "version": "==0.2.0" + }, + "decorator": { + "hashes": [ + "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760", + "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7" + ], + "version": "==4.4.2" + }, + "iniconfig": { + "hashes": [ + "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437", + "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69" + ], + "version": "==1.0.1" + }, + "ipython": { + "hashes": [ + "sha256:5a8f159ca8b22b9a0a1f2a28befe5ad2b703339afb58c2ffe0d7c8d7a3af5999", + "sha256:b70974aaa2674b05eb86a910c02ed09956a33f2dd6c71afc60f0b128a77e7f28" + ], + "index": "pypi", + "version": "==7.17.0" + }, + "ipython-genutils": { + "hashes": [ + "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", + "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" + ], + "version": "==0.2.0" + }, + "jedi": { + "hashes": [ + "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20", + "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.17.2" + }, + "more-itertools": { + "hashes": [ + "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5", + "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2" + ], + "markers": "python_version >= '3.5'", + "version": "==8.4.0" + }, + "packaging": { + "hashes": [ + "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", + "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.4" + }, + "parso": { + "hashes": [ + "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea", + "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.7.1" + }, + "pexpect": { + "hashes": [ + "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", + "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" + ], + "markers": "sys_platform != 'win32'", + "version": "==4.8.0" + }, + "pickleshare": { + "hashes": [ + "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", + "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" + ], + "version": "==0.7.5" + }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.13.1" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:683397077a64cd1f750b71c05afcfc6612a7300cb6932666531e5a54f38ea564", + "sha256:7630ab85a23302839a0f26b31cc24f518e6155dea1ed395ea61b42c45941b6a6" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==3.0.6" + }, + "ptyprocess": { + "hashes": [ + "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", + "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" + ], + "version": "==0.6.0" + }, + "py": { + "hashes": [ + "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", + "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.9.0" + }, + "pygments": { + "hashes": [ + "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", + "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" + ], + "markers": "python_version >= '3.5'", + "version": "==2.6.1" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.4.7" + }, + "pytest": { + "hashes": [ + "sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4", + "sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad" + ], + "index": "pypi", + "version": "==6.0.1" + }, + "six": { + "hashes": [ + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" + }, + "toml": { + "hashes": [ + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + ], + "version": "==0.10.1" + }, + "traitlets": { + "hashes": [ + "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44", + "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7" + ], + "version": "==4.3.3" + }, + "wcwidth": { + "hashes": [ + "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", + "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" + ], + "version": "==0.2.5" + } + } +} diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000000..0ce3a600db --- /dev/null +++ b/alembic.ini @@ -0,0 +1,71 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = /code/api/alembic + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +#truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat alembic/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[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 + +[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 diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/alembic/env.py b/api/alembic/env.py new file mode 100644 index 0000000000..f3b51ed2f5 --- /dev/null +++ b/api/alembic/env.py @@ -0,0 +1,79 @@ +from __future__ import with_statement + +import os +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import engine_from_config, pool + +from app.conf import settings + +# 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) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +# target_metadata = None + +from app.db.base import Base + +target_metadata = Base.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 = settings.SQLALCHEMY_DATABASE_URI + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True, compare_type=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. + + """ + configuration = config.get_section(config.config_ini_section) + configuration["sqlalchemy.url"] = settings.SQLALCHEMY_DATABASE_URI + connectable = engine_from_config(configuration, prefix="sqlalchemy.", poolclass=pool.NullPool,) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata, compare_type=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/api/alembic/script.py.mako b/api/alembic/script.py.mako new file mode 100644 index 0000000000..2c0156303a --- /dev/null +++ b/api/alembic/script.py.mako @@ -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"} diff --git a/api/alembic/versions/32b566cd7100_init.py b/api/alembic/versions/32b566cd7100_init.py new file mode 100644 index 0000000000..b7c7f752f4 --- /dev/null +++ b/api/alembic/versions/32b566cd7100_init.py @@ -0,0 +1,59 @@ +"""init + +Revision ID: 32b566cd7100 +Revises: +Create Date: 2020-08-24 05:59:27.853902 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "32b566cd7100" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "user", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("full_name", sa.String(), nullable=True), + sa.Column("email", sa.String(), nullable=False), + sa.Column("hashed_password", sa.String(), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=True), + sa.Column("is_superuser", sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True) + op.create_index(op.f("ix_user_full_name"), "user", ["full_name"], unique=False) + op.create_index(op.f("ix_user_id"), "user", ["id"], unique=False) + op.create_table( + "item", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.Column("owner_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(["owner_id"], ["user.id"],), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_item_description"), "item", ["description"], unique=False) + op.create_index(op.f("ix_item_id"), "item", ["id"], unique=False) + op.create_index(op.f("ix_item_title"), "item", ["title"], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_item_title"), table_name="item") + op.drop_index(op.f("ix_item_id"), table_name="item") + op.drop_index(op.f("ix_item_description"), table_name="item") + op.drop_table("item") + op.drop_index(op.f("ix_user_id"), table_name="user") + op.drop_index(op.f("ix_user_full_name"), table_name="user") + op.drop_index(op.f("ix_user_email"), table_name="user") + op.drop_table("user") + # ### end Alembic commands ### diff --git a/api/app/__init__.py b/api/app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/app/conf.py b/api/app/conf.py new file mode 100644 index 0000000000..4bb1ee0d3f --- /dev/null +++ b/api/app/conf.py @@ -0,0 +1,45 @@ +from secrets import token_urlsafe +from typing import Any, Dict, Optional + +from pydantic import BaseSettings, HttpUrl, PostgresDsn, validator + + +class Settings(BaseSettings): + PROJECT_NAME: str = "electioncal" + API_V1_STR: str = "/v1" + SECRET_KEY: str = token_urlsafe(32) + BASE_URL: str + + SENTRY_DSN: Optional[HttpUrl] = None + + POSTGRES_HOST: str + POSTGRES_USER: str + POSTGRES_PASSWORD: str + POSTGRES_DB: str + POSTGRES_PORT: str + SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None + + @validator("SQLALCHEMY_DATABASE_URI", pre=True) + def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any],) -> Any: + if isinstance(v, str): + return v + return PostgresDsn.build( + scheme="postgresql", + user=values.get("POSTGRES_USER"), + password=values.get("POSTGRES_PASSWORD"), + host=values.get("POSTGRES_HOST"), + port=values.get("POSTGRES_PORT"), + path=f"/{values.get('POSTGRES_DB') or ''}", + ) + + TWILIO_SID: Optional[str] + TWILIO_AUTH_TOKEN: Optional[str] + TWILIO_SECRET: Optional[str] + + SUPERUSER_EMAIL: str + SUPERUSER_PASSWORD: str + + +settings = Settings() + +__all__ = (settings,) diff --git a/api/app/crud.py b/api/app/crud.py new file mode 100644 index 0000000000..754d90d11e --- /dev/null +++ b/api/app/crud.py @@ -0,0 +1,24 @@ +from sqlalchemy.orm import Session + +from . import models, schemas + + +def get_user(db: Session, user_id: int): + return db.query(models.User).filter(models.User.id == user_id).first() + + +def get_user_by_email(db: Session, email: str): + return db.query(models.User).filter(models.User.email == email).first() + + +def get_users(db: Session, skip: int = 0, limit: int = 100): + return db.query(models.User).offset(skip).limit(limit).all() + + +def create_user(db: Session, user: schemas.UserCreate): + fake_hashed_password = user.password + "notreallyhashed" + db_user = models.User(email=user.email, hashed_password=fake_hashed_password) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user diff --git a/api/app/db/__init__.py b/api/app/db/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/app/db/base.py b/api/app/db/base.py new file mode 100644 index 0000000000..73403500b8 --- /dev/null +++ b/api/app/db/base.py @@ -0,0 +1,6 @@ +# Import all the models, so that Base has them before being +# imported by Alembic +from .base_class import Base # noqa +from ..models import Item # noqa +from ..models import User # noqa + diff --git a/api/app/db/base_class.py b/api/app/db/base_class.py new file mode 100644 index 0000000000..3b759110b6 --- /dev/null +++ b/api/app/db/base_class.py @@ -0,0 +1,13 @@ +from typing import Any + +from sqlalchemy.ext.declarative import as_declarative, declared_attr + + +@as_declarative() +class Base: + id: Any + __name__: str + # Generate __tablename__ automatically + @declared_attr + def __tablename__(cls) -> str: + return cls.__name__.lower() diff --git a/api/app/db/init_db.py b/api/app/db/init_db.py new file mode 100644 index 0000000000..eea051b0e9 --- /dev/null +++ b/api/app/db/init_db.py @@ -0,0 +1,25 @@ +from sqlalchemy.orm import Session + +from .. import crud, schemas +from ..conf import settings +from . import base # noqa: F401 + +# make sure all SQL Alchemy models are imported (api.db.base) before initializing DB +# otherwise, SQL Alchemy might fail to initialize relationships properly +# for more details: https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/28 + + +def init_db(db: Session) -> None: + # Tables should be created with Alembic migrations + # But if you don't want to use migrations, create + # the tables un-commenting the next line + # Base.metadata.create_all(bind=engine) + + user = crud.get_user_by_email(db, email=settings.SUPERUSER_EMAIL) + if not user: + user_in = schemas.UserCreate( + email=settings.SUPERUSER_EMAIL, + password=settings.SUPERUSER_PASSWORD, + is_superuser=True, + ) + user = crud.create_user(db, user=user_in) diff --git a/api/app/db/session.py b/api/app/db/session.py new file mode 100644 index 0000000000..b4c31c7904 --- /dev/null +++ b/api/app/db/session.py @@ -0,0 +1,7 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from ..conf import settings + +engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) diff --git a/api/app/main.py b/api/app/main.py new file mode 100644 index 0000000000..2c40d23275 --- /dev/null +++ b/api/app/main.py @@ -0,0 +1,73 @@ +import time +from typing import List + +from fastapi import Depends, FastAPI, HTTPException, Request +from sqlalchemy.orm import Session + +from . import crud, schemas +from .db.session import SessionLocal +from .conf import settings + + +app = FastAPI(title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json)") + + +@app.middleware("http") +async def add_process_time_header(request: Request, call_next): + start_time = time.time() + response = await call_next(request) + process_time = time.time() - start_time + response.headers["X-Process-Time"] = str(process_time) + return response + + +@app.get("/") +async def root(): + return {"message": "Hello World"} + + +@app.get("/teapot", status_code=418) +async def i_am_a_teapot(): + return + + +# Dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +@app.post("/users/", response_model=schemas.User) +def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): + db_user = crud.get_user_by_email(db, email=user.email) + if db_user: + raise HTTPException(status_code=400, detail="Email already registered") + return crud.create_user(db=db, user=user) + + +@app.get("/users/", response_model=List[schemas.User]) +def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + users = crud.get_users(db, skip=skip, limit=limit) + return users + + +@app.get("/users/{user_id}", response_model=schemas.User) +def read_user(user_id: int, db: Session = Depends(get_db)): + db_user = crud.get_user(db, user_id=user_id) + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + return db_user + + +@app.post("/users/{user_id}/items/", response_model=schemas.Item) +def create_item_for_user(user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)): + return crud.create_user_item(db=db, item=item, user_id=user_id) + + +@app.get("/items/", response_model=List[schemas.Item]) +def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + items = crud.get_items(db, skip=skip, limit=limit) + return items diff --git a/api/app/models.py b/api/app/models.py new file mode 100644 index 0000000000..60d3a80e6e --- /dev/null +++ b/api/app/models.py @@ -0,0 +1,22 @@ +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from .db.base_class import Base + + +class User(Base): + id = Column(Integer, primary_key=True, index=True) + full_name = Column(String, index=True) + email = Column(String, unique=True, index=True, nullable=False) + hashed_password = Column(String, nullable=False) + is_active = Column(Boolean(), default=True) + is_superuser = Column(Boolean(), default=False) + items = relationship("Item", back_populates="owner") + + +class Item(Base): + id = Column(Integer, primary_key=True, index=True) + title = Column(String, index=True) + description = Column(String, index=True) + owner_id = Column(Integer, ForeignKey("user.id")) + owner = relationship("User", back_populates="items") diff --git a/api/app/schemas.py b/api/app/schemas.py new file mode 100644 index 0000000000..5238266649 --- /dev/null +++ b/api/app/schemas.py @@ -0,0 +1,38 @@ +from typing import List, Optional + +from pydantic import BaseModel + + +class ItemBase(BaseModel): + title: str + description: Optional[str] = None + + +class ItemCreate(ItemBase): + pass + + +class Item(ItemBase): + id: int + owner_id: int + + class Config: + orm_mode = True + + +class UserBase(BaseModel): + email: str + + +class UserCreate(UserBase): + password: str + is_superuser: bool + + +class User(UserBase): + id: int + is_active: bool + items: List[Item] = [] + + class Config: + orm_mode = True diff --git a/api/scripts/__init__.py b/api/scripts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/scripts/init_data.py b/api/scripts/init_data.py new file mode 100644 index 0000000000..c50646d2df --- /dev/null +++ b/api/scripts/init_data.py @@ -0,0 +1,22 @@ +import logging + +from app.db.init_db import init_db +from app.db.session import SessionLocal + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def init() -> None: + db = SessionLocal() + init_db(db) + + +def main() -> None: + logger.info("Creating initial data") + init() + logger.info("Initial data created") + + +if __name__ == "__main__": + main() diff --git a/api/scripts/wait_for_postgres.py b/api/scripts/wait_for_postgres.py new file mode 100644 index 0000000000..656b3951a3 --- /dev/null +++ b/api/scripts/wait_for_postgres.py @@ -0,0 +1,37 @@ +import logging + +from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed + +from api.db.session import SessionLocal + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +max_tries = 60 * 5 # 5 minutes +wait_seconds = 1 + + +@retry( + stop=stop_after_attempt(max_tries), + wait=wait_fixed(wait_seconds), + before=before_log(logger, logging.INFO), + after=after_log(logger, logging.WARN), +) +def init() -> None: + try: + db = SessionLocal() + # Try to create session to check if DB is awake + db.execute("SELECT 1") + except Exception as e: + logger.error(e) + raise e + + +def main() -> None: + logger.info("Initializing service") + init() + logger.info("Service finished initializing") + + +if __name__ == "__main__": + main() diff --git a/docker-compose-local.yml b/docker-compose-local.yml new file mode 100644 index 0000000000..dac9cce594 --- /dev/null +++ b/docker-compose-local.yml @@ -0,0 +1,67 @@ +version: "3.3" + +volumes: + local_postgres_data: {} + +services: + traefik: + image: traefik:v1.7 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./docker/local/traefik/traefik.toml:/traefik.toml + ports: + - "80:80" + - "8080:8080" + + mailhog: + image: mailhog/mailhog + labels: + traefik.enable: "True" + traefik.backend: "mailhog" + traefik.mailhog.port: 8025 + traefik.mailhog.frontend.rule: "Host:mailhog.docker" + + postgres: + image: postgres:12-alpine + volumes: + - local_postgres_data:/var/lib/postgresql/data + env_file: + - ./envs/local/postgres + + pgadmin: + image: dpage/pgadmin4 + depends_on: + - postgres + env_file: + - ./envs/local/pgadmin + labels: + traefik.enable: "True" + traefik.backend: "pgadmin" + traefik.mailhog.port: 5050 + traefik.mailhog.frontend.rule: "Host:pgadmin.docker" + + fastapi: + build: + context: . + dockerfile: ./docker/local/fastapi/Dockerfile + image: electioncal_local_fastapi + depends_on: + - postgres + - traefik + volumes: + - .:/code + - ./docker/production/fastapi/entrypoint:/entrypoint + - ./docker/local/fastapi/start:/start + - ./.history:/commandhistory + env_file: + - ./envs/local/fastapi + - ./envs/local/postgres + labels: + traefik.enabled: "True" + traefik.backend: "fastapi" + traefik.fastapi.port: 8000 + traefik.fastapi.frontend.rule: "Host:api.electioncal.docker" + traefik.static.port: 4242 + traefik.static.frontend.rule: "Host:electioncal.docker" + entrypoint: /entrypoint + command: /start diff --git a/docker-compose-production.yml b/docker-compose-production.yml new file mode 100644 index 0000000000..5f082c028b --- /dev/null +++ b/docker-compose-production.yml @@ -0,0 +1,31 @@ +version: "3" + +services: + django: + image: ${CI_REGISTRY_IMAGE}/fastapi:${CI_COMMIT_REF_SLUG} + working_dir: /code + env_file: + - /srv/environ/fastapi + - /srv/environ/postgres + networks: + - traefik-net + - back + deploy: + labels: + traefik.docker.network: traefik-net + traefik.frontend.headers.SSLProxyHeaders: "X-FORWARDED-PROTO:https" + traefik.backend: "fastapi" + traefik.fastapi.frontend.rule: "Host:api.electioncal.us" + traefik.fastapi.port: 8000 + traefik.fastapi.frontend.protocol: "http" + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + entrypoint: /entrypoint + command: /start + +networks: + traefik-net: + external: true + back: diff --git a/docker/local/fastapi/Dockerfile b/docker/local/fastapi/Dockerfile new file mode 100644 index 0000000000..38678cfebd --- /dev/null +++ b/docker/local/fastapi/Dockerfile @@ -0,0 +1,56 @@ +FROM jetblackpope/pybuntu:3.8-focal + +ARG DEBIAN_FRONTEND=noninteractive + +ARG USERNAME=code +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +# Install a couple python "system" packages +RUN python -m pip install pip pipenv black isort apt-select --upgrade + +# Find a faster mirror cause the achrive ones are almost always super slow +RUN apt-select \ + && cp /etc/apt/sources.list /etc/apt/sources.list.backup \ + && mv sources.list /etc/apt/ + +RUN apt-get update + +RUN apt-get install --no-install-recommends --no-install-suggests -y \ + apt-utils \ + dialog \ + ca-certificates + +RUN apt-get install --no-install-recommends --no-install-suggests -y \ + git \ + gpg \ + openssh-client \ + less \ + iproute2 \ + procps \ + curl \ + wget \ + lsb-release \ + build-essential \ + libssl1.1 \ + libpq-dev \ + postgresql-client \ + htop + +# Should probably write this into some sort of bash script that reads in vars +# and doesn't always do all the things. +RUN groupadd --gid $USER_GID $USERNAME \ + && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME -s /bin/bash \ + && apt-get install --no-install-recommends --no-install-suggests -y sudo \ + && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ + && chmod 0440 /etc/sudoers.d/$USERNAME + +RUN echo "export PROMPT_COMMAND='history -a'" >> "/root/.bashrc" \ + && echo "export HISTFILE=/commandhistory/.bash_history_root" >> "/root/.bashrc" + +RUN echo "export PROMPT_COMMAND='history -a'" >> "/home/code/.bashrc" \ + && echo "export HISTFILE=/commandhistory/.bash_history_code" >> "/home/code/.bashrc" + +ENV PYTHONPATH="/code/api" + +WORKDIR /code \ No newline at end of file diff --git a/docker/local/fastapi/entrypoint b/docker/local/fastapi/entrypoint new file mode 100644 index 0000000000..29bb0681fb --- /dev/null +++ b/docker/local/fastapi/entrypoint @@ -0,0 +1,6 @@ +#!/bin/bash + +# Let the DB start +pipenv run python /code/api/scripts/wait_for_postgres.py + +exec "$@" \ No newline at end of file diff --git a/docker/local/fastapi/start b/docker/local/fastapi/start new file mode 100644 index 0000000000..37e7caa079 --- /dev/null +++ b/docker/local/fastapi/start @@ -0,0 +1,13 @@ +#!/bin/bash + +set -o errexit +set -o pipefail +set -o nounset + +# Run migrations +alembic upgrade head + +# Create initial data in DB +python /code/api/scripts/init_data.py + +uvicorn api.app.main:app --reload --host 0.0.0.0 --port 8000 \ No newline at end of file diff --git a/docker/local/traefik/traefik.toml b/docker/local/traefik/traefik.toml new file mode 100644 index 0000000000..9e9995beeb --- /dev/null +++ b/docker/local/traefik/traefik.toml @@ -0,0 +1,23 @@ +debug = true + +logLevel = "debug" +defaultEntryPoints = ["http"] + +[api] +entryPoint = "admin" +dashboard = true + + +[entryPoints] + [entryPoints.admin] + address = ":8080" + + [entryPoints.http] + address = ":80" + +[retry] + +[docker] +domain = "docker" +watch = true +exposedByDefault = false diff --git a/docker/production/fastapi/Dockerfile b/docker/production/fastapi/Dockerfile new file mode 100644 index 0000000000..4a54cd30dc --- /dev/null +++ b/docker/production/fastapi/Dockerfile @@ -0,0 +1,33 @@ +FROM jetblackpope/pybuntu:3.8-focal + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update + +RUN apt-get install --no-install-recommends --no-install-suggests -y \ + apt-utils \ + dialog \ + ca-certificates + +RUN apt-get install --no-install-recommends --no-install-suggests -y \ + postgresql-client \ + libpq-dev \ + git \ + gpg \ + build-essential \ + iproute2 \ + libssl1.1 \ + lsb-release + + +RUN python -m pip install pip pipenv --upgrade + +WORKDIR /code + +COPY . . + +COPY ./docker/production/fastapi/entrypoint /entrypoint + +COPY ./docker/production/fastapi/start /start + +RUN pipenv install --system diff --git a/docker/production/fastapi/entrypoint b/docker/production/fastapi/entrypoint new file mode 100644 index 0000000000..0650ee2834 --- /dev/null +++ b/docker/production/fastapi/entrypoint @@ -0,0 +1,6 @@ +#!/bin/bash + +# Let the DB start +python /code/api/scripts/wait_for_postgres.py + +exec "$@" \ No newline at end of file diff --git a/docker/production/fastapi/start b/docker/production/fastapi/start new file mode 100644 index 0000000000..070f2bc72a --- /dev/null +++ b/docker/production/fastapi/start @@ -0,0 +1,7 @@ +#!/bin/bash + +set -o errexit +set -o pipefail +set -o nounset + +uvicorn api.main:app --host 0.0.0.0 --port 8000 \ No newline at end of file diff --git a/envs/local/fastapi b/envs/local/fastapi new file mode 100644 index 0000000000..43beadaea4 --- /dev/null +++ b/envs/local/fastapi @@ -0,0 +1,30 @@ +# Python Config things for local dev +PYTHONDONTWRITEBYTECODE=1 +PIPENV_VENV_IN_PROJECT=1 +PIPENV_VERBOSITY=-1 + +# Set to the number of processors times 2 +PIPENV_MAX_SUBPROCESS=32 + +# Backend Stuff +SECRET_KEY="" +SUPERUSER_EMAIL=admin@api.electioncal.us +SUPERUSER_PASSWORD=changethis + +# Email Config +SMTP_TLS=True +SMTP_PORT=587 +SMTP_HOST="" +SMTP_USER="" +SMTP_PASSWORD="" + +# Error reporting +SENTRY_DSN="" + +# Domain Configurations +BASE_URL=electioncal.docker + +# TWILIO STUFF +TWILIO_SID="" +TWILIO_AUTH_TOKEN="" +TWILIO_SECRET="" diff --git a/envs/local/pgadmin b/envs/local/pgadmin new file mode 100644 index 0000000000..669b1a6e3a --- /dev/null +++ b/envs/local/pgadmin @@ -0,0 +1,3 @@ +PGADMIN_LISTEN_PORT=5050 +PGADMIN_DEFAULT_EMAIL=admin +PGADMIN_DEFAULT_PASSWORD=changethis \ No newline at end of file diff --git a/envs/local/postgres b/envs/local/postgres new file mode 100644 index 0000000000..f277416d5d --- /dev/null +++ b/envs/local/postgres @@ -0,0 +1,5 @@ +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_DB=electioncal +POSTGRES_USER=electioncal +POSTGRES_PASSWORD=simpledevpassword diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..93fa75f649 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[tool.isort] +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +line_length = 99 + +[tool.black] +line-length = 99 +target-version = ['py38'] +# exclude = '''''' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 39eb2d3672..96a1c7ef46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,12 @@ -tomlkit -jinja2 -icalendar -pendulum +-i https://pypi.org/simple +icalendar==4.0.6 +jinja2==2.11.2 +markupsafe==1.1.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' mistune==2.0.0a4 -Pillow +pendulum==2.1.2 +pillow==7.2.0 +python-dateutil==2.8.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' +pytz==2020.1 +pytzdata==2020.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +six==1.15.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' +tomlkit==0.7.0