added proper DEBUG prints, attempts at fixing timezone fails with If-Modified-Since header, conversion methods between old and new scores data format

This commit is contained in:
Xevion
2020-06-22 16:10:49 -05:00
parent 952f1c22a2
commit bddc0b9746
8 changed files with 106 additions and 30 deletions

View File

@@ -1,2 +1,2 @@
FLASK_APP=trivia.create_app
FLASK_ENV=development
FLASK_ENV=production

View File

@@ -11,9 +11,11 @@ on at `localhost:5000` unless
git clone https://github.com/Xevion/trivia.git
cd trivia
pip install -r requirements.txt
python wsgi.py
flask run
```
Edit .flaskenv to change the configuration preset.
## Application Design
The webapp has two sides: Client and Server.

28
cli.py
View File

@@ -5,7 +5,6 @@ A simple CLI implementation using the application's API.
"""
import curses
import time
from datetime import datetime
from typing import List
@@ -18,7 +17,7 @@ lastAttempt: float = -1
lastUpdate: float = -1
def refreshScores() -> bool:
def refreshScores() -> None:
"""
Refreshes scoreboard data safely, handling a unresponsive or downed scoreboard.
Uses If-Modified-Since headers properly.
@@ -30,21 +29,24 @@ def refreshScores() -> bool:
global lastUpdate, lastAttempt, scores
# Send with If-Modified-Since header if this is not the first time
headers = {'If-Modified-Since': datetime.fromtimestamp(lastAttempt).strftime('%a, %d %b %Y %I:%M:%S')} if lastAttempt > 0 else {}
useTime = max(lastAttempt, lastUpdate)
headers = {
'If-Modified-Since': datetime.fromtimestamp(useTime, pytz.utc).strftime(
'%a, %d %b %Y %I:%M:%S %Z')} if useTime > 0 else {}
# Send request with headers
try:
resp = requests.get('http://127.0.0.1:5000/api/scores/', headers=headers)
except requests.exceptions.ConnectionError:
resp = None
finally:
lastAttempt = time.time()
lastAttempt = datetime.utcnow().timestamp()
if resp is not None and resp.ok:
if resp.status_code == 304 and len(scores) != 0:
pass
else:
# Changes found, update!
lastUpdate = time.time()
lastUpdate = datetime.utcnow().timestamp()
scores = resp.json()
# Calculate totals, preliminary sort by total
@@ -54,25 +56,25 @@ def refreshScores() -> bool:
# Calculate ranks with tie handling logic
for i, team in enumerate(scores):
# Check that previous score is the same, if so add a 'T' for tie
if i > 0 and scores[i - 1]['total'] == team['total']:
team['rank'] = scores[i - 1]['rank']
# Check if we have a T
if not team['rank'].startswith('T'):
team['rank'] = 'T' + team['rank'].strip()
# Check if previous score has a T
if not scores[i - 1]['rank'].startswith('T'):
scores[i - 1]['rank'] = 'T' + scores[i - 1]['rank'].strip()
else:
# Otherwise just add a space in front instead
team['rank'] = " " + str(i + 1)
return True
return False
def main(screen) -> None:
"""
Mainloop function
Mainloop function.
:param screen: Curses screen
"""
@@ -80,7 +82,7 @@ def main(screen) -> None:
screen.redrawwin()
while True:
# Refresh scores every 10 seconds
if time.time() - lastAttempt > 0.5:
if datetime.utcnow().timestamp() - lastAttempt > 0.5:
refreshScores()
# Get current terminal size and clear
@@ -90,6 +92,7 @@ def main(screen) -> None:
# Build table data
global scores
table = [[team['rank'], team['id'], team['name'], team['total'], *team['scores']] for team in scores[:y - 4]]
# Round number headers
scoreSet = map(str, range(1, max(8, len(scores[0]['scores'])) + 1)) if scores else []
table.insert(0, ['Rank', 'ID', 'Team Name', 'T', *scoreSet])
table = SingleTable(table, title='EfTA Trivia Night')
@@ -104,7 +107,8 @@ def main(screen) -> None:
# Terminal Size
strpos = str((x, y))
screen.addstr(y - 1, 1, strpos)
screen.addstr(y - 1, 1 + len(strpos) + 1, f'({str(round(0.5 - (time.time() - lastAttempt), 3)).zfill(3)})')
screen.addstr(y - 1, 1 + len(strpos) + 1,
f'({str(round(0.5 - (datetime.utcnow().timestamp() - lastAttempt), 3)).zfill(3)})')
# Update curses screen
screen.refresh()

View File

@@ -27,7 +27,14 @@ def scores():
try:
if request.headers['If-Modified-Since']:
# Acquire epoch time from header
epoch = time.mktime(time.strptime(request.headers['If-Modified-Since'], "%a, %d %b %Y %I:%M:%S"))
if current_app.config['DEBUG']:
print(request.headers['If-Modified-Since'])
epoch = datetime.strptime(request.headers['If-Modified-Since'], "%a, %d %b %Y %I:%M:%S %Z")
if current_app.config['DEBUG']:
print(epoch)
epoch = epoch.timestamp()
if current_app.config['DEBUG']:
print(epoch, lastChange, lastChange - epoch)
if epoch >= lastChange:
status_code = 304
except KeyError:

View File

@@ -6,6 +6,7 @@ config.py
configs = {
'production': 'trivia.config.Config',
'productiondep': 'trivia.config.ConfigDeprecated',
'development': 'trivia.config.Config',
'demo': 'trivia.config.DemoConfig'
}
@@ -15,6 +16,8 @@ class Config(object):
# Main Configuration
SCORE_FILE = 'scores.json'
POLLING_INTERVAL = 5
DEBUG = False
CONVERT_OLD = True
# Demo Configuration
DEMO = False
@@ -23,9 +26,14 @@ class Config(object):
DEMO_MAX_SCORES = 0
def ConfigDeprecated(Config):
CONVERT_OLD = True
class DemoConfig(Config):
# Main Configuration
SCORE_FILE = 'demo.json'
DEBUG = True
# Demo Configuration
DEMO = True

View File

@@ -1,10 +1,11 @@
from flask_apscheduler import APScheduler
from flask import Flask
from flask_apscheduler import APScheduler
from trivia.config import configs
scheduler: APScheduler = None
def create_app(env=None):
app = Flask(__name__)
@@ -23,14 +24,16 @@ def create_app(env=None):
scheduler.start()
# Add score file polling
scheduler.add_job(id='polling', func=utils.refreshScores, trigger="interval", seconds=app.config['POLLING_INTERVAL'])
scheduler.add_job(id='polling', func=utils.refreshScores, trigger="interval",
seconds=app.config['POLLING_INTERVAL'])
if app.config['DEMO']:
app.logger.info('Generating Demo Data...')
# Generate initial Demo data
utils.generateDemo()
# Begin altering demo data regularly
scheduler.add_job(id='altering', func=utils.alterDemo, trigger="interval", seconds=app.config['DEMO_ALTERATION_INTERVAL'])
scheduler.add_job(id='altering', func=utils.alterDemo, trigger="interval",
seconds=app.config['DEMO_ALTERATION_INTERVAL'])
utils.refreshScores()

View File

@@ -86,7 +86,11 @@
{% for team in teams %}
<tr id="{{ team.id }}">
<td>{{ range(1, teams | length) | random }}</td>
{% if team.name | length > 0 %}
<td>{{ team.name }}</td>
{% else %}
<td>Team {{ team.id }}</td>
{% endif %}
{% for score in team.scores %}
<td>{{ score }}</td>
{% endfor %}

View File

@@ -5,9 +5,10 @@ Stores important backend application functionality.
"""
import json
import os
import time
import random
import time
from collections import namedtuple
from datetime import datetime
from typing import List
# Simple fake 'class' for passing to jinja templates
@@ -30,23 +31,22 @@ def lastModified() -> float:
"""
returns epoch time of last modification to the scores file.
"""
return os.stat(SCORES_FILE).st_mtime
if current_app.config['DEBUG']:
print(datetime.fromtimestamp(os.path.getmtime(SCORES_FILE)), datetime.fromtimestamp(time.time()))
return os.path.getmtime(SCORES_FILE)
def refreshScores() -> None:
"""
Refreshes scores data safely.
:return:
"""
global lastChange
curChange = lastModified()
from trivia.create_app import scheduler
app = scheduler.app
with app.app_context():
global lastChange
curChange = lastModified()
if lastChange < curChange:
try:
# Update tracking var
@@ -55,6 +55,8 @@ def refreshScores() -> None:
current_app.logger.debug('Attempting to load and parse scores file.')
with open(SCORES_FILE, 'r') as file:
temp = json.load(file)
if current_app.config['CONVERT_OLD']:
temp = convertFrom(temp)
# Place all values into Team object for jinja
temp = [
@@ -75,6 +77,10 @@ def refreshScores() -> None:
def generateDemo() -> None:
"""
Generate a base demo scores file. Overwrites the given SCORES_FILE.
"""
fake = faker.Faker()
data = [
{
@@ -85,17 +91,22 @@ def generateDemo() -> None:
]
with open(SCORES_FILE, 'w') as file:
json.dump(data, file)
json.dump(convertTo(data) if current_app.config['CONVERT_OLD'] else data, file)
def alterDemo() -> None:
"""
Alters the current scores file. Intended for demo application mode.
Adds a new score each alteration. Triggers a application fresh with 'refreshScores'
"""
from trivia.create_app import scheduler
app = scheduler.app
with app.app_context():
current_app.logger.debug('Altering Demo Data...')
with open(SCORES_FILE, 'r') as file:
data = json.load(file)
data = convertFrom(json.load(file)) if current_app.config['CONVERT_OLD'] else json.load(file)
if len(data) > 0:
if len(data[0]['scores']) >= current_app.config['DEMO_MAX_SCORES']:
@@ -105,4 +116,41 @@ def alterDemo() -> None:
team['scores'].append(random.randint(2, 8) if random.random() > 0.25 else 0)
with open(SCORES_FILE, 'w') as file:
json.dump(data, file)
json.dump(convertTo(data) if current_app.config['CONVERT_OLD'] else data, file)
refreshScores()
def convertFrom(data) -> List[dict]:
"""
Converts scores data from old to new format.
:param data: Old format data
:return: Old format data
"""
return [
{
'teamno': oldteam['Team']['Number'],
'teamname': oldteam['Team']['DisplayName'],
'scores': oldteam['Scores']
}
for oldteam in data
]
def convertTo(data) -> List[dict]:
"""
Converst scores from new to old format
:param data: New format data
:return: Old format data
"""
return [
{
'Team': {
'Number': team['teamno'],
'DisplayName': team['teamname']
},
'Scores': team['scores'],
'TotalGuess': -1
} for team in data
]