Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Sumukh committed Mar 24, 2020
0 parents commit 381e435
Show file tree
Hide file tree
Showing 19 changed files with 402 additions and 0 deletions.
48 changes: 48 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
*.pyc
*.pyo
*.swp
*~

# Packages
*.egg
*.egg-info
dist
build/
eggs
bin
sdist

#Sphinx builds
_build

# Installer logs
pip-log.txt

# Flake8 violation file
violations.flake8.txt

# Unit test / coverage reports
.coverage
.tox
nosetests.xml
__pycache__
database.db
.vscode
.pytest_cache

# Sphinx
docs/_build

# Virtual environments
env
env*

# Env Vars
*.env*

.webassets-cache
*.cache
.DS_Store
*.sublime-*

appname/static/public/*
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Create Flask API

A boilerplate Flask application on which to build APIs using best practices.

- ✅ RESTful API (with argument validation & output schemas)
- ✅ 100% Code Coverage in Tests
- ✅ Using Flask Best Practices
- ✅ Support Heroku Deployment
- ✅ With an example image API

## Installation

```bash
$ git clone
$ cd create-flask-api
$ python3 -m venv env; source env/bin/activate # To set up an virtual env
$ ./dev-server.sh # runs: FLASK_DEBUG="true" FLASK_APP="server:create_app" flask run
```

### Key Files:

The API Endpoints are defined in `server/api` and registered to specific routes in `server/api/__init__.py`.

## Testing

To test with a coverage report:

`pytest --cov-report term-missing --cov=server`

## Limitations

This repo is designed to be the barebones for an API. If you want to hook into a database or do authentication, you should look into [Flask Ignite](https://github.com/sumukh/ignite)
29 changes: 29 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "Create Flask API",
"description": "Flask API",
"keywords": [
"flask",
"api",
"scaffolding",
"ignite"
],
"website": "https://github.com/sumukh/create-flask-api/",
"repository": "https://github.com/sumukh/create-flask-apiapi/",
"logo": "https://github.com/Sumukh/Ignite/raw/master/appname/static/public/ignite/ignite-icon.png",
"success_url": "/",
"scripts": {},
"env": {
"SECRET_TOKEN": {
"description": "A secret key for verifying the integrity of signed cookies.",
"generator": "secret"
},
"FLASK_APP": {
"description": "Where the flask app lives.",
"value": "wsgi.py"
},
"FLASK_ENV": {
"description": "What environment is this in",
"value": "prod"
}
}
}
7 changes: 7 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import pytest

from server import create_app

@pytest.fixture
def app():
return create_app('test')
1 change: 1 addition & 0 deletions dev-server.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FLASK_DEBUG="true" FLASK_APP="server:create_app" flask run
29 changes: 29 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Flask==1.1.1
jinja2==2.11.1
Werkzeug~=0.16.1

requests
gunicorn

Flask-Caching>=1.3.3
Flask-RESTful==0.3.8
Flask-Limiter

# Flask-SocketIO>=3.1.0 # Realtime Websockets
# python-engineio>=3.0.0 # Needed to fix startup error

# Timezones
pytz
arrow

# Other
itsdangerous==1.1.0
hashids==1.2.0
humanize==2.0.0

# Testing
pytest==5.4.1
pytest-cov==2.8.1
pytest-flask
mccabe==0.6.1
flake8==3.7.9
16 changes: 16 additions & 0 deletions server/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from flask import Flask
from server.api import api_blueprint

def create_app(object_name='server.settings.DevConfig'):
"""
An flask application factory, as explained here:
http://flask.pocoo.org/docs/patterns/appfactories/
Arguments:
object_name: the python path of the config object,
e.g. appname.settings.ProdConfig
"""

app = Flask(__name__)
app.config.from_object(object_name)
app.register_blueprint(api_blueprint, url_prefix="/api")
return app
46 changes: 46 additions & 0 deletions server/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from flask import Blueprint, request
import flask_restful as restful

from server.api.info import ApiInfo
from server.api.categories import CategoriesApi
from server.api.wallpaper import (UnsplashSearchApi, UnsplashCategoryApi,
RedditSearchApi)

api_blueprint = Blueprint('api', __name__)
api_blueprint.config = {}

api = restful.Api(api_blueprint)

@api_blueprint.record
def record_params(setup_state):
""" Load used app configs into local config on registration from
server/__init__.py """
app = setup_state.app
api_blueprint.config['tz'] = app.config.get('TIMEZONE', 'utc')
api_blueprint.config['debug'] = app.debug

@api.representation('application/json')
def envelope_api(data, code, headers=None):
""" API response envelope (for metadata/pagination).
Optionally wraps JSON response in envelope.
This is for successful requests only.
data is the object returned by the API.
code is the HTTP status code as an int
"""
if not request.args.get('envelope'):
return restful.representations.json.output_json(data, code, headers)
message = 'success'
data = {
'data': data,
'code': code,
'message': message
}
return restful.representations.json.output_json(data, code, headers)


# If you want to version your API, you can do that by adding a prefix to the route here
api.add_resource(ApiInfo, '/info')
api.add_resource(CategoriesApi, '/categories')
api.add_resource(UnsplashSearchApi, '/wallpaper/unsplash/search')
api.add_resource(UnsplashCategoryApi, '/wallpaper/unsplash/category/<int:category_id>')
api.add_resource(RedditSearchApi, '/wallpaper/reddit/<string:subreddit>')
24 changes: 24 additions & 0 deletions server/api/categories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from flask_restful import Resource, fields, marshal_with

class Category:
def __init__(self, id, name, state='featured'):
self.id = id
self.name = name
self.state = state

# Example of returning objects and using a schema to marshal out specific fields
class CategoriesApi(Resource):
get_fields = {
'name': fields.String,
'id': fields.Integer
}

@marshal_with(get_fields)
def get(self):
return [
Category(3330448, 'nature'),
Category(3356570, 'travel'),
Category(1065976, 'wallpapers'),
Category(3330445, 'patterns'),
Category(3694365, 'gradients'),
]
8 changes: 8 additions & 0 deletions server/api/info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from flask_restful import Resource

class ApiInfo(Resource):
def get(self):
return {
'version': '1.0',
'example_endpoints': ['/categories']
}
26 changes: 26 additions & 0 deletions server/api/wallpaper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from flask_restful import Resource, reqparse
from services.images import (search_unsplash, get_unsplash_collection,
get_reddit_images)

class UnsplashSearchApi(Resource):
parser = reqparse.RequestParser()
parser.add_argument('query', required=True)
parser.add_argument('page', type=int, required=False)

def get(self):
args = self.parser.parse_args()
response = search_unsplash(args['query'], page=args['page'])
return response

class UnsplashCategoryApi(Resource):
parser = reqparse.RequestParser()
parser.add_argument('page', required=False)

def get(self, category_id=None):
args = self.parser.parse_args()
return get_unsplash_collection(collection_id=category_id, page=args['page'])

class RedditSearchApi(Resource):
def get(self, subreddit):
response = get_reddit_images(subreddit)
return response
Empty file added server/extensions.py
Empty file.
14 changes: 14 additions & 0 deletions server/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import os

class BaseConfig:
TESTING = False

class DevConfig(BaseConfig):
SERVICE_API_KEY = os.getenv('SERVICE_API_KEY', "ABC")

class TestConfig(BaseConfig):
TESTING = True
SERVICE_API_KEY = os.getenv('SERVICE_API_KEY', "BDC")

class ProdConfig(BaseConfig):
SERVICE_API_KEY = os.getenv('SERVICE_API_KEY', "DEF")
54 changes: 54 additions & 0 deletions services/images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import requests

USER_AGENT = 'Create Flask Api 1.0'

def get_reddit_images(subreddit):
response = requests.get("https://reddit.com/r/{}.json".format(subreddit),
headers={'User-agent': USER_AGENT}).json()
if 'error' in response:
return []
posts = response["data"]["children"]
posts_with_image = [p["data"] for p in posts
if p["data"].get("post_hint") == "image"]
return [{
'url': p['url'],
'title': p['title'],
'source': "https://reddit.com{}".format(p['permalink']),
'author': {
'name': 'Reddit',
'description': subreddit,
'profile_pic': '',
'link': "https://reddit.com/r/{}".format(subreddit),
}
} for p in posts_with_image if p and not p['over_18']]

def format_unsplash_image(p):
return {
'url': p['urls']['raw'],
'title': p['description'] or p['alt_description'],
'source': p['links']['html'],
'author': {
'name': p['user']['name'],
'description': p['user']['bio'],
'profile_pic': p['user']['profile_image'].get('medium'),
'link': p['user']['links']['html'],
}
}

def get_unsplash_collection(collection_id=3330448, page=1):
endpoint_url = 'https://unsplash.com/napi/collections/{}/photos'.format(
collection_id
)
photos = requests.get(endpoint_url, {'page': page},
headers={'User-agent': USER_AGENT}).json()

return [format_unsplash_image(p) for p in photos if p]

def search_unsplash(search_term, page=1):
response = requests.get('https://unsplash.com/napi/search/photos', {
'query': search_term,
'per_page': 25,
'page': page,
}, headers={'User-agent': USER_AGENT})
results = response.json().get('results', [])
return [format_unsplash_image(p) for p in results]
10 changes: 10 additions & 0 deletions tests/test_categories_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import pytest

def test_categories_endpoint(client):
response = client.get('/api/categories')
assert response.status_code == 200
data = response.json
assert len(data) > 0
assert data[0]['id'] is not None
assert data[0]['name'] is not None

10 changes: 10 additions & 0 deletions tests/test_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import pytest

def test_info_endpoint(client):
response = client.get('/api/info')
assert response.status_code == 200

def test_envelope(client):
response = client.get('/api/info?envelope=1')
assert response.status_code == 200

7 changes: 7 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import pytest
from server.settings import DevConfig, TestConfig, ProdConfig

def test_testing_settings():
assert DevConfig.TESTING == False
assert ProdConfig.TESTING == False
assert TestConfig.TESTING == True
Loading

0 comments on commit 381e435

Please sign in to comment.