Skip to content

Commit f585ddb

Browse files
committed
Add backend;
1 parent fd9e194 commit f585ddb

22 files changed

+31280
-0
lines changed

backend/.env

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

backend/Pipfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[[source]]
2+
url = "https://pypi.org/simple"
3+
verify_ssl = true
4+
name = "pypi"
5+
6+
[packages]
7+
flask = "==2.0.1"
8+
python-decouple = "==3.3"
9+
flask-restful = "==0.3.9"
10+
pandas = "==1.3.0"
11+
plotly = "==5.1.0"
12+
13+
[dev-packages]
14+
15+
[requires]
16+
python_version = "3.9"

backend/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Backend
2+
3+
## Project setup
4+
5+
### Install requirements
6+
```
7+
pip install -r requirements.pip
8+
```
9+
10+
### Run locally
11+
```
12+
FLASK_APP=src/app.py flask run -h localhost -p 5000
13+
```

backend/requirements.pip

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#
2+
# These requirements were autogenerated by pipenv
3+
# To regenerate from the project's Pipfile, run:
4+
#
5+
# pipenv lock --requirements
6+
#
7+
8+
-i https://pypi.org/simple
9+
aniso8601==9.0.1
10+
click==8.0.1; python_version >= '3.6'
11+
flask-restful==0.3.9
12+
flask==2.0.1
13+
itsdangerous==2.0.1; python_version >= '3.6'
14+
jinja2==3.0.1; python_version >= '3.6'
15+
markupsafe==2.0.1; python_version >= '3.6'
16+
numpy==1.21.1; python_version >= '3.7'
17+
pandas==1.3.0
18+
plotly==5.1.0
19+
python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
20+
python-decouple==3.3
21+
pytz==2021.1
22+
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
23+
tenacity==8.0.1; python_version >= '3.6'
24+
werkzeug==2.0.1; python_version >= '3.6'

backend/src/api/__init__.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from flask_restful import Api as RestAPI
2+
3+
from api.healthcheck.resources import HealthyCheckResource
4+
from api import reports
5+
6+
7+
class API(RestAPI):
8+
9+
def init_app(self, app):
10+
super().init_app(app)
11+
app.after_request(self.add_cors_headers)
12+
13+
@staticmethod
14+
def add_cors_headers(response):
15+
"""
16+
Enable support for CORS Headers.
17+
"""
18+
response.headers.add('Access-Control-Allow-Origin', '*')
19+
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
20+
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
21+
return response
22+
23+
24+
api = API()
25+
26+
# Charts
27+
api.add_resource(
28+
reports.GenresByUserScoreAVGChartResource,
29+
'/genres-by-user-score-avg')
30+
31+
api.add_resource(
32+
reports.PlatformsByUserScoreAVGChartResource,
33+
'/platforms-by-user-score-avg')
34+
35+
api.add_resource(
36+
reports.GameReleasePercentByPlatformInTheYearsChartResource,
37+
'/game-release-percent-by-platform-in-the-years')
38+
39+
# Infra
40+
api.add_resource(HealthyCheckResource, '/healthy')

backend/src/api/healthcheck/__init__.py

Whitespace-only changes.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from flask_restful import Resource
2+
from werkzeug import Response
3+
4+
5+
class HealthyCheckResource(Resource):
6+
7+
def get(self):
8+
return Response('OK', content_type='text/plain')

backend/src/api/reports/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from api.reports.game_release_percent_by_platform_in_the_years import \
2+
GameReleasePercentByPlatformInTheYearsChartResource
3+
from api.reports.genres_by_user_score_avg_chart import GenresByUserScoreAVGChartResource
4+
from api.reports.platforms_by_user_score_avg_chart import PlatformsByUserScoreAVGChartResource

backend/src/api/reports/data.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import pandas as pd
2+
from flask import current_app
3+
4+
5+
def get_data():
6+
"""
7+
Load dataset and prepare for reports.
8+
"""
9+
path = current_app.config['BASE_DIR'] / 'data/games.csv'
10+
11+
# load dataset.
12+
df = pd.read_csv(path)
13+
14+
# convert date column into python timestamps
15+
df['date'] = pd.to_datetime(df['date'], format='%B %d, %Y')
16+
df['year'] = df['date'].apply(lambda x: x.year)
17+
18+
# convert user score into float
19+
df['userscore'] = pd.to_numeric(df['userscore'], errors='coerce')
20+
21+
# strip platforms text
22+
df['platforms'] = df['platforms'] \
23+
.apply(str.strip) \
24+
.apply(lambda x: x.replace('\n', '')) \
25+
.apply(lambda x: x.replace(' ', ''))
26+
27+
# drop invalid values
28+
df = df.dropna()
29+
30+
return df
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import datetime
2+
3+
import plotly.graph_objects as go
4+
from flask import request
5+
6+
from api.reports.data import get_data
7+
from commons import parser
8+
from commons.resources import PlotlyChartResource
9+
10+
11+
class GameReleasePercentByPlatformInTheYearsChartResource(PlotlyChartResource):
12+
13+
def get(self):
14+
# load data
15+
df = get_data()
16+
17+
all_genres = list(df['genre'].unique())
18+
19+
# filters
20+
current_year = datetime.datetime.now().year
21+
start_year = parser.parse(request.args.get('start_year'), cast=int, default=current_year - 5)
22+
end_year = parser.parse(request.args.get('end_year'), cast=int, default=current_year)
23+
genres = parser.parse(request.args.get('genres'), cast=parser.csv(), default=[]) or all_genres[:5]
24+
years = list(df['year'].unique())
25+
26+
# filter by year
27+
df = df[(df['year'] >= start_year) & (df['year'] <= end_year)]
28+
29+
df = df[['genre', 'year']] \
30+
.groupby(['genre', 'year'], as_index=False) \
31+
.size()
32+
33+
# calc release percent size
34+
df['p'] = df.apply(lambda x: x[2] / df[(df['year'] == x[1])]['size'].sum(), axis=1)
35+
36+
# filter genres
37+
df = df[(df['genre'].isin(genres))]
38+
39+
# build the chart
40+
traces = [go.Scatter(
41+
name=genre,
42+
x=data['year'],
43+
y=data['p'],
44+
mode='markers+lines'
45+
) for genre, data in map(lambda x: (x, df[(df['genre'] == x)]), genres)]
46+
47+
fig = go.Figure(data=traces)
48+
49+
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='#f7f7f7')
50+
fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='#f7f7f7')
51+
fig.update_layout(
52+
xaxis_title='Year',
53+
yaxis_title='% of Realeases',
54+
yaxis_tickformat='1%',
55+
plot_bgcolor='#FFFFFF'
56+
)
57+
58+
return {
59+
'properties': {
60+
'start_year': start_year,
61+
'end_year': end_year,
62+
'years': years,
63+
'selected_genres': genres,
64+
'genres': all_genres
65+
},
66+
'chart': fig.to_dict()
67+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import plotly.graph_objects as go
2+
from flask import request
3+
4+
from api.reports.data import get_data
5+
from commons import parser
6+
from commons.resources import PlotlyChartResource
7+
8+
9+
class GenresByUserScoreAVGChartResource(PlotlyChartResource):
10+
11+
def get(self):
12+
# load data
13+
df = get_data()
14+
15+
# filters
16+
top = parser.parse(request.args.get('top'), cast=int, default=5)
17+
total = len(df['platforms'].unique())
18+
19+
# calc the mean grouped by platforms.
20+
df = df[['genre', 'userscore']] \
21+
.groupby(['genre'], as_index=False) \
22+
.mean()[:top] \
23+
.sort_values('userscore', ascending=True, ignore_index=True)
24+
25+
# build the figure.
26+
fig = go.Figure() \
27+
.add_trace(go.Bar(
28+
x=df['userscore'],
29+
y=df['genre'],
30+
text=df['userscore'],
31+
orientation='h',
32+
hovertemplate='%{y}: %{x:.2f}<extra></extra>',
33+
marker=dict(color='#000')
34+
))
35+
36+
fig.update_traces(texttemplate='%{text:.2f}', textposition='outside')
37+
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='#f7f7f7')
38+
fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='#f7f7f7')
39+
fig.update_layout(
40+
xaxis_title='Avg. Userscore',
41+
yaxis_title='Genre',
42+
bargap=0.3,
43+
plot_bgcolor='#FFFFFF',
44+
margin=dict(t=40, r=20, b=20, l=50)
45+
)
46+
47+
return {
48+
'properties': {
49+
'top': top,
50+
'total': total
51+
},
52+
'chart': fig.to_dict()
53+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import plotly.graph_objects as go
2+
from flask import request
3+
4+
from api.reports.data import get_data
5+
from commons import parser
6+
from commons.resources import PlotlyChartResource
7+
8+
9+
class PlatformsByUserScoreAVGChartResource(PlotlyChartResource):
10+
11+
def get(self):
12+
# load data
13+
df = get_data()
14+
15+
# filters
16+
top = parser.parse(request.args.get('top'), cast=int, default=10)
17+
total = len(df['platforms'].unique())
18+
19+
# calc the mean grouped by platforms.
20+
df = df[['platforms', 'userscore']] \
21+
.groupby(['platforms'], as_index=False) \
22+
.mean()[:top] \
23+
.sort_values('userscore', ascending=True, ignore_index=True)
24+
25+
# build the figure.
26+
fig = go.Figure() \
27+
.add_trace(go.Bar(
28+
x=df['userscore'],
29+
y=df['platforms'],
30+
text=df['userscore'],
31+
orientation='h',
32+
hovertemplate='%{y}: %{x:.2f}<extra></extra>',
33+
marker=dict(color='#000')
34+
))
35+
36+
fig.update_traces(texttemplate='%{text:.2f}', textposition='outside')
37+
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='#f7f7f7')
38+
fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='#f7f7f7')
39+
fig.update_layout(
40+
xaxis_title='Avg. Userscore',
41+
yaxis_title='Platform',
42+
bargap=0.3,
43+
plot_bgcolor='#FFFFFF',
44+
margin=dict(t=40, r=20, b=20, l=50)
45+
)
46+
47+
return {
48+
'properties': {
49+
'top': top,
50+
'total': total,
51+
},
52+
'chart': fig.to_dict()
53+
}

backend/src/app.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import os
2+
import pathlib
3+
4+
5+
from flask import Flask
6+
7+
from api import api
8+
9+
PROJECT_ROOT = pathlib.Path(__file__).resolve().parent
10+
11+
# set default settings file.
12+
os.environ.setdefault('FLASK_SETTINGS_FILE', str(PROJECT_ROOT / 'settings/development.py'))
13+
14+
15+
def create_app(config_file=None, settings_override=None):
16+
app = Flask(__name__)
17+
18+
if config_file:
19+
# apply settings from config file, if defined.
20+
app.config.from_pyfile(config_file)
21+
22+
else:
23+
# otherwise load settings from environment variable.
24+
app.config.from_envvar('FLASK_SETTINGS_FILE')
25+
26+
if settings_override:
27+
# apply settings override if necessary.
28+
app.config.update(settings_override)
29+
30+
# Load app modules.
31+
api.init_app(app)
32+
33+
return app

backend/src/commons/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)