43 Commits

Author SHA1 Message Date
416b2ccfe0 changing responsive design 2025-06-12 23:08:58 +02:00
919d64ca5e Merge branch 'master' into frontend 2025-06-05 10:30:04 +02:00
2cfa5f1fa4 poland-style changes 2025-06-05 10:27:11 +02:00
9b45a3f26f polandmode statbox color 2025-06-05 00:47:59 +02:00
3dfc40cdb0 trophies update and hamburger fix 2025-06-05 00:40:47 +02:00
be951d296f representation changes 2025-06-04 17:02:57 +02:00
6e1e8ccc7d stats maches club style changes 2025-06-04 16:55:31 +02:00
03463905ef fixing responsive 2025-06-04 15:32:03 +02:00
35db71b8cc feat: get sportsmen full name and birthday from id 2025-06-04 00:19:57 +02:00
f65a174089 skeleton to all sites (i hope) 2025-06-03 23:26:30 +02:00
bdfa31c8ea fix: check for id in simple_insert_one() to avoid breaking autoincrement 2025-06-03 21:46:59 +02:00
206f7d6fb3 Merge branch 'frontend' of https://gitea.7o7.cx/roberteam/lewangoalski into frontend 2025-06-03 09:10:08 +02:00
df0e47c610 base page style 2025-06-03 09:09:59 +02:00
3b9aa8150b feat: new last_goal_for endpoint, simple_select_all improvements
also introduces sample usage for the new endpoint in lewy_routes
2025-06-03 02:38:13 +02:00
bc557b35af header style and font changes 2025-06-03 02:01:19 +02:00
4987dc4cf7 . 2025-06-02 00:34:03 +02:00
48825185b8 begining of history section 2025-06-02 00:32:27 +02:00
42c60f9db5 style poland-mode 2025-06-01 17:55:19 +02:00
504702700c Responsive navigation menu 2025-06-01 00:06:10 +02:00
ca961320e7 feat: scraper usage example, fixed session handling 2025-05-31 05:45:27 +02:00
56f90efe40 feat: orm support for db calls
sample call from debugger_halt():
`getDb().simple_select_all("mecze", id_meczu=1)`
2025-05-30 01:45:35 +02:00
72141768d4 feat: add annotations about functionalities of some endpoints 2025-05-28 14:47:49 +02:00
67307f216f fix: proper(?) db support, add examples of data insertion and selection 2025-05-28 03:11:21 +02:00
65ec7ff73d feat: wrap the db object around baza, which will provide helper methods
also adds the list of tracked sportsmen
2025-05-27 12:28:51 +02:00
b6ae33861e feat: differentiate between proxied and non-proxied instances in logs
will show IP either from remote address, or X-Forwarded-For if proxied
2025-05-27 02:00:05 +02:00
69e911b1b8 fix: increment bad requests when trying to access authed endpoint
without a token
2025-05-27 01:33:04 +02:00
4893715118 fix: db revamp, add relations, annotate the db
also allows for request retrieval, and adds decorator for authed access
2025-05-27 01:28:40 +02:00
00a30695b7 feat: add a basic example of scraper usage 2025-05-17 00:41:36 +02:00
8689426ae3 fix: make the project runnable again from visual studio 2025-05-11 02:10:36 +02:00
a372459298 fix: another typo, and updated readme 2025-05-11 01:54:13 +02:00
8a0c9ae9b8 fix: db_prefix typo 2025-05-11 01:43:52 +02:00
c1facf00fb feat: major rewrite of the webserver
gets rid of __init__ and runserver in favor of new modular design
also introduces db model, first api endpoints, as well as their wrappers
2025-05-11 01:30:32 +02:00
96e2c53484 chore: update dependencies to known, working versions 2025-05-11 00:44:15 +02:00
0b52d5b527 chore: introduce new required dependencies 2025-04-30 16:03:13 +02:00
5f13949cd4 feat: differentiate between commit and html-formatted commit 2025-04-16 12:07:04 +02:00
c35da4a043 update readme 2025-04-10 12:03:37 +02:00
4eb107d3ad chore: add project files to visual studio project, fix horizontal separator 2025-04-10 09:18:38 +02:00
dc845cf884 Merge branch 'pr-3' 2025-04-10 09:12:18 +02:00
76db85765c Embedy fix (mam nadzieję) 2025-04-09 22:45:52 +02:00
a707edcb30 Embedy discordowe 2025-04-09 22:28:52 +02:00
6175f7171f add todo to readme
Also tests a new webhook.
2025-04-09 18:36:09 +02:00
Pc
ca58821361 Centered Style 2025-04-09 18:26:13 +02:00
b5ebcfbe68 Merge pull request 'Basic Flask app' (#2) from pr-3 into master
Reviewed-on: #2
2025-04-09 16:46:53 +02:00
33 changed files with 2616 additions and 250 deletions

7
.gitignore vendored
View File

@@ -364,6 +364,13 @@ FodyWeavers.xsd
# Wirtualne środowisko pythona # Wirtualne środowisko pythona
FlaskWebProject/env FlaskWebProject/env
.venv
# Wersja pythona # Wersja pythona
FlaskWebProject/FlaskWebProject.pyproj FlaskWebProject/FlaskWebProject.pyproj
# Baza sqlite
FlaskWebProject/FlaskWebProject/instance
# Poufne dane
config.toml

View File

@@ -7,18 +7,30 @@
<ProjectGuid>89df7a6e-dc87-40f3-8b4b-f609a6e889d1</ProjectGuid> <ProjectGuid>89df7a6e-dc87-40f3-8b4b-f609a6e889d1</ProjectGuid>
<ProjectHome>.</ProjectHome> <ProjectHome>.</ProjectHome>
<ProjectTypeGuids>{789894c7-04a9-4a11-a6b5-3f4435165112};{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{349c5851-65df-11da-9384-00065b846f21};{888888a0-9f3d-457c-b088-3a5042f75d52}</ProjectTypeGuids> <ProjectTypeGuids>{789894c7-04a9-4a11-a6b5-3f4435165112};{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{349c5851-65df-11da-9384-00065b846f21};{888888a0-9f3d-457c-b088-3a5042f75d52}</ProjectTypeGuids>
<StartupFile>runserver.py</StartupFile> <StartupFile>FlaskWebProject\lewy.py</StartupFile>
<SearchPath> <SearchPath>
</SearchPath> </SearchPath>
<WorkingDirectory>.</WorkingDirectory> <WorkingDirectory>.\FlaskWebProject\</WorkingDirectory>
<LaunchProvider>Web launcher</LaunchProvider> <LaunchProvider>Web launcher</LaunchProvider>
<WebBrowserUrl>http://localhost</WebBrowserUrl> <WebBrowserUrl>http://127.0.0.1</WebBrowserUrl>
<OutputPath>.</OutputPath> <OutputPath>.</OutputPath>
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles> <SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
<Name>FlaskWebProject</Name> <Name>FlaskWebProject</Name>
<RootNamespace>FlaskWebProject</RootNamespace> <RootNamespace>FlaskWebProject</RootNamespace>
<InterpreterId> <InterpreterId>MSBuild|env|$(MSBuildProjectFullPath)</InterpreterId>
</InterpreterId> <PythonRunWebServerCommandEnvironment>
</PythonRunWebServerCommandEnvironment>
<PythonDebugWebServerCommandEnvironment>
</PythonDebugWebServerCommandEnvironment>
<IsWindowsApplication>False</IsWindowsApplication>
<WebBrowserPort>5000</WebBrowserPort>
<Environment>
</Environment>
<PythonRunWebServerCommandArguments>
</PythonRunWebServerCommandArguments>
<PythonDebugWebServerCommandArguments>
</PythonDebugWebServerCommandArguments>
<CommandLineArguments>-i 127.0.0.1 -p 5000</CommandLineArguments>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@@ -29,8 +41,14 @@
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging> <EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="FlaskWebProject\fs_scraper.py" />
<Compile Include="FlaskWebProject\lewy.py" />
<Compile Include="FlaskWebProject\lewy_api.py" />
<Compile Include="FlaskWebProject\lewy_api_v1.py" />
<Compile Include="FlaskWebProject\lewy_db.py" />
<Compile Include="FlaskWebProject\lewy_globals.py" />
<Compile Include="FlaskWebProject\lewy_routes.py" />
<Compile Include="runserver.py" /> <Compile Include="runserver.py" />
<Compile Include="FlaskWebProject\__init__.py" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="FlaskWebProject\" /> <Folder Include="FlaskWebProject\" />
@@ -41,6 +59,7 @@
<Folder Include="FlaskWebProject\templates\" /> <Folder Include="FlaskWebProject\templates\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="FlaskWebProject\config.toml" />
<Content Include="FlaskWebProject\static\script.js" /> <Content Include="FlaskWebProject\static\script.js" />
<Content Include="FlaskWebProject\static\style.css" /> <Content Include="FlaskWebProject\static\style.css" />
<Content Include="FlaskWebProject\templates\base.html" /> <Content Include="FlaskWebProject\templates\base.html" />
@@ -71,6 +90,17 @@
<Content Include="FlaskWebProject\static\scripts\_references.js" /> <Content Include="FlaskWebProject\static\scripts\_references.js" />
<Content Include="FlaskWebProject\templates\index.html" /> <Content Include="FlaskWebProject\templates\index.html" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Interpreter Include="env\">
<Id>env</Id>
<Version>3.13</Version>
<Description>env (Python 3.13 (64-bit))</Description>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
<Architecture>X64</Architecture>
</Interpreter>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" /> <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" />
<!-- Specify pre- and post-build commands in the BeforeBuild and <!-- Specify pre- and post-build commands in the BeforeBuild and
AfterBuild targets below. --> AfterBuild targets below. -->

View File

@@ -1,41 +0,0 @@
from flask import Flask, render_template
from .lewy_globals import *
app = Flask(__name__)
@app.route('/')
def index():
stats = {
'goals': 38,
'assists': 12,
'matches': 45,
'matches_list': [
{'date': '2024-10-12', 'opponent': 'Real Madrid', 'goals': 2, 'assists': 1, 'minutes': 90},
{'date': '2024-10-19', 'opponent': 'Valencia', 'goals': 1, 'assists': 0, 'minutes': 85},
# Możesz dodać więcej meczów...
]
}
return render_template('index.html', goals=stats['goals'], assists=stats['assists'],
matches=stats['matches'], matches_list=stats['matches_list'],
commit=lewy_globals.getCommit())
@app.route('/mecze')
def mecze():
# Możesz dostarczyć szczegóły dotyczące meczów
matches = [
{'date': '2024-10-12', 'opponent': 'Real Madrid', 'goals': 2, 'assists': 1, 'minutes': 90},
{'date': '2024-10-19', 'opponent': 'Valencia', 'goals': 1, 'assists': 0, 'minutes': 85},
]
return render_template('matches.html', matches=matches)
@app.route('/statystyki')
def statystyki():
stats = {
'goals': 38,
'assists': 12,
'matches': 45,
}
return render_template('stats.html', stats=stats)
if __name__ == '__main__':
app.run(debug=True)

View File

@@ -0,0 +1,31 @@
[general]
db_path_url = "postgresql+psycopg2://user:password@hostname/database_name"
db_prefix = "" # What (if any) prefix will be appended to table names.
is_proxied = false # Will ignore discrepancies between retrieved IP and public-facing URL.
public_facing_url = "http://127.0.0.1:5000/" # Used for URL rewriting. Note the trailing forward slash /.
[api]
# Leave empty to automatically generate API key every launch (insecure).
api_key = ""
[scraper]
user-agent = "" # Leave empty for default (Firefox ESR).
[sportsmen]
tracked_ids = [
"MVC8zHZD", # Robert Lewandowski
"WGOY4FSt", # Cristiano Ronaldo
"vgOOdZbd", # Lionel Messi
"Wn6E2SED", # Kylian Mbappe
"AiH2zDve", # Zlatan Ibrahimovic
"dUShzrBp", # Luis Suarez
"UmV9iQmE", # Erling Haaland
"tpV0VX0S", # Karim Benzema
"vw8ZV7HC", # Sergio Aguero
"Qgx2trzH", # Edinson Cavani
"2oMimkAU", # Radamel Falcao
"WfXv1DCa", # Wayne Rooney
"0vgcq6un", # Robin van Persie
"v5HSlEAa", # Harry Kane
"4S9fNUYh" # Ciro Immobile
]

View File

@@ -0,0 +1,170 @@
from lewy_db import baza as ldb
from lewy_globals import colors as c
import json
import lewy_globals
import requests
import time
def safe_traverse(obj: dict, path: list, default=None):
result = obj
try:
for x in path:
result = result[x]
except KeyError:
result = default
# print(f"error reading: {' -> '.join(path)} - returning: {default}")
finally:
return result
class scraper:
headers = {
'x-fsign': 'SW9D1eZo'
}
db = None
def __init__(self):
self.db = lewy_globals.getDb()
pass
def pobierzDaneNajlepszegoSportowcaNaSwiecie(self) -> dict:
response = requests.get('https://3.flashscore.ninja/3/x/feed/plm_MVC8zHZD_0', headers=headers)
return json.loads(response.text)
def pobierz_pojedyncza_strone(self, zewnetrzne_id_sportowca: str = "MVC8zHZD", nr_strony: int = 0) -> dict:
if len(zewnetrzne_id_sportowca) != 8:
raise ValueError("Zewnętrzne ID sportowca powinno być długości 8!")
response = requests.get(f'https://3.flashscore.ninja/3/x/feed/plm_{zewnetrzne_id_sportowca}_{nr_strony}', headers=self.headers)
return json.loads(response.text)
def __czy_x_istnieje(self, typ, **id):
rekord = self.db.simple_select_all(typ, **id)
if rekord is not None and rekord != []:
return True
else:
return False
def czy_mecz_istnieje(self, zewnetrzne_id_meczu: str):
# mecz = db.simple_select_all(ldb.mecze, zewnetrzne_id_meczu=zewnetrzne_id_meczu)
# if mecz is not None and mecz != []:
# return True
# else:
# return False
return self.__czy_x_istnieje("mecze", zewnetrzne_id_meczu=zewnetrzne_id_meczu)
def czy_klub_istnieje(self, id_klubu: str):
# mecz = db.simple_select_all(ldb.mecze, zewnetrzne_id_meczu=zewnetrzne_id_meczu)
# if mecz is not None and mecz != []:
# return True
# else:
# return False
return self.__czy_x_istnieje("kluby", id_klubu=id_klubu)
def id_na_imie_nazwisko_urodziny(self, zewnetrzne_id_sportowca: str = "MVC8zHZD"):
"""
Scraper z dykty xD
Pobiera imiona, nazwiska i dni urodzin sportowców z zewnętrznego id.
Działa na słowo honoru.
:param zewnetrzne_id_sportowca: Zewnętrzne id sportowca
:type zewnetrzne_id_sportowca: str
"""
if len(zewnetrzne_id_sportowca) != 8:
raise ValueError("Zewnętrzne ID sportowca powinno być długości 8!")
r = requests.get(f'https://www.flashscore.pl/?r=4:{zewnetrzne_id_sportowca}')
page = r.text
name_start_pos = page.find("data-testid=\"wcl-scores-heading-02\">") + 36
name_end_pos = page.find("</", name_start_pos)
name = page[name_start_pos:name_end_pos].strip().split(' ')
# Tak wiem... można by było użyć beautifulsoup4, ale nie ma sensu dodawać nowych zależności dla tylko jednej metody.
birthday_start_pos_1 = page.find("data-testid=\"wcl-scores-simpleText-01\">", name_end_pos) + 39
birthday_start_pos_2 = page.find("data-testid=\"wcl-scores-simpleText-01\">", birthday_start_pos_1) + 39
birthday_start_pos_3 = page.find("data-testid=\"wcl-scores-simpleText-01\">", birthday_start_pos_2) + 39
birthday_start_pos = page.find("data-testid=\"wcl-scores-simpleText-01\">", birthday_start_pos_3) + 39
birthday_end_pos = page.find("</", birthday_start_pos) - 1
birthday = None if birthday_end_pos - birthday_start_pos > 20 else page[birthday_start_pos:birthday_end_pos].strip(" ()")
return name, birthday
def aktualizuj_dane_sportowca(self, zewnetrzne_id_sportowca: str = "MVC8zHZD"):
stop_scraping = False
matches_to_add = []
# TODO: Sprawdź, czy sportowiec istnieje w bazie.
# Jeśli nie, dodaj go w podobny sposób, jak
# w sample_data_init() (w lewy_db.py).
page = 0
match_num = 0
while not stop_scraping:
retrieved_page = self.pobierz_pojedyncza_strone(zewnetrzne_id_sportowca=zewnetrzne_id_sportowca, nr_strony=page)
if not safe_traverse(retrieved_page, ["hasMoreLastMatches"], default=False):
stop_scraping = True
break
print(f"{c.WARNING}Pobrano nową stronę {page}{c.ENDC}:\n{retrieved_page}")
retrieved_matches = safe_traverse(retrieved_page, ["lastMatches"], default=[])
for match in retrieved_matches:
match_id = safe_traverse(match, ["eventEncodedId"], default="non-existent-match-id")
home_club_id = safe_traverse(match, ["homeParticipantUrl"], default="non-existent-club-id")
away_club_id = safe_traverse(match, ["awayParticipantUrl"], default="non-existent-club-id")
# Sprawdź, czy mecz nie znajduje się już w bazie
if self.czy_mecz_istnieje(zewnetrzne_id_meczu=match_id):
stop_scraping = True
break
# Sprawdź, czy klub znajduje się już w bazie. Jeśli nie,
# trzeba go dodać przed meczem.
if not self.czy_klub_istnieje(id_klubu=home_club_id):
print(f"{c.OKCYAN}Nowy klub{c.ENDC}: {home_club_id}")
self.db.simple_insert_one("kluby",
id_klubu=home_club_id,
pelna_nazwa=safe_traverse(match, ["homeParticipantName"]),
skrocona_nazwa=safe_traverse(match, ["homeParticipant3CharName"]))
if not self.czy_klub_istnieje(id_klubu=away_club_id):
print(f"{c.OKCYAN}Nowy klub{c.ENDC}: {away_club_id}")
self.db.simple_insert_one("kluby",
id_klubu=away_club_id,
pelna_nazwa=safe_traverse(match, ["awayParticipantName"]),
skrocona_nazwa=safe_traverse(match, ["awayParticipant3CharName"]))
# TODO: (opcjonalnie) zamień *słownik match* na *obiekt mecz*
# TODO: dodaj obiekt mecz do bazy (simple_insert_one(), simple_insert_many())
print(f"{c.OKCYAN}Nowy mecz ({match_num}){c.ENDC}: {match}")
match_num += 1
# TODO: Zaktualizuj statystyki sportowca
# Opcjonalnie: odczekaj kilka sekund (?)
# Problem w tym, że time.sleep() jest blokujące,
# a asyncio i flask nie idą ze sobą w parze.
# Można to załatwić osobnym skryptem, ale
# martwmy się tym dopiero, gdy dostaniemy
# rate limita. - sherl
page += 1
time.sleep(15)
def aktualizuj_dane(self):
"""
Pobiera mecze dla każdego sportowca wymienionego
w pliku konfiguracyjnym.
"""
for id_sportowca in lewy_globals.config['sportsmen']['tracked_ids']:
self.aktualizuj_dane_sportowca(zewnetrzne_id_sportowca=id_sportowca)
time.sleep(15)

View File

@@ -0,0 +1,176 @@
from argparse import ArgumentParser
from flask import Flask, Response, render_template
from flask_apscheduler import APScheduler
from fs_scraper import scraper as scr
from lewy_globals import colors as c
import lewy_api
import lewy_db
import lewy_globals
import lewy_routes
import os
import time
app = Flask(__name__)
app_host = "None"
app_port = "None"
scrape = None
def setup():
# sanity check: make sure config is set
# required to make `flask --app lewy run --debug` work
global config, app_host, app_port, scrape
try:
if not config['general']:
lewy_globals.setConfig(lewy_globals.configfile)
config = lewy_globals.config
except:
lewy_globals.setConfig(lewy_globals.configfile)
config = lewy_globals.config
# setting all the variables
lewy_globals.starttime = int(time.time())
lewy_globals.realUptime = 0
lewy_globals.apiRequests = 0
lewy_globals.apiFailedRequests = 0
lewy_globals.isProxied = config['general']['is_proxied']
lewy_globals.outsideApiHits = 0
are_we_sure_of_host_and_port = True
if app_host == "None":
app_host = "127.0.0.1"
are_we_sure_of_host_and_port = False
if app_port == "None":
app_port = "5000"
are_we_sure_of_host_and_port = False
public_facing_url = config['general']['public_facing_url']
if len(public_facing_url) >= 4 and public_facing_url[0:5].lower() == "https":
https_str = f"{c.OKBLUE}INFO:{c.ENDC} You're trying to run this web server on HTTPS, but currently it's not possible to do that!\n"
https_str += f" Please consider running this service behind a reverse proxy if you need HTTPS.\n"
print(https_str)
rewrite_sanity_check = public_facing_url.replace(f"{app_host}:{app_port}", "")
if not config['general']['is_proxied'] and public_facing_url == rewrite_sanity_check:
sanity_string = f"{c.OKBLUE}INFO:{c.ENDC} Public facing URL does not match the IP and port the server is running on.\n"
sanity_string += f" Expected: {c.OKCYAN}{config['general']['public_facing_url']}{c.ENDC}, but"
if not are_we_sure_of_host_and_port: sanity_string += " (assuming it's)"
sanity_string += f" running on: {c.OKCYAN}{app_host}:{app_port}{c.ENDC}.\n"
sanity_string += f" This is just a sanity check and may not neccessarily mean bad configuration.\n"
sanity_string += f" If you're running a reverse proxy, set {c.OKCYAN}is_proxied{c.ENDC} to true to silence this message.\n"
print(sanity_string)
app.config['SQLALCHEMY_DATABASE_URI'] = f"{config['general']['db_path_url']}"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Widoki widoczne dla "normalnego" użytkownika:
app.add_url_rule('/', view_func=lewy_routes.index)
app.add_url_rule('/index.html', view_func=lewy_routes.index)
app.add_url_rule('/mecze', view_func=lewy_routes.mecze)
app.add_url_rule('/statystyki', view_func=lewy_routes.statystyki)
app.add_url_rule('/toggle_dark_mode', view_func=lewy_routes.toggle_dark_mode)
app.add_url_rule('/club', view_func=lewy_routes.clubs)
app.add_url_rule('/representation', view_func=lewy_routes.representation)
app.add_url_rule('/compare', view_func=lewy_routes.compare)
app.add_url_rule('/trophies', view_func=lewy_routes.trophies)
# API:
app.add_url_rule('/api/', view_func=lewy_api.api_greeting)
app.add_url_rule('/api/<path:received_request>', view_func=lewy_api.api_global_catchall)
db = lewy_globals.setupDb(app, config)
scraper = scr()
with app.app_context():
db.create_all()
# job scheduler for repetetive tasks
scheduler = APScheduler()
scheduler.add_job(func=every5seconds, trigger='interval', id='5sec', seconds=5)
scheduler.add_job(func=every2hours, trigger='interval', id='2hr', hours=2)
scheduler.start()
# gets called every 5 seconds
def every5seconds():
# update the "real" uptime counter
lewy_globals.realUptime += 5
def every2hours():
# zaktualizuj bazę danych scrapując FS
# ...
# scraper.aktualizuj_dane()
return
@app.route('/<string:val>', methods=['GET'])
def blank(val):
return Response(f"{val}: not implemented in lewangoalski {lewy_globals.getCommitWithFailsafe()}", mimetype="text/plain")
def main(args):
print(f"{c.BOLD + c.HEADER}Witaj w webaplikacji 'lewangoalski' ({lewy_globals.getCommitWithFailsafe()})!{c.ENDC}")
print(f"Aby uruchomić w trybie deweloperskim (aby włączyć automatyczne przeładowanie zmian), użyj: {c.OKCYAN}flask --app lewy run --debug{c.ENDC}.")
print( "Aby uruchomić lokalnie, użyj adresu IP 127.0.0.1. Aby uruchomić na każdym z interfejsów, użyj 0.0.0.0.\n")
global config, app_host, app_port
try:
# if specified, use custom config file
lewy_globals.configfile = args.config
lewy_globals.setConfig(lewy_globals.configfile)
except:
# if not, try using the default "config.toml"
if os.path.exists("config.toml"):
lewy_globals.configfile = "config.toml"
else:
# unless it's not there, if that's the case then use the dummy file
lewy_globals.configfile = ""
# but try to set the API secret if provided by the user
if args.secret:
lewy_globals.randomly_generated_passcode = args.secret
lewy_globals.setConfig(lewy_globals.configfile)
config = lewy_globals.config
try:
host = args.ip
port = args.port
if not host or not port:
raise Exception
except:
config_ip, config_port = lewy_globals.extractIpAndPortFromPublicUrl()
print(f"Wpisz nazwę_hosta:port, na których należy uruchomić serwer Flask [domyślnie: {config_ip}:{config_port}]:")
try:
host_port = input('> ').split(':')
if host_port == ['']:
host_port = [config_ip, config_port] # defaults
except KeyboardInterrupt:
print(" ...wychodzę z programu."), quit() # handle Ctrl+C
host = host_port[0]
port = host_port[1]
print()
app_host = host
app_port = port
setup()
app.run(host=host, port=int(port))
if __name__ == "__main__":
#app.run(host="127.0.0.1", port=5000)
#app.run(host="0.0.0.0", port=5000)
parser = ArgumentParser(description='Aplikacja webowa do śledzenia statystyk Roberta Lewandowskiego.')
parser.add_argument("-i", "--ip", dest="ip", help="ip address/interface to bind to")
parser.add_argument("-p", "--port", dest="port", help="port on which the flask web backend should be ran")
parser.add_argument("-c", "--config", dest="config", help="path to TOML config file")
parser.add_argument("-s", "--secret", dest="secret", help="API key for resource access") # NOT tested
args = parser.parse_args()
main(args)
else:
app_host = os.getenv("FLASK_RUN_HOST", "None")
app_port = os.getenv("FLASK_RUN_PORT", "None")
setup()

View File

@@ -0,0 +1,43 @@
from flask import Response, request
from lewy_globals import colors as c
from markupsafe import escape
import json
import lewy_globals
import lewy_api_v1
import requests
import time
import traceback
def api_greeting():
string = {'status': 200, 'msg': f"ok (lewangoalski {lewy_globals.version})", 'latest_api': f"v{lewy_globals.apiVersion}"}
string = json.dumps(string)
return Response(string, mimetype='application/json')
def api_global_catchall(received_request):
lewy_globals.apiRequests += 1
if request.environ['REMOTE_ADDR'] != "127.0.0.1" or (lewy_globals.isProxied and request.environ['X-Forwarded-For'] != "127.0.0.1"):
lewy_globals.outsideApiHits += 1
request_list = received_request.split('/')
api_version = request_list[0]
if request_list[0] == 'v1':
# use v1 api
del request_list[0]
# if list is empty, aka /api/v1/, or /api/v1
if request_list == [''] or request_list == []:
resp = api_greeting()
try:
status, received, data = lewy_api_v1.lookup(request_list, request)
except Exception as e:
lewy_globals.apiFailedRequests += 1
stripped_filename = __file__[max(__file__.rfind("/"), __file__.rfind("\\")) + 1:]
print(f"\n{c.FAIL}Error! /api/{received_request} -> {stripped_filename}:L{e.__traceback__.tb_lineno} -> {type(e).__name__}{c.ENDC}:")
print(traceback.format_exc())
status, received, data = 500, f"internal server error: call ended in failure: {e} ({stripped_filename}:L{e.__traceback__.tb_lineno})", []
resp = Response(json.dumps({'status': status, 'msg': received, 'data': data}), mimetype='application/json', status=status)
else:
lewy_globals.apiFailedRequests += 1
status, received, data = 405, f'error: unsupported api version: "{request_list[0]}". try: "v{lewy_globals.apiVersion}".', []
resp = Response(json.dumps({'status': status, 'msg': received, 'data': data}), mimetype='application/json', status=status)
return resp

View File

@@ -0,0 +1,148 @@
# 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]}", []
# 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):
"""
TODO: Zwraca mecze.
"""
pass
# 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':
get_matches(r = request)
case _:
increment_bad_requests()
return not_implemented(data)

View File

@@ -0,0 +1,391 @@
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
from functools import wraps
from sqlalchemy import ForeignKey, select, insert, update
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase, Session, relationship
from typing import List
import time
import toml
import traceback
global db
class c:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
ENDL = '\n'
class baza():
# global sportowcy, trofea, sportowcy_w_meczach, statystyki_sportowcow, kluby, mecze
db = None
entities = {}
session = None
app = None
def __init__(self, app, config):
self.app = app
self.db = self.initDB(self.app, config)
self.refresh_session()
def initDB(self, app, config):
global sportowcy, trofea, sportowcy_w_meczach, statystyki_sportowcow, kluby, mecze
tnp = config['general']['db_prefix'] + "_lewangoalski_"
class Base(DeclarativeBase):
pass
db = SQLAlchemy(app, model_class=Base)
class sportowcy(Base):
__tablename__ = tnp + "sportowcy"
id_zawodnika: Mapped[ int] = mapped_column(primary_key=True, autoincrement=True)
zewnetrzne_id_zawodnika: Mapped[ str] = mapped_column(unique=True)
imie: Mapped[ str] = mapped_column()
nazwisko: Mapped[ str] = mapped_column()
data_urodzenia: Mapped[ str] = mapped_column()
czy_aktywny: Mapped[ bool] = mapped_column()
klub_id: Mapped[ List[str]] = mapped_column(ForeignKey(f"{tnp}kluby.id_klubu"), nullable=True)
klub: Mapped[ List["kluby"]] = relationship(back_populates="sportowcy_w_klubie", foreign_keys=[klub_id])
narodowosc: Mapped[ str] = mapped_column()
ilosc_trofeow: Mapped[ int] = mapped_column()
ostatnie_trofeum_id: Mapped[ int] = mapped_column(ForeignKey(f"{tnp}trofea.id_trofeum"), nullable=True)
ostatnie_trofeum: Mapped[ "trofea"] = relationship(back_populates="zawodnik", foreign_keys=[ostatnie_trofeum_id])
pierwszy_mecz_id: Mapped[ int] = mapped_column(ForeignKey(f"{tnp}mecze.id_meczu"), nullable=True)
pierwszy_mecz: Mapped[ "mecze"] = relationship()
wycena: Mapped[ int] = mapped_column()
ostatni_gol_dla_id: Mapped[ str] = mapped_column(ForeignKey(f"{tnp}kluby.id_klubu"), nullable=True)
ostatni_gol_dla: Mapped[ "kluby"] = relationship(back_populates="sportowcy_ostatni_gol", foreign_keys=[ostatni_gol_dla_id])
statystyki_id: Mapped[ List[int]] = mapped_column(ForeignKey(f"{tnp}statystyki_sportowcow.id_statystyki"), nullable=True)
statystyki: Mapped[List["statystyki_sportowcow"]] = relationship(back_populates="sportowiec")
trofea: Mapped[ List["trofea"]] = relationship(back_populates="zawodnik", foreign_keys="[trofea.id_zawodnika]")
def __repr__(self):
return f"<Sportowiec #{self.id_zawodnika} ({self.imie} {self.nazwisko})>"
# Co było pierwsze, jajko czy kura? https://docs.sqlalchemy.org/en/20/orm/relationship_persistence.html#rows-that-point-to-themselves-mutually-dependent-rows
# Kiepskie rozwiązanie, ale jednak działające: pozwolić obcym kluczom na bycie "null"
class trofea(Base):
__tablename__ = tnp + "trofea"
id_trofeum: Mapped[ int] = mapped_column(primary_key=True, autoincrement=True)
id_zawodnika: Mapped[ int] = mapped_column(ForeignKey(f"{tnp}sportowcy.id_zawodnika", name="fk_zawodnik"), nullable=True)
zawodnik: Mapped[ "sportowcy"] = relationship(back_populates="trofea", foreign_keys=[id_zawodnika], post_update=True)
nazwa: Mapped[ str] = mapped_column()
sezon: Mapped[ str] = mapped_column()
rok: Mapped[ int] = mapped_column()
def __repr__(self):
return f"<Trofeum #{self.id_trofeum} ({self.nazwa})>"
class sportowcy_w_meczach(Base):
__tablename__ = tnp + "sportowcy_w_meczach"
id_rekordu: Mapped[ int] = mapped_column(primary_key=True, autoincrement=True)
id_zawodnika: Mapped[ int] = mapped_column(ForeignKey(f"{tnp}sportowcy.id_zawodnika"))
zawodnik: Mapped[ "sportowcy"] = relationship()
zewnetrzne_id_meczu: Mapped[ str] = mapped_column(ForeignKey(f"{tnp}mecze.zewnetrzne_id_meczu"))
czas_gry: Mapped[ int] = mapped_column()
goli: Mapped[ int] = mapped_column()
asyst: Mapped[ int] = mapped_column()
interwencje_bramkarza: Mapped[ int] = mapped_column()
suma_interwencji_na_bramke: Mapped[ int] = mapped_column()
zolte_kartki: Mapped[ int] = mapped_column()
czerwone_kartki: Mapped[ int] = mapped_column()
wygrana: Mapped[ int] = mapped_column()
wynik: Mapped[ float] = mapped_column()
def __repr__(self):
return f"<Sportowiec #{self.id_zawodnika} ({self.imie} {self.nazwisko})>"
class statystyki_sportowcow(Base):
__tablename__ = tnp + "statystyki_sportowcow"
id_statystyki: Mapped[ int] = mapped_column(primary_key=True, autoincrement=True)
sportowiec: Mapped[ "sportowcy"] = relationship(back_populates="statystyki")
ostatni_mecz: Mapped[ int] = mapped_column(ForeignKey(f"{tnp}mecze.id_meczu"))
ilosc_wystapien: Mapped[ int] = mapped_column()
minut_gry: Mapped[ int] = mapped_column()
gier_sum: Mapped[ int] = mapped_column()
goli_sum: Mapped[ int] = mapped_column()
asyst_sum: Mapped[ int] = mapped_column()
interwencji_sum: Mapped[ int] = mapped_column()
nieobronionych_interwencji_sum: Mapped[ int] = mapped_column()
zoltych_kartek_sum: Mapped[ int] = mapped_column()
czerwonych_kartek_sum: Mapped[ int] = mapped_column()
wygranych_sum: Mapped[ int] = mapped_column()
wynik_sum: Mapped[ int] = mapped_column()
meczow_do_wynikow_sum: Mapped[ int] = mapped_column()
def __repr__(self):
return f"<Statystyka #{self.id_statystyki} ({self.sportowiec.imie} {self.sportowiec.nazwisko})>"
class kluby(Base):
__tablename__ = tnp + "kluby"
id_klubu: Mapped[ str] = mapped_column(primary_key=True)
pelna_nazwa: Mapped[ str] = mapped_column()
skrocona_nazwa: Mapped[ str] = mapped_column()
sportowcy_w_klubie: Mapped[ List["sportowcy"]] = relationship(back_populates="klub", foreign_keys="[sportowcy.klub_id]")
sportowcy_ostatni_gol: Mapped[ "sportowcy"] = relationship(back_populates="ostatni_gol_dla", foreign_keys="[sportowcy.ostatni_gol_dla_id]")
def __repr__(self):
return f"<Klub #{self.id_klubu} ({self.skrocona_nazwa})>"
class mecze(Base):
__tablename__ = tnp + "mecze"
id_meczu: Mapped[ int] = mapped_column(primary_key=True, autoincrement=True)
zewnetrzne_id_meczu: Mapped[ str] = mapped_column(unique=True)
data: Mapped[ datetime] = mapped_column()
gospodarze_id: Mapped[ str] = mapped_column(ForeignKey(f"{tnp}kluby.id_klubu"))
gospodarze: Mapped[ "kluby"] = relationship(foreign_keys=[gospodarze_id])
goscie_id: Mapped[ str] = mapped_column(ForeignKey(f"{tnp}kluby.id_klubu"))
goscie: Mapped[ "kluby"] = relationship(foreign_keys=[goscie_id])
gosp_wynik: Mapped[ int] = mapped_column()
gosc_wynik: Mapped[ int] = mapped_column()
sezon: Mapped[ str] = mapped_column()
nazwa_turnieju: Mapped[ str] = mapped_column()
skrocona_nazwa_turnieju: Mapped[ str] = mapped_column()
flaga: Mapped[ int] = mapped_column()
def __repr__(self):
return f"<Mecz #{self.id_meczu} ({self.zewnetrzne_id_meczu}, {self.gospodarze.skrocona_nazwa} vs. {self.goscie.skrocona_nazwa})>"
self.entities = {
'sportowcy': sportowcy,
'trofea': trofea,
'sportowcy_w_meczach': sportowcy_w_meczach,
'statystyki_sportowcow': statystyki_sportowcow,
'kluby': kluby,
'mecze': mecze
}
return db
def create_all(self):
self.db.create_all()
def refresh_session(self):
with self.app.app_context():
self.session = Session(self.db.engine)
def exit_gracefully(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
return_val = None
try:
return_val = func(self, *args, **kwargs)
except:
print(f"{c.FAIL}"
f"Wystąpił błąd podczas wykonywania zapytania SQL:"
f"{c.ENDC}"
"\n"
f"{traceback.format_exc()}")
self.session.rollback()
self.session.close()
self.refresh_session()
return return_val
return wrapper
def str_to_column(self, string: str):
"""
Zamienia tekstowy zapis "tabela.kolumna"
na obiekt modelu bazy (ze zmiennej self.entities).
Zwraca None jeśli takowego nie znajdzie.
Zamiennik dla niepożądanego eval().
:param string: Zapis tekstowy
:type string: str
"""
table_str = string[:string.find('.')]
column_str = string[string.find('.') + 1:]
if hasattr(self.entities[table_str], column_str):
return getattr(self.entities[table_str], column_str)
return None
@exit_gracefully
def simple_select_all(self, entity_type, **kwargs):
"""
Użycie:
simple_select_all(ldb.sportowcy, zewnetrzne_id_zawodnika="MVC8zHZD")
simple_select_all("sportowcy", id_zawodnika=1)
simple_select_all("kluby", ..., LIMIT=5, ORDER_BY="kluby.skrocona_nazwa")
https://stackoverflow.com/a/75316945
Did they make it harder to query dynamically on purpose? ~Frank 19.11.2023
"""
if not isinstance(entity_type, str):
entity_type = entity_type.__name__
# Save special arguments received with kwargs,
# that are meant for SQL operations to special_args,
# and delete from the rest, that will be passed
# directly to filter_by().
# They will not be passed as search query, but serve
# as an additional search parameter.
special_keywords = ("ORDER_BY", "ORDER_BY_DESC", "LIMIT")
special_args = {}
for arg in special_keywords:
if arg in kwargs:
special_args[arg] = kwargs[arg]
del kwargs[arg]
print(f"[{round(time.time())}] SELECT")
results = (
self.session.
query(self.entities[entity_type]).
filter_by(**kwargs)
)
if "ORDER_BY" in special_args:
column = self.str_to_column(special_args["ORDER_BY"])
if column is not None:
results = results.order_by(column)
if "ORDER_BY_DESC" in special_args:
column = self.str_to_column(special_args["ORDER_BY_DESC"])
if column is not None:
results = results.order_by(column.desc())
if "LIMIT" in special_args:
results = results.limit(special_args["LIMIT"])
results_objs = results.all()
print(f"[{round(time.time())}] SELECT RESULTS: {results_objs}")
return results_objs
@exit_gracefully
def simple_insert_one(self, entity_type, **kwargs):
"""
Użycie:
simple_insert_one(ldb.kluby, id_klubu="polska", pelna_nazwa="Reprezentacja Polski", skrocona_nazwa="PL")
https://docs.sqlalchemy.org/en/20/tutorial/data_insert.html
https://docs.sqlalchemy.org/en/20/orm/session_basics.html
"""
if not isinstance(entity_type, str):
entity_type = entity_type.__name__
if "id" in kwargs:
print(f"{c.FAIL}UWAGA!{c.ENDC}")
print(f"Próbujesz dodać obiekt do tabeli, który ma już identyfikator.\n"
f"To spowoduje problemy w przyszłości, gdy będziesz chciał dodać nowy obiekt do bazy bez ustawiania id na sztywno\n"
f"(id klucza głównego nie zostanie zaktualizowane w sekwencji, przez co baza będzie próbowała dodać obiekt z id już istniejącego rekordu!).\n"
f"Aby naprawić dodawanie z autoinkrementującym kluczem zobacz {c.WARNING}https://stackoverflow.com/a/8745101{c.ENDC}\n"
f"Zostałeś ostrzeżony!")
print(f"[{round(time.time())}] INSERT")
obj = self.entities[entity_type](**kwargs)
#with Session(self.db.engine) as session:
self.session.add(obj)
self.session.commit()
return 0
#return 1
@exit_gracefully
def simple_insert_many(self, objs_list):
"""
Użycie:
simple_insert_many([sportowiec_a, sportowiec_b])
https://docs.sqlalchemy.org/en/20/tutorial/data_insert.html
https://docs.sqlalchemy.org/en/20/orm/session_basics.html
"""
#with Session(self.db.engine) as session:
self.session.add_all(objs_list)
self.session.commit()
return 0
#return 1
@exit_gracefully
def sample_data_init(self, override_safety_check=False):
"""
Użycie:
sample_data_init()
Uwaga! Poniższe populuje pustą bazę danych.
Nie należy tego używać na nie-pustej bazie, ponieważ spowoduje
to wpisanie śmieciowych danych!
Jeżeli wiesz co robisz w takiej sytuacji, uruchom metodę z:
sample_data_init(override_safety_check=True)
Metoda głównie używana do testów.
"""
is_table_empty = False
try:
self.simple_select_all(self.sportowcy, zewnetrzne_id_zawodnika="MVC8zHZD")
except:
is_table_empty = True
if not is_table_empty and override_safety_check:
raise EnvironmentError("sample_data_init() ran on a non-empty database. Ignore with override_safety_check=True.")
with Session(self.db.engine) as session:
self.simple_insert_one(kluby,
id_klubu="undefined",
pelna_nazwa="Klub niezdefiniowany",
skrocona_nazwa="N/A")
self.simple_insert_one(mecze,
zewnetrzne_id_meczu="dummy_match",
data=datetime.strptime("1970-01-01 00:00:00", '%Y-%m-%d %H:%M:%S'),
gospodarze_id="undefined",
goscie_id="undefined",
gosp_wynik=0,
gosc_wynik=0,
sezon="1970/1970",
nazwa_turnieju="Nieznany turniej",
skrocona_nazwa_turnieju="N/A",
flaga=0)
self.simple_insert_one(statystyki_sportowcow,
ostatni_mecz=1,
ilosc_wystapien=0,
minut_gry=0,
gier_sum=0,
goli_sum=0,
asyst_sum=0,
interwencji_sum=0,
nieobronionych_interwencji_sum=0,
zoltych_kartek_sum=0,
czerwonych_kartek_sum=0,
wygranych_sum=0,
wynik_sum=0,
meczow_do_wynikow_sum=0)
sportowiec = sportowcy(
zewnetrzne_id_zawodnika="MVC8zHZD",
imie="Robert",
nazwisko="Lewandowski",
data_urodzenia="21.08.1988",
czy_aktywny=True,
klub_id="undefined",
narodowosc="PL",
ilosc_trofeow=0,
pierwszy_mecz_id=1,
ostatni_gol_dla_id="undefined",
statystyki_id=1,
wycena=64_940_000)
trofeum = trofea(
nazwa="Nieznane trofeum",
sezon="0000/0000",
rok=1970)
session.add(sportowiec)
session.flush()
session.add(trofeum)
session.flush()
trofeum.zawodnik = sportowiec
sportowiec.ostatnie_trofeum = trofeum
session.commit()
return 0
return 1

View File

@@ -1,10 +1,158 @@
from git import Repo # hash ostatniego commitu from git import Repo # hash ostatniego commitu
import os
import time
import toml
import lewy_db
def getCommit(): global db, config, randomly_generated_passcode
repo = "<p>Brak informacji o wersji skryptu</p>"
class colors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
ENDL = '\n'
def safeTraverse(obj: dict, path: list, default=None):
result = obj
try: try:
repo = Repo(search_parent_directories=True).head.object.hexsha for x in path:
repo = f"<p>Commit: <a href='https://gitea.7o7.cx/roberteam/lewangoalski/commit/{repo}' style='width: 50%'>{repo[:11]}</a></p>" result = result[x]
except KeyError:
result = default
# print(f"error reading: {' -> '.join(path)} - returning: {default}")
finally:
return result
def getCommit() -> str | None:
try:
return Repo(search_parent_directories=True).head.object.hexsha
except Exception as e:
return None
def getCommitInFormattedHTML():
repo = "<p>Brak informacji o wersji skryptu</p>"
commit = getCommit()
if commit is not None:
repo = f"<p>Commit: <a href='https://gitea.7o7.cx/roberteam/lewangoalski/commit/{commit}'>{commit[:11]}</a></p>"
return repo
def getCommitWithFailsafe():
commit = getCommit()
if commit is None:
commit = "(unknown commit)"
else:
commit = "#" + commit
return commit[:12]
def ensureRandomlyGeneratedPassword():
global randomly_generated_passcode
# iff the passcode is 0, as we manually set it elsewhere!
if randomly_generated_passcode == 0:
# generate a pseudorandom one and use it in the temporary config
randomly_generated_passcode = str(int(time.time() * 1337 % 899_999 + 100_000))
print(f"{colors.WARNING}WARNING{colors.ENDC}: Default config populated with one-time, insecure pseudorandom API key: {colors.OKCYAN}{randomly_generated_passcode}{colors.ENDC}.\n"
f" The API key is not the Flask debugger PIN. You need to provide a config file for persistence!{colors.ENDL}")
def getConfig(configfile: str) -> dict:
global randomly_generated_passcode
if not os.path.exists(configfile):
dummy_config = {'general': {'db_path_url': 'sqlite:///lewangoalski.sqlite', 'is_proxied': False, 'public_facing_url': 'http://127.0.0.1:5000/', 'db_prefix': 'lewy_sqlite'}, 'api': {'api_key': 'CHANGEME'}, 'scraper': {'user-agent': ''}, 'sportsmen': {'tracked_ids': ["MVC8zHZD", "WGOY4FSt", "vgOOdZbd", "Wn6E2SED", "AiH2zDve", "dUShzrBp", "UmV9iQmE", "tpV0VX0S", "vw8ZV7HC", "Qgx2trzH", "2oMimkAU", "WfXv1DCa", "0vgcq6un", "v5HSlEAa", "4S9fNUYh"]}}
# if a passcode has not been provided by the user (config file doesn't exist, and user didn't specify it using an argument)
print(f"{colors.WARNING}WARNING{colors.ENDC}: Using default, baked in config data. {colors.ENDL}"
f" Consider copying and editing the provided example file ({colors.OKCYAN}config.example.toml{colors.ENDC}).")
ensureRandomlyGeneratedPassword()
dummy_config['api']['api_key'] = str(randomly_generated_passcode)
return dummy_config
else:
return toml.load(configfile)
def setupDb(app, config) -> lewy_db.baza:
global db
db = lewy_db.baza(app, config)
return db
def getDb() -> lewy_db.baza:
"""
Akcesor dla wrappera bazy danych wspólnego dla całego projektu
(klasy baza z lewy_db)
"""
return db
def setConfig(configfile):
"""
Zapewnia, że konfiguracja nie jest pusta,
nawet, gdy sam plik jest pusty.
:param configfile: Ścieżka do pliku
:type configfile: str
"""
global config
config = getConfig(configfile)
if safeTraverse(config['api']['api_key'], []) is None or not config['api']['api_key']:
ensureRandomlyGeneratedPassword()
config['api']['api_key'] = str(randomly_generated_passcode)
def getHeaders():
"""
Zwraca hardkodowane nagłówki do scrapowania, bądź te,
z config.toml (o ile użytkownik jakieś podał).
"""
# NOTE: use ESR user-agent
# user_agent = 'Mozilla/5.0 (Windows NT 10.0; rv:130.0) Gecko/20100101 Firefox/130.0'
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0'
if safeTraverse(config[scraper]['user-agent'], []) is not None:
user_agent = config[scraper]['user-agent']
return user_agent
def getUptime():
"""
Zwraca informację o czasie działania serwera.
"""
return int(time.time()) - starttime
def extractIpAndPortFromPublicUrl() -> tuple:
"""
Pobiera dane z konfiguracji i zwraca
krotkę: adres IP i port.
"""
ip, port = "127.0.0.1", "5000"
try:
url = config['general']['public_facing_url'].replace(":/", "")
url_parts = url.split('/')
ip_and_port = url_parts[1]
ip, port = ip_and_port.split(':')
except: except:
pass pass
return repo
return ip, port
# Please leave at the bottom of this file.
config = {}
configfile = "config.toml"
version = getCommitWithFailsafe()
apiVersion = "1"
randomly_generated_passcode = 0

View File

@@ -0,0 +1,108 @@
from flask import render_template, request, make_response
import lewy_api_v1
import lewy_db
import lewy_globals
def get_lewy_stats():
return {
'all_time_stats': {
'goals': 380,
'assists': 120,
'matches': 450,
},
'club_stats': {
'goals': 132,
'assists': 112,
'matches': 245,
},
'nation_stats': {
'goals': 86,
'assists': 52,
'matches': 158,
},
'worldcup': {
'goals': 7,
'assists': 2,
'matches': 11,
},
'euro': {
'goals': 6,
'assists': 2,
'matches': 18,
},
'cards': {
'yellow': 24,
'red': 4,
}
}
def index():
dark_mode = request.cookies.get('darkMode', 'disabled')
# Przykładowe użycie endpointu last_goal_for():
# roberts_last_goals_club = lewy_api_v1.last_goal_for()
# print(roberts_last_goals_club.id_klubu)
stats = {
'goals': 38,
'assists': 12,
'matches': 45,
'matches_list': [
{'date': '2024-10-12', 'opponent': 'Real Madrid', 'goals': 2, 'assists': 1, 'minutes': 90},
{'date': '2024-10-19', 'opponent': 'Valencia', 'goals': 1, 'assists': 0, 'minutes': 85},
# Możesz dodać więcej meczów...
]
}
return render_template('index.html', goals=stats['goals'], assists=stats['assists'],
matches=stats['matches'], matches_list=stats['matches_list'],
commit_in_html=lewy_globals.getCommitInFormattedHTML(),
dark_mode=dark_mode)
def mecze():
# Możesz dostarczyć szczegóły dotyczące meczów
matches = [
{'date': '2024-10-12', 'opponent': 'Real Madrid', 'goals': 2, 'assists': 1, 'minutes': 90},
{'date': '2024-10-19', 'opponent': 'Valencia', 'goals': 1, 'assists': 0, 'minutes': 85},
]
return render_template('matches.html', matches=matches)
def statystyki():
dane=get_lewy_stats()
return render_template('stats.html', **dane)
def clubs():
selected_club = request.args.get("club","FC Barcelona")
clubs = [
{'club': 'FC Barcelona', 'goals': 22,'assist':12},
{'club': 'Bayern Monachium', 'goals': 132,'assist':12},
{'club': 'Borussia Dortmund', 'goals': 132,'assist':12},
{'club': 'Lech Poznan', 'goals': 132,'assist':12},
]
return render_template('club.html', clubs=clubs, selected_club=selected_club)
def representation():
nation_stats = {
'goals': 86,
'assists': 52,
'matches': 158,
}
return render_template('representation.html', nation_stats=nation_stats)
def compare():
selected_player = request.args.get("player","Leo Messi")
lewy=get_lewy_stats()
player2 = [
{'name':'Leo Messi','goals': 34,'assists': 12},
]
return render_template('compare.html',player2=player2, selected_player=selected_player,**lewy, )
def trophies():
trophy = [
{'name': 'asdasd', 'year': 2023},
{'name': 'ssss', 'sezon': '2022/2023'},
]
return render_template('trophies.html',trophy=trophy)
def toggle_dark_mode():
# Przełącz tryb i zapisz w ciasteczku
dark_mode = request.cookies.get('darkMode', 'disabled')
new_mode = 'enabled' if dark_mode == 'disabled' else 'disabled'
response = make_response("OK")
response.set_cookie('darkMode', new_mode, max_age=31536000) # Ustawienie ciasteczka na 1 rok
return response

View File

@@ -1,17 +0,0 @@
from flask import render_template, request, make_response
from FlaskWebProject import app
@app.route('/')
def index():
# Odczyt ciasteczka "darkMode" <20> domy<6D>lnie "disabled"
dark_mode = request.cookies.get('darkMode', 'disabled')
return render_template('index.html', dark_mode=dark_mode)
@app.route('/toggle_dark_mode')
def toggle_dark_mode():
# Prze<7A><65>cz tryb i zapisz w ciasteczku
dark_mode = request.cookies.get('darkMode', 'disabled')
new_mode = 'enabled' if dark_mode == 'disabled' else 'disabled'
response = make_response("OK")
response.set_cookie('darkMode', new_mode, max_age=31536000) # Ustawienie ciasteczka na 1 rok
return response

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View File

@@ -1,140 +1,882 @@
/* Podstawowy styl */ * {
box-sizing: border-box;
}
@font-face {
font-family: 'Exo2SemiBold';
src: url('fonts/Exo2-SemiBold.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Exo2ExtraBold';
src: url('fonts/Exo2-ExtraBold.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
:root {
--barca-blue: #002147;
--barca-red: #A50044;
--barca-gold: #FDB913;
--polska-red-dark: #DC143C;
--polska-red: #E30B17;
--polska-white: #FFFFFF;
--polska-section-color: #121623;
--section-color: #051839;
--pink-highlight: #E1317E;
--blue-highlight: #00B9BF;
--yellow-highlight: #FFD23F;
--border-radius: 5px;
}
/* Podstawowy styl */
body { body {
font-family: 'Arial', sans-serif; font-family: 'Exo2ExtraBold', sans-serif;
margin: 0; margin: 0;
padding: 0; padding: 0;
background: #f7f7f7; background-color: var(--section-color);
color: #222; color: black;
transition: all 0.3s ease; transition: all 0s ease;
}
nav {
background: #d32f2f;
padding: 10px;
display: flex; display: flex;
justify-content: space-around; flex-direction: column;
color: white; align-items: stretch;
/* Wyśrodkowanie elementów w poziomie */
justify-content: flex-start;
/* Ustalamy początek na górze */
min-height: 100vh;
} }
nav a, nav button { /* Header */
.header-content {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 20px 0;
background: var(--section-color);
color: white; color: white;
text-decoration: none; font-size: 15px;
font-weight: bold; z-index: -2;
background: none; position: relative;
border: none; margin-top: 60px;
cursor: pointer; /* box-shadow: 0px -5px 10px 2px rgba(0, 0, 0, 0.347); */
}
.header-content h1 {
border-bottom: 10px solid var(--barca-red);
border-radius: 10px;
padding: 5px;
animation: header-content 500ms ease;
transform: skewX(-5deg);
}
.header-content-special {
font-size: 50px;
color: var(--barca-gold);
}
.profile-image {
width: 40%;
padding: 20px;
position: relative;
}
.profile-image-cover {
background: linear-gradient(185deg, transparent 40% 60%, var(--section-color) 85%, var(--section-color) 90%);
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
.profile-image img {
width: 100%;
max-width: 600px;
animation: header-content 400ms ease;
background: linear-gradient(90deg, var(--barca-blue) 50%, var(--barca-red) 50% 100%);
border-radius: var(--border-radius);
transform: skewX(2deg) skewY(0deg);
}
@keyframes header-content {
0% {
opacity: 00%;
transform: skewX(30deg);
} }
main { 100% {
opacity: 100%;
}
}
header button {
border: none;
font-size: 16px;
cursor: pointer;
width: 100%;
height: 100%;
border-radius: var(--border-radius);
}
/* Styl nawigacji */
.navbar {
background: linear-gradient(to right, #002147, #A50044);
padding: 2.3rem 1rem;
height: 1.5rem;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 999;
display: flex;
justify-content: space-around;
align-items: center;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
.navbar ul {
display: flex;
align-items: center;
}
.logo {
display: flex;
align-items: center;
}
.logo-text {
font-size: 1.5rem;
font-weight: bold;
color: var(--barca-gold);
}
.logo-icon {
font-size: 2.8em;
}
.logo-link {
text-decoration: none;
}
.nav-links {
display: flex;
gap: 0rem;
list-style: none;
height: 100%;
}
.nav-links li a {
display: block;
color: white;
/* dopasowane do .navbar padding */
font-weight: 500;
border: none;
cursor: pointer;
align-items: center;
text-decoration: none;
border-radius: var(--border-radius);
padding: 10px;
height: 100%;
transition: 100ms ease;
position: relative;
}
.nav-links li a::before {
display: flex;
justify-content: center;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0%;
font-size: 25px;
transition: 100ms ease;
}
.nav-links li:hover a::before {
opacity: 100%;
transform: translateY(-20px);
}
.nav-links li:nth-child(1) a::before {
content: "🏠";
}
.nav-links li:nth-child(2) a::before {
content: "📅";
}
.nav-links li:nth-child(3) a::before {
content: "📊";
}
.nav-links li:nth-child(4) a::before {
content: "🤝";
}
.nav-links li:nth-child(5) a::before {
content: "⚽";
}
.nav-links li:nth-child(6) a::before {
content: "🏆";
}
.nav-links li:nth-child(7) a::before {
content: "🔎";
}
.nav-links li {
display: block;
}
.nav-links li:has(button) {
padding: 20px; padding: 20px;
} }
.nav-links li button {
width: 1.5em;
height: 1.5em;
padding: 20px;
font-size: 30px;
border-radius: 50%;
padding: 0;
margin: 0;
background-color: var(--bg-color);
border: none;
}
.nav-links li button {
background-color: var(--barca-blue);
position: relative;
overflow: hidden;
box-shadow: 0px 0px 6px 1px #FDB913;
transition: 100ms ease;
}
.nav-links li button:hover {
transform: scale(1.1, 1.1);
}
.nav-links li button::after {
content: "";
display: block;
position: absolute;
top: 0;
right: -1px;
background-color: var(--barca-red);
width: 50%;
height: 100%;
}
.nav-links li button::before {
content: "";
background: url('FC_Barcelona.png');
background-size: contain;
z-index: 1;
display: block;
position: absolute;
top: 5px;
left: 5px;
width: 80%;
height: 80%;
}
.nav-links a:hover {
background-color: var(--barca-gold, #FDB913);
color: var(--barca-blue, #002147);
}
.hamburger {
display: none;
font-size: 2rem;
color: var(--barca-gold);
cursor: pointer;
}
@media (max-width: 1090px) {
/* .base-header .navbar:not(:has(.show))
{
margin-bottom: 400px;
} */
.nav-links {
position: absolute;
flex-direction: column;
top: 57px;
right: 0px;
padding: 0rem;
gap: 0;
height: auto;
backdrop-filter: blur(4px);
width: 30%;
justify-content: center;
box-shadow: 0px 0px 5px 5px rgba(0, 0, 0, 0.105);
}
.nav-links li {
width: 100%;
margin: 0;
display: flex;
justify-content: center;
}
.nav-links li a,
.nav-links li button {
display: block;
width: 100%;
text-align: left;
padding: 1rem;
margin: 0;
border-radius: 0;
/* brak zaokrągleń w mobilnym menu */
}
.nav-links li button
{
background-color: none;
width: 45px;
border-radius: 50%;
}
.nav-links li button::before,.nav-links li button::after
{
background-color: none
;
}
.nav-links li button:hover
{
transform: none;
}
.nav-links.show {
display: none;
}
.hamburger {
display: block;
}
}
@media (max-width: 1000px)
{
.header-content
{
flex-direction: column;
}
.header-content .profile-image
{
display: flex;
justify-content: center;
width: 100%;
}
.header-content .profile-image img{
width: 100%;
}
.about-section-image
{
display: none;
}
}
@media (max-width: 600px){
.section__matches
{
font-size: 10px;
}
.section__matches th{
padding: 3px;
}
.club-stats-grid
{
grid-template-columns: 1fr 1fr !important;
}
.club-stats-grid .stat-box
{
width: 100%;
}
.section-stats-center .section-stats .stats
{
display: flex;
flex-direction: column;
align-items: center;
}
.section-stats-center .section-stats .stats .stat-box
{
width: 100%;
padding: 0;
}
}
/* Wyśrodkowanie głównej zawartości */
main {
display: flex;
align-items: center;
flex-direction: column;
}
.main-index {
border-radius: var(--border-radius);
margin-top: 20px;
padding: 20px;
width: 80%;
max-width: 1200px;
background-color: rgba(255, 255, 255, 0.086);
color: white;
}
.about-section {
border-radius: var(--border-radius);
background-color: var(--barca-blue);
padding: 20px;
color: white;
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 20px;
grid-template-areas:
"how how"
"- about-section-image"
"- about-section-image"
"- about-section-image";
}
.about-section article h3 {
background-color: var(--barca-gold);
color: black;
text-align: center;
border-radius: var(--border-radius);
padding: 10px;
}
.about-section article h4 {
background-color: #00B9BF;
width: fit-content;
padding: 10px;
color: black;
border-radius: (--border-radius)
}
.article__how-it-works {
grid-area: how;
}
.about-section article a {
display: block;
width: 100%;
color: var(--barca-gold);
text-align: center;
}
.about-section-image {
grid-area: about-section-image;
z-index: 0;
position: relative;
}
.about-section-image::after {
content: "";
display: block;
position: absolute;
right: 0;
bottom: 0;
width: 90%;
height: 90%;
z-index: -1;
background-color: #051839;
border-radius: 20px;
}
.about-section-image img {
width: 100%;
z-index: 3;
}
.general-stats-section {
border-radius: var(--border-radius);
display: flex;
flex-direction: column;
align-items: center;
}
.general-stats-section h2 {
width: 100%;
background-color: #002147;
padding: 20px;
text-align: center;
}
.general-stats-section .grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
height: 150px;
gap: 20px;
width: 100%;
}
.general-stats-section .grid article {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
font-size: 1.3em;
}
.general-stats-section .grid article:nth-child(1){background-color: var(--blue-highlight);}
.general-stats-section .grid article:nth-child(2){background-color: var(--yellow-highlight);}
.general-stats-section .grid article:nth-child(3){background-color: var(--pink-highlight);}
.general-stats-section .grid p
{
font-size: 40px;
}
.general-stats-section .grid h3, .general-stats-section .grid p
{
margin: 0;
padding: 0;
}
/* Styl dla tabeli */
table { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin-top: 20px; margin-top: 20px;
} }
th, td { th,
td {
padding: 10px; padding: 10px;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
text-align: center; text-align: center;
} }
/* Styl dla zdjęcia */
.photo { .photo {
width: 200px; width: 100px;
height: 100px;
border-radius: 50%; border-radius: 50%;
display: block; display: block;
margin: 20px auto; margin: 0 auto;
/* Wyśrodkowanie obrazka */
} }
/* Styl dla trybu ciemnego */ .section__matches
body.dark-mode { {
background: #121212; background-color: white;
color: #e0e0e0; width: 80%;
border-radius: var(--border-radius);
max-width: 1000px;
margin-bottom: 50px;
}
.section__matches h2
{
margin: 0;
border-radius: var(--border-radius);
background-color: var(--barca-gold);
padding: 20px;
text-align: center;
}
.section__matches td:nth-child(1)
{
border-radius: 25px 0 0px 25px;
} }
body.dark-mode nav { .section__matches td:last-child
background: #333; {
} border-radius: 0 25px 25px 0;
}
.section__matches th
{
font-size: 1.3em;
color: var(--barca-red)
}
.section__matches tr
{
transition: 100ms ease;
}
.section__matches tr:hover:has(:not(th))
{
transform: scale(1.05,1.05);
background-color: #00B9BF;
}
body.dark-mode table { /* Styl dla trybu polskiego */
color: #e0e0e0; body.poland-mode {
} background-color: var(--polska-section-color);
}
body.dark-mode h1 { body.poland-mode .navbar {
color: #eaeaea; background: linear-gradient(to bottom, #bd4148, #dc1414);
} }
body.dark-mode header button { body.poland-mode .logo {
background-color: #444; color: var(--polska-white)
color: #eaeaea; }
}
body.dark-mode header button:hover { body.poland-mode .logo-text {
background-color: #666;
}
body.dark-mode ul li {
background-color: #333;
}
body.dark-mode ul li:hover {
background-color: #444;
}
/* Dodatkowe style */
body.dark-mode h1 {
color: #eaeaea;
}
body.dark-mode header button {
background-color: #444;
color: #eaeaea;
}
body.dark-mode header button:hover {
background-color: #666;
}
body.dark-mode ul li {
background-color: #333;
}
body.dark-mode ul li:hover {
background-color: #444;
}
/* Przyciski i elementy */
header button {
padding: 10px 15px;
border: none;
background-color: #007bff;
color: white; color: white;
font-size: 16px;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.3s;
} }
header button:hover { body.poland-mode .nav-links li a {
background-color: #0056b3; color: white;
}
body.poland-mode .nav-links li a:hover {
color: #220000;
background-color: white;
}
body.poland-mode .nav-links li button::before {
background: none;
}
body.poland-mode .nav-links li button {
background-color: red;
box-shadow: 0px 0px 6px 5px rgba(109, 0, 0, 0.219);
position: relative;
overflow: hidden;
}
body.poland-mode .nav-links li button::after {
content: "";
display: block;
position: absolute;
top: 0;
left: 0;
background-color: white;
width: 100%;
height: 50%;
}
body.poland-mode .profile-image img {
width: 100%;
animation: header-content 300ms ease;
background: linear-gradient(rgba(255, 255, 255, 0.534) 50%, rgba(255, 0, 0, 0.551) 50% 100%);
border-radius: var(--border-radius);
}
body.poland-mode .header-content-special {
font-size: 50px;
color: red;
}
body.poland-mode .header-content {
background-color: var(--polska-section-color)
}
body.poland-mode .header-content h1 {
border-bottom-color: red;
}
body.poland-mode .profile-image-cover {
background: linear-gradient(185deg, transparent 40% 60%, var(--polska-section-color) 85%, var(--polska-section-color) 90%);
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
body.poland-mode .hamburger {
color: white;
}
@media (max-width: 768px) {
/* body.poland-mode .nav-links {
background-color: var(--polska-red);
/*Ale oczopląs*/
} }
body.poland-mode .section-stats {
background: linear-gradient(to bottom, #bd4148, #dc1414);
}
body.poland-mode .section-stats h2 {
color: white;
}
body.poland-mode .stat-box {
border-color: white;
background: linear-gradient(to bottom, #ff0000,#231212);
}
body.poland-mode .stat-box h3 {
color: white;
}
body.poland-mode .club-stats h2{
background: linear-gradient(to bottom, #ff0000,#231212);
border: 4px solid white;
color: white;
}
body.poland-mode .about-section {
background-color: var(--polska-section-color);
color: white;
}
body.poland-mode .about-section article h3 {
background-color: var(--polska-red-dark);
color: white;
}
body.poland-mode .about-section article h4 {
background-color: #ffcaca;
color: black;
border-radius: 5px;
}
body.poland-mode .general-stats-section h2 {
background: var(--polska-section-color);
}
body.poland-mode .general-stats-section .grid article:nth-child(1){background-color: var(--polska-red-dark);}
body.poland-mode .general-stats-section .grid article:nth-child(2){background-color: var(--yellow-highlight);}
body.poland-mode .general-stats-section .grid article:nth-child(3){background-color: var(--barca-blue);}
body.poland-mode .about-section-image::after{
background:var(--polska-red-dark);
}
/* Przyciski i elementy */
/* Style dla listy meczów */ /* Style dla listy meczów */
ul { .section-stats {
list-style: none; background: linear-gradient(135deg, #002147, #A50044);
padding: 0; color: white;
border-radius: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
padding: 2rem 2rem;
max-width: 1000px;
margin: 0 auto;
text-align: center;
margin-bottom: 10px;
width: 100%;
}
.section-stats-center
{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
justify-content: center;
}
@media (max-width: 1400px) {
.section-stats-center
{
grid-template-columns: 1fr !important;
}
}
.section-stats h2 {
font-size: 2rem;
margin-bottom: 1rem;
color: var(--barca-red);
} }
ul li { .stats {
background-color: #eaeaea; display: flex;
margin-bottom: 10px; justify-content: space-around;
padding: 10px; text-align: center;
border-radius: 5px; margin-top: 2rem;
transition: background-color 0.3s; gap: 2rem;
} }
ul li:hover { .stat-box {
background-color: #d1d1d1; background-color: rgba(255, 255, 255, 0.1);
} border: 2px solid var(--barca-gold);
color: white;
padding: 2rem;
border-radius: 15px;
width: 160px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
transition: transform 0.3s ease;
}
.stat-box:hover {
transform: scale(1.1, 1.1);
}
.stat-box h3 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
color: var(--barca-gold);
}
.stat-box p {
font-size: 1.1rem;
}
.choose-club
{
margin: 10px;
padding: 20px;
display: flex;
gap: 20px;
justify-content: space-around;
}
.choose-club button {
width: 100px;
border: none;
background-color:transparent;
border-radius: 50%;
}
.choose-club button img {
width: 100%;
height: 100%;
background-color:transparent;
transition: transform 100ms ease;
}
.choose-club button img:hover {
transform: scale(1.3,1.3) translateY(-10px);
}
.club-stats h2{
color: var(--barca-gold);
text-align: center;
border: 5px solid var(--barca-red);
background-color: var(--barca-blue);
padding: 20px;
border-radius: var(--border-radius);
}
.stat-box
{
padding: 20px;
background-color: var(--barca-blue);
display: grid;
grid-template-rows: 1fr 1fr;
text-align: center;
}
.stat-box-special
{text-align: center;
display: block;
font-size: 40px;
color: var(--barca-gold);
}
.club-stats-grid
{
display: grid;
margin-bottom: 50px;
gap: 10px;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
}
select
{
color: white;
padding: 20px;
margin: 10px;
font-size: 24px;
background-color: var(--barca-blue);
border-radius: 10px;
border: 2px solid white;
}

View File

@@ -1,47 +1,77 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="pl"> <html lang="pl">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Lewandowski Stats{% endblock %}</title> <title>{% block title %}Lewandowski Stats{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<meta property="og:title" content="Robert Lewandowski Stats">
<meta property="og:type" content="website">
<meta property="og:url" content="https://lewy.7o7.cx/">
<meta property="og:description" content="Najnowsze informacje o meczach, golach, asystach i innych statystykach">
<meta property="og:image" content="{{ url_for('static', filename='lewandowski.jpg') }}">
<meta property="og:image:height" content="143">
<meta property="og:image:width" content="100">
</head> </head>
<body> <body>
<nav> <header class="base-header">
<a href="/">🏠 Strona główna</a> | <nav class="navbar">
<a href="/mecze">📅 Mecze</a> | <a class="logo-link" href="/"><div class="logo-text">Robert Lewandowski</div></a>
<a href="/statystyki">📊 Statystyki</a> | <ul class="nav-links">
<button id="theme-toggle" onclick="toggleTheme()">🌙 / 🌞</button> <li><a href="/">Strona główna</a></li>
<li><a href="/mecze">Mecze</a></li>
<li><a href="/statystyki">Statystyki</a></li>
<li><a href="/club">Kluby</a></li>
<li><a href="/representation">Reprezentacja</a></li>
<li><a href="/trophies">Trofea</a></li>
<li><a href="/compare">Porównaj</a></li>
<li><button id="theme-toggle" onclick="toggleTheme()"></button></li>
</ul>
<div class="hamburger"></div>
</nav> </nav>
<header> <div class="header-content">
<img src="{{ url_for('static', filename='lewandowski.jpg') }}" alt="Robert Lewandowski" style="width: 200px; height: auto; border-radius: 50%;"> <div class="profile-image">
<h1>Statystyki Roberta Lewandowskiego</h1> <img src="{{ url_for('static', filename='lewandowski_no_bg.png') }}" alt="Robert Lewandowski">
<div class="profile-image-cover"></div>
</div>
<h1>Statystyki <br><span class="header-content-special"> Roberta <br> Lewandowskiego</span></h1>
</div>
<div></div>
</header> </header>
<main> <main>
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
<script>
const hamburger = document.querySelector('.hamburger');
const navLinks = document.querySelector('.nav-links');
hamburger.addEventListener('click', () => {
navLinks.classList.toggle('show');
});
</script>
<script> <script>
function toggleTheme() { function toggleTheme() {
const currentMode = document.body.classList.contains('dark-mode') ? 'dark' : 'light'; const currentMode = document.body.classList.contains('poland-mode') ? 'poland' : 'fcb';
const newMode = currentMode === 'light' ? 'dark' : 'light'; const newMode = currentMode === 'fcb' ? 'poland' : 'fcb';
document.body.classList.toggle('dark-mode'); document.body.classList.toggle('poland-mode');
localStorage.setItem('theme', newMode); localStorage.setItem('theme', newMode);
} }
window.onload = function () { window.onload = function () {
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') { if (savedTheme === 'poland') {
document.body.classList.add('dark-mode'); document.body.classList.add('poland-mode');
} }
} }
</script> </script>
<!--!>Footer<--> <!--!>Footer<-->
<hr/> <hr style='width: 50%' />
{% block footer %}{% endblock %} {% block footer %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -0,0 +1,64 @@
{% extends "base.html" %}
{% block title %}Strona Główna{% endblock %}
{% block content %}
<section class="choose-club">
<a href="{{ url_for('clubs', club='FC Barcelona') }}">
<button><img src="{{ url_for('static', filename='FC_Barcelona.png') }}"></button>
</a>
<a href="{{ url_for('clubs', club='Bayern Monachium') }}">
<button><img src="{{ url_for('static', filename='FC_Bayern.png') }}"></button>
</a>
<a href="{{ url_for('clubs', club='Borussia Dortmund') }}">
<button><img src="{{ url_for('static', filename='Borussia_Dortmund.png') }}"></button>
</a>
<!--Jak nie będzie statysytk dla lecha to usunać-->
<a href="{{ url_for('clubs', club='Lech Poznan') }}">
<button><img src="{{ url_for('static', filename='Lech_Poznan.png') }}"></button>
</a>
</section>
<!-- Wyświetlanie danych tylko dla wybranego klubu -->
{% for stats in clubs %}
{% if stats.club == selected_club %}
<section class="club-stats">
<h2>Statystyki dla {{selected_club}}</h2>
<div class="wybrany{{selected_club}}"></div>
<div class="club-stats-grid">
<div class="stat-box">
<p>Gole:</p> <span class="stat-box-special"> {{ stats.goals }} </span>
</div>
<div class="stat-box">
<p>Asysty:</p> <span class="stat-box-special"> {{ stats.assist }} </span>
</div>
<div class="stat-box">
<p>Występy:</p> <span class="stat-box-special"> {{ stats.goals }} </span>
</div>
<div class="stat-box">
<p>Łączny czas gry:</p> <span class="stat-box-special"> {{ stats.goals }}</span>
</div>
<div class="stat-box">
<p>Hat-tricki:</p> <span class="stat-box-special"> {{ stats.goals }}</span>
</div>
<div class="stat-box">
<p>Zwycięstwa:</p> <span class="stat-box-special"> {{ stats.goals }}</span>
</div>
<div class="stat-box">
<p>Porażki:</p> <span class="stat-box-special"> {{ stats.goals }}</span>
</div>
<div class="stat-box">
<p>Żółte kartki:</p> <span class="stat-box-special"> {{ stats.goals }}</span>
</div>
<div class="stat-box">
<p>Czerwone kartki:</p> <span class="stat-box-special"> {{ stats.goals }}</span>
</div>
</div>
</section>
{% endif %}
{% endfor %}
{% endblock %}
{% block footer %}
{{ commit_in_html | safe }}
{% endblock %}

View File

@@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block title %}Statystyki{% endblock %}
{% block content %}
<select onchange="location = this.value;">
<option disabled selected>Wybierz zawodnika</option>
<option value="{{ url_for('compare', player='Leo Messi') }}">Leo Messi</option>
<option value="{{ url_for('compare', player='Ronaldo') }}">Cristiano Ronaldo</option>
<option value="{{ url_for('compare', player='Neymar') }}">Neymar</option>
</select>
{%for player in player2 %}
{% if player.name == selected_player %}
<section class="section-stats">
<h2>Gole</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ all_time_stats.goals }}</h3>
</div>
<div class="stat-box">
<h3>{{ player.goals}}</h3>
</div>
</div>
<h2>Asysty</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ all_time_stats.assists }}</h3>
</div>
<div class="stat-box">
<h3>{{ player.assists}}</h3>
</div>
</div>
<h2>Wystąpienia</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ all_time_stats.assists }}</h3>
</div>
<div class="stat-box">
<h3>{{ player.assists}}</h3>
</div>
</div>
<h2>Minuty zagrane</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ all_time_stats.assists }}</h3>
</div>
<div class="stat-box">
<h3>{{ player.assists}}</h3>
</div>
</div>
</section>
{% endif %}
{% endfor%}
{% endblock %}

View File

@@ -3,16 +3,59 @@
{% block title %}Strona Główna{% endblock %} {% block title %}Strona Główna{% endblock %}
{% block content %} {% block content %}
<h2>Witaj na stronie poświęconej statystykom Roberta Lewandowskiego!</h2> <div class="main-index">
<p>Tu znajdziesz najnowsze informacje o meczach, golach, asystach i innych statystykach.</p> <h2>Witaj na stronie poświęconej <br> statystykom Roberta Lewandowskiego!</h2>
<div> <p>Tu znajdziesz najnowsze informacje o meczach, golach, asystach i innych statystykach.</p>
<h3>Ogólne statystyki:</h3> <section class="about-section">
<p>Gole: {{ goals }}</p> <article class="article__how-it-works">
<p>Asysty: {{ assists }}</p> <h3>Jak to działa?</h3>
<p>Liczba meczów: {{ matches }}</p> <h4>Pobieranie statystyk</h4>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Ratione harum minus hic, voluptate perspiciatis laborum? Alias maxime, voluptate reprehenderit iusto dolorem officiis porro voluptatibus repellat dicta doloribus, blanditiis similique accusantium.</p>
<h4>Porównywanie zawodników</h4>
<p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Fuga, in perspiciatis. Sequi laborum et animi quas sit voluptatibus alias sed ad molestias nulla vel cum, consectetur commodi odio aliquam officia.</p>
</article>
<div class="about-section-image">
<img src="{{ url_for('static', filename='gigabuła.png') }}">
</div>
<article>
<h3>Mecze</h3>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Soluta ullam iusto ex? Quo amet officia aliquam odio sint harum nam eaque nihil ipsa quos aliquid, illum voluptatum, numquam, magnam omnis?</p>
<a href="/mecze">Zobacz mecze</a>
</article>
<article>
<h3>Statystyki</h3>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Temporibus dolore tenetur nulla sint recusandae illo dolores aspernatur ducimus, omnis vitae ipsam neque animi voluptates eos porro, nihil iusto veniam commodi!</p>
<a href="/statystyki">Zobacz statystyki</a>
</article>
<article>
<h3>Osiągnięcia</h3>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quod dicta veritatis quibusdam eligendi corrupti. Expedita delectus assumenda ipsum illum molestias a voluptates, voluptas quia reprehenderit, quod non, eum veritatis tenetur!</p>
<a href="/club">Zobacz osiągnięcia</a>
</article>
</section>
<!--
<section class="general-stats-section">
<h2>Ogólne statystyki:</h3>
<div class="grid">
<article>
<h3>Gole:</h3>
<p>{{ goals }}</p>
</article>
<article>
<h3>Asysty</h3>
<p>{{ assists }}</p>
</article>
<article>
<h3>Liczba meczów</h3>
<p>{{ matches }}</p>
</article>
</div>
</section>
-->
</div> </div>
{% endblock %} {% endblock %}
{% block footer %} {% block footer %}
{{ commit | safe }} {{ commit_in_html | safe }}
{% endblock %} {% endblock %}

View File

@@ -3,8 +3,21 @@
{% block title %}Lista meczów{% endblock %} {% block title %}Lista meczów{% endblock %}
{% block content %} {% block content %}
<h2>📅 Mecze Roberta</h2> <select>
<table> <option disabled selected>Wybierz rok</option>
<option value="{{ url_for('compare', player='Leo Messi') }}">2024/2025</option>
<option value="{{ url_for('compare', player='Ronaldo') }}">2023/2024</option>
<option value="{{ url_for('compare', player='Neymar') }}">2022/2023</option>
<option value="{{ url_for('compare', player='Neymar') }}">2021/2022</option>
<option value="{{ url_for('compare', player='Neymar') }}">2020/2021</option>
<option value="{{ url_for('compare', player='Neymar') }}">2019/2020</option>
<option value="{{ url_for('compare', player='Neymar') }}">2018/2019</option>
<option value="{{ url_for('compare', player='Neymar') }}">2017/2018</option>
<option value="{{ url_for('compare', player='Neymar') }}">2016/2017</option>
</select>
<section class="section__matches">
<h2>📅 Mecze Roberta</h2>
<table>
<tr> <tr>
<th>Data</th> <th>Data</th>
<th>Przeciwnik</th> <th>Przeciwnik</th>
@@ -21,5 +34,6 @@
<td>{{ match.minutes }}</td> <td>{{ match.minutes }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
</section>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block title %}Statystyki{% endblock %}
{% block content %}
<section class="club-stats club-stats-poland">
<h2>Statystyki w reprezentacji Polski</h2>
<div class="wybrany{{selected_club}}"></div>
<div class="club-stats-grid">
<div class="stat-box">
<p>Gole:</p> <span class="stat-box-special"> {{ nation_stats.goals }} </span>
</div>
<div class="stat-box">
<p>Asysty:</p> <span class="stat-box-special"> {{ nation_stats.assist }} </span>
</div>
<div class="stat-box">
<p>Występy:</p> <span class="stat-box-special"> {{ nation_stats.goals }} </span>
</div>
<div class="stat-box">
<p>Łączny czas gry:</p> <span class="stat-box-special"> {{ nation_stats.goals }}</span>
</div>
<div class="stat-box">
<p>Hat-tricki:</p> <span class="stat-box-special"> {{ nation_stats.goals }}</span>
</div>
<div class="stat-box">
<p>Zwycięstwa:</p> <span class="stat-box-special"> {{ nation_stats.goals }}</span>
</div>
<div class="stat-box">
<p>Porażki:</p> <span class="stat-box-special"> {{ nation_stats.goals }}</span>
</div>
<div class="stat-box">
<p>Żółte kartki:</p> <span class="stat-box-special"> {{ nation_stats.goals }}</span>
</div>
<div class="stat-box">
<p>Czerwone kartki:</p> <span class="stat-box-special"> {{ nation_stats.goals }}</span>
</div>
</div>
</section>
{% endblock %}

View File

@@ -3,10 +3,104 @@
{% block title %}Statystyki{% endblock %} {% block title %}Statystyki{% endblock %}
{% block content %} {% block content %}
<h2>Statystyki Roberta Lewandowskiego</h2> <div class="section-stats-center">
<ul>
<li>Gole: {{ stats.goals }}</li> <section class="section-stats">
<li>Asysty: {{ stats.assists }}</li> <h2>Ogólne statystyki</h2>
<li>Mecze: {{ stats.matches }}</li> <div class="stats">
</ul> <div class="stat-box">
<h3>{{ all_time_stats.goals }}</h3>
<p>Gole</p>
</div>
<div class="stat-box">
<h3>{{ all_time_stats.assists }}</h3>
<p>Asysty</p>
</div>
<div class="stat-box">
<h3>{{ all_time_stats.matches }}</h3>
<p>Występy</p>
</div>
</div>
</section>
<section class="section-stats">
<h2>Klubowe statystyki</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ club_stats.goals }}</h3>
<p>Gole</p>
</div>
<div class="stat-box">
<h3>{{ club_stats.assists }}</h3>
<p>Asysty</p>
</div>
<div class="stat-box">
<h3>{{ club_stats.matches }}</h3>
<p>Występy</p>
</div>
</div>
</section>
<section class="section-stats">
<h2>Reprezentacja statystyki</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ nation_stats.goals }}</h3>
<p>Gole</p>
</div>
<div class="stat-box">
<h3>{{ nation_stats.assists }}</h3>
<p>Asysty</p>
</div>
<div class="stat-box">
<h3>{{ nation_stats.matches }}</h3>
<p>Występy</p>
</div>
</section>
<section class="section-stats">
<h2>Mistrzostwa świata</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ worldcup.goals }}</h3>
<p>Gole</p>
</div>
<div class="stat-box">
<h3>{{ worldcup.assists }}</h3>
<p>Asysty</p>
</div>
<div class="stat-box">
<h3>{{ worldcup.matches }}</h3>
<p>Występy</p>
</div>
</div>
</section>
<section class="section-stats">
<h2>EURO</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ euro.goals }}</h3>
<p>Gole</p>
</div>
<div class="stat-box">
<h3>{{ euro.assists }}</h3>
<p>Asysty</p>
</div>
<div class="stat-box">
<h3>{{ euro.matches }}</h3>
<p>Występy</p>
</div>
</div>
</section>
<section class="section-stats">
<h2>Kartki</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ cards.yellow }}</h3>
<p>Żółte</p>
</div>
<div class="stat-box">
<h3>{{ cards.red }}</h3>
<p>Czerwone</p>
</div>
</div>
</div>
</section>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block title %}Statystyki{% endblock %}
{% block content %}
<section class="section__matches">
<h2>📅 Trofea</h2>
<table>
<tr>
<th>Nazwa</th>
<th>Data/Sezon</th>
</tr>
{% for trophy in trophy %}
<tr>
<td>{{ trophy.name }}</td>
{% if trophy.year == NULL %}
<td>{{ trophy.sezon }}</td>
{% endif %}
{% if trophy.sezon == NULL %}
<td>{{ trophy.year }}</td>
{% endif %}
</tr>
{% endfor %}
</table>
</section>
{% endblock %}

View File

@@ -1,2 +1,7 @@
Flask>=2.2.3 Flask~=2.2.3
gitpython gitpython~=3.1.44
Flask-SQLAlchemy~=3.1.1
psycopg2~=2.9.10
Flask-APScheduler~=1.13.1
requests~=2.32.3
toml~=0.10.2

View File

@@ -1,14 +1,5 @@
""" """
This script runs the FlaskWebProject application using a development server. Please see README.md for more tips on how to get your server running.
""" """
from os import environ print("runserver.py is obsolete. Please run your server with lewy.py.")
from FlaskWebProject import app
if __name__ == '__main__':
HOST = environ.get('SERVER_HOST', 'localhost')
try:
PORT = int(environ.get('SERVER_PORT', '5555'))
except ValueError:
PORT = 5555
app.run(HOST, PORT)

View File

@@ -1,2 +1,56 @@
# lewangoalski # lewangoalski
## Uruchamianie projektu:
- Zacznij od pobrania plików projektu lub sklonuj repozytorium, używając git:
```
git clone https://gitea.7o7.cx/roberteam/lewangoalski.git
```
- Przejdź do katalogu z plikami projektu:
```
cd lewangoalski
```
- Stwórz wirtualne środowisko:
```
python -m venv .venv
```
Powyższe polecenie stworzy ukryty folder o nazwie *.venv*.
- Aby aktywować to wirtualne środowisko należy użyć:
- na Linuxie (bash):
```
source .venv/bin/activate
```
- na Windowsie (cmd):
```
.venv\Scripts\activate
```
- Zainstaluj niezbędne pakiety w świeżo utworzonym środowisku wirtualnym:
```
pip install -r FlaskWebProject\requirements.txt
```
- Wejdź do katalogu ze skryptem:
```
cd FlaskWebProject/FlaskWebProject
```
- Uruchom skrypt:
- w sposób ciągły:
```
python lewy.py
```
- z automatycznym przeładowaniem (kod zostanie przeładowany po zmianie w bazie kodu):
```
flask --app lewy run --debug
```
## Przydatne do rozruchu parametry:
```
python lewy.py -h
```
## Konfiguracja
Przykładowa konfiguracja została umieszczona w pliku config.example.toml. Skopiuj go, a następnie zmień jego nazwę na config.toml, aby móc swobodnie nanieść swoje zmiany.