Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jpmunz committed Dec 18, 2019
0 parents commit 2b1c72a
Show file tree
Hide file tree
Showing 16 changed files with 585 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[flake8]
max-line-length = 88
ignore = E231,E501
14 changes: 14 additions & 0 deletions .gitignore
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/
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
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
78 changes: 78 additions & 0 deletions DEPLOYMENT.md
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'
```
39 changes: 39 additions & 0 deletions README.md
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
```

![](https://github.com/jpmunz/decidel-flask/workflows/Build%20and%20Deploy/badge.svg)
21 changes: 21 additions & 0 deletions conftest.py
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
13 changes: 13 additions & 0 deletions decidel.ini
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
31 changes: 31 additions & 0 deletions decidel/__init__.py
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
1 change: 1 addition & 0 deletions decidel/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .endpoints import blueprint # noqa: F401
74 changes: 74 additions & 0 deletions decidel/api/endpoints.py
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()
96 changes: 96 additions & 0 deletions decidel/api/models.py
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)
Loading

0 comments on commit 2b1c72a

Please sign in to comment.