Skip to content

Commit 1e6176d

Browse files
committed
Initial Commit
0 parents  commit 1e6176d

File tree

14 files changed

+373
-0
lines changed

14 files changed

+373
-0
lines changed

.gitignore

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
5+
# C extensions
6+
*.so
7+
8+
# Distribution / packaging
9+
.Python
10+
env/
11+
build/
12+
develop-eggs/
13+
dist/
14+
downloads/
15+
eggs/
16+
.eggs/
17+
lib/
18+
lib64/
19+
parts/
20+
sdist/
21+
var/
22+
*.egg-info/
23+
.installed.cfg
24+
*.egg
25+
26+
# PyInstaller
27+
# Usually these files are written by a python script from a template
28+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
29+
*.manifest
30+
*.spec
31+
32+
# Installer logs
33+
pip-log.txt
34+
pip-delete-this-directory.txt
35+
36+
# Unit test / coverage reports
37+
htmlcov/
38+
.tox/
39+
.coverage
40+
.coverage.*
41+
.cache
42+
nosetests.xml
43+
coverage.xml
44+
45+
# Translations
46+
*.mo
47+
*.pot
48+
49+
# Django stuff:
50+
*.log
51+
52+
# Sphinx documentation
53+
docs/_build/
54+
55+
# PyBuilder
56+
target/
57+
58+
# PyCharm
59+
.idea/
60+
env/*
61+
env
62+
migrations
63+
migrations/*
64+
65+
# OSX
66+
.DS_Store

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Flask Rest API
2+
This program shows how to set up a flaskrestapi with postgres_db, blueprint, sqlalchemy, marshmallow, wsgi, unittests
3+
4+
# Install guide
5+
6+
### Clone the repo
7+
8+
```$ git clone https://github.com/bisratyalew/flask-rest-api.git```
9+
```$ cd flask-api-example```
10+
11+
### Create the virtualenv
12+
13+
```$ mkvirtualenv flask-api-example```
14+
15+
### Install dependencies
16+
17+
```$ pip install -r requirements.txt```
18+
19+
### Running on development machine
20+
```
21+
python manage.py runserver
22+
```
23+
24+
### Features
25+
26+
* Rest Api Flask App
27+
* Serialize object into response
28+
* Integration with Flask-IO to parse parameters from request
29+
* Configuration per environment
30+
* Integration with SQL Alchemy
31+
* Uses Postgres DB
32+
* Unit tests per module

flast_rest_app/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from flask_io import FlaskIO
2+
from flask_sqlalchemy import SQLAlchemy
3+
4+
db = SQLAlchemy()
5+
io = FlaskIO()

flast_rest_app/application.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import logging
2+
import os
3+
4+
from flask import Flask
5+
from werkzeug.utils import import_string
6+
from . import config, db, io
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
def create_app(environment):
12+
"""Creates a new Flask application and initialize application."""
13+
14+
config_map = {
15+
'development': config.Development(),
16+
'testing': config.Testing(),
17+
'production': config.Production(),
18+
}
19+
20+
config_obj = config_map[environment.lower()]
21+
22+
app = Flask(__name__)
23+
app.config.from_object(config_obj)
24+
app.url_map.strict_slashes = False
25+
app.add_url_rule('/', 'home', home)
26+
27+
register_blueprints(app)
28+
29+
db.init_app(app)
30+
io.init_app(app)
31+
32+
return app
33+
34+
35+
def home():
36+
return dict(name='Flask REST API')
37+
38+
39+
def register_blueprints(app):
40+
root_folder = 'flask_rest_api'
41+
42+
for dir_name in os.listdir(root_folder):
43+
module_name = root_folder + '.' + dir_name + '.views'
44+
module_path = os.path.join(root_folder, dir_name, 'views.py')
45+
46+
if os.path.exists(module_path):
47+
module = import_string(module_name)
48+
obj = getattr(module, 'app', None)
49+
if obj:
50+
app.register_blueprint(obj)

flast_rest_app/companies/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
##

flast_rest_app/companies/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from datetime import datetime
2+
from .. import db
3+
4+
5+
class Company(db.Model):
6+
__tablename__ = 'table_companies'
7+
8+
id = db.Column(db.Integer(), primary_key=True, autoincrement=True)
9+
name = db.Column(db.String(20), nullable=False)
10+
country_code = db.Column(db.String(2), nullable=False)
11+
website = db.Column(db.String(100))
12+
enabled = db.Column(db.Boolean(), nullable=False, default=True)
13+
updated_at = db.Column(db.DateTime(), nullable=False, default=lambda: datetime.utcnow())
14+
created_at = db.Column(db.DateTime(), nullable=False, default=lambda: datetime.utcnow())

flast_rest_app/companies/schemas.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from flask_io import fields, Schema, post_dump
2+
from .models import Company
3+
4+
5+
class CompanySchema(Schema):
6+
id = fields.Integer(dump_only=True)
7+
name = fields.String(required=True)
8+
country_code = fields.String(required=True)
9+
website = fields.String(allow_none=True)
10+
enabled = fields.Boolean(required=True)
11+
updated_at = fields.DateTime(dump_only=True)
12+
created_at = fields.DateTime(dump_only=True)
13+
14+
@post_dump
15+
def make_object(self, data):
16+
return Company(**data)

flast_rest_app/companies/views.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from datetime import datetime
2+
from flask import Blueprint
3+
from flask_io import fields
4+
from sqlalchemy import func
5+
from sqlalchemy_utils.functions import sort_query
6+
from .models import Company
7+
from .schemas import CompanySchema
8+
from .. import db, io
9+
10+
app = Blueprint('companies', __name__, url_prefix='/companies')
11+
12+
13+
@app.route('/', methods=['POST'])
14+
@io.from_body('company', CompanySchema)
15+
def add_company(company):
16+
db.session.add(company)
17+
db.session.commit()
18+
return company
19+
20+
21+
@app.route('/<int:id>', methods=['DELETE'])
22+
def delete_company(id):
23+
company = Company.query.filter_by(id=id).first()
24+
25+
if not company:
26+
return io.not_found('Company not found: ' + str(id))
27+
28+
db.session.delete(company)
29+
db.session.commit()
30+
31+
return io.no_content()
32+
33+
34+
@app.route('/<int:id>', methods=['GET'])
35+
@io.marshal_with(CompanySchema)
36+
def get_company(id):
37+
company = Company.query.filter_by(id=id).first()
38+
39+
if not company:
40+
return io.not_found('Company not found: ' + str(id))
41+
42+
return company
43+
44+
45+
@app.route('/', methods=['GET'])
46+
@io.from_query('name', fields.String())
47+
@io.from_query('order_by', fields.String(missing='name'))
48+
@io.from_query('offset', fields.Integer(missing=0))
49+
@io.from_query('limit', fields.Integer(missing=10))
50+
@io.marshal_with(CompanySchema)
51+
def list_companies(name, order_by, offset, limit):
52+
query = Company.query
53+
54+
if name:
55+
query = query.filter(func.lower(Company.name).contains(func.lower(name)))
56+
if order_by:
57+
query = sort_query(query, order_by)
58+
if offset:
59+
query = query.offset(offset)
60+
if limit:
61+
query = query.limit(limit)
62+
63+
return query.all()
64+
65+
66+
@app.route('/<int:id>', methods=['POST', 'PATCH'])
67+
@io.from_body('update_company', CompanySchema)
68+
@io.marshal_with(CompanySchema)
69+
def update_company(id, new_company):
70+
company = Company.query.filter_by(id=id).first()
71+
72+
if not company:
73+
return io.not_found('Company not found: ' + str(id))
74+
75+
company.name = new_company.name
76+
company.country_code = new_company.country_code
77+
company.website = new_company.website
78+
company.enabled = new_company.enabled
79+
company.updated_at = datetime.now()
80+
81+
return company

flast_rest_app/config.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
class Development(object):
2+
DEBUG = True
3+
4+
## DB URL FOR DEVELOPMENT
5+
SQLALCHEMY_DATABASE_URI = 'postgres://user:pass@localhost/dbname'
6+
7+
8+
class Testing(object):
9+
DEBUG = False
10+
11+
## DB URL FOR TESTING
12+
SQLALCHEMY_DATABASE_URI = 'postgres://user:pass@test/dbname'
13+
14+
15+
class Production(object):
16+
DEBUG = False
17+
18+
## DB URL FOR PRODUCTION
19+
SQLALCHEMY_DATABASE_URI = 'postgres://user:pass@production/dbname'

manage.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from flask_migrate import Migrate, MigrateCommand
2+
from flask_script import Manager
3+
from flask_rest_api import db
4+
from flask_rest_api.application import create_app
5+
6+
app = create_app('development')
7+
8+
migrate = Migrate(app, db)
9+
10+
manager = Manager(app)
11+
manager.add_command('db', MigrateCommand)
12+
13+
if __name__ == "__main__":
14+
manager.run()

requirements.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
## Requirements to install
2+
flask==0.10.1
3+
flask-io==1.10.0
4+
flask-migrate==1.7.0
5+
flask-script==2.0.5
6+
flask-sqlalchemy==2.1
7+
sqlalchemy-utils==0.31.6
8+
psycopg2==2.6.1

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
##

tests/company_test.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import json
2+
3+
from flask_rest_api import db
4+
from flask_rest_api.application import create_app
5+
6+
## Module to make a unit test
7+
from unittest import TestCase
8+
9+
10+
class TestView(TestCase):
11+
def setUp(self):
12+
self.app = create_app('development')
13+
self.client = self.app.test_client()
14+
db.create_all(app=self.app)
15+
16+
def test_create_company(self):
17+
data = dict(name='test', country_code='ET', website='http://example.com', enabled=True)
18+
response = self.client.post('/companies', data=json.dumps(data), content_type='application/json')
19+
assert response.status_code == 200
20+
21+
def test_delete_company(self):
22+
data = dict(name='test', country_code='ET', website='http://example.com', enabled=True)
23+
response = self.client.post('/companies', data=json.dumps(data), content_type='application/json')
24+
25+
company = json.loads(response.get_data(as_text=True))
26+
27+
response = self.client.delete('/companies/' + str(company.get('id')))
28+
assert response.status_code == 204
29+
30+
31+
def get_company(self):
32+
data = dict(name='test', country_code='ET', website='http://example.com', enabled=True)
33+
response = self.client.post('/companies', data=json.dumps(data), content_type='application/json')
34+
company = json.loads(response.get_data(as_text=True))
35+
36+
response = self.client.get('/companies/' + str(company.get('id')))
37+
38+
assert response.status_code == 200
39+
40+
company = json.loads(response.get_data(as_text=True))
41+
42+
assert company.get('name') == 'test'
43+
44+
45+
def list_companies(self):
46+
data = dict(name='test', country_code='ET', website='http://example.com', enabled=True)
47+
self.client.post('/companies', data=json.dumps(data), content_type='application/json')
48+
49+
response = self.client.get('/companies')
50+
51+
assert response.status_code == 200
52+
53+
companies = json.loads(response.get_data(as_text=True))
54+
55+
assert len(companies) == 1

wsgi.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import os
2+
3+
from flask_rest_app.application import create_app
4+
5+
6+
env = os.environ.get('APP_ENV')
7+
8+
if not env:
9+
raise Exception('APP_ENV can not be found')
10+
11+
application = create_app(env)

0 commit comments

Comments
 (0)