-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 2b1c72a
Showing
16 changed files
with
585 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
[flake8] | ||
max-line-length = 88 | ||
ignore = E231,E501 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
venv/ | ||
|
||
*.pyc | ||
__pycache__/ | ||
|
||
instance/ | ||
|
||
.pytest_cache/ | ||
.coverage | ||
htmlcov/ | ||
|
||
dist/ | ||
build/ | ||
*.egg-info/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
repos: | ||
- repo: https://github.com/ambv/black | ||
rev: stable | ||
hooks: | ||
- id: black | ||
language_version: python3.6 | ||
- repo: https://github.com/pre-commit/pre-commit-hooks | ||
rev: v1.2.3 | ||
hooks: | ||
- id: flake8 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
### Initial Setup | ||
|
||
See [here](https://www.digitalocean.com/community/tutorials/how-to-serve-flask-applications-with-uswgi-and-nginx-on-ubuntu-18-04) for initial server setup. | ||
|
||
Create the directories: | ||
|
||
``` | ||
sudo mkdir -p /var/app/decidel | ||
sudo chown -R $USER:$USER /var/app/decidel | ||
sudo mkdir -p /var/log/decidel | ||
sudo chown -R $USER:$USER /var/log/decidel | ||
``` | ||
|
||
Create `/etc/systemd/system/decidel.service` to run the app under supervision: | ||
|
||
``` | ||
[Unit] | ||
Description=uWSGI instance to serve decidel | ||
After=network.target | ||
[Service] | ||
User=<non-root-user> | ||
Group=www-data | ||
WorkingDirectory=/var/app/decidel | ||
Environment="PATH=/var/app/decidel/venv/bin" | ||
ExecStart=/var/app/decidel/venv/bin/uwsgi --ini decidel.ini | ||
[Install] | ||
WantedBy=multi-user.target | ||
``` | ||
|
||
Start the service and set it to run on boot: | ||
|
||
``` | ||
sudo systemctl start decidel | ||
sudo systemctl enable decidel | ||
``` | ||
|
||
Setup nginx directive: | ||
|
||
``` | ||
# /etc/nginx/sites-available/api.decidel.ca | ||
server { | ||
# ssl directives | ||
... | ||
server_name api.decidel.ca; | ||
location / { | ||
include uwsgi_params; | ||
uwsgi_pass unix:/var/app/decidel/decidel.sock; | ||
} | ||
} | ||
``` | ||
|
||
``` | ||
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/ | ||
sudo systemctl restart nginx | ||
``` | ||
|
||
Install redis: | ||
|
||
* https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-on-ubuntu-18-04 | ||
* https://www.digitalocean.com/community/tutorials/how-to-back-up-and-restore-your-redis-data-on-ubuntu-14-04 | ||
|
||
Make sure `REDIS_URL` is set correctly in /var/app/decidel/instance/config.py | ||
|
||
### Updates | ||
|
||
``` | ||
. venv/bin/activate | ||
pip install -r requirements.txt | ||
rsync -avzr --delete --exclude '__pycache__*' decidel.ini default_config.py wsgi.py decidel venv <server>/var/app/decidel | ||
ssh -t <server> 'systemctl restart decidel' | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
Swagger UI at https://api.decidel.ca/v1/docs | ||
|
||
[Front-end](https://github.com/jpmunz/decidel-web) at https://decidel.ca | ||
|
||
See [DEPLOYMENT.md](DEPLOYMENT.md) for deployment instructions. | ||
|
||
View the [Roadmap on Trello](https://trello.com/b/z4REn8Mg/decidel-roadmap). | ||
|
||
### DEVELOPMENT | ||
|
||
### Setup | ||
|
||
``` | ||
python3 -m venv venv | ||
. venv/bin/activate | ||
pip install -r requirements.txt | ||
``` | ||
|
||
Make sure redis is installed | ||
|
||
``` | ||
wget http://download.redis.io/redis-stable.tar.gz | ||
tar xvzf redis-stable.tar.gz | ||
cd redis-stable | ||
make | ||
``` | ||
|
||
### Run | ||
|
||
``` | ||
redis-server | ||
``` | ||
|
||
``` | ||
. venv/bin/activate | ||
FLASK_APP=decidel FLASK_ENV=development flask run | ||
``` | ||
|
||
 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import pytest | ||
from decidel import create_app, redis_store | ||
|
||
|
||
def create_test_app(): | ||
return create_app( | ||
test_config=dict(REDIS_URL="redis://:@localhost:6379/1", TESTING=True,) | ||
) | ||
|
||
|
||
@pytest.fixture(autouse=True, scope="session") | ||
def redis_cleanup(): | ||
yield | ||
redis_store.flushdb() | ||
|
||
|
||
@pytest.fixture(scope="function") | ||
def client(): | ||
app = create_test_app() | ||
with app.test_client() as client: | ||
yield client |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
[uwsgi] | ||
module = wsgi:app | ||
|
||
master = true | ||
processes = 5 | ||
|
||
socket = decidel.sock | ||
chmod-socket = 660 | ||
vacuum = true | ||
|
||
die-on-term = true | ||
|
||
logto = /var/log/decidel/%n.log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import os | ||
from flask import Flask | ||
from flask_cors import CORS | ||
from .api import blueprint as api_blueprint | ||
from .db import redis_store | ||
|
||
|
||
def create_app(test_config=None): | ||
app = Flask(__name__, instance_relative_config=True) | ||
|
||
if app.config["ENV"] == "production": | ||
app.config.from_object("default_config.ProductionConfig") | ||
else: | ||
app.config.from_object("default_config.DevelopmentConfig") | ||
|
||
if test_config: | ||
app.config.from_mapping(test_config) | ||
else: | ||
app.config.from_pyfile("config.py", silent=True) | ||
|
||
# ensure the instance folder exists | ||
try: | ||
os.makedirs(app.instance_path) | ||
except OSError: | ||
pass | ||
|
||
CORS(app) | ||
redis_store.init_app(app) | ||
app.register_blueprint(api_blueprint, url_prefix="/v1") | ||
|
||
return app |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .endpoints import blueprint # noqa: F401 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import jsonschema | ||
|
||
from flask import abort, request, Blueprint | ||
from flask_restplus import Api, Resource | ||
|
||
from .models import DecidelModel | ||
|
||
blueprint = Blueprint("api", __name__) | ||
|
||
api = Api( | ||
blueprint, | ||
doc="/docs", | ||
title="Decidel API", | ||
version="1.0", | ||
description="API for interacting with Decidels", | ||
) | ||
|
||
decidel = api.namespace("decidels", description="Core interactions with Decidels") | ||
decidel_schema_model = decidel.schema_model("Decidel", DecidelModel.schema) | ||
|
||
|
||
class DecidelResource(Resource): | ||
@staticmethod | ||
def validate_request(value): | ||
try: | ||
jsonschema.validate(request.json, DecidelModel.schema) | ||
except jsonschema.ValidationError as e: | ||
abort(400, e.message) | ||
|
||
|
||
@decidel.route("/") | ||
class DecidelList(DecidelResource): | ||
@decidel.expect(decidel_schema_model) | ||
@decidel.response(201, "Created", decidel_schema_model) | ||
@decidel.response(400, description="Invalid Decidel") | ||
def post(self): | ||
""" | ||
Creates a new Decidel | ||
""" | ||
|
||
self.validate_request(request.json) | ||
decidel = DecidelModel.create(request.json) | ||
|
||
return decidel.as_json(), 201 | ||
|
||
|
||
@decidel.route("/<string:id>") | ||
class Decidel(DecidelResource): | ||
@decidel.response(200, "Success", decidel_schema_model) | ||
@decidel.response(404, description="Not Found") | ||
def get(self, id): | ||
""" | ||
Gets a Decidel | ||
""" | ||
|
||
decidel = DecidelModel.get_or_404(id) | ||
|
||
return decidel.as_json() | ||
|
||
@decidel.expect(decidel_schema_model) | ||
@decidel.response(200, "Success", decidel_schema_model) | ||
@decidel.response(400, description="Invalid Decidel") | ||
@decidel.response(404, description="Not Found") | ||
def put(self, id): | ||
""" | ||
Updates a Decidel | ||
""" | ||
|
||
decidel = DecidelModel.get_or_404(id) | ||
|
||
self.validate_request(request.json) | ||
decidel.update(request.json) | ||
|
||
return decidel.as_json() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import uuid | ||
import json | ||
from flask import abort | ||
from decidel.db import redis_store | ||
|
||
|
||
class DecidelModel(object): | ||
schema = { | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"$id": "https://decidels.ca/schemas/decidel.json", | ||
"description": "An object that encapsulates making a decision among a group of friends", | ||
"type": "object", | ||
"properties": { | ||
"id": {"type": "string"}, | ||
"title": {"type": "string"}, | ||
"options": { | ||
"type": "array", | ||
"minItems": 2, | ||
"items": { | ||
"type": "object", | ||
"properties": { | ||
"title": {"type": "string"}, | ||
"isRemoved": {"type": "boolean"}, | ||
}, | ||
"required": ["title"], | ||
"additionalProperties": False, | ||
}, | ||
}, | ||
"deciders": { | ||
"type": "array", | ||
"minItems": 2, | ||
"items": { | ||
"type": "object", | ||
"properties": { | ||
"name": {"type": "string"}, | ||
"isNext": {"type": "boolean"}, | ||
}, | ||
"required": ["name"], | ||
"additionalProperties": False, | ||
}, | ||
}, | ||
"history": { | ||
"type": "array", | ||
"items": { | ||
"type": "object", | ||
"properties": { | ||
"userName": {"type": "string"}, | ||
"title": {"type": "string"}, | ||
"action": {"type": "string", "enum": ["ADDED", "REMOVED"]}, | ||
}, | ||
"required": ["userName", "title", "action"], | ||
"additionalProperties": False, | ||
}, | ||
}, | ||
}, | ||
"required": ["title", "options", "deciders", "history"], | ||
"additionalProperties": False, | ||
} | ||
|
||
@staticmethod | ||
def key(id): | ||
return "decidels:{}".format(id) | ||
|
||
@staticmethod | ||
def get_or_404(id): | ||
stored_value = redis_store.get(DecidelModel.key(id)) | ||
|
||
if not stored_value: | ||
abort(404) | ||
|
||
return DecidelModel(stored_value) | ||
|
||
@staticmethod | ||
def create(from_json): | ||
id = uuid.uuid4().hex | ||
from_json["id"] = id | ||
|
||
stored_value = json.dumps(from_json) | ||
redis_store.set(DecidelModel.key(id), stored_value) | ||
|
||
return DecidelModel(stored_value) | ||
|
||
def __init__(self, stored_value): | ||
self.stored_value = stored_value | ||
|
||
def update(self, from_json): | ||
current = self.as_json() | ||
current.update(from_json) | ||
|
||
self.stored_value = json.dumps(current) | ||
redis_store.set(DecidelModel.key(self.as_json()["id"]), self.stored_value) | ||
|
||
return self | ||
|
||
def as_json(self): | ||
return json.loads(self.stored_value) |
Oops, something went wrong.