Skip to content

Instantly share code, notes, and snippets.

@regner
Last active January 24, 2016 16:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save regner/7d97e640043ccf78fbe5 to your computer and use it in GitHub Desktop.
Save regner/7d97e640043ccf78fbe5 to your computer and use it in GitHub Desktop.
Fitter

A restful API for Fitter... which I really don't know why it exists other than it seemed like a funny thing to do so why the fuck not. The basic idea is to create a clone of Tinder but for ship fits for EVE Online pulled from zKillboard. The secondary reason for doing this, other than "lol why not" is to give myself a project to do from start to finish as with TDD.

TODO

  • Get new kills from zKB
  • Add statistic resources

API

Characters

POST /characters/<character_id>/

Done when logging in for

Is authenticated.

Character Details

GET /characters/<character_id>/

Get details about a specific character.

Character Fit History

GET /characters/<character_id>/fits/

Returns a history of fits that the character has liked or passed on.

Setting The "Like" Status Of A Fit

PUT /characters/<character_id>/fits/<fit_id>/

Set a fits liked status to True or False. False being the same as passing. Will return 404 if the character has not set a status for this fit yet.

Is authenticated.

New Fit

GET /characters/<character_id>/newfit/

Gets a new fit that the character has not liked or passed on yet. Fits will never be more than 30 days old and are specifically ones that the character has not liked or passed on. The character may have seen it before though.

Is authenticated.

Top Liked Fit

GET /fits/like/top/

A list of the top liked fits.

Most Passed Fit

GET /fits/pass/top/

A list of fits that have the most passes.

General Statistics

GET /statistics/

General statistics from the fitter backend such as how many fits are in the database, how many likes or passes have been processed, how many characters have participated, and more.

import os
import functools
from eveauth.contrib.flask import authenticate
from flask import Flask, abort, request, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_restful import Resource, Api, reqparse
from random import choice
from sqlalchemy import not_
from datetime import datetime
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('FITTER_SQLALCHEMY_URI', 'sqlite:///fitter.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
api = Api(app)
db = SQLAlchemy(app)
def get_or_404(model, object_id):
result = model.query.get(object_id)
if result is None:
abort(404)
return result
class CharacterDetailsModel(db.Model):
__tablename__ = 'character.details'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
liked = db.Column(db.Integer)
passed = db.Column(db.Integer)
fits = db.relationship('CharacterFitModel', backref='character', lazy='dynamic')
join_date = db.Column(db.DateTime)
def __init__(self, character_id, character_name):
self.id = character_id
self.name = character_name
self.join_date = datetime.now()
class CharacterFitModel(db.Model):
__tablename__ = 'character.fits'
id = db.Column(db.Integer, primary_key=True)
character_id = db.Column(db.Integer, db.ForeignKey('character.details.id'), primary_key=True)
liked = db.Column(db.Boolean)
def __init__(self, fit_id, character_id, liked):
self.id = fit_id
self.character_id = character_id
self.liked = liked
class FitModel(db.Model):
__tablename__ = 'fits'
id = db.Column(db.Integer, primary_key=True)
datetime = db.Column(db.DateTime)
items = db.relationship('FitItemModel', backref='fit', lazy='dynamic')
def __init__(self, fit_id, datetime):
self.id = fit_id
self.datetime = datetime
class FitItemModel(db.Model):
__tablename__ = 'fits.items'
id = db.Column(db.Integer, primary_key=True)
fit_id = db.Column(db.Integer, db.ForeignKey('fits.id'))
type_id = db.Column(db.Integer)
flag = db.Column(db.Integer)
quantity = db.Column(db.Integer)
def __init__(self, fit_id, type_id, flag, quantity):
self.fit_id = fit_id
self.type_id = type_id
self.flag = flag
self.quantity = quantity
class Characters(Resource):
@authenticate()
def post(self):
result = CharacterDetailsModel.query.get(request.token['character_id'])
if result is not None:
abort(409, 'Character already exists in the database.')
new_char = CharacterDetailsModel(
request.token['character_id'],
request.token['character_name']
)
db.session.add(new_char)
db.session.commit()
return {}, 201, {'Location': url_for('characterdetails', character_id=request.token['character_id'])}
class CharacterDetails(Resource):
def get(self, character_id):
character = get_or_404(CharacterDetailsModel, character_id)
return {
'id': character_id,
'name': character.name,
'liked': character.liked,
'passed': character.passed,
}
class CharacterFits(Resource):
def get(self, character_id):
character = get_or_404(CharacterDetailsModel, character_id)
data = {
'id': character_id,
'fits': []
}
for fit in character.fits:
data['fits'].append({
'id': fit.id,
'liked': fit.liked,
})
return data
class CharacterFit(Resource):
@authenticate(match_data=['character_id'])
def put(self, character_id, fit_id):
parser = reqparse.RequestParser()
parser.add_argument('liked', type=bool, required=True, help='Does the character like the fit?')
args = parser.parse_args(strict=True)
character = get_or_404(CharacterDetailsModel, character_id)
fit = get_or_404(FitModel, fit_id)
character_fit = CharacterFitModel.query.get((fit_id, character_id))
if character_fit is None:
character_fit = CharacterFitModel(fit_id, character_id, args['liked'])
status_code = 201
else:
character_fit.liked = args['liked']
status_code = 200
return {
'id': character_fit.id,
'liked': character_fit.liked,
}, status_code
class CharacterNewFit(Resource):
@authenticate(match_data=['character_id'])
def get(self, character_id):
# TODO: Clean this bit of close up and make it do the following:
# SELECT a random fit from FitModel WHERE FitModel.id is not in
# CharacterFitModel.query.get((FitModel.id, character_id)
# AND FitModel.datetime < 30 days old
character = get_or_404(CharacterDetailsModel, character_id)
fit_ids = [fit.id for fit in character.fits]
fits = FitModel.query.filter(not_(FitModel.id.in_(fit_ids))).all()
fit = choice(fits)
return {
'id': fit.id,
'items': [{'id': x.type_id, 'flag': x.flag, 'quantity': x.quantity} for x in fit.items],
}
api.add_resource(Characters, '/characters/')
api.add_resource(CharacterDetails, '/characters/<int:character_id>/')
api.add_resource(CharacterFits, '/characters/<int:character_id>/fits/')
api.add_resource(CharacterFit, '/characters/<int:character_id>/fits/<int:fit_id>/')
api.add_resource(CharacterNewFit, '/characters/<int:character_id>/newfit/')
if __name__ == '__main__':
app.run(debug=True)
import json
import pytest
import fitter_api
from datetime import datetime
class TestFitterApi:
def setup_class(self):
"""Setup the test class."""
self.app = fitter_api.app
self.app.config['TESTING'] = True
self.app.config['AUTH_TESTING'] = True
self.client = self.app.test_client()
def setup_method(self, method):
"""Setup for each method."""
with self.app.app_context():
fitter_api.db.create_all()
populate_test_data()
def teardown_method(self, method):
"""Teardown after each method."""
with self.app.app_context():
fitter_api.db.drop_all()
def test_character_details(self):
"""Test getting a characters details."""
response = self.client.get('/characters/1/')
assert response.status_code == 200
assert json.loads(response.data) == {
'id': 1,
'name': 'Test Character',
'liked': 10,
'passed': 5,
}
def test_character_details_404(self):
"""Ensure we 404 if the character is not found."""
response = self.client.get('/characters/99999/')
assert response.status_code == 404
def test_character_history(self):
"""Test getting a characters history of fit descisions."""
test_character_fit = fitter_api.CharacterFitModel(9, 1, True)
fitter_api.db.session.add(test_character_fit)
response = self.client.get('/characters/1/fits/')
assert response.status_code == 200
assert json.loads(response.data) == {
'id': 1,
'fits': [
{'id': 9, 'liked': True},
],
}
def test_get_new_fit(self):
"""Test we get a new fit that the character has not used yet."""
fit_item_one = fitter_api.FitItemModel(9, 555, 89, 5)
fit_item_two = fitter_api.FitItemModel(8, 555, 89, 5)
character_fit = fitter_api.CharacterFitModel(8, 1, True)
fitter_api.db.session.add(fit_item_one)
fitter_api.db.session.add(fit_item_two)
fitter_api.db.session.add(character_fit)
# TODO: Mock out random.choice and ensure it is called with just
# fit_item_one to rule out a test passing/failing based on random.
response = self.client.get('/characters/1/newfit/')
assert response.status_code == 200
assert json.loads(response.data) == {
'id': 9,
'items': [
{'id': 555, 'flag': 89, 'quantity': 5},
],
}
def test_set_fit_status(self):
"""Test we can correctly set a fits status for a character."""
test_character_fit = fitter_api.CharacterFitModel(9, 1, False)
fitter_api.db.session.add(test_character_fit)
response = self.client.put('/characters/1/fits/9/', data={'liked': True})
assert response.status_code == 200
assert json.loads(response.data) == {
'id': 9,
'liked': True,
}
def test_set_fit_status_on_creation(self):
"""Test 201 if the status of the fit is set for the first time."""
response = self.client.put('/characters/1/fits/9/', data={'liked': True})
assert response.status_code == 201
assert json.loads(response.data) == {
'id': 9,
'liked': True,
}
def test_set_fit_status_on_nonexistent_fit(self):
"""Test we 404 if the fit specified doesn't exist in our DB."""
response = self.client.put( '/characters/1/fits/5/', data={'liked': True})
assert response.status_code == 404
def test_set_fit_with_no_data(self):
"""Test 404 if no data passed in the PUT for setting a fits status."""
response = self.client.put('/characters/1/fits/999/')
assert response.status_code == 400
def test_set_fit_status_nonexistent_character(self):
"""Test 404 if the specified character doesn't exist in our DB."""
response = self.client.put('/characters/2/fits/9/', data={'liked': True})
assert response.status_code == 404
def test_character_post(self):
"""Test adding new characters."""
self.app.config['TEST_TOKEN_DATA'] = {
'character_id': 2,
'character_name': 'Test',
}
response = self.client.post('/characters/')
assert response.status_code == 201
assert response.headers['Location'] == 'http://localhost/characters/2/'
def test_character_post_existing_character(self):
"""Test adding a character that already exists."""
self.app.config['TEST_TOKEN_DATA'] = {
'character_id': 1,
'character_name': 'Test',
}
response = self.client.post('/characters/')
assert response.status_code == 409
def populate_test_data():
test_character = fitter_api.CharacterDetailsModel(1, 'Test Character')
test_character.liked = 10
test_character.passed = 5
test_fit = fitter_api.FitModel(9, datetime.now())
fitter_api.db.session.add(test_character)
fitter_api.db.session.add(test_fit)
fitter_api.db.session.commit()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment