309 lines
9.3 KiB
Python
309 lines
9.3 KiB
Python
# API is expected to return a tuple of:
|
|
# - HTTP status code,
|
|
# - human-readable status message,
|
|
# - json with appropriate data
|
|
from datetime import datetime
|
|
from flask_sqlalchemy import SQLAlchemy
|
|
from fs_scraper import scraper
|
|
from functools import wraps
|
|
from lewy_globals import getDb, colors as c
|
|
import flask, json, time
|
|
import lewy_db as ldb
|
|
import lewy_globals
|
|
|
|
def require_authentication(func):
|
|
"""
|
|
Ten dekorator służy do wymuszenia parametru "token"
|
|
podczas obsługi zapytania. Powinien on zostać doklejony
|
|
do żądania, np. /api/v1/halt?token=XXX...
|
|
Wartość tokenu jest pobierana z pola api_key w config.toml.
|
|
Jeżeli skrypt jej tam nie znajdzie, jest generowana losowo
|
|
na starcie i drukowana w terminalu.
|
|
"""
|
|
@wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
token = kwargs["r"].args.get('token')
|
|
if token == lewy_globals.config['api']['api_key']:
|
|
try:
|
|
status, received, data = func(*args, **kwargs)
|
|
return status, received, data
|
|
except:
|
|
raise AssertionError(f"Function \"{func.__name__}\" does not return status, code, and data as it should!")
|
|
else:
|
|
increment_bad_requests()
|
|
return 401, "error", {'error_msg': "Unauthorized"}
|
|
return wrapper
|
|
|
|
def increment_bad_requests():
|
|
"""
|
|
Zwiększa globalny, tymczasowy licznik nieprawidłowych zapytań.
|
|
"""
|
|
lewy_globals.apiFailedRequests += 1
|
|
|
|
def not_implemented(data):
|
|
"""
|
|
Zwraca kod 501 wraz z endpointem, który wywołał błąd.
|
|
|
|
:param data: Ścieżka zapytania
|
|
:type data: list
|
|
"""
|
|
# TODO: change list to string -> data, not data[0]
|
|
return 501, f"not recognised/implemented: {data[0]}", []
|
|
|
|
def __czy_x_istnieje(typ, **id):
|
|
rekord = getDb().simple_select_all(typ, **id)
|
|
if rekord is not None and rekord != []:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def czy_sportowiec_istnieje(id_zawodnika: str):
|
|
return __czy_x_istnieje("sportowcy", id_zawodnika=int(id_zawodnika))
|
|
|
|
def czy_klub_istnieje(id_klubu: str):
|
|
return __czy_x_istnieje("kluby", id_klubu=id_klubu)
|
|
|
|
# GET /api/v1
|
|
def stub_hello():
|
|
"""
|
|
Prosta funkcja witająca użytkowników w /api/v1
|
|
"""
|
|
return 200, 'hello from v1! stats are at /api/v1/stats', []
|
|
|
|
def epoch_to_date(epoch):
|
|
"""
|
|
Zamienia Unix'owy epoch na lokalny czas,
|
|
w formacie przypominającym format ISO.
|
|
|
|
:param epoch: Epoch - sekundy po 1. stycznia 1970
|
|
:type epoch: int
|
|
"""
|
|
return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(epoch))
|
|
|
|
# GET /api/v1/
|
|
# GET /api/v1/stats
|
|
def stats():
|
|
"""
|
|
Zwraca ogólne statystyki serwera.
|
|
"""
|
|
data_to_send = {
|
|
"start_time": lewy_globals.starttime,
|
|
"uptime": lewy_globals.getUptime(),
|
|
"real_uptime": lewy_globals.realUptime,
|
|
"total_api_requests": lewy_globals.apiRequests,
|
|
"failed_api_requests": lewy_globals.apiFailedRequests,
|
|
"outside_api_requests": lewy_globals.outsideApiHits,
|
|
"local_api_requests": lewy_globals.apiRequests - lewy_globals.outsideApiHits
|
|
}
|
|
return 200, "ok", data_to_send
|
|
|
|
# GET /api/v1/matches
|
|
def get_matches(r = None, id_zawodnika: str | None = None, rok: int | None = None):
|
|
"""
|
|
Zwraca mecze.
|
|
Przykład wywołania:
|
|
get_matches(r, id_zawodnika=1), tożsame z GET /api/v1/matches?id_zawodnika=1
|
|
get_matches(r, rok=2024), tożsame z GET /api/v1/matches?rok=2024
|
|
get_matches(r), tożsame z GET /api/v1/matches
|
|
"""
|
|
response_json = []
|
|
mecze = None
|
|
|
|
if id_zawodnika is None:
|
|
# Gdy nie podano id wprost, sprawdź, czy podano je przez parametr.
|
|
id_zawodnika = r.args.get('id_zawodnika', -1)
|
|
|
|
if rok is None:
|
|
# Gdy nie podano roku wprost, sprawdź, czy podano je przez parametr.
|
|
# Jeśli nie, przyjmij None (2025).
|
|
rok = r.args.get('rok', None)
|
|
|
|
# Sprawdź, czy podano jakiekolwiek ID sportowca. Jeżeli nie, wypisz wszystkie mecze.
|
|
if id_zawodnika == -1:
|
|
mecze = getDb().get_sportsman_matches(year=rok)
|
|
|
|
# Sprawdź, czy sportowiec o podanym (lub niepodanym) id istnieje.
|
|
# Jeśli nie istnieje, wypisz wszystkie mecze.
|
|
elif not czy_sportowiec_istnieje(id_zawodnika=id_zawodnika):
|
|
return 404, "error", {"error_msg": "This sportsman has not been found in the database. Try: id_zawodnika=1"}
|
|
|
|
# Gdy sportowiec istnieje, wypisz jego mecze.
|
|
else:
|
|
mecze = getDb().get_sportsman_matches(id_zawodnika=id_zawodnika, year=rok)
|
|
|
|
for mecz in mecze:
|
|
response_json.append(mecz.jsonify())
|
|
# print(f"zwracam mecze: {response_json}")
|
|
return 200, "ok", response_json
|
|
|
|
# GET /api/v1/player_stats
|
|
def player_stats(r = None, id_zawodnika: str | None = None):
|
|
"""
|
|
Zwraca statystyki gracza.
|
|
Przykład wywołania:
|
|
player_stats(r, id_zawodnika=1), tożsame z GET /api/v1/player_stats?id_zawodnika=1
|
|
player_stats(r), tożsame z GET /api/v1/player_stats
|
|
"""
|
|
response_json = []
|
|
|
|
if id_zawodnika is None:
|
|
# Gdy nie podano id wprost, sprawdź, czy podano je przez parametr.
|
|
id_zawodnika = r.args.get('id_zawodnika', 0)
|
|
|
|
# Sprawdź, czy sportowiec o podanym (lub niepodanym) id istnieje.
|
|
elif not czy_sportowiec_istnieje(id_zawodnika=id_zawodnika):
|
|
return 404, "error", {"error_msg": "This sportsman has not been found in the database. Try: id_zawodnika=1"}
|
|
|
|
# Gdy sportowiec istnieje, wypisz jego statystyki.
|
|
else:
|
|
staty = getDb().get_basic_stats(id_zawodnika=id_zawodnika)
|
|
|
|
# for stat in staty:
|
|
response_json.append({
|
|
'unique_items': staty[0],
|
|
'time_played': staty[1],
|
|
'goals': staty[2],
|
|
'assists': staty[3],
|
|
'yellow_cards': staty[4],
|
|
'red_cards': staty[5],
|
|
'avg_score': staty[6],
|
|
'wins': staty[7],
|
|
'draws': staty[8],
|
|
'losses': staty[9]
|
|
}
|
|
)
|
|
print(f"zwracam staty: {response_json}")
|
|
return 200, "ok", response_json
|
|
|
|
# GET /api/v1/robert_stats
|
|
def robert_stats(r = None, id_klubu: str | None = None):
|
|
"""
|
|
Zwraca statystyki Roberta w danym klubie.
|
|
Przykład wywołania:
|
|
robert_stats(r, id_klubu="barcelona"), tożsame z GET /api/v1/robert_stats?id_klubu=barcelona
|
|
"""
|
|
response_json = []
|
|
|
|
if id_klubu is None:
|
|
# Gdy nie podano id wprost, sprawdź, czy podano je przez parametr.
|
|
id_klubu = r.args.get('id_klubu', "non-existent-club-id")
|
|
|
|
# Sprawdź, czy klub o podanym (lub niepodanym) id istnieje.
|
|
if not czy_klub_istnieje(id_klubu=id_klubu):
|
|
return 404, "error", {"error_msg": "This club has not been found in the database. Try: id_klubu=barcelona"}
|
|
|
|
# Gdy klub istnieje, wypisz statystyki z tego klubu.
|
|
else:
|
|
staty = getDb().get_sportsman_club_stats(id_zawodnika=1, id_klubu=id_klubu)
|
|
|
|
# for stat in staty:
|
|
print(staty)
|
|
response_json.append({
|
|
'unique_items': staty[0],
|
|
'time_played': staty[1],
|
|
'goals': staty[2],
|
|
'assists': staty[3],
|
|
'yellow_cards': staty[4],
|
|
'red_cards': staty[5],
|
|
'avg_score': staty[6],
|
|
'wins': staty[7],
|
|
'draws': staty[8],
|
|
'losses': staty[9]
|
|
}
|
|
)
|
|
print(f"zwracam staty roberta: {response_json}")
|
|
return 200, "ok", response_json
|
|
|
|
# GET /api/v1/clubs
|
|
def clubs(r, id_klubu: str = None):
|
|
"""
|
|
Zwraca informacje o klubach.
|
|
Przykład wywołania:
|
|
clubs(r, id_klubu="barcelona"), tożsame z GET /api/v1/clubs?id_klubu=barcelona
|
|
clubs(r), tożsame z GET /api/v1/clubs
|
|
"""
|
|
response_json = []
|
|
|
|
if id_klubu is None:
|
|
# Gdy nie podano id wprost, sprawdź, czy podano je przez parametr.
|
|
id_klubu = r.args.get('id_klubu', -1)
|
|
|
|
if id_klubu == -1:
|
|
kluby = getDb().simple_select_all("kluby")
|
|
|
|
# Sprawdź, czy sportowiec o podanym (lub niepodanym) id istnieje.
|
|
# Jeśli nie istnieje, wypisz wszystkie mecze.
|
|
elif not czy_klub_istnieje(id_klubu=id_klubu):
|
|
return 404, "error", {"error_msg": "This club has not been found in the database. Try: id_klubu=barcelona"}
|
|
|
|
# Gdy sportowiec istnieje, wypisz jego mecze.
|
|
else:
|
|
kluby = getDb().simple_select_all("kluby", id_klubu=id_klubu)
|
|
|
|
for klub in kluby:
|
|
response_json.append(klub.jsonify())
|
|
|
|
print(f"zwracam kluby: {response_json}")
|
|
return 200, "ok", response_json
|
|
|
|
# GET /api/v1/debugger_halt?token=XXX...
|
|
@require_authentication
|
|
def debugger_halt(r):
|
|
"""
|
|
Zatrzymuje wykonywanie skryptu, aby pozwolić
|
|
administratorowi na wykonywanie dowolnego polecenia z konsoli.
|
|
"""
|
|
if lewy_globals.config['general']['is_proxied']:
|
|
print(f"{c.WARNING}[{epoch_to_date(time.time())}]{c.ENDC} {r.headers['X-Forwarded-For']} triggered a debugger halt!")
|
|
else:
|
|
print(f"{c.WARNING}[{epoch_to_date(time.time())}]{c.ENDC} {r.remote_addr} triggered a debugger halt!")
|
|
breakpoint()
|
|
return 200, "ok", []
|
|
|
|
def last_goal_for(sportowiec: str = "MVC8zHZD"):
|
|
"""
|
|
Pobierz klub, dla którego sportowiec (domyślnie Robert) oddał ostatni gol.
|
|
|
|
:returns: Klub, lub None
|
|
:rtype: kluby | None
|
|
"""
|
|
|
|
sportowiec = getDb().simple_select_all("sportowcy", zewnetrzne_id_zawodnika=sportowiec)
|
|
if sportowiec != []:
|
|
return sportowiec[0].ostatni_gol_dla
|
|
|
|
return None
|
|
|
|
def lookup(data, request):
|
|
"""
|
|
Obsługuje zapytania zwrócone do /api/v1/...
|
|
|
|
:param data: Lista ze ścieżką zapytania
|
|
:type data: list
|
|
:param request: Zapytanie
|
|
:type request: flask.request
|
|
|
|
:returns: Wartość zwróconą przez którąś z przywołanych funkcji.
|
|
"""
|
|
if data == []:
|
|
return stub_hello()
|
|
match data[0].lower():
|
|
case 'stats' | '':
|
|
return stats()
|
|
case 'user':
|
|
return stub_hello()
|
|
case 'info':
|
|
return stub_hello()
|
|
case 'halt':
|
|
return debugger_halt(r = request)
|
|
case 'matches':
|
|
return get_matches(r = request)
|
|
case 'player_stats':
|
|
return player_stats(r = request)
|
|
case 'robert_stats':
|
|
return robert_stats(r = request)
|
|
case 'clubs':
|
|
return clubs(r = request)
|
|
case _:
|
|
increment_bad_requests()
|
|
return not_implemented(data) |