[Scummvm-git-logs] scummvm-sites integrity -> 10f1459eae5223c9d3db82c348046a9748d8bbf6
sev-
noreply at scummvm.org
Sun Aug 31 14:03:33 UTC 2025
This automated email contains information about 19 new commits which have been
pushed to the 'scummvm-sites' repo located at https://api.github.com/repos/scummvm/scummvm-sites .
Summary:
0743d7f05c INTEGRITY: Fix join query building for engineid search
b843fc8fe1 INTEGRITY: Improve some log messages.
5c0ff6ea72 INTEGRITY: Add button for bulk fileset deletion.
a38df7e868 INTEGRITY: Improve 'Compare Fileset' feature
91b68bf8e3 INTEGRITY: Restructure 'Developer Actions' panel
eecf2bb8e5 INTEGRITY: Fix incorrect field name
0ec5e60889 INTEGRITY: Remove flask rate-limitter
3b257c9e95 INTEGRITY: Add github oauth with role based access
3196da9e58 INTEGRITY: Remove apache basic auth
939b8359f8 INTEGRITY: Get moderator username for logs from user session
2b945af89f INTEGRITY: Update readme with new instructions
736aef2dca INTEGRITY: Add manual user email notificication log button
88dc8ecc2f INTEGRITY: Hide buttons for logged in users with No Access
5df88e5ad7 INTEGRITY: Show Fileset Database title only to logged in users.
05cbfb15b2 INTEGRITY: Add confirmation page before deleting filesets
aa3ee1e807 INTEGRITY: Add confirmation page before deleting files
cce0e68c25 INTEGRITY: Shift clear database button to config page
ab950c3812 INTEGRITY: Hide metadata addition feature for read-only users
10f1459eae INTEGRITY: Update UV files
Commit: 0743d7f05c76cf1b183783587a9604d9f63344f0
https://github.com/scummvm/scummvm-sites/commit/0743d7f05c76cf1b183783587a9604d9f63344f0
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Fix join query building for engineid search
The 'if' block for joining filechecksum table should come under 'elif' block otherwise the 'else' block would always run for t == engine case building an incorrect query.
Changed paths:
src/app/pagination.py
diff --git a/src/app/pagination.py b/src/app/pagination.py
index cd3bfde..e5bc001 100644
--- a/src/app/pagination.py
+++ b/src/app/pagination.py
@@ -91,7 +91,7 @@ def create_page(
from_query += " JOIN engine ON engine.id = game.engine"
else:
from_query += " JOIN game ON game.id = fileset.game JOIN engine ON engine.id = game.engine"
- if t == "filechecksum":
+ elif t == "filechecksum":
from_query += " JOIN file ON file.fileset = fileset.id JOIN filechecksum ON file.id = filechecksum.file"
else:
from_query += (
Commit: b843fc8fe1c5c4a5b3b543e2b1b1562fb43b1f39
https://github.com/scummvm/scummvm-sites/commit/b843fc8fe1c5c4a5b3b543e2b1b1562fb43b1f39
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Improve some log messages.
Changed paths:
src/app/fileset.py
src/scripts/db_functions.py
diff --git a/src/app/fileset.py b/src/app/fileset.py
index 5c66776..069dddb 100644
--- a/src/app/fileset.py
+++ b/src/app/fileset.py
@@ -1589,9 +1589,15 @@ def mark_as_full(id):
try:
conn = db_connect()
with conn.cursor() as cursor:
+ user = f"cli:{getpass.getuser()}"
update_query = "UPDATE fileset SET status = 'full' WHERE id = %s"
cursor.execute(update_query, (id,))
- create_log("Manual from Web", "Dev", f"Marked Fileset:{id} as full", conn)
+ create_log(
+ "Fileset marked full",
+ user,
+ f"Fileset:{id} marked as full by moderator: {user}",
+ conn,
+ )
conn.commit()
except Exception as e:
print(f"Error updating fileset status: {e}")
diff --git a/src/scripts/db_functions.py b/src/scripts/db_functions.py
index 3495fa9..3ef43dd 100644
--- a/src/scripts/db_functions.py
+++ b/src/scripts/db_functions.py
@@ -2514,7 +2514,7 @@ def log_match_with_full(
category_text = "Mismatch with Full set"
if fully_matched:
category_text = "Existing as Full set."
- log_text = f"""Files mismatched with Full Fileset:{candidate_id}. data_path: {relative_path}.Unmatched Files in scan fileset = {len(unmatched_scan_files)}. Unmatched Files in full fileset = {len(unmatched_candidate_files)}. List of unmatched files scan.dat : {", ".join(scan_file for scan_file in unmatched_scan_files)}, List of unmatched files full fileset : {", ".join(scan_file for scan_file in unmatched_candidate_files)}"""
+ log_text = f"""Files mismatched with Full Fileset:{candidate_id}. data_path: {relative_path} Unmatched Files in scan fileset: {len(unmatched_scan_files)} Unmatched Files in full fileset: {len(unmatched_candidate_files)} List of unmatched files scan.dat: {", ".join(scan_file for scan_file in unmatched_scan_files)} List of unmatched files full fileset: {", ".join(scan_file for scan_file in unmatched_candidate_files)}"""
if fully_matched:
log_text = (
f"Fileset matched completely with Full Fileset:{candidate_id}. Dropping."
@@ -2810,7 +2810,7 @@ def user_integrity_check(data, ip, game_metadata=None):
match_text = f"Candidates {', '.join(f'Fileset:{id}' for id in candidate_filesets)}"
if len(candidate_filesets) == 1:
match_text = f"Matched Fileset:{candidate_filesets[0]}"
- log_text = f"Possible new variant Fileset:{user_fileset_id} from user. {match_text}"
+ log_text = f"Possible new variant Fileset:{user_fileset_id} from user. {match_text}. Match count: 1."
create_log(
category_text,
user,
Commit: 5c0ff6ea7287a18342d200a1285b0f6af3e8ba60
https://github.com/scummvm/scummvm-sites/commit/5c0ff6ea7287a18342d200a1285b0f6af3e8ba60
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Add button for bulk fileset deletion.
Changed paths:
src/app/fileset.py
src/app/pagination.py
src/scripts/db_functions.py
static/navbar_string.html
diff --git a/src/app/fileset.py b/src/app/fileset.py
index 069dddb..8b0d1f7 100644
--- a/src/app/fileset.py
+++ b/src/app/fileset.py
@@ -1842,15 +1842,13 @@ def logs():
"text": "log",
}
logs_per_page = get_logs_per_page()
- return render_template_string(
- create_page(
- filename, logs_per_page, records_table, select_query, order, filters
- )
+ render_html_string, _, _ = create_page(
+ filename, logs_per_page, records_table, select_query, order, filters
)
+ return render_template_string(render_html_string)
- at app.route("/fileset_search")
-def fileset_search():
+def get_fileset_search_details():
filename = "fileset_search"
records_table = "fileset"
select_query = """
@@ -1882,19 +1880,78 @@ def fileset_search():
"file.id": "filechecksum.file",
}
filesets_per_page = get_filesets_per_page()
- return render_template_string(
- create_page(
- filename,
- filesets_per_page,
- records_table,
- select_query,
- order,
- filters,
- mapping,
- )
+
+ return (
+ filename,
+ records_table,
+ select_query,
+ order,
+ filters,
+ mapping,
+ filesets_per_page,
)
+ at app.route("/fileset_search")
+def fileset_search():
+ (
+ filename,
+ records_table,
+ select_query,
+ order,
+ filters,
+ mapping,
+ filesets_per_page,
+ ) = get_fileset_search_details()
+ render_html_string, _, _ = create_page(
+ filename,
+ filesets_per_page,
+ records_table,
+ select_query,
+ order,
+ filters,
+ mapping,
+ )
+ return render_template_string(render_html_string)
+
+
+ at app.route("/delete_filtered_filesets", methods=["GET"])
+def delete_filtered_filesets():
+ (
+ filename,
+ records_table,
+ select_query,
+ order,
+ filters,
+ mapping,
+ filesets_per_page,
+ ) = get_fileset_search_details()
+ _, select_query, condition = create_page(
+ filename,
+ filesets_per_page,
+ records_table,
+ select_query,
+ order,
+ filters,
+ mapping,
+ )
+ query = f"{select_query} {condition}"
+ connection = db_connect()
+ with connection.cursor() as cursor:
+ cursor.execute(query)
+ filtered_filesets = cursor.fetchall()
+ filtered_filesets_id = [f["fileset"] for f in filtered_filesets]
+ placeholders = ",".join(["%s"] * len(filtered_filesets_id))
+ cursor.execute(
+ f"DELETE FROM fileset WHERE id IN ({placeholders})", filtered_filesets_id
+ )
+ user = f"cli:{getpass.getuser()}"
+ log_text = f"{len(filtered_filesets_id)} filesets deleted by moderator: {user}."
+ create_log("Filesets Deleted", user, log_text, connection)
+ connection.commit()
+ return redirect("/logs")
+
+
@app.route("/email_notification/<int:fileset_id>", methods=["GET"])
def email_notification(fileset_id):
connection = db_connect()
diff --git a/src/app/pagination.py b/src/app/pagination.py
index e5bc001..dc968cd 100644
--- a/src/app/pagination.py
+++ b/src/app/pagination.py
@@ -138,6 +138,12 @@ def create_page(
with open(navbar_path, "r") as f:
html = f.read()
+ if records_table != "fileset":
+ html = html.replace(
+ '<button type="submit">Delete Filtered Filesets</button>',
+ '<button type="submit" style="display:none;" disabled>Delete Filtered Filesets</button>',
+ )
+
# Generate HTML
html += """
<form id='filters-form' method='GET' onsubmit='remove_empty_inputs()'>
@@ -319,4 +325,4 @@ def create_page(
html += "<input type='submit' value='Submit'>"
html += "</div></form>"
- return html
+ return html, select_query, condition
diff --git a/src/scripts/db_functions.py b/src/scripts/db_functions.py
index 3ef43dd..1130ab3 100644
--- a/src/scripts/db_functions.py
+++ b/src/scripts/db_functions.py
@@ -324,12 +324,6 @@ def add_all_equal_checksums(checksize, checktype, checksum, file_id, conn):
)
-def delete_filesets(conn):
- query = "DELETE FROM fileset WHERE `delete` = TRUE"
- with conn.cursor() as cursor:
- cursor.execute(query)
-
-
def create_log(category, user, text, conn):
with conn.cursor() as cursor:
try:
diff --git a/static/navbar_string.html b/static/navbar_string.html
index 2a80d9e..03c80d3 100644
--- a/static/navbar_string.html
+++ b/static/navbar_string.html
@@ -1,11 +1,59 @@
<!DOCTYPE html>
- <html>
- <head>
- <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
- <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
- <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
- </head>
- <body>
+<html>
+
+<head>
+ <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
+ <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
+ <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
+ <style>
+ .dev {
+ background-color: #fafeff;
+ padding: 10px;
+ border-radius: 5px;
+ margin-left: auto;
+ }
+
+ button {
+ background-color: #d9534f;
+ color: white;
+ padding: 10px 20px;
+ font-size: 16px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background-color 0.3s, box-shadow 0.3s;
+ }
+
+ button:hover {
+ background-color: #c9302c;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ }
+
+ @media (max-width: 768px) {
+ .fileset_database {
+ font-size: 48px;
+ }
+ }
+
+ @media (max-width: 480px) {
+ .fileset_database {
+ font-size: 32px;
+ }
+
+ nav {
+ padding: 10px;
+ }
+
+ .nav-buttons a {
+ margin-bottom: 5px;
+ display: block;
+ text-align: center;
+ }
+ }
+ </style>
+</head>
+
+<body>
<nav>
<div class="logo">
<a href="{{ url_for('home') }}">
@@ -19,4 +67,12 @@
<a href="{{ url_for('logs') }}">Logs</a>
<a href="{{ url_for('config') }}">Config</a>
</div>
+ <div class="dev">
+ <form onsubmit="return confirm('Are you sure you want to delete the filtered filesets?');" action="{{ url_for('delete_filtered_filesets') }}" method="GET">
+ {% for key, value in request.args.items() %}
+ <input type="hidden" name="{{ key }}" value="{{ value }}">
+ {% endfor %}
+ <button type="submit">Delete Filtered Filesets</button>
+ </form>
+ </div>
</nav>
Commit: a38df7e86846710fb13020b9217672fc21b545d7
https://github.com/scummvm/scummvm-sites/commit/a38df7e86846710fb13020b9217672fc21b545d7
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Improve 'Compare Fileset' feature
The filtering for searching filesets for comparing is now done using the fileset search page, with additional button 'Compare' for redirecting to the merge page.
Changed paths:
src/app/fileset.py
src/app/pagination.py
diff --git a/src/app/fileset.py b/src/app/fileset.py
index 8b0d1f7..21e21a8 100644
--- a/src/app/fileset.py
+++ b/src/app/fileset.py
@@ -154,7 +154,7 @@ def fileset():
"""
if old_id is not None:
html += f"""<h3><u>Redirected from Fileset: {old_id}</u></h3>"""
- html += f"<button type='button' onclick=\"location.href='/fileset/{id}/merge'\">Manual Merge</button>"
+ html += f"<button type='button' onclick=\"location.href='/fileset/{id}/merge'\">Compare Filesets</button>"
html += f"""
<form action="/fileset/{id}/mark_full" method="post" style="display:inline;">
<button type='submit'>Mark as full</button>
@@ -836,111 +836,8 @@ def update_fileset(id):
@app.route("/fileset/<int:id>/merge", methods=["GET", "POST"])
def merge_fileset(id):
- if request.method == "POST":
- search_query = request.form["search"]
-
- connection = db_connect()
-
- try:
- with connection.cursor() as cursor:
- query = f"""
- SELECT
- fs.*,
- g.name AS game_name,
- g.engine AS game_engine,
- g.platform AS game_platform,
- g.language AS game_language,
- g.extra AS extra
- FROM
- fileset fs
- LEFT JOIN
- game g ON fs.game = g.id
- WHERE g.name LIKE '%{search_query}%' OR g.platform LIKE '%{search_query}%' OR g.language LIKE '%{search_query}%'
- """
- cursor.execute(query)
- results = cursor.fetchall()
-
- html = f"""
- <!DOCTYPE html>
- <html>
- <head>
- <link rel="stylesheet" type="text/css" href="{{{{ url_for('static', filename='style.css') }}}}">
- <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
- <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
- </head>
- <body>
- <nav>
- <div class="logo">
- <a href="{{{{ url_for('home') }}}}">
- <img src="{{{{ url_for('static', filename='integrity_service_logo_256.png') }}}}" alt="Logo">
- </a>
- </div>
- <div class="nav-buttons">
- <a href="{{{{ url_for('user_games_list') }}}}">User Games List</a>
- <a href="{{{{ url_for('ready_for_review') }}}}">Ready for review</a>
- <a href="{{{{ url_for('fileset_search') }}}}">Fileset Search</a>
- <a href="{{{{ url_for('logs') }}}}">Logs</a>
- <a href="{{{{ url_for('config') }}}}">Config</a>
- </div>
- </nav>
- <h2 style="margin-top: 80px;">Search Results for '{search_query}'</h2>
- <form method="POST">
- <input type="text" name="search" placeholder="Search fileset">
- <input type="submit" value="Search">
- </form>
- <table>
- <tr><th>ID</th><th>Game Name</th><th>Platform</th><th>Language</th><th>Extra</th><th>Action</th></tr>
- """
- for result in results:
- html += f"""
- <tr>
- <td>{result["id"]}</td>
- <td>{result["game_name"]}</td>
- <td>{result["game_platform"]}</td>
- <td>{result["game_language"]}</td>
- <td>{result["extra"]}</td>
- <td><a href="/fileset/{id}/merge/confirm?target_id={result["id"]}">Select</a></td>
- </tr>
- """
- html += "</table>\n"
- html += "</body>\n</html>"
-
- return render_template_string(html)
-
- finally:
- connection.close()
-
- return """
- <!DOCTYPE html>
- <html>
- <head>
- <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
- <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
- <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
- </head>
- <body>
- <nav>
- <div class="logo">
- <a href="{{ url_for('home') }}">
- <img src="{{ url_for('static', filename='integrity_service_logo_256.png') }}" alt="Logo">
- </a>
- </div>
- <div class="nav-buttons">
- <a href="{{ url_for('user_games_list') }}">User Games List</a>
- <a href="{{ url_for('ready_for_review') }}">Ready for review</a>
- <a href="{{ url_for('fileset_search') }}">Fileset Search</a>
- <a href="{{ url_for('logs') }}">Logs</a>
- <a href="{{ url_for('config') }}">Config</a>
- </div>
- </nav>
- <h2 style="margin-top: 80px;">Search Fileset to Merge</h2>
- <form method="POST">
- <input type="text" name="search" placeholder="Search fileset">
- <input type="submit" value="Search">
- </form>
- </body>
- </html>
- """
+ url = f"/fileset_search?source_id={id}"
+ return redirect(url)
@app.route("/fileset/<int:id>/possible_merge", methods=["GET", "POST"])
@@ -1296,7 +1193,7 @@ def confirm_merge(id):
# For matched_files, files is a tuple of filename from source file and target file
# For unmatched_files, files is the filename of the files that was not common.
for files in file_category:
- if is_common_file:
+ if is_common_file and len(matched_files) != 0:
(target_filename, source_filename) = files
# Also remove common files from source and target filenames set
@@ -1325,7 +1222,11 @@ def confirm_merge(id):
keys = sorted(set(source_dict.keys()) | set(target_dict.keys()))
- tr_class = "matched" if is_common_file else "unmatched"
+ tr_class = (
+ "matched"
+ if (is_common_file and len(matched_files) != 0)
+ else "unmatched"
+ )
html += f"""<tr class="{tr_class}">
<td colspan='3'>
<strong>{source_filename}</strong> {" - mac_file" if is_mac_file else ""}
@@ -1369,9 +1270,14 @@ def confirm_merge(id):
and source_filename.lower() in detection_files_set
):
is_detection = "1"
- fname = source_to_target_matched_map[
+ fname = (
source_filename.lower()
- ]
+ if source_filename.lower()
+ not in source_to_target_matched_map
+ else source_to_target_matched_map[
+ source_filename.lower()
+ ]
+ )
detection_type = target_files_map[fname].get(
"detection_type", ""
)
diff --git a/src/app/pagination.py b/src/app/pagination.py
index dc968cd..343cdb5 100644
--- a/src/app/pagination.py
+++ b/src/app/pagination.py
@@ -62,9 +62,10 @@ def create_page(
with conn.cursor() as cursor:
tables = set()
where_clauses = []
+ compare_fileset_source_id = request.args.get("source_id", "")
for key, value in request.args.items():
- if key in ("page", "sort") or value == "":
+ if key in ("page", "sort", "source_id") or value == "":
continue
tables.add(filters[key])
col = f"{filters[key]}.{'id' if key == 'fileset' else key}"
@@ -138,7 +139,7 @@ def create_page(
with open(navbar_path, "r") as f:
html = f.read()
- if records_table != "fileset":
+ if records_table != "fileset" or compare_fileset_source_id != "":
html = html.replace(
'<button type="submit">Delete Filtered Filesets</button>',
'<button type="submit" style="display:none;" disabled>Delete Filtered Filesets</button>',
@@ -181,7 +182,6 @@ def create_page(
width = get_width(name, default)
html += f"<col style='width: {width}%;'>"
html += "</colgroup>"
-
if filters:
html += """<tr class='filter'><td class='filter'><input type='submit' value='Submit'></td>"""
for key in filters.keys():
@@ -245,6 +245,13 @@ def create_page(
</a>
</th>"""
+ if compare_fileset_source_id != "":
+ html += """<th>
+ <div style="display:flex; align-items:center; width:100%;">
+ <span style="flex:1; text-align:center;">Action</span>
+ </div>
+ </th>"""
+
if results:
counter = offset + 1
for row in results:
@@ -284,6 +291,8 @@ def create_page(
)
html += f"<td>{'' if value is None else value}</td>\n"
+ if compare_fileset_source_id != "":
+ html += f"""<td><a href="/fileset/{compare_fileset_source_id}/merge/confirm?target_id={fileset_id}">Compare</a></td>"""
html += "</tr>\n"
counter += 1
Commit: 91b68bf8e3f455f544a04b2eda00cba1394b6de0
https://github.com/scummvm/scummvm-sites/commit/91b68bf8e3f455f544a04b2eda00cba1394b6de0
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Restructure 'Developer Actions' panel
-Shift 'Compare Filesets' and 'Mark Fileset Full' buttons to developer actions
-Add confirmation window for marking fileset full
Changed paths:
src/app/fileset.py
diff --git a/src/app/fileset.py b/src/app/fileset.py
index 21e21a8..0ad96b9 100644
--- a/src/app/fileset.py
+++ b/src/app/fileset.py
@@ -154,13 +154,6 @@ def fileset():
"""
if old_id is not None:
html += f"""<h3><u>Redirected from Fileset: {old_id}</u></h3>"""
- html += f"<button type='button' onclick=\"location.href='/fileset/{id}/merge'\">Compare Filesets</button>"
- html += f"""
- <form action="/fileset/{id}/mark_full" method="post" style="display:inline;">
- <button type='submit'>Mark as full</button>
- </form>
- """
-
cursor.execute(
"SELECT fileset FROM history WHERE oldfileset = %s AND oldfileset != fileset",
(id,),
@@ -171,6 +164,32 @@ def fileset():
cursor.execute("SELECT status FROM fileset WHERE id = %s", (id,))
status = cursor.fetchone()["status"]
+ # -------------------------------------------------------------------------------------------------
+ # developer actions
+ # -------------------------------------------------------------------------------------------------
+
+ html += "<h3>Developer Actions</h3>"
+
+ # Compare Fileset
+ html += f"<button type='button' onclick=\"location.href='/fileset/{id}/merge'\">Compare Filesets</button>"
+
+ # Mark fileset full
+ if status != "full":
+ html += f"""
+ <form action="/fileset/{id}/mark_full" method="post" onsubmit="return confirm('Are you sure you want to mark the fileset as full?');">
+ <button type='submit' style="margin-left: 10px;">Mark as full</button>
+ </form>
+ """
+
+ # Delete a fileset
+ html += f"""<form action="{url_for("delete_fileset", id=id)}" method="POST" onsubmit="return confirm('Are you sure you want to delete the fileset?');">"""
+ html += "<button type='submit' style='margin-left: 10px;'>Delete the Fileset</button>"
+ html += "</form>"
+
+ # -------------------------------------------------------------------------------------------------
+ # metadata
+ # -------------------------------------------------------------------------------------------------
+
if status == "dat":
cursor.execute(
"""SELECT id, game, status, src, `key`, timestamp, set_dat_metadata FROM fileset WHERE id = %s""",
@@ -462,16 +481,6 @@ def fileset():
html += """<input style="margin-left: 10px;" type="submit" name="action" value="Delete Selected Files">"""
html += "</form>\n"
- # -------------------------------------------------------------------------------------------------
- # developer actions
- # -------------------------------------------------------------------------------------------------
-
- # Generate the HTML for the developer actions
- html += "<h3>Developer Actions</h3>"
- html += f"""<form action="{url_for("delete_fileset", id=id)}" method="POST" onsubmit="return confirm('Are you sure you want to delete the fileset?');">"""
- html += "<button type='submit'>Delete the Fileset</button>"
- html += "</form>"
-
# -------------------------------------------------------------------------------------------------
# logs
# -------------------------------------------------------------------------------------------------
Commit: eecf2bb8e51a39263215bcf0e333f8a959fd8169
https://github.com/scummvm/scummvm-sites/commit/eecf2bb8e51a39263215bcf0e333f8a959fd8169
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Fix incorrect field name
Changed paths:
src/app/fileset.py
diff --git a/src/app/fileset.py b/src/app/fileset.py
index 0ad96b9..49060d8 100644
--- a/src/app/fileset.py
+++ b/src/app/fileset.py
@@ -659,7 +659,8 @@ def files_action(id):
"checksum",
"detection",
"detection_type",
- "timestampmodification-time",
+ "timestamp",
+ "modification-time",
"language",
"md5-0",
"md5-1M",
Commit: 0ec5e6088926e264a46b74e488408ecc7ef2b1ad
https://github.com/scummvm/scummvm-sites/commit/0ec5e6088926e264a46b74e488408ecc7ef2b1ad
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Remove flask rate-limitter
Changed paths:
requirements.txt
src/app/fileset.py
diff --git a/requirements.txt b/requirements.txt
index 6547c1e..8340e26 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,4 +16,3 @@ pytest
setuptools
Werkzeug
wheel
-Flask-Limiter
diff --git a/src/app/fileset.py b/src/app/fileset.py
index 49060d8..d68cf42 100644
--- a/src/app/fileset.py
+++ b/src/app/fileset.py
@@ -29,18 +29,10 @@ from src.utils.db_config import db_connect, db_connect_root
from collections import defaultdict
from src.scripts.schema import init_database
from src.app.validate_user_payload import validate_user_payload
-from flask_limiter import Limiter
-from flask_limiter.util import get_remote_address
from src.utils.cookie import get_filesets_per_page, get_logs_per_page
from src.utils.db_config import STATIC_DIR, TEMPLATES_DIR
app = Flask(__name__, static_folder=STATIC_DIR, template_folder=TEMPLATES_DIR)
-limiter = Limiter(
- get_remote_address,
- app=app,
- default_limits=[],
- storage_uri="memory://",
-)
secret_key = os.urandom(24)
@@ -1636,7 +1628,6 @@ def config():
@app.route("/validate", methods=["POST"])
- at limiter.limit("3 per minute")
def validate():
error_codes = {
"unknown": -1,
Commit: 3b257c9e959304033806b2781799f779ec9639ba
https://github.com/scummvm/scummvm-sites/commit/3b257c9e959304033806b2781799f779ec9639ba
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Add github oauth with role based access
-Following roles have been added - Admin, Moderators and Read Only.
-Extra features for Admins is deleting the database as well as deleting multiple filtered filesets in bulk.
-Moderators can merge filesets, delete/update filesets, delete/update files, mark filesets as full.
-Users with Read Only access have view access and a feature to compare filesets.
Changed paths:
A .env.example
A src/app/auth/__init__.py
A src/app/auth/github_oauth.py
A src/app/auth/helper.py
A src/app/auth/role_based_auth.py
A src/app/env_loader.py
app.wsgi
requirements.txt
src/app/fileset.py
static/navbar_string.html
templates/home.html
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..672f85d
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,4 @@
+GITHUB_ORG=ScummVM
+GITHUB_CLIENT_ID=github_client_id_from_oauth_app
+GITHUB_CLIENT_SECRET=github_client_secret_from_oauth_app
+FLASK_SECRET_KEY=any_random_key
diff --git a/app.wsgi b/app.wsgi
index 8604033..9a9b19e 100644
--- a/app.wsgi
+++ b/app.wsgi
@@ -1,9 +1,12 @@
import sys
+import os
import logging
-sys.path.insert(0, "/home/ubuntu/projects/python/scummvm_sites_2025/scummvm-sites")
+project_root = os.path.dirname(os.path.abspath(__file__))
+if project_root not in sys.path:
+ sys.path.insert(0, project_root)
-from src.app.fileset import app as application
+from src.app.fileset import app as application # noqa
logging.basicConfig(stream=sys.stderr)
sys.stderr = sys.stdout
diff --git a/requirements.txt b/requirements.txt
index 8340e26..4ff1860 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,3 +16,7 @@ pytest
setuptools
Werkzeug
wheel
+authlib
+python-dotenv
+requests
+Flask-Dance
diff --git a/src/app/auth/__init__.py b/src/app/auth/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/auth/github_oauth.py b/src/app/auth/github_oauth.py
new file mode 100644
index 0000000..81c8453
--- /dev/null
+++ b/src/app/auth/github_oauth.py
@@ -0,0 +1,25 @@
+from authlib.integrations.flask_client import OAuth
+import os
+
+GITHUB_ORG = os.environ.get("GITHUB_ORG")
+TEAM_ROLES = ["integrity-admins", "integrity-devs", "integrity-ro"]
+
+
+def init_oauth(app):
+ oauth = OAuth(app)
+
+ oauth.register(
+ name="github",
+ client_id=os.environ.get("GITHUB_CLIENT_ID"),
+ client_secret=os.environ.get("GITHUB_CLIENT_SECRET"),
+ access_token_url="https://github.com/login/oauth/access_token",
+ access_token_params=None,
+ authorize_url="https://github.com/login/oauth/authorize",
+ authorize_params=None,
+ api_base_url="https://api.github.com/",
+ client_kwargs={
+ "scope": "read:user read:org",
+ },
+ )
+
+ return oauth
diff --git a/src/app/auth/helper.py b/src/app/auth/helper.py
new file mode 100644
index 0000000..e069a49
--- /dev/null
+++ b/src/app/auth/helper.py
@@ -0,0 +1,27 @@
+import getpass
+from flask import session
+
+
+def get_current_user():
+ user = f"cli:{getpass.getuser()}"
+ return user
+
+
+def get_user_role():
+ user_role = session.get("user", {}).get("role", "No Access")
+ return user_role
+
+
+def get_username():
+ username = session.get("user", {}).get("username", "")
+ return username
+
+
+def is_moderator_access():
+ """
+ Returns true if there is more than read only access, i.e its either admin or moderator.
+ """
+ user_role = session.get("user", {}).get("role", "")
+ if user_role in ["Admin", "Moderator"]:
+ return True
+ return False
diff --git a/src/app/auth/role_based_auth.py b/src/app/auth/role_based_auth.py
new file mode 100644
index 0000000..34c7ee3
--- /dev/null
+++ b/src/app/auth/role_based_auth.py
@@ -0,0 +1,25 @@
+from functools import wraps
+from flask import session, redirect, url_for, abort
+
+
+def role_required(*roles):
+ """
+ Decorator Usage: @role_required("Admin", "Moderator", "Read Only")
+ """
+
+ def wrapper(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if "user" not in session:
+ return redirect(url_for("home"))
+
+ user = session.get("user")
+ user_role = user["role"]
+
+ if user_role not in roles:
+ abort(403)
+ return f(*args, **kwargs)
+
+ return decorated_function
+
+ return wrapper
diff --git a/src/app/env_loader.py b/src/app/env_loader.py
new file mode 100644
index 0000000..5929937
--- /dev/null
+++ b/src/app/env_loader.py
@@ -0,0 +1,11 @@
+from pathlib import Path
+from dotenv import load_dotenv
+import sys
+import os
+
+project_root = Path(__file__).resolve().parents[2]
+if project_root not in sys.path:
+ sys.path.insert(0, project_root)
+
+env_path = os.path.join(project_root, ".env")
+load_dotenv(dotenv_path=env_path)
diff --git a/src/app/fileset.py b/src/app/fileset.py
index d68cf42..33e30f2 100644
--- a/src/app/fileset.py
+++ b/src/app/fileset.py
@@ -7,13 +7,20 @@ from flask import (
jsonify,
render_template,
make_response,
+ session,
)
+
+
+import requests
+from datetime import timedelta
import json
import html as html_lib
import os
import getpass
from src.app.pagination import create_page
import difflib
+
+import src.app.env_loader # noqa
from src.scripts.db_functions import (
insert_game,
get_all_related_filesets,
@@ -31,15 +38,20 @@ from src.scripts.schema import init_database
from src.app.validate_user_payload import validate_user_payload
from src.utils.cookie import get_filesets_per_page, get_logs_per_page
from src.utils.db_config import STATIC_DIR, TEMPLATES_DIR
+from src.app.auth.github_oauth import init_oauth, GITHUB_ORG, TEAM_ROLES
+from src.app.auth.role_based_auth import role_required
+from src.app.auth.helper import (
+ get_user_role,
+ get_current_user,
+ get_username,
+ is_moderator_access,
+)
app = Flask(__name__, static_folder=STATIC_DIR, template_folder=TEMPLATES_DIR)
-
-secret_key = os.urandom(24)
-
-
-def get_current_user():
- user = f"cli:{getpass.getuser()}"
- return user
+app.secret_key = os.environ.get("FLASK_SECRET_KEY")
+app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=7)
+oauth = init_oauth(app)
+github = oauth.github
@app.route("/")
@@ -47,12 +59,58 @@ def index():
return redirect(url_for("logs"))
+ at app.route("/login")
+def login():
+ redirect_uri = url_for("authorize", _external=True)
+ return github.authorize_redirect(
+ redirect_uri, prompt="select_account", allow_signup="false"
+ )
+
+
+ at app.route("/logout")
+def logout():
+ session.pop("user", None)
+ return redirect("/home")
+
+
+ at app.route("/authorize")
+def authorize():
+ token = github.authorize_access_token()
+ access_token = token["access_token"]
+ headers = {"Authorization": f"Bearer {access_token}"}
+
+ user_resp = requests.get("https://api.github.com/user", headers=headers)
+ user_data = user_resp.json()
+ username = user_data["login"]
+
+ role = "No Access"
+ for team_name in TEAM_ROLES:
+ team_url = f"https://api.github.com/orgs/{GITHUB_ORG}/teams/{team_name}/memberships/{username}"
+
+ team_resp = requests.get(team_url, headers=headers)
+ if team_resp.status_code == 200:
+ if team_name == "integrity-devs":
+ role = "Moderator"
+ if team_name == "integrity-admins":
+ role = "Admin"
+ if team_name == "integrity-ro":
+ role = "Read Only"
+ break
+
+ session["user"] = {"username": username, "role": role}
+
+ return redirect("/home")
+
+
@app.route("/home")
def home():
- return render_template("home.html")
+ user_role = get_user_role()
+ username = get_username()
+ return render_template("home.html", user_role=user_role, username=username)
@app.route("/clear_database", methods=["POST"])
+ at role_required("Admin")
def clear_database():
try:
(conn, db_name) = db_connect_root()
@@ -71,6 +129,7 @@ def clear_database():
@app.route("/fileset", methods=["GET", "POST"])
+ at role_required("Admin", "Moderator", "Read Only")
def fileset():
id = request.args.get("id", default=1, type=int)
old_id = request.args.get("redirected_from", default=None, type=int)
@@ -157,26 +216,30 @@ def fileset():
status = cursor.fetchone()["status"]
# -------------------------------------------------------------------------------------------------
- # developer actions
+ # Compare Filesets
# -------------------------------------------------------------------------------------------------
- html += "<h3>Developer Actions</h3>"
-
# Compare Fileset
html += f"<button type='button' onclick=\"location.href='/fileset/{id}/merge'\">Compare Filesets</button>"
- # Mark fileset full
- if status != "full":
- html += f"""
- <form action="/fileset/{id}/mark_full" method="post" onsubmit="return confirm('Are you sure you want to mark the fileset as full?');">
- <button type='submit' style="margin-left: 10px;">Mark as full</button>
- </form>
- """
-
- # Delete a fileset
- html += f"""<form action="{url_for("delete_fileset", id=id)}" method="POST" onsubmit="return confirm('Are you sure you want to delete the fileset?');">"""
- html += "<button type='submit' style='margin-left: 10px;'>Delete the Fileset</button>"
- html += "</form>"
+ # -------------------------------------------------------------------------------------------------
+ # developer actions
+ # -------------------------------------------------------------------------------------------------
+ if is_moderator_access():
+ html += "<h3>Developer Actions</h3>"
+
+ # Mark fileset full
+ if status != "full":
+ html += f"""
+ <form action="/fileset/{id}/mark_full" method="post" onsubmit="return confirm('Are you sure you want to mark the fileset as full?');">
+ <button type='submit'>Mark as full</button>
+ </form>
+ """
+
+ # Delete a fileset
+ html += f"""<form action="{url_for("delete_fileset", id=id)}" method="POST" onsubmit="return confirm('Are you sure you want to delete the fileset?');">"""
+ html += "<button type='submit' style='margin-left: 10px;'>Delete the Fileset</button>"
+ html += "</form>"
# -------------------------------------------------------------------------------------------------
# metadata
@@ -351,7 +414,8 @@ def fileset():
if not (
not result["game"] and (status == "user" or status == "ReadyForReview")
):
- html += "<button type='submit' name='action' value='update_metadata'>Update metadata</button>"
+ if is_moderator_access():
+ html += "<button type='submit' name='action' value='update_metadata'>Update metadata</button>"
html += "</form>"
# -------------------------------------------------------------------------------------------------
@@ -435,7 +499,8 @@ def fileset():
# Generate table header
html += "<tr>\n"
html += "<th/>" # Numbering column
- html += "<th>delete</th>" # Checkbox column
+ if is_moderator_access():
+ html += "<th>delete</th>" # Checkbox column
sortable_columns = share_columns + list(temp_set)
for column in sortable_columns:
@@ -453,7 +518,8 @@ def fileset():
for row in result:
html += "<tr>\n"
html += f"<td>{counter}.</td>\n"
- html += f"<td><input type='checkbox' name='file_ids' value='{row['id']}' /></td>\n" # Checkbox for selecting file
+ if is_moderator_access():
+ html += f"<td><input type='checkbox' name='file_ids' value='{row['id']}' /></td>\n" # Checkbox for selecting file
for column in all_columns:
if column != "id":
value = row.get(column, "")
@@ -469,8 +535,9 @@ def fileset():
counter += 1
html += "</table>\n"
- html += """<input type="submit" name="action" value="Update Files">"""
- html += """<input style="margin-left: 10px;" type="submit" name="action" value="Delete Selected Files">"""
+ if is_moderator_access():
+ html += """<input type="submit" name="action" value="Update Files">"""
+ html += """<input style="margin-left: 10px;" type="submit" name="action" value="Delete Selected Files">"""
html += "</form>\n"
# -------------------------------------------------------------------------------------------------
@@ -576,8 +643,10 @@ def fileset():
html += """
<h3 style="margin-top: 30px;">Possible Merges</h3>
<table>
- <tr><th>ID</th><th>Game Name</th><th>Platform</th><th>Language</th><th>Extra</th><th>Details</th><th>Action</th></tr>
+ <tr><th>ID</th><th>Game Name</th><th>Platform</th><th>Language</th><th>Extra</th><th>Details</th>
"""
+ if is_moderator_access():
+ html += "<th>Action</th>"
for result in results:
html += f"""
<tr>
@@ -587,9 +656,10 @@ def fileset():
<td>{result["game_language"]}</td>
<td>{result["extra"]}</td>
<td><a href="/fileset?id={result["id"]}">View Details</a></td>
- <td><a href="/fileset/{id}/merge/confirm?target_id={result["id"]}">Merge</a></td>
- </tr>
"""
+ if is_moderator_access():
+ f"""<td><a href="/fileset/{id}/merge/confirm?target_id={result["id"]}">Merge</a></td>"""
+ html += "</tr>"
html += "</table>\n"
html += "<script src='{{ url_for('static', filename='js/track_metadata_update.js') }}'></script>"
return render_template_string(html)
@@ -598,6 +668,7 @@ def fileset():
@app.route("/fileset/delete/<int:id>", methods=["POST"])
+ at role_required("Admin", "Moderator")
def delete_fileset(id):
connection = db_connect()
with connection.cursor() as cursor:
@@ -611,6 +682,7 @@ def delete_fileset(id):
@app.route("/files_action/<int:id>", methods=["POST"])
+ at role_required("Admin", "Moderator")
def files_action(id):
action = request.form.get("action")
if action == "Delete Selected Files":
@@ -725,6 +797,7 @@ def files_action(id):
@app.route("/fileset/<int:id>/update", methods=["POST"])
+ at role_required("Admin", "Moderator")
def update_fileset(id):
connection = db_connect()
try:
@@ -837,12 +910,14 @@ def update_fileset(id):
@app.route("/fileset/<int:id>/merge", methods=["GET", "POST"])
+ at role_required("Admin", "Moderator", "Read Only")
def merge_fileset(id):
url = f"/fileset_search?source_id={id}"
return redirect(url)
@app.route("/fileset/<int:id>/possible_merge", methods=["GET", "POST"])
+ at role_required("Admin", "Moderator")
def possible_merge_filesets(id):
connection = db_connect()
@@ -961,6 +1036,7 @@ def get_file_status(candidate_fileset, fileset, conn):
@app.route("/fileset/<int:id>/merge/confirm", methods=["GET", "POST"])
+ at role_required("Admin", "Moderator", "Read Only")
def confirm_merge(id):
target_id = (
request.args.get("target_id", type=int)
@@ -1344,14 +1420,25 @@ def confirm_merge(id):
</table>
<input type="hidden" name="source_id" value="{{ source_fileset['id'] }}">
<input type="hidden" name="target_id" value="{{ target_fileset['id'] }}">
- <button id="confirm_merge_submit" type="submit">Confirm Merge</button>
- </form>
- <div id="merging-status" style="display: none; font-weight: bold; margin-top: 10px;">
- Merging... Please wait.
- </div>
- <form action="{{ url_for('fileset', id=id) }}">
- <input id="confirm_merge_cancel" type="submit" value="Cancel">
- </form>
+ """
+
+ if is_moderator_access():
+ """<button id="confirm_merge_submit" type="submit">Confirm Merge</button>"""
+
+ html += """</form>
+ <div id="merging-status" style="display: none; font-weight: bold; margin-top: 10px;">
+ Merging... Please wait.
+ </div>
+ """
+
+ if is_moderator_access():
+ html += """
+ <form action="{{ url_for('fileset', id=id) }}">
+ <input id="confirm_merge_cancel" type="submit" value="Cancel">
+ </form>
+ """
+
+ html += """
<script src="{{ url_for('static', filename='js/confirm_merge_form_handler.js') }}"></script>
<script src="{{ url_for('static', filename='js/update_merge_table_rows.js') }}"></script>
<script>
@@ -1376,6 +1463,7 @@ def confirm_merge(id):
@app.route("/fileset/<int:id>/merge/execute", methods=["POST"])
+ at role_required("Admin", "Moderator")
def execute_merge(id):
connection = db_connect()
with connection.cursor() as cursor:
@@ -1493,6 +1581,7 @@ def execute_merge(id):
@app.route("/fileset/<int:id>/mark_full", methods=["POST"])
+ at role_required("Admin", "Moderator")
def mark_as_full(id):
try:
conn = db_connect()
@@ -1517,6 +1606,7 @@ def mark_as_full(id):
@app.route("/config", methods=["GET", "POST"])
+ at role_required("Admin", "Moderator", "Read Only")
def config():
"""
Stores the user configurations in the cookies
@@ -1724,18 +1814,21 @@ def validate():
@app.route("/user_games_list")
+ at role_required("Admin", "Moderator", "Read Only")
def user_games_list():
url = "fileset_search?extra=&platform=&language=&megakey=&status=user"
return redirect(url)
@app.route("/ready_for_review")
+ at role_required("Admin", "Moderator", "Read Only")
def ready_for_review():
url = "fileset_search?extra=&platform=&language=&megakey=&status=ReadyForReview"
return redirect(url)
@app.route("/logs")
+ at role_required("Admin", "Moderator", "Read Only")
def logs():
filename = "logs"
records_table = "log"
@@ -1800,6 +1893,7 @@ def get_fileset_search_details():
@app.route("/fileset_search")
+ at role_required("Admin", "Moderator", "Read Only")
def fileset_search():
(
filename,
@@ -1819,10 +1913,12 @@ def fileset_search():
filters,
mapping,
)
- return render_template_string(render_html_string)
+ user_role = get_user_role()
+ return render_template_string(render_html_string, user_role=user_role)
@app.route("/delete_filtered_filesets", methods=["GET"])
+ at role_required("Admin")
def delete_filtered_filesets():
(
filename,
@@ -1875,5 +1971,4 @@ def email_notification(fileset_id):
if __name__ == "__main__":
- app.secret_key = secret_key
app.run(port=5001, debug=True, host="0.0.0.0")
diff --git a/static/navbar_string.html b/static/navbar_string.html
index 03c80d3..1a4d787 100644
--- a/static/navbar_string.html
+++ b/static/navbar_string.html
@@ -72,7 +72,9 @@
{% for key, value in request.args.items() %}
<input type="hidden" name="{{ key }}" value="{{ value }}">
{% endfor %}
- <button type="submit">Delete Filtered Filesets</button>
+ {% if user_role == "Admin" %}
+ <button type="submit">Delete Filtered Filesets</button>
+ {% endif %}
</form>
</div>
</nav>
diff --git a/templates/home.html b/templates/home.html
index 15691d3..19903de 100644
--- a/templates/home.html
+++ b/templates/home.html
@@ -88,16 +88,28 @@
</a>
</div>
<div class="nav-buttons">
- <a href="{{ url_for('user_games_list') }}">User Games List</a>
- <a href="{{ url_for('ready_for_review') }}">Ready for review</a>
- <a href="{{ url_for('fileset_search') }}">Fileset Search</a>
- <a href="{{ url_for('logs')}}">Logs</a>
- <a href="{{ url_for('config') }}">Config</a>
+ {% if username == "" %}
+ <a href="{{ url_for('login') }}">Login with GitHub</a>
+ {% else %}
+ <a href="{{ url_for('user_games_list') }}">User Games List</a>
+ <a href="{{ url_for('ready_for_review') }}">Ready for review</a>
+ <a href="{{ url_for('fileset_search') }}">Fileset Search</a>
+ <a href="{{ url_for('logs')}}">Logs</a>
+ <a href="{{ url_for('config') }}">Config</a>
+ <a href="{{ url_for('logout') }}" onclick="return confirm('Are you sure you want to log out?');">Logout</a>
+ {% endif %}
</div>
<div class="dev">
- <form action="{{ url_for('clear_database') }}" method="POST">
- <button type="submit">Clear Database</button>
- </form>
+ {% if user_role == "Admin" %}
+ <form style="margin-right: 20px;" action="{{ url_for('clear_database') }}" method="POST">
+ <button type="submit" onclick="return confirm('Are you sure you want to clear the database?');">Clear Database</button>
+ </form>
+ {% endif %}
+ {% if username != "" %}
+ <span>{{username}} - {{user_role}}</span>
+ {% else %}
+ <span>{{user_role}}</span>
+ {% endif %}
</div>
</nav>
<div class="title">
Commit: 3196da9e588a731f0145d0bae5960175a5d47b21
https://github.com/scummvm/scummvm-sites/commit/3196da9e588a731f0145d0bae5960175a5d47b21
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Remove apache basic auth
Changed paths:
apache2-config/gamesdb.sev.zone.conf
diff --git a/apache2-config/gamesdb.sev.zone.conf b/apache2-config/gamesdb.sev.zone.conf
index 27fb6c6..37a9053 100644
--- a/apache2-config/gamesdb.sev.zone.conf
+++ b/apache2-config/gamesdb.sev.zone.conf
@@ -13,16 +13,7 @@
Alias /static /home/ubuntu/projects/python/scummvm_sites_2025/scummvm-sites/static
<Directory /home/ubuntu/projects/python/scummvm_sites_2025/scummvm-sites>
- AuthType Basic
- AuthName "nope"
- AuthUserFile /home/ubuntu/projects/python/scummvm_sites_2025/.htpasswd
- Require valid-user
- </Directory>
-
- <Location "/validate">
- AuthType None
Require all granted
- Satisfy Any
- </Location>
+ </Directory>
</VirtualHost>
Commit: 939b8359f8f766e9b16e8e3a4de163fd1c8196b8
https://github.com/scummvm/scummvm-sites/commit/939b8359f8f766e9b16e8e3a4de163fd1c8196b8
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Get moderator username for logs from user session
Changed paths:
src/app/auth/helper.py
src/app/fileset.py
diff --git a/src/app/auth/helper.py b/src/app/auth/helper.py
index e069a49..e018434 100644
--- a/src/app/auth/helper.py
+++ b/src/app/auth/helper.py
@@ -1,12 +1,6 @@
-import getpass
from flask import session
-def get_current_user():
- user = f"cli:{getpass.getuser()}"
- return user
-
-
def get_user_role():
user_role = session.get("user", {}).get("role", "No Access")
return user_role
diff --git a/src/app/fileset.py b/src/app/fileset.py
index 33e30f2..fd267b6 100644
--- a/src/app/fileset.py
+++ b/src/app/fileset.py
@@ -16,7 +16,6 @@ from datetime import timedelta
import json
import html as html_lib
import os
-import getpass
from src.app.pagination import create_page
import difflib
@@ -42,7 +41,6 @@ from src.app.auth.github_oauth import init_oauth, GITHUB_ORG, TEAM_ROLES
from src.app.auth.role_based_auth import role_required
from src.app.auth.helper import (
get_user_role,
- get_current_user,
get_username,
is_moderator_access,
)
@@ -674,7 +672,7 @@ def delete_fileset(id):
with connection.cursor() as cursor:
query = "DELETE FROM fileset WHERE id = %s"
cursor.execute(query, (id,))
- user = get_current_user()
+ user = get_username()
log_text = f"Fileset deleted by moderator: {user} id:{id}"
create_log("Filset Deleted", user, log_text, connection)
connection.commit()
@@ -696,7 +694,7 @@ def files_action(id):
)
connection.commit()
- user = f"cli:{getpass.getuser()}"
+ user = get_username()
log_text = (
f"{len(file_ids)} file(s) of Fileset:{id} deleted by moderator: {user}."
)
@@ -789,7 +787,7 @@ def files_action(id):
values = values_by_table["filechecksum"] + [file]
cursor.execute(query, values)
print(f"File:{file} for Fileset:{id} updated successfully.")
- user = f"cli:{getpass.getuser()}"
+ user = get_username()
log_text = f"{len(changes_map)} file(s) of Fileset:{id} updated by moderator: {user}."
create_log("Files Updated", user, log_text, connection)
connection.commit()
@@ -865,7 +863,7 @@ def update_fileset(id):
query = f"UPDATE engine SET {', '.join(updates_by_table['engine'])} WHERE id = %s"
values = values_by_table["engine"] + [engine_id]
cursor.execute(query, values)
- user = f"cli:{getpass.getuser()}"
+ user = get_username()
log_text = f"Fileset:{id} metadata updated by moderator: {user}."
create_log("Metadata Updated", user, log_text, connection)
print(f"Fileset:{id} updated successfully.")
@@ -896,7 +894,7 @@ def update_fileset(id):
cursor.execute(
"UPDATE fileset SET game = %s WHERE id = %s", (game_pk_id, id)
)
- user = f"cli:{getpass.getuser()}"
+ user = get_username()
log_text = (
f"Fileset:{id} additional metadata added by moderator: {user}."
)
@@ -1562,7 +1560,7 @@ def execute_merge(id):
delete_original_fileset(source_id, connection)
category_text = "Manually Merged"
- user = f"cli:{getpass.getuser()}"
+ user = get_username()
log_text = f"Manually merged Fileset:{source_id} with Fileset:{target_id} by moderator: {user}."
create_log(category_text, user, log_text, connection)
@@ -1586,7 +1584,7 @@ def mark_as_full(id):
try:
conn = db_connect()
with conn.cursor() as cursor:
- user = f"cli:{getpass.getuser()}"
+ user = get_username()
update_query = "UPDATE fileset SET status = 'full' WHERE id = %s"
cursor.execute(update_query, (id,))
create_log(
@@ -1948,7 +1946,7 @@ def delete_filtered_filesets():
cursor.execute(
f"DELETE FROM fileset WHERE id IN ({placeholders})", filtered_filesets_id
)
- user = f"cli:{getpass.getuser()}"
+ user = get_username()
log_text = f"{len(filtered_filesets_id)} filesets deleted by moderator: {user}."
create_log("Filesets Deleted", user, log_text, connection)
connection.commit()
Commit: 2b945af89f84e06c9ae8ab3f7d9e6cb448960eec
https://github.com/scummvm/scummvm-sites/commit/2b945af89f84e06c9ae8ab3f7d9e6cb448960eec
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Update readme with new instructions
Changed paths:
README.md
diff --git a/README.md b/README.md
index 5c9538e..79c00b6 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,33 @@
-# ScummVM File Integrity Check (GSoC 2025)
+# ScummVM File Integrity Check
-This repository contains the server-side code for the upcoming file integrity check for game datafiles. This repository is part of the Google Summer of Code 2025 program.
+This repository contains the server-side code for the ScummVM's File Integrity service.
-## Prerequisites
-### Local
+## Web Application Access Roles
+Access roles are determined by the GitHub team a user belongs to in the ScummVM organization.
+Authentication and authorization are managed via GitHub OAuth.
+### Moderators
+Moderators have the following permissions:
+- Delete individual filesets
+- Update or add fileset metadata
+- Delete or update files
+- Mark a fileset as a Full fileset
+- Manually merge two filesets
+- Log User Email Notification
-- Python 3.x
-- MySQL
+### Admins
+Admins inherit all moderator permissions and have additional capabilities:
+- Clear the entire database
+- Delete all filtered filesets in bulk
-### Deployment
-- Apache2
+### Read-Only
+Read-only users can:
+- View filesets
+- Compare two filesets
-## Step-by-step Setup
+### No Access
+All other users (not logged in or not part of any ScummVM GitHub team) have no access to the application.
+
+## Development Setup
### 1. Clone the Repository
```bash
@@ -34,10 +50,18 @@ git checkout integrity
```
### 5. Install Required Python Packages
-You can also create a virtual environment (optional)
+You can install dependencies using either **pip** or **uv**.
+#### 5.1 Using pip
+(Optional) create and activate a virtual environment, then run:
```bash
pip install -r requirements.txt
```
+#### 5.2 Using UV
+Make sure uv is already installed, then run:
+```bash
+uv sync
+```
+This will set up the virtual environment and install all dependencies as specified in `uv.lock`.
### 6. Install and Configure MySQL (if not already installed)
Ensure MySQL is running and properly configured.
@@ -56,61 +80,106 @@ A `sample_mysql_config.json` is also present in the same directory.
### 8. Run Schema to Create Tables
```bash
-python schema.py
+python -m src.scripts.schema
+```
+or using uv:
+```bash
+uv run -m src.scripts.schema
+```
+### 9. Set up .env file (includes GitHub OAuth secrets)
+There is a .env.example file in the root directory. Copy it to create your .env file:
+```bash
+cp .env.example .env
+```
+Then update the values in .env as needed:
+```
+GITHUB_ORG=ScummVM
+GITHUB_CLIENT_ID=github_client_id_from_oauth_app
+GITHUB_CLIENT_SECRET=github_client_secret_from_oauth_app
+FLASK_SECRET_KEY=any_random_key
```
-## General Usecases
-
-### 1. Manual Generation of dat files (scan.dat) from existing games collection
-This utility helps in generating dat files from the existing game collections with the developers and then upload it to the database.
-#### Dat File Generation :
-This will generate the `.dat` file with complete checksums but no metadata.
+## Deployment Guide
+The Flask application is deployed using `mod_wsgi`, an Apache module for hosting WSGI applications.
+Assuming Apache and mod_wsgi are already installed:
+Copy the provided Apache configuration file from the `apache2-config` directory into Apacheâs `sites-available` folder:
+```bash
+sudo cp apache2-config/gamesdb.sev.zone.conf /etc/apache2/sites-available/
+```
+Enable the site:
+```bash
+sudo a2ensite gamesdb.sev.zone.conf
+```
+Reload Apache to apply changes:
```bash
-python compute_hash.py --directory <path_to_directory> --depth 0 --size 0
+sudo systemctl reload apache2
```
-- `--directory` : Path of directory with game files
-- `--depth` : Depth from root to game directories
-- `--size` : Use first n bytes of file to calculate checksum
-#### Database upload (for developers) :
-Uploading the `.dat` file to the database.
+## CLI Script Usecases
+- If using **pip**:
+ - On Debian/Ubuntu (or any system where `python` points to Python 2), use:
+ ```bash
+ python3 -m <module>
+ ```
+ - Otherwise:
+ ```bash
+ python -m <module>
+ ```
+- If using **uv**:
```bash
-python dat_parser.py --upload <scanned_dat_file/scan>.dat --user <username> --skiplog
+ uv run -m <module>
```
-- `--upload` : Upload DAT file(s) to the database
-- `--match` : Populate matching games in the database
-- `--user` : Username for database
-- `-r` : Recurse through directories
-- `--skiplog` : Skip logging dups
-### 2. Uploading dat files (scummvm.dat) generated from detection entries to the DB (Initial Seeding)
+### 1. Initial Seeding: Uploading dat files (scummvm.dat) generated from detection entries to the DB
Upload the `.dat` file to the database using dat_parser script -
```bash
-python dat_parser.py --upload <detection_dat_file/scummvm>.dat --user <username> --skiplog
+python -m src.scripts.dat_parser --upload <path_to_scummvm.dat> --user <username> --skiplog
```
+- `--upload` : Upload DAT file(s) to the database (seeding)
+- `--match` : Populate matching games in the database
+- `--user` : (Optional) Username for database
+- `-r` : (Optional) Recurse through directories
+- `--skiplog` : (Optional) Skip logging dups
+
`scummvm.dat` can be generated using -
```bash
./scummvm --dump-all-detection-entries
```
-### 3. Uploading already existing dat files (set.dat) from old collections to the DB
-Upload the `.dat` file to the database using dat_parser script -
+### 2. Uploading already existing dat files (set.dat) from old collections to the DB
+Match the filesets from the `.dat` file using dat_parser script -
```bash
-python dat_parser.py --upload <old_dat_file/set>.dat --user <username> --skiplog
+python -m src.scripts.dat_parser --match <path_to_set.dat> --user <username> --skiplog
```
+### 3. Scan Utility: Manual Generation of dat files (scan.dat) from existing games collection
+This utility helps in generating dat files from the existing game collections which can be uploaded to the database.
+
+#### Dat File Generation (Scan Utility) :
+This will generate the `.dat` file with complete checksums and sizes (size, size-r and size-rd in case of macfiles)
-### 4. Validate Game Files from Client Side (integrity.json)
-Make a POST request to the following endpoint:
+```bash
+python -m src.scripts.compute_hash --directory <path_to_directory> --depth 0 --size 0 --limit-timestamps 2003-09-12
+```
+- `--directory` : (Required) Path of directory with game files
+- `--depth` : (Optional: Default = 0) Depth from root to game directories (e.g. 0 while scanning a single directory)
+- `--size` : (Optional: Default = 0) Use first n bytes of file to calculate checksum
+- `--limit-timestamps` : (Optional) Format - YYYY-MM-DD or YYYY-MM or YYYY. Filters out the files those
+ were modified after the given timestamp. Note that if the
+ modification time is today, it would not be filtered out.
+
+#### Perform Matching :
+Perform matching with the exisiting filesets in database.
+```bash
+python -m src.scripts.dat_parser --match <scanned_dat_file/scan>.dat --user <username> --skiplog
+```
+
+## Integrity Service: Validate Game Files from Client Side
+There exists a check_integrity button in the scummvm application which makes a POST request to the following endpoint:
#### Local :
```bash
http://localhost:5000/validate
```
-with the body in JSON format as shown in `sample_json_request.json` present in the main directory.
-There also exists a check_integrity button in the scummvm application itself which is under development.
-
-## Deployment
-
-The apache2 .conf file is located under `apache2-config/`.
+with the request body in JSON format as shown in `sample_json_request.json` present in the root directory.
Commit: 736aef2dcaa54514d05897d097b4ef8578ba4503
https://github.com/scummvm/scummvm-sites/commit/736aef2dcaa54514d05897d097b4ef8578ba4503
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Add manual user email notificication log button
Changed paths:
src/app/fileset.py
diff --git a/src/app/fileset.py b/src/app/fileset.py
index fd267b6..512e908 100644
--- a/src/app/fileset.py
+++ b/src/app/fileset.py
@@ -239,6 +239,11 @@ def fileset():
html += "<button type='submit' style='margin-left: 10px;'>Delete the Fileset</button>"
html += "</form>"
+ # Manually log email notification
+ html += f"""<form action="{url_for("manual_email_notification", fileset_id=id)}" method="POST" onsubmit="return confirm('Are you sure you want to log a user email notification for the given fileset?');">"""
+ html += "<button type='submit' style='margin-left: 10px;'>Log User Email Notification</button>"
+ html += "</form>"
+
# -------------------------------------------------------------------------------------------------
# metadata
# -------------------------------------------------------------------------------------------------
@@ -1953,12 +1958,16 @@ def delete_filtered_filesets():
return redirect("/logs")
- at app.route("/email_notification/<int:fileset_id>", methods=["GET"])
-def email_notification(fileset_id):
+def log_user_email_notification(fileset_id):
connection = db_connect()
log_text = f"User email received for Fileset:{fileset_id}"
create_log("Email Received", "Mail Server", log_text, connection)
connection.commit()
+
+
+ at app.route("/email_notification/<int:fileset_id>", methods=["POST"])
+def email_notification(fileset_id):
+ log_user_email_notification(fileset_id)
return jsonify(
{
"status": "success",
@@ -1968,5 +1977,12 @@ def email_notification(fileset_id):
), 200
+ at app.route("/manual_email_notification/<int:fileset_id>", methods=["POST"])
+ at role_required("Admin", "Moderator")
+def manual_email_notification(fileset_id):
+ log_user_email_notification(fileset_id)
+ return redirect("/logs")
+
+
if __name__ == "__main__":
app.run(port=5001, debug=True, host="0.0.0.0")
Commit: 88dc8ecc2f6a95cde8399e7a29ecb7c6878f9a24
https://github.com/scummvm/scummvm-sites/commit/88dc8ecc2f6a95cde8399e7a29ecb7c6878f9a24
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Hide buttons for logged in users with No Access
Changed paths:
src/app/auth/role_based_auth.py
templates/home.html
diff --git a/src/app/auth/role_based_auth.py b/src/app/auth/role_based_auth.py
index 34c7ee3..2124226 100644
--- a/src/app/auth/role_based_auth.py
+++ b/src/app/auth/role_based_auth.py
@@ -16,6 +16,9 @@ def role_required(*roles):
user = session.get("user")
user_role = user["role"]
+ if user_role == "No Access":
+ return redirect(url_for("home"))
+
if user_role not in roles:
abort(403)
return f(*args, **kwargs)
diff --git a/templates/home.html b/templates/home.html
index 19903de..ebee3e1 100644
--- a/templates/home.html
+++ b/templates/home.html
@@ -91,11 +91,13 @@
{% if username == "" %}
<a href="{{ url_for('login') }}">Login with GitHub</a>
{% else %}
- <a href="{{ url_for('user_games_list') }}">User Games List</a>
- <a href="{{ url_for('ready_for_review') }}">Ready for review</a>
- <a href="{{ url_for('fileset_search') }}">Fileset Search</a>
- <a href="{{ url_for('logs')}}">Logs</a>
- <a href="{{ url_for('config') }}">Config</a>
+ {% if user_role != "No Access" %}
+ <a href="{{ url_for('user_games_list') }}">User Games List</a>
+ <a href="{{ url_for('ready_for_review') }}">Ready for review</a>
+ <a href="{{ url_for('fileset_search') }}">Fileset Search</a>
+ <a href="{{ url_for('logs')}}">Logs</a>
+ <a href="{{ url_for('config') }}">Config</a>
+ {% endif %}
<a href="{{ url_for('logout') }}" onclick="return confirm('Are you sure you want to log out?');">Logout</a>
{% endif %}
</div>
Commit: 5df88e5ad73025239d5088b2a3ac14d11cd2a738
https://github.com/scummvm/scummvm-sites/commit/5df88e5ad73025239d5088b2a3ac14d11cd2a738
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Show Fileset Database title only to logged in users.
Changed paths:
templates/home.html
diff --git a/templates/home.html b/templates/home.html
index ebee3e1..42bff43 100644
--- a/templates/home.html
+++ b/templates/home.html
@@ -114,9 +114,11 @@
{% endif %}
</div>
</nav>
- <div class="title">
- <div class="fileset_database">Fileset Database</div>
- </div>
+ {% if username != "" %}
+ <div class="title">
+ <div class="fileset_database">Fileset Database</div>
+ </div>
+ {% endif %}
</body>
</html>
Commit: 05cbfb15b24ea402dcfb1781c7ac7593099d4a41
https://github.com/scummvm/scummvm-sites/commit/05cbfb15b24ea402dcfb1781c7ac7593099d4a41
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Add confirmation page before deleting filesets
Changed paths:
src/app/fileset.py
src/app/pagination.py
static/navbar_string.html
diff --git a/src/app/fileset.py b/src/app/fileset.py
index 512e908..5c8216f 100644
--- a/src/app/fileset.py
+++ b/src/app/fileset.py
@@ -1845,7 +1845,7 @@ def logs():
"text": "log",
}
logs_per_page = get_logs_per_page()
- render_html_string, _, _ = create_page(
+ render_html_string = create_page(
filename, logs_per_page, records_table, select_query, order, filters
)
return render_template_string(render_html_string)
@@ -1907,7 +1907,7 @@ def fileset_search():
mapping,
filesets_per_page,
) = get_fileset_search_details()
- render_html_string, _, _ = create_page(
+ render_html_string = create_page(
filename,
filesets_per_page,
records_table,
@@ -1920,9 +1920,9 @@ def fileset_search():
return render_template_string(render_html_string, user_role=user_role)
- at app.route("/delete_filtered_filesets", methods=["GET"])
+ at app.route("/fileset_search/delete_filtered_filesets/confirmation", methods=["GET"])
@role_required("Admin")
-def delete_filtered_filesets():
+def delete_filtered_filesets_confirmation():
(
filename,
records_table,
@@ -1932,7 +1932,8 @@ def delete_filtered_filesets():
mapping,
filesets_per_page,
) = get_fileset_search_details()
- _, select_query, condition = create_page(
+ filesets_per_page = 1000000
+ page = create_page(
filename,
filesets_per_page,
records_table,
@@ -1940,19 +1941,26 @@ def delete_filtered_filesets():
order,
filters,
mapping,
+ delete_confirmation=True,
)
- query = f"{select_query} {condition}"
+ return render_template_string(page)
+
+
+ at app.route("/fileset_search/delete_filtered_filesets/execute", methods=["POST"])
+ at role_required("Admin")
+def delete_filtered_filesets():
connection = db_connect()
with connection.cursor() as cursor:
- cursor.execute(query)
- filtered_filesets = cursor.fetchall()
- filtered_filesets_id = [f["fileset"] for f in filtered_filesets]
- placeholders = ",".join(["%s"] * len(filtered_filesets_id))
- cursor.execute(
- f"DELETE FROM fileset WHERE id IN ({placeholders})", filtered_filesets_id
- )
+ ids_str = request.form.get("ids")
+ filters_for_logging = request.form.get("filters")
+ ids = [int(i) for i in ids_str.split(",")]
+ placeholders = ",".join(["%s"] * len(ids))
+ query = f"DELETE FROM fileset WHERE id IN ({placeholders})"
+ cursor.execute(query, ids)
user = get_username()
- log_text = f"{len(filtered_filesets_id)} filesets deleted by moderator: {user}."
+ log_text = (
+ f"{len(ids)} filesets deleted by moderator: {user}. {filters_for_logging}"
+ )
create_log("Filesets Deleted", user, log_text, connection)
connection.commit()
return redirect("/logs")
diff --git a/src/app/pagination.py b/src/app/pagination.py
index 343cdb5..4db0b4f 100644
--- a/src/app/pagination.py
+++ b/src/app/pagination.py
@@ -56,6 +56,7 @@ def create_page(
order,
filters={},
mapping={},
+ delete_confirmation=False,
):
conn = db_connect()
@@ -63,11 +64,15 @@ def create_page(
tables = set()
where_clauses = []
compare_fileset_source_id = request.args.get("source_id", "")
+ total_filtered_filesets = 0
+ ids = []
+ filters_for_logging = ""
for key, value in request.args.items():
if key in ("page", "sort", "source_id") or value == "":
continue
tables.add(filters[key])
+ filters_for_logging += f"{key}: {value} "
col = f"{filters[key]}.{'id' if key == 'fileset' else key}"
parsed = build_search_condition(value, col)
if parsed:
@@ -133,6 +138,14 @@ def create_page(
cursor.execute(query)
results = cursor.fetchall()
+ # Total filesets
+ if delete_confirmation:
+ query = f"{select_query} {condition}"
+ cursor.execute(query)
+ all_ids = cursor.fetchall()
+ ids = [ids["fileset"] for ids in all_ids]
+ total_filtered_filesets = len(all_ids)
+
# Initial html code including the navbar is stored in a separate html file.
html = ""
navbar_path = os.path.join(STATIC_DIR, "navbar_string.html")
@@ -144,12 +157,28 @@ def create_page(
'<button type="submit">Delete Filtered Filesets</button>',
'<button type="submit" style="display:none;" disabled>Delete Filtered Filesets</button>',
)
-
- # Generate HTML
- html += """
- <form id='filters-form' method='GET' onsubmit='remove_empty_inputs()'>
- <table class="fixed-table" style="margin-top: 80px;">
- """
+ if delete_confirmation:
+ ids_str = ",".join(map(str, ids))
+ html += f"""
+ <div style="margin-top: 100px;">
+ <h2>Are you sure you want to delete the given {total_filtered_filesets} filtered filesets?</h2>
+ <form onsubmit="return confirm('{total_filtered_filesets} filesets will be deleted.');" action="/fileset_search/delete_filtered_filesets/execute" method="POST">
+ <input type="hidden" name="ids" value="{ids_str}">
+ <input type="hidden" name="filters" value="{filters_for_logging}">
+ <button style="margin-bottom: 10px;" type="submit">Yes, delete</button>
+ </form>
+ </div>
+ """
+ html += """
+ <form id='filters-form' method='GET' onsubmit='remove_empty_inputs()'>
+ <table class="fixed-table">
+ """
+ else:
+ # Generate HTML
+ html += """
+ <form id='filters-form' method='GET' onsubmit='remove_empty_inputs()'>
+ <table class="fixed-table" style="margin-top: 80px;">
+ """
if records_table == "fileset":
fileset_dashboard_widths_default = {
@@ -182,19 +211,21 @@ def create_page(
width = get_width(name, default)
html += f"<col style='width: {width}%;'>"
html += "</colgroup>"
- if filters:
- html += """<tr class='filter'><td class='filter'><input type='submit' value='Submit'></td>"""
- for key in filters.keys():
- if key == "checksum":
- continue
- filter_value = request.args.get(key, "")
- if key == "transaction":
- html += f"<td style='display: flex;' class='filter'><input type='text' class='filter' placeholder='{key}' name='{key}' value='{filter_value}'/>"
- filter_value = request.args.get("checksum", "")
- html += f"<input type='text' class='filter' placeholder='checksum' name='checksum' value='{filter_value}'/></td>"
- else:
- html += f"<td class='filter'><input type='text' class='filter' placeholder='{key}' name='{key}' value='{filter_value}'/></td>"
- html += "</tr>"
+
+ if not delete_confirmation:
+ if filters:
+ html += """<tr class='filter'><td class='filter'><input type='submit' value='Submit'></td>"""
+ for key in filters.keys():
+ if key == "checksum":
+ continue
+ filter_value = request.args.get(key, "")
+ if key == "transaction":
+ html += f"<td style='display: flex;' class='filter'><input type='text' class='filter' placeholder='{key}' name='{key}' value='{filter_value}'/>"
+ filter_value = request.args.get("checksum", "")
+ html += f"<input type='text' class='filter' placeholder='checksum' name='checksum' value='{filter_value}'/></td>"
+ else:
+ html += f"<td class='filter'><input type='text' class='filter' placeholder='{key}' name='{key}' value='{filter_value}'/></td>"
+ html += "</tr>"
html += "<th>S. No.</th>"
current_sort = request.args.get("sort", "")
@@ -224,26 +255,45 @@ def create_page(
base_params["sort"] = sort_param
query_string = "&".join(f"{k}={v}" for k, v in base_params.items())
+ icon_src = url_for("static", filename=icon_path + icon_name)
+
if key != "checksum":
- icon_src = url_for("static", filename=icon_path + icon_name)
- if icon_name != "no_icon":
- html += f"""<th>
- <a href='{filename}?{query_string}' class="header-link">
+ if not delete_confirmation:
+ # clickable header (sorting allowed)
+ if icon_name != "no_icon":
+ html += f"""<th>
+ <a href='{filename}?{query_string}' class="header-link">
+ <div style="display:flex; align-items:center; width:100%;">
+ <span style="flex:1; text-align:center;">{key}</span>
+ <img src="{icon_src}" class="filter-icon" alt="asc" style="margin-left:auto;">
+ </div>
+ </a>
+ </th>"""
+ else:
+ html += f"""<th>
+ <a href='{filename}?{query_string}' class="header-link">
+ <div style="display:flex; align-items:center; width:100%;">
+ <span style="flex:1; text-align:center;">{key}</span>
+ <span style="width: 18px"></span>
+ </div>
+ </a>
+ </th>"""
+ else:
+ # non-clickable header (sorting disabled)
+ if icon_name != "no_icon":
+ html += f"""<th>
<div style="display:flex; align-items:center; width:100%;">
<span style="flex:1; text-align:center;">{key}</span>
- <img src="{icon_src}" class="filter-icon" alt="asc" style="margin-left:auto;">
+ <img src="{icon_src}" class="filter-icon disabled" alt="asc" style="margin-left:auto; opacity:0.5;">
</div>
- </a>
- </th>"""
- else:
- html += f"""<th>
- <a href='{filename}?{query_string}' class="header-link">
+ </th>"""
+ else:
+ html += f"""<th>
<div style="display:flex; align-items:center; width:100%;">
<span style="flex:1; text-align:center;">{key}</span>
<span style="width: 18px"></span>
</div>
- </a>
- </th>"""
+ </th>"""
if compare_fileset_source_id != "":
html += """<th>
@@ -334,4 +384,4 @@ def create_page(
html += "<input type='submit' value='Submit'>"
html += "</div></form>"
- return html, select_query, condition
+ return html
diff --git a/static/navbar_string.html b/static/navbar_string.html
index 1a4d787..0966931 100644
--- a/static/navbar_string.html
+++ b/static/navbar_string.html
@@ -68,7 +68,7 @@
<a href="{{ url_for('config') }}">Config</a>
</div>
<div class="dev">
- <form onsubmit="return confirm('Are you sure you want to delete the filtered filesets?');" action="{{ url_for('delete_filtered_filesets') }}" method="GET">
+ <form action="{{ url_for('delete_filtered_filesets_confirmation') }}" method="GET">
{% for key, value in request.args.items() %}
<input type="hidden" name="{{ key }}" value="{{ value }}">
{% endfor %}
Commit: aa3ee1e807031d5b5a358f6abaf7e0276c5a4f82
https://github.com/scummvm/scummvm-sites/commit/aa3ee1e807031d5b5a358f6abaf7e0276c5a4f82
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Add confirmation page before deleting files
Changed paths:
A static/js/update_files.js
A templates/delete_files.html
src/app/fileset.py
diff --git a/src/app/fileset.py b/src/app/fileset.py
index 5c8216f..8ede05c 100644
--- a/src/app/fileset.py
+++ b/src/app/fileset.py
@@ -439,7 +439,7 @@ def fileset():
html += "<input type='submit' value='Hide extra checksums' />"
html += "</form>"
- html += f"""<form id="file_action_form" method="POST" action="{url_for("files_action", id=id)}" onsubmit="return confirm('Are you sure you want to perform this action on the files?');">"""
+ html += f"""<form id="file_action_form" method="POST" action="{url_for("files_action", id=id)}">"""
# Table
html += "<table>\n"
@@ -539,7 +539,7 @@ def fileset():
html += "</table>\n"
if is_moderator_access():
- html += """<input type="submit" name="action" value="Update Files">"""
+ html += """<input type="submit" name="action" value="Update Files" onclick="return update_files()">"""
html += """<input style="margin-left: 10px;" type="submit" name="action" value="Delete Selected Files">"""
html += "</form>\n"
@@ -665,6 +665,7 @@ def fileset():
html += "</tr>"
html += "</table>\n"
html += "<script src='{{ url_for('static', filename='js/track_metadata_update.js') }}'></script>"
+ html += "<script src='{{ url_for('static', filename='js/update_files.js') }}'></script>"
return render_template_string(html)
finally:
connection.close()
@@ -684,12 +685,29 @@ def delete_fileset(id):
return redirect(url_for("logs"))
- at app.route("/files_action/<int:id>", methods=["POST"])
+ at app.route("/files_action/<int:id>/delete_files/confirm", methods=["GET", "POST"])
@role_required("Admin", "Moderator")
-def files_action(id):
- action = request.form.get("action")
- if action == "Delete Selected Files":
- file_ids = request.form.getlist("file_ids")
+def delete_files_confirmation(id):
+ if request.method == "GET":
+ file_ids_str = request.args.get("file_ids")
+ file_ids = [i for i in file_ids_str.split(",")]
+ connection = db_connect()
+ with connection.cursor() as cursor:
+ placeholders = ",".join(["%s"] * len(file_ids))
+ cursor.execute(
+ f"SELECT id, name FROM file WHERE id IN ({placeholders})", file_ids
+ )
+ files = cursor.fetchall()
+ return render_template(
+ "delete_files.html",
+ id=id,
+ files=files,
+ file_ids=",".join(file_ids),
+ total_files=len(files),
+ )
+
+ elif request.method == "POST":
+ file_ids = request.form.get("file_ids").split(",")
if file_ids:
connection = db_connect()
with connection.cursor() as cursor:
@@ -699,12 +717,27 @@ def files_action(id):
)
connection.commit()
- user = get_username()
- log_text = (
- f"{len(file_ids)} file(s) of Fileset:{id} deleted by moderator: {user}."
- )
- create_log("Files Deleted", user, log_text, connection)
- connection.commit()
+ user = get_username()
+ log_text = (
+ f"{len(file_ids)} file(s) of Fileset:{id} deleted by moderator: {user}."
+ )
+ create_log("Files Deleted", user, log_text, connection)
+ connection.commit()
+
+ return redirect(url_for("fileset", id=id))
+
+
+ at app.route("/files_action/<int:id>", methods=["POST"])
+ at role_required("Admin", "Moderator")
+def files_action(id):
+ action = request.form.get("action")
+ if action == "Delete Selected Files":
+ file_ids = request.form.getlist("file_ids")
+ if file_ids:
+ ids_str = ",".join(file_ids)
+ return redirect(
+ url_for("delete_files_confirmation", id=id, file_ids=ids_str)
+ )
elif action == "Update Files":
connection = db_connect()
diff --git a/static/js/update_files.js b/static/js/update_files.js
new file mode 100644
index 0000000..fe1d129
--- /dev/null
+++ b/static/js/update_files.js
@@ -0,0 +1,3 @@
+function update_files() {
+ return confirm("Are you sure you want to update the given files?");
+}
diff --git a/templates/delete_files.html b/templates/delete_files.html
new file mode 100644
index 0000000..c676910
--- /dev/null
+++ b/templates/delete_files.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
+ <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
+ <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
+ <style>
+ button {
+ background-color: #d9534f;
+ color: white;
+ padding: 10px 20px;
+ font-size: 16px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background-color 0.3s, box-shadow 0.3s;
+ }
+
+ button:hover {
+ background-color: #c9302c;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ }
+</style>
+</head>
+
+<body>
+ <nav>
+ <div class="logo">
+ <a href="{{ url_for('home') }}">
+ <img src="{{ url_for('static', filename='integrity_service_logo_256.png') }}" alt="Logo">
+ </a>
+ </div>
+ <div class="nav-buttons">
+ <a href="{{ url_for('user_games_list') }}">User Games List</a>
+ <a href="{{ url_for('ready_for_review') }}">Ready for review</a>
+ <a href="{{ url_for('fileset_search') }}">Fileset Search</a>
+ <a href="{{ url_for('logs') }}">Logs</a>
+ <a href="{{ url_for('config') }}">Config</a>
+ </div>
+ </nav>
+ <h2 style="margin-top: 80px;"><u>Fileset: {{id}}</u></h2>
+ <div>
+ <h2>Are you sure you want to delete the given {{total_files}} file(s)?</h2>
+ <form method="POST" onsubmit="return confirm('{{total_files}} file(s) will be deleted.');"
+ action="{{ url_for('delete_files_confirmation', id=id) }}">
+ <input type="hidden" name="file_ids" value="{{ file_ids }}">
+ <button style="margin-bottom: 10px;" type="submit">Yes, delete</button>
+ </form>
+ <ol>
+ {% for file in files %}
+ <li>{{ file.name }} (ID: {{ file.id }})</li>
+ {% endfor %}
+ </ol>
+ </div>
+</body>
+
+</html>
Commit: cce0e68c253be0628656235478c6529e3af098ce
https://github.com/scummvm/scummvm-sites/commit/cce0e68c253be0628656235478c6529e3af098ce
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Shift clear database button to config page
Changed paths:
src/app/fileset.py
templates/config.html
templates/home.html
diff --git a/src/app/fileset.py b/src/app/fileset.py
index 8ede05c..5ba0536 100644
--- a/src/app/fileset.py
+++ b/src/app/fileset.py
@@ -1742,6 +1742,7 @@ def config():
for field, default in log_dashboard_widths_default.items()
}
+ user_role = get_user_role()
return render_template(
"config.html",
filesets_per_page=filesets_per_page,
@@ -1750,6 +1751,7 @@ def config():
fileset_fields=fileset_fields,
log_dashboard_widths=log_dashboard_widths,
log_fields=log_fields,
+ user_role=user_role,
)
diff --git a/templates/config.html b/templates/config.html
index 64b616b..ecd7731 100644
--- a/templates/config.html
+++ b/templates/config.html
@@ -20,6 +20,26 @@
}
.title {
+ margin-top: 220px;
+ text-align: center;
+ background-color: #ffffff;
+ color: #000000;
+ padding: 10px;
+ align-self: flex-start;
+ margin-left: 2vh;
+ }
+
+ .config_title_admin {
+ margin-top: 20px;
+ text-align: center;
+ background-color: #ffffff;
+ color: #000000;
+ padding: 10px;
+ align-self: flex-start;
+ margin-left: 2vh;
+ }
+
+ .config_title {
margin-top: 150px;
text-align: center;
background-color: #ffffff;
@@ -96,6 +116,23 @@
flex-wrap: wrap;
}
+ .clear_database {
+ background-color: #d9534f;
+ color: white;
+ padding: 10px 20px;
+ font-size: 16px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background-color 0.3s, box-shadow 0.3s;
+ margin-left: 0;
+ }
+
+ .clear_database:hover {
+ background-color: #c9302c;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ }
+
@media (max-width: 768px) {
.config {
font-size: 40px;
@@ -136,7 +173,15 @@
</div>
</nav>
<div class="content">
- <h1 class="title">User Configurations</h1>
+ {% if user_role == "Admin" %}
+ <h2 class="title">Admin Features</h2>
+ <form style="align-self: flex-start; margin-left: 40px;" action="{{ url_for('clear_database') }}" method="POST">
+ <button class="clear_database" type="submit" onclick="return confirm('Are you sure you want to clear the database?');">Clear Database</button>
+ </form>
+ <h2 class="config_title_admin">User Configurations</h2>
+ {% else %}
+ <h2 class="config_title">User Configurations</h2>
+ {% endif %}
<div class="main">
<form class="config-form" method="POST" action="{{ url_for('config') }}">
<div class="config-section">
diff --git a/templates/home.html b/templates/home.html
index 42bff43..7aa6c6f 100644
--- a/templates/home.html
+++ b/templates/home.html
@@ -102,11 +102,6 @@
{% endif %}
</div>
<div class="dev">
- {% if user_role == "Admin" %}
- <form style="margin-right: 20px;" action="{{ url_for('clear_database') }}" method="POST">
- <button type="submit" onclick="return confirm('Are you sure you want to clear the database?');">Clear Database</button>
- </form>
- {% endif %}
{% if username != "" %}
<span>{{username}} - {{user_role}}</span>
{% else %}
Commit: ab950c38124e9ffd25e15dccb1c6ecd0e2d9d044
https://github.com/scummvm/scummvm-sites/commit/ab950c38124e9ffd25e15dccb1c6ecd0e2d9d044
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Hide metadata addition feature for read-only users
Changed paths:
src/app/fileset.py
diff --git a/src/app/fileset.py b/src/app/fileset.py
index 5ba0536..2af709e 100644
--- a/src/app/fileset.py
+++ b/src/app/fileset.py
@@ -289,7 +289,9 @@ def fileset():
cursor.execute(query, (id,))
result = {**result, **cursor.fetchone()}
else:
- if status == "user" or status == "ReadyForReview":
+ if (
+ status == "user" or status == "ReadyForReview"
+ ) and is_moderator_access():
html += "<h4>Add additional metadata</h4>"
cursor.execute(
Commit: 10f1459eae5223c9d3db82c348046a9748d8bbf6
https://github.com/scummvm/scummvm-sites/commit/10f1459eae5223c9d3db82c348046a9748d8bbf6
Author: ShivangNagta (shivangnag at gmail.com)
Date: 2025-08-31T16:03:25+02:00
Commit Message:
INTEGRITY: Update UV files
Changed paths:
pyproject.toml
uv.lock
diff --git a/pyproject.toml b/pyproject.toml
index 6614fd6..a1b4e60 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,15 +1,15 @@
[project]
name = "scummvm-sites"
version = "0.1.0"
-description = "Add your description here"
requires-python = ">=3.12"
dependencies = [
+ "authlib>=1.6.3",
"blinker>=1.9.0",
"cffi>=1.17.1",
"click>=8.2.1",
"cryptography>=45.0.6",
- "flask>=3.1.1",
- "flask-limiter>=3.12",
+ "flask>=3.1.2",
+ "flask-dance>=7.1.0",
"iniconfig>=2.1.0",
"itsdangerous>=2.2.0",
"jinja2>=3.1.6",
@@ -18,8 +18,10 @@ dependencies = [
"pluggy>=1.6.0",
"pycparser>=2.22",
"pygments>=2.19.2",
- "pymysql>=1.1.1",
+ "pymysql>=1.1.2",
"pytest>=8.4.1",
+ "python-dotenv>=1.1.1",
+ "requests>=2.32.5",
"setuptools>=80.9.0",
"werkzeug>=3.1.3",
"wheel>=0.45.1",
diff --git a/uv.lock b/uv.lock
index 9f90dee..3b6bc20 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,6 +2,18 @@ version = 1
revision = 3
requires-python = ">=3.12"
+[[package]]
+name = "authlib"
+version = "1.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5d/c6/d9a9db2e71957827e23a34322bde8091b51cb778dcc38885b84c772a1ba9/authlib-1.6.3.tar.gz", hash = "sha256:9f7a982cc395de719e4c2215c5707e7ea690ecf84f1ab126f28c053f4219e610", size = 160836, upload-time = "2025-08-26T12:13:25.206Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/25/2f/efa9d26dbb612b774990741fd8f13c7cf4cfd085b870e4a5af5c82eaf5f1/authlib-1.6.3-py2.py3-none-any.whl", hash = "sha256:7ea0f082edd95a03b7b72edac65ec7f8f68d703017d7e37573aee4fc603f2a48", size = 240105, upload-time = "2025-08-26T12:13:23.889Z" },
+]
+
[[package]]
name = "blinker"
version = "1.9.0"
@@ -11,6 +23,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
]
+[[package]]
+name = "certifi"
+version = "2025.8.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
+]
+
[[package]]
name = "cffi"
version = "1.17.1"
@@ -44,6 +65,48 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
]
+[[package]]
+name = "charset-normalizer"
+version = "3.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" },
+ { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" },
+ { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" },
+ { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" },
+ { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" },
+ { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" },
+ { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" },
+ { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" },
+ { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" },
+ { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" },
+ { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" },
+ { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" },
+ { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" },
+ { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" },
+ { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" },
+ { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" },
+ { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
+]
+
[[package]]
name = "click"
version = "8.2.1"
@@ -100,21 +163,9 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/bc/16e0276078c2de3ceef6b5a34b965f4436215efac45313df90d55f0ba2d2/cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", size = 3390459, upload-time = "2025-08-05T23:59:03.358Z" },
]
-[[package]]
-name = "deprecated"
-version = "1.2.18"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "wrapt" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" },
-]
-
[[package]]
name = "flask"
-version = "3.1.1"
+version = "3.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
@@ -124,24 +175,35 @@ dependencies = [
{ name = "markupsafe" },
{ name = "werkzeug" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c0/de/e47735752347f4128bcf354e0da07ef311a78244eba9e3dc1d4a5ab21a98/flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e", size = 753440, upload-time = "2025-05-13T15:01:17.447Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305, upload-time = "2025-05-13T15:01:15.591Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
]
[[package]]
-name = "flask-limiter"
-version = "3.12"
+name = "flask-dance"
+version = "7.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "flask" },
- { name = "limits" },
- { name = "ordered-set" },
- { name = "rich" },
+ { name = "oauthlib" },
+ { name = "requests" },
+ { name = "requests-oauthlib" },
+ { name = "urlobject" },
+ { name = "werkzeug" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/70/75/92b237dd4f6e19196bc73007fff288ab1d4c64242603f3c401ff8fc58a42/flask_limiter-3.12.tar.gz", hash = "sha256:f9e3e3d0c4acd0d1ffbfa729e17198dd1042f4d23c130ae160044fc930e21300", size = 303162, upload-time = "2025-03-15T02:23:10.734Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/b3/38aff96fbafe850f7f4186dc06e96ebc29625d68d1427ad65c9d41c4ec9e/flask_dance-7.1.0.tar.gz", hash = "sha256:6d0510e284f3d6ff05af918849791b17ef93a008628ec33f3a80578a44b51674", size = 140993, upload-time = "2024-03-05T12:43:21.558Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/66/ba/40dafa278ee6a4300179d2bf59a1aa415165c26f74cfa17462132996186b/flask_limiter-3.12-py3-none-any.whl", hash = "sha256:b94c9e9584df98209542686947cf647f1ede35ed7e4ab564934a2bb9ed46b143", size = 28490, upload-time = "2025-03-15T02:23:08.919Z" },
+ { url = "https://files.pythonhosted.org/packages/75/8c/4125e9f1196e5ab9675d38ff445ae4abd7085aba7551335980ac19196389/flask_dance-7.1.0-py3-none-any.whl", hash = "sha256:81599328a2b3604fd4332b3d41a901cf36980c2067e5e38c44ce3b85c4e1ae9c", size = 62176, upload-time = "2024-03-05T12:43:19.149Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
@@ -174,32 +236,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
-[[package]]
-name = "limits"
-version = "5.5.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "deprecated" },
- { name = "packaging" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/76/17/7a2e9378c8b8bd4efe3573fd18d2793ad2a37051af5ccce94550a4e5d62d/limits-5.5.0.tar.gz", hash = "sha256:ee269fedb078a904608b264424d9ef4ab10555acc8d090b6fc1db70e913327ea", size = 95514, upload-time = "2025-08-05T18:23:54.771Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/bf/68/ee314018c28da75ece5a639898b4745bd0687c0487fc465811f0c4b9cd44/limits-5.5.0-py3-none-any.whl", hash = "sha256:57217d01ffa5114f7e233d1f5e5bdc6fe60c9b24ade387bf4d5e83c5cf929bae", size = 60948, upload-time = "2025-08-05T18:23:53.335Z" },
-]
-
-[[package]]
-name = "markdown-it-py"
-version = "3.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "mdurl" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
-]
-
[[package]]
name = "markupsafe"
version = "3.0.2"
@@ -239,21 +275,12 @@ wheels = [
]
[[package]]
-name = "mdurl"
-version = "0.1.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
-]
-
-[[package]]
-name = "ordered-set"
-version = "4.1.0"
+name = "oauthlib"
+version = "3.3.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/4c/ca/bfac8bc689799bcca4157e0e0ced07e70ce125193fc2e166d2e685b7e2fe/ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8", size = 12826, upload-time = "2022-01-26T14:38:56.6Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/33/55/af02708f230eb77084a299d7b08175cff006dea4f2721074b92cdb0296c0/ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562", size = 7634, upload-time = "2022-01-26T14:38:48.677Z" },
+ { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" },
]
[[package]]
@@ -294,11 +321,11 @@ wheels = [
[[package]]
name = "pymysql"
-version = "1.1.1"
+version = "1.1.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/ce59b5e5ed4ce8512f879ff1fa5ab699d211ae2495f1adaa5fbba2a1eada/pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0", size = 47678, upload-time = "2024-05-21T11:03:43.722Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f5/ae/1fe3fcd9f959efa0ebe200b8de88b5a5ce3e767e38c7ac32fb179f16a388/pymysql-1.1.2.tar.gz", hash = "sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03", size = 48258, upload-time = "2025-08-24T12:55:55.146Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0c/94/e4181a1f6286f545507528c78016e00065ea913276888db2262507693ce5/PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c", size = 44972, upload-time = "2024-05-21T11:03:41.216Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9", size = 45300, upload-time = "2025-08-24T12:55:53.394Z" },
]
[[package]]
@@ -318,16 +345,40 @@ wheels = [
]
[[package]]
-name = "rich"
-version = "13.9.4"
+name = "python-dotenv"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "markdown-it-py" },
- { name = "pygments" },
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+]
+
+[[package]]
+name = "requests-oauthlib"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "oauthlib" },
+ { name = "requests" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" },
]
[[package]]
@@ -335,12 +386,13 @@ name = "scummvm-sites"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
+ { name = "authlib" },
{ name = "blinker" },
{ name = "cffi" },
{ name = "click" },
{ name = "cryptography" },
{ name = "flask" },
- { name = "flask-limiter" },
+ { name = "flask-dance" },
{ name = "iniconfig" },
{ name = "itsdangerous" },
{ name = "jinja2" },
@@ -351,6 +403,8 @@ dependencies = [
{ name = "pygments" },
{ name = "pymysql" },
{ name = "pytest" },
+ { name = "python-dotenv" },
+ { name = "requests" },
{ name = "setuptools" },
{ name = "werkzeug" },
{ name = "wheel" },
@@ -358,12 +412,13 @@ dependencies = [
[package.metadata]
requires-dist = [
+ { name = "authlib", specifier = ">=1.6.3" },
{ name = "blinker", specifier = ">=1.9.0" },
{ name = "cffi", specifier = ">=1.17.1" },
{ name = "click", specifier = ">=8.2.1" },
{ name = "cryptography", specifier = ">=45.0.6" },
- { name = "flask", specifier = ">=3.1.1" },
- { name = "flask-limiter", specifier = ">=3.12" },
+ { name = "flask", specifier = ">=3.1.2" },
+ { name = "flask-dance", specifier = ">=7.1.0" },
{ name = "iniconfig", specifier = ">=2.1.0" },
{ name = "itsdangerous", specifier = ">=2.2.0" },
{ name = "jinja2", specifier = ">=3.1.6" },
@@ -372,8 +427,10 @@ requires-dist = [
{ name = "pluggy", specifier = ">=1.6.0" },
{ name = "pycparser", specifier = ">=2.22" },
{ name = "pygments", specifier = ">=2.19.2" },
- { name = "pymysql", specifier = ">=1.1.1" },
+ { name = "pymysql", specifier = ">=1.1.2" },
{ name = "pytest", specifier = ">=8.4.1" },
+ { name = "python-dotenv", specifier = ">=1.1.1" },
+ { name = "requests", specifier = ">=2.32.5" },
{ name = "setuptools", specifier = ">=80.9.0" },
{ name = "werkzeug", specifier = ">=3.1.3" },
{ name = "wheel", specifier = ">=0.45.1" },
@@ -389,12 +446,21 @@ wheels = [
]
[[package]]
-name = "typing-extensions"
-version = "4.14.1"
+name = "urllib3"
+version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
+]
+
+[[package]]
+name = "urlobject"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2a/fd/163e6b835b9fabf9c3999f71c5f224daa9d68a38012cccd7ab2a2f861af9/urlobject-3.0.0.tar.gz", hash = "sha256:bfdfe70746d92a039a33e964959bb12cecd9807a434fdb7fef5f38e70a295818", size = 28237, upload-time = "2025-07-11T17:53:22.877Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/38/18c4bbe751a7357b3f6a33352e3af3305ad78f3e72ab7e3d667de4663ed9/urlobject-3.0.0-py3-none-any.whl", hash = "sha256:fd2465520d0a8c5ed983aa47518a2c5bcde0c276a4fd0eb28b0de5dcefd93b1e", size = 16261, upload-time = "2025-07-11T17:53:21.989Z" },
]
[[package]]
@@ -417,45 +483,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" },
]
-
-[[package]]
-name = "wrapt"
-version = "1.17.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" },
- { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" },
- { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" },
- { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" },
- { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" },
- { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" },
- { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" },
- { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" },
- { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" },
- { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" },
- { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" },
- { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" },
- { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" },
- { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" },
- { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690, upload-time = "2025-01-14T10:34:28.058Z" },
- { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861, upload-time = "2025-01-14T10:34:29.167Z" },
- { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174, upload-time = "2025-01-14T10:34:31.702Z" },
- { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721, upload-time = "2025-01-14T10:34:32.91Z" },
- { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763, upload-time = "2025-01-14T10:34:34.903Z" },
- { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585, upload-time = "2025-01-14T10:34:36.13Z" },
- { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676, upload-time = "2025-01-14T10:34:37.962Z" },
- { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871, upload-time = "2025-01-14T10:34:39.13Z" },
- { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312, upload-time = "2025-01-14T10:34:40.604Z" },
- { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062, upload-time = "2025-01-14T10:34:45.011Z" },
- { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155, upload-time = "2025-01-14T10:34:47.25Z" },
- { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471, upload-time = "2025-01-14T10:34:50.934Z" },
- { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208, upload-time = "2025-01-14T10:34:52.297Z" },
- { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339, upload-time = "2025-01-14T10:34:53.489Z" },
- { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232, upload-time = "2025-01-14T10:34:55.327Z" },
- { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476, upload-time = "2025-01-14T10:34:58.055Z" },
- { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377, upload-time = "2025-01-14T10:34:59.3Z" },
- { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986, upload-time = "2025-01-14T10:35:00.498Z" },
- { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750, upload-time = "2025-01-14T10:35:03.378Z" },
- { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" },
-]
More information about the Scummvm-git-logs
mailing list