Skip to content

Commit

Permalink
Add backend;
Browse files Browse the repository at this point in the history
  • Loading branch information
arkanister committed Jul 24, 2021
1 parent fd9e194 commit f585ddb
Show file tree
Hide file tree
Showing 22 changed files with 31,280 additions and 0 deletions.
1 change: 1 addition & 0 deletions backend/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

16 changes: 16 additions & 0 deletions backend/Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
flask = "==2.0.1"
python-decouple = "==3.3"
flask-restful = "==0.3.9"
pandas = "==1.3.0"
plotly = "==5.1.0"

[dev-packages]

[requires]
python_version = "3.9"
13 changes: 13 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Backend

## Project setup

### Install requirements
```
pip install -r requirements.pip
```

### Run locally
```
FLASK_APP=src/app.py flask run -h localhost -p 5000
```
24 changes: 24 additions & 0 deletions backend/requirements.pip
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#
# These requirements were autogenerated by pipenv
# To regenerate from the project's Pipfile, run:
#
# pipenv lock --requirements
#

-i https://pypi.org/simple
aniso8601==9.0.1
click==8.0.1; python_version >= '3.6'
flask-restful==0.3.9
flask==2.0.1
itsdangerous==2.0.1; python_version >= '3.6'
jinja2==3.0.1; python_version >= '3.6'
markupsafe==2.0.1; python_version >= '3.6'
numpy==1.21.1; python_version >= '3.7'
pandas==1.3.0
plotly==5.1.0
python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
python-decouple==3.3
pytz==2021.1
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
tenacity==8.0.1; python_version >= '3.6'
werkzeug==2.0.1; python_version >= '3.6'
40 changes: 40 additions & 0 deletions backend/src/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from flask_restful import Api as RestAPI

from api.healthcheck.resources import HealthyCheckResource
from api import reports


class API(RestAPI):

def init_app(self, app):
super().init_app(app)
app.after_request(self.add_cors_headers)

@staticmethod
def add_cors_headers(response):
"""
Enable support for CORS Headers.
"""
response.headers.add('Access-Control-Allow-Origin', '*')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
return response


api = API()

# Charts
api.add_resource(
reports.GenresByUserScoreAVGChartResource,
'/genres-by-user-score-avg')

api.add_resource(
reports.PlatformsByUserScoreAVGChartResource,
'/platforms-by-user-score-avg')

api.add_resource(
reports.GameReleasePercentByPlatformInTheYearsChartResource,
'/game-release-percent-by-platform-in-the-years')

# Infra
api.add_resource(HealthyCheckResource, '/healthy')
Empty file.
8 changes: 8 additions & 0 deletions backend/src/api/healthcheck/resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from flask_restful import Resource
from werkzeug import Response


class HealthyCheckResource(Resource):

def get(self):
return Response('OK', content_type='text/plain')
4 changes: 4 additions & 0 deletions backend/src/api/reports/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from api.reports.game_release_percent_by_platform_in_the_years import \
GameReleasePercentByPlatformInTheYearsChartResource
from api.reports.genres_by_user_score_avg_chart import GenresByUserScoreAVGChartResource
from api.reports.platforms_by_user_score_avg_chart import PlatformsByUserScoreAVGChartResource
30 changes: 30 additions & 0 deletions backend/src/api/reports/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import pandas as pd
from flask import current_app


def get_data():
"""
Load dataset and prepare for reports.
"""
path = current_app.config['BASE_DIR'] / 'data/games.csv'

# load dataset.
df = pd.read_csv(path)

# convert date column into python timestamps
df['date'] = pd.to_datetime(df['date'], format='%B %d, %Y')
df['year'] = df['date'].apply(lambda x: x.year)

# convert user score into float
df['userscore'] = pd.to_numeric(df['userscore'], errors='coerce')

# strip platforms text
df['platforms'] = df['platforms'] \
.apply(str.strip) \
.apply(lambda x: x.replace('\n', '')) \
.apply(lambda x: x.replace(' ', ''))

# drop invalid values
df = df.dropna()

return df
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import datetime

import plotly.graph_objects as go
from flask import request

from api.reports.data import get_data
from commons import parser
from commons.resources import PlotlyChartResource


class GameReleasePercentByPlatformInTheYearsChartResource(PlotlyChartResource):

def get(self):
# load data
df = get_data()

all_genres = list(df['genre'].unique())

# filters
current_year = datetime.datetime.now().year
start_year = parser.parse(request.args.get('start_year'), cast=int, default=current_year - 5)
end_year = parser.parse(request.args.get('end_year'), cast=int, default=current_year)
genres = parser.parse(request.args.get('genres'), cast=parser.csv(), default=[]) or all_genres[:5]
years = list(df['year'].unique())

# filter by year
df = df[(df['year'] >= start_year) & (df['year'] <= end_year)]

df = df[['genre', 'year']] \
.groupby(['genre', 'year'], as_index=False) \
.size()

# calc release percent size
df['p'] = df.apply(lambda x: x[2] / df[(df['year'] == x[1])]['size'].sum(), axis=1)

# filter genres
df = df[(df['genre'].isin(genres))]

# build the chart
traces = [go.Scatter(
name=genre,
x=data['year'],
y=data['p'],
mode='markers+lines'
) for genre, data in map(lambda x: (x, df[(df['genre'] == x)]), genres)]

fig = go.Figure(data=traces)

fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='#f7f7f7')
fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='#f7f7f7')
fig.update_layout(
xaxis_title='Year',
yaxis_title='% of Realeases',
yaxis_tickformat='1%',
plot_bgcolor='#FFFFFF'
)

return {
'properties': {
'start_year': start_year,
'end_year': end_year,
'years': years,
'selected_genres': genres,
'genres': all_genres
},
'chart': fig.to_dict()
}
53 changes: 53 additions & 0 deletions backend/src/api/reports/genres_by_user_score_avg_chart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import plotly.graph_objects as go
from flask import request

from api.reports.data import get_data
from commons import parser
from commons.resources import PlotlyChartResource


class GenresByUserScoreAVGChartResource(PlotlyChartResource):

def get(self):
# load data
df = get_data()

# filters
top = parser.parse(request.args.get('top'), cast=int, default=5)
total = len(df['platforms'].unique())

# calc the mean grouped by platforms.
df = df[['genre', 'userscore']] \
.groupby(['genre'], as_index=False) \
.mean()[:top] \
.sort_values('userscore', ascending=True, ignore_index=True)

# build the figure.
fig = go.Figure() \
.add_trace(go.Bar(
x=df['userscore'],
y=df['genre'],
text=df['userscore'],
orientation='h',
hovertemplate='%{y}: %{x:.2f}<extra></extra>',
marker=dict(color='#000')
))

fig.update_traces(texttemplate='%{text:.2f}', textposition='outside')
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='#f7f7f7')
fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='#f7f7f7')
fig.update_layout(
xaxis_title='Avg. Userscore',
yaxis_title='Genre',
bargap=0.3,
plot_bgcolor='#FFFFFF',
margin=dict(t=40, r=20, b=20, l=50)
)

return {
'properties': {
'top': top,
'total': total
},
'chart': fig.to_dict()
}
53 changes: 53 additions & 0 deletions backend/src/api/reports/platforms_by_user_score_avg_chart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import plotly.graph_objects as go
from flask import request

from api.reports.data import get_data
from commons import parser
from commons.resources import PlotlyChartResource


class PlatformsByUserScoreAVGChartResource(PlotlyChartResource):

def get(self):
# load data
df = get_data()

# filters
top = parser.parse(request.args.get('top'), cast=int, default=10)
total = len(df['platforms'].unique())

# calc the mean grouped by platforms.
df = df[['platforms', 'userscore']] \
.groupby(['platforms'], as_index=False) \
.mean()[:top] \
.sort_values('userscore', ascending=True, ignore_index=True)

# build the figure.
fig = go.Figure() \
.add_trace(go.Bar(
x=df['userscore'],
y=df['platforms'],
text=df['userscore'],
orientation='h',
hovertemplate='%{y}: %{x:.2f}<extra></extra>',
marker=dict(color='#000')
))

fig.update_traces(texttemplate='%{text:.2f}', textposition='outside')
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='#f7f7f7')
fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='#f7f7f7')
fig.update_layout(
xaxis_title='Avg. Userscore',
yaxis_title='Platform',
bargap=0.3,
plot_bgcolor='#FFFFFF',
margin=dict(t=40, r=20, b=20, l=50)
)

return {
'properties': {
'top': top,
'total': total,
},
'chart': fig.to_dict()
}
33 changes: 33 additions & 0 deletions backend/src/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os
import pathlib


from flask import Flask

from api import api

PROJECT_ROOT = pathlib.Path(__file__).resolve().parent

# set default settings file.
os.environ.setdefault('FLASK_SETTINGS_FILE', str(PROJECT_ROOT / 'settings/development.py'))


def create_app(config_file=None, settings_override=None):
app = Flask(__name__)

if config_file:
# apply settings from config file, if defined.
app.config.from_pyfile(config_file)

else:
# otherwise load settings from environment variable.
app.config.from_envvar('FLASK_SETTINGS_FILE')

if settings_override:
# apply settings override if necessary.
app.config.update(settings_override)

# Load app modules.
api.init_app(app)

return app
Empty file added backend/src/commons/__init__.py
Empty file.
Loading

0 comments on commit f585ddb

Please sign in to comment.