Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
189c6ae1c7 | ||
|
|
b4fa3aa04d | ||
|
|
08624052aa | ||
|
|
1a3a2aef86 |
+2
-1
@@ -18,4 +18,5 @@ wheels/
|
|||||||
|
|
||||||
data
|
data
|
||||||
|
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
*.tar
|
||||||
+12
@@ -1,5 +1,17 @@
|
|||||||
# Eurovision 25 Backend Changelog
|
# Eurovision 25 Backend Changelog
|
||||||
|
|
||||||
|
## 1.0rc8 (2025-05-17)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Add more result types (b4fa3aa)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- Fix regression in db seeding (0862405)
|
||||||
|
|
||||||
|
### Chores
|
||||||
|
- Add docker exports to git ignore (1a3a2ae)
|
||||||
|
|
||||||
|
|
||||||
## 1.0rc7 (2025-05-16)
|
## 1.0rc7 (2025-05-16)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Eurovision 25 Homereview Backend
|
# Eurovision 25 Homereview Backend
|
||||||
|
|
||||||
**Version: 1.0rc7**
|
**Version: 1.0rc8**
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: eurovision-25-backend:1.0rc7
|
image: eurovision-25-backend:1.0rc8
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "Eurovision-25-backend"
|
name = "Eurovision-25-backend"
|
||||||
version = "1.0rc7"
|
version = "1.0rc8"
|
||||||
description = "Backend for Eurovision 25"
|
description = "Backend for Eurovision 25"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ from lib.db import init_db
|
|||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Eurovision 25 Homereview API",
|
title="Eurovision 25 Homereview API",
|
||||||
description="Backend API for Eurovision 25 Homereview application",
|
description="Backend API for Eurovision 25 Homereview application",
|
||||||
version="1.0rc7",
|
version="1.0rc8",
|
||||||
openapi_url="/sec_schema.json",
|
openapi_url="/sec_schema.json",
|
||||||
docs_url="/docut",
|
docs_url="/docut",
|
||||||
redoc_url=None,
|
redoc_url=None,
|
||||||
|
|||||||
+98
-3
@@ -11,8 +11,10 @@ DROP TABLE IF EXISTS "User"; -- Quoted because USER is a reserved keyword
|
|||||||
DROP TABLE IF EXISTS Team;
|
DROP TABLE IF EXISTS Team;
|
||||||
DROP TABLE IF EXISTS Contest;
|
DROP TABLE IF EXISTS Contest;
|
||||||
DROP TABLE IF EXISTS CountryCodes;
|
DROP TABLE IF EXISTS CountryCodes;
|
||||||
DROP VIEW IF EXISTS ReviewSummary;
|
DROP VIEW IF EXISTS ReviewSummaryGlobal;
|
||||||
DROP VIEW IF EXISTS ReviewSummaryByTeam;
|
DROP VIEW IF EXISTS ReviewSummaryByTeam;
|
||||||
|
DROP VIEW IF EXISTS ReviewSummaryByUser;
|
||||||
|
DROP VIEW IF EXISTS ReviewAllSongs;
|
||||||
|
|
||||||
DROP SEQUENCE IF EXISTS group_id_seq;
|
DROP SEQUENCE IF EXISTS group_id_seq;
|
||||||
DROP SEQUENCE IF EXISTS user_id_seq;
|
DROP SEQUENCE IF EXISTS user_id_seq;
|
||||||
@@ -177,6 +179,44 @@ JOIN CountryCodes cc ON s.country = cc.code
|
|||||||
GROUP BY r.song_id, u.team_id, cc.name_fi, cc.name_sv, s.artist, s.title
|
GROUP BY r.song_id, u.team_id, cc.name_fi, cc.name_sv, s.artist, s.title
|
||||||
ORDER BY avg_total_score DESC;
|
ORDER BY avg_total_score DESC;
|
||||||
|
|
||||||
|
CREATE VIEW ReviewSummaryByUser AS
|
||||||
|
SELECT
|
||||||
|
r.song_id,
|
||||||
|
cc.name_fi AS country_fi,
|
||||||
|
cc.name_sv AS country_sv,
|
||||||
|
s.artist,
|
||||||
|
s.title,
|
||||||
|
u.id AS user_id,
|
||||||
|
COUNT(*) AS total_reviews,
|
||||||
|
AVG(r.score_song) AS avg_score_song,
|
||||||
|
AVG(r.score_show) AS avg_score_show,
|
||||||
|
AVG(r.score_costume) AS avg_score_costume,
|
||||||
|
AVG((r.score_song + r.score_show + r.score_costume)/3) AS avg_total_score
|
||||||
|
FROM Review r
|
||||||
|
JOIN "User" u ON r.user_id = u.id
|
||||||
|
JOIN Song s ON r.song_id = s.id
|
||||||
|
JOIN CountryCodes cc ON s.country = cc.code
|
||||||
|
GROUP BY r.song_id, u.id, cc.name_fi, cc.name_sv, s.artist, s.title
|
||||||
|
ORDER BY avg_total_score DESC;
|
||||||
|
|
||||||
|
CREATE VIEW ReviewAllSongs AS
|
||||||
|
SELECT
|
||||||
|
r.song_id,
|
||||||
|
cc.name_fi AS country_fi,
|
||||||
|
cc.name_sv AS country_sv,
|
||||||
|
s.artist,
|
||||||
|
s.title,
|
||||||
|
COUNT(*) AS total_reviews,
|
||||||
|
AVG(r.score_song) AS avg_score_song,
|
||||||
|
AVG(r.score_show) AS avg_score_show,
|
||||||
|
AVG(r.score_costume) AS avg_score_costume,
|
||||||
|
AVG((r.score_song + r.score_show + r.score_costume)/3) AS avg_total_score
|
||||||
|
FROM Review r
|
||||||
|
JOIN Song s ON r.song_id = s.id
|
||||||
|
JOIN CountryCodes cc ON s.country = cc.code
|
||||||
|
GROUP BY r.song_id, cc.name_fi, cc.name_sv, s.artist, s.title
|
||||||
|
ORDER BY avg_total_score DESC;
|
||||||
|
|
||||||
-- Insert country codes data
|
-- Insert country codes data
|
||||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ALB', 'Albania', 'Albania', 'Albanien');
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ALB', 'Albania', 'Albania', 'Albanien');
|
||||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ARM', 'Armenia', 'Armenia', 'Armenien');
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ARM', 'Armenia', 'Armenia', 'Armenien');
|
||||||
@@ -218,7 +258,61 @@ INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('GBR', 'Unite
|
|||||||
|
|
||||||
|
|
||||||
-- TODO: Clean this next year. Its now here just to get the show running
|
-- TODO: Clean this next year. Its now here just to get the show running
|
||||||
INSERT INTO Contest (year, finals_date) VALUES (2025, '2025-05-16');
|
-- INSERT INTO Contest (year, finals_date) VALUES (2025, '2025-05-16');
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- DEBUG DATA
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- Insert Sample Teams
|
||||||
|
-- INSERT INTO "Team" (name) VALUES ('The Eurovisionaries');
|
||||||
|
-- INSERT INTO "Team" (name) VALUES ('douze_points_posse');
|
||||||
|
|
||||||
|
-- -- Insert Sample Users (Passwords are 'password' hashed with bcrypt - for debug only!)
|
||||||
|
-- -- For Team 1 (The Eurovisionaries)
|
||||||
|
-- INSERT INTO "User" (username, first_name, last_name, hashed_password, email, team_id, avatar_id, is_admin)
|
||||||
|
-- VALUES ('debuguser1', 'Debug', 'UserOne', '$2b$12$7.xP5Y6.K7kZ5yL0HjEPIOjBqzsRcg8yU/gQcNcmauHAFsdn7Y2tu', '[email protected]', (SELECT id FROM "Team" WHERE name = 'The Eurovisionaries'), 1, FALSE);
|
||||||
|
-- INSERT INTO "User" (username, first_name, last_name, hashed_password, email, team_id, avatar_id, is_admin)
|
||||||
|
-- VALUES ('debugadmin', 'Debug', 'Admin', '$2b$12$7.xP5Y6.K7kZ5yL0HjEPIOjBqzsRcg8yU/gQcNcmauHAFsdn7Y2tu', '[email protected]', (SELECT id FROM "Team" WHERE name = 'The Eurovisionaries'), 2, TRUE);
|
||||||
|
|
||||||
|
-- -- For Team 2 (douze_points_posse)
|
||||||
|
-- INSERT INTO "User" (username, first_name, last_name, hashed_password, email, team_id, avatar_id, is_admin)
|
||||||
|
-- VALUES ('debuguser2', 'Another', 'Debugger', '$2b$12$7.xP5Y6.K7kZ5yL0HjEPIOjBqzsRcg8yU/gQcNcmauHAFsdn7Y2tu', '[email protected]', (SELECT id FROM "Team" WHERE name = 'douze_points_posse'), 3, FALSE);
|
||||||
|
|
||||||
|
-- -- Insert Sample Songs for Contest 2025
|
||||||
|
-- -- Assuming a contest with id=1 (or use (SELECT id FROM Contest WHERE year = 2025) if only one entry)
|
||||||
|
-- INSERT INTO Song (year, country, artist, title, running_order, img_url, language, confidence)
|
||||||
|
-- VALUES
|
||||||
|
-- (2025, 'SWE', 'Swedish Singers', 'Dancing in Stockholm', 1, 'https://example.com/swe.jpg', 'English', 0.95),
|
||||||
|
-- (2025, 'FIN', 'Finnish Rockers', 'Sauna Anthem', 2, 'https://example.com/fin.jpg', 'Finnish', 0.90),
|
||||||
|
-- (2025, 'NOR', 'Nordic Balladeers', 'Fjord Dreams', 3, 'https://example.com/nor.jpg', 'Norwegian', 0.85),
|
||||||
|
-- (2025, 'ITA', 'Italian Pop Sensations', 'Roman Holiday', 4, 'https://example.com/ita.jpg', 'Italian', 0.92),
|
||||||
|
-- (2025, 'GER', 'German Electro Duo', 'Berlin Beats', 5, 'https://example.com/ger.jpg', 'German', 0.88);
|
||||||
|
|
||||||
|
-- -- Insert Sample Reviews
|
||||||
|
-- -- User 1 reviews Song 1 and Song 2
|
||||||
|
-- INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review)
|
||||||
|
-- VALUES
|
||||||
|
-- ((SELECT id FROM "User" WHERE username = 'debuguser1'), (SELECT id FROM Song WHERE title = 'Dancing in Stockholm'), 80, 75, 70, 'Great song!'),
|
||||||
|
-- ((SELECT id FROM "User" WHERE username = 'debuguser1'), (SELECT id FROM Song WHERE title = 'Sauna Anthem'), 90, 85, 80, 'Loved the energy!');
|
||||||
|
|
||||||
|
-- -- User 2 reviews Song 2 and Song 3
|
||||||
|
-- INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review)
|
||||||
|
-- VALUES
|
||||||
|
-- ((SELECT id FROM "User" WHERE username = 'debuguser2'), (SELECT id FROM Song WHERE title = 'Sauna Anthem'), 88, 82, 78, 'Rock on Finland!'),
|
||||||
|
-- ((SELECT id FROM "User" WHERE username = 'debuguser2'), (SELECT id FROM Song WHERE title = 'Fjord Dreams'), 70, 65, 60, 'A bit slow for me.');
|
||||||
|
|
||||||
|
-- -- Admin reviews Song 1
|
||||||
|
-- INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review)
|
||||||
|
-- VALUES
|
||||||
|
-- ((SELECT id FROM "User" WHERE username = 'debugadmin'), (SELECT id FROM Song WHERE title = 'Dancing in Stockholm'), 99, 92, 1, 'Absolutely stellar! A clear frontrunner!'),
|
||||||
|
-- -- More reviews with varied scores
|
||||||
|
-- ((SELECT id FROM "User" WHERE username = 'debuguser1'), (SELECT id FROM Song WHERE title = 'Roman Holiday'), 42, 23, 23, 'The song was okay, staging average, costumes needed work.'),
|
||||||
|
-- ((SELECT id FROM "User" WHERE username = 'debuguser1'), (SELECT id FROM Song WHERE title = 'Berlin Beats'), 96, 91, 87, 'Incredible energy and visuals! Loved it!'),
|
||||||
|
-- ((SELECT id FROM "User" WHERE username = 'debuguser2'), (SELECT id FROM Song WHERE title = 'Dancing in Stockholm'), 68, 77, 53, 'Decent entry, but not outstanding. Costumes were a bit plain.'),
|
||||||
|
-- ((SELECT id FROM "User" WHERE username = 'debuguser2'), (SELECT id FROM Song WHERE title = 'Roman Holiday'), 12, 33, 7, 'Really disliked this one. Song was boring, show forgettable, and costumes awful.'),
|
||||||
|
-- ((SELECT id FROM "User" WHERE username = 'debugadmin'), (SELECT id FROM Song WHERE title = 'Sauna Anthem'), 100, 11, 99, 'Perfection! Finland has outdone themselves!'),
|
||||||
|
-- ((SELECT id FROM "User" WHERE username = 'debugadmin'), (SELECT id FROM Song WHERE title = 'Berlin Beats'), 2, 94, 78, 'Strong contender. Great production, though costumes could be more imaginative.');
|
||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
-- Views (Optional - Not Created Here Based on Documentation Decision)
|
-- Views (Optional - Not Created Here Based on Documentation Decision)
|
||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
@@ -226,4 +320,5 @@ INSERT INTO Contest (year, finals_date) VALUES (2025, '2025-05-16');
|
|||||||
-- layer. No CREATE VIEW statements are included.
|
-- layer. No CREATE VIEW statements are included.
|
||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
|
|
||||||
-- End of Schema Definition
|
-- End of Schema Definition
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -36,12 +36,13 @@ def seed_db() -> None:
|
|||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
logger.debug(f"Inserting song {idx + 1} of {len(data)}", item)
|
logger.debug(f"Inserting song {idx + 1} of {len(data)}", item)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO Song (year, artist, country, title, lyrics_url, artist_url, flag_url, running_order, lyrics_original, lyrics_translation_fi, tags, confidence, language) VALUES (2025, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
"INSERT INTO Song (year, artist, country, title, lyrics_url, img_url, artist_url, flag_url, running_order, lyrics_original, lyrics_translation_fi, tags, confidence, language) VALUES (2025, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
(
|
(
|
||||||
item["artist"],
|
item["artist"],
|
||||||
item["country"],
|
item["country"],
|
||||||
item["title"],
|
item["title"],
|
||||||
item["lyrics_url"],
|
item["lyrics_url"],
|
||||||
|
item["img_url"],
|
||||||
item["artist_url"],
|
item["artist_url"],
|
||||||
item["flag_url"],
|
item["flag_url"],
|
||||||
item["running_order"],
|
item["running_order"],
|
||||||
|
|||||||
+111
-2
@@ -1,13 +1,110 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
from enum import Enum
|
||||||
|
import duckdb # Added for specific exception handling
|
||||||
|
|
||||||
from models.result import Result, TeamResult
|
from models.result import Result, TeamResult
|
||||||
|
from models.msg import Message
|
||||||
from lib.db import get_connection
|
from lib.db import get_connection
|
||||||
|
from lib.logger import logger
|
||||||
|
|
||||||
router = APIRouter(prefix="/results", tags=["results"])
|
router = APIRouter(prefix="/results", tags=["results"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list[Result])
|
# It's good practice for FastAPI to have Enum derive from str as well
|
||||||
async def list_results():
|
class ResultType(str, Enum):
|
||||||
|
DEVIATION = "deviation"
|
||||||
|
COSTUME = "costume"
|
||||||
|
SHOW = "show"
|
||||||
|
SONG = "song"
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{result_type}", response_model=list[Result] | Message)
|
||||||
|
async def list_results(result_type: ResultType):
|
||||||
|
query = ""
|
||||||
|
try:
|
||||||
|
with get_connection() as conn:
|
||||||
|
if result_type == ResultType.DEVIATION:
|
||||||
|
# Selects the song with the highest standard deviation in its total review scores
|
||||||
|
query = """
|
||||||
|
WITH SongReviewDeviations AS (
|
||||||
|
SELECT
|
||||||
|
song_id,
|
||||||
|
COALESCE(STDDEV_SAMP((score_song + score_show + score_costume) / 3.0), 0) AS deviation_value
|
||||||
|
FROM Review
|
||||||
|
GROUP BY song_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ras.song_id,
|
||||||
|
ras.country_fi,
|
||||||
|
ras.country_sv,
|
||||||
|
ras.artist,
|
||||||
|
ras.title,
|
||||||
|
ras.avg_score_song,
|
||||||
|
ras.avg_score_show,
|
||||||
|
ras.avg_score_costume,
|
||||||
|
ras.avg_total_score
|
||||||
|
FROM ReviewAllSongs ras
|
||||||
|
JOIN SongReviewDeviations srd ON ras.song_id = srd.song_id
|
||||||
|
ORDER BY srd.deviation_value DESC
|
||||||
|
LIMIT 1;
|
||||||
|
"""
|
||||||
|
df = conn.execute(query).fetchdf()
|
||||||
|
logger.debug(f"Query for DEVIATION: {query}")
|
||||||
|
logger.debug(f"Result DataFrame for DEVIATION: {df}")
|
||||||
|
elif result_type == ResultType.COSTUME:
|
||||||
|
# Selects the song with the highest average costume score
|
||||||
|
query = """SELECT * FROM ReviewAllSongs ORDER BY avg_score_costume DESC LIMIT 1 """
|
||||||
|
df = conn.execute(query).fetchdf()
|
||||||
|
elif result_type == ResultType.SHOW:
|
||||||
|
# Selects the song with the highest average show score
|
||||||
|
query = """SELECT * FROM ReviewAllSongs ORDER BY avg_score_show DESC LIMIT 1 """
|
||||||
|
df = conn.execute(query).fetchdf()
|
||||||
|
elif result_type == ResultType.SONG:
|
||||||
|
# Selects the song with the highest average song score
|
||||||
|
query = """SELECT * FROM ReviewAllSongs ORDER BY avg_score_song DESC LIMIT 1 """
|
||||||
|
df = conn.execute(query).fetchdf()
|
||||||
|
else:
|
||||||
|
# This case should ideally not be reached if using Enums properly with FastAPI
|
||||||
|
return Message(type="error", message="Invalid result type")
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
# If the dataframe is empty, return an empty list.
|
||||||
|
# This applies if a view exists but has no data, or LIMIT 1 returns no row.
|
||||||
|
return []
|
||||||
|
|
||||||
|
# The existing code implies Result model can handle various structures.
|
||||||
|
# For DEVIATION, row will be {'deviation_value': X}
|
||||||
|
# For COSTUME/SHOW/SONG (LIMIT 1), it will be a single row from ReviewAllSongs.
|
||||||
|
# For others, it will be multiple rows from their respective views.
|
||||||
|
# All these are converted to a list of Result objects.
|
||||||
|
results = [Result(**row) for row in df.to_dict(orient="records")]
|
||||||
|
return results
|
||||||
|
|
||||||
|
except duckdb.CatalogException as e:
|
||||||
|
# Handles errors like "View not found" (e.g., ReviewAllSongs doesn't exist)
|
||||||
|
# You might want to log the error e
|
||||||
|
print(f"Database Catalog Error: {e}")
|
||||||
|
return Message(
|
||||||
|
type="error",
|
||||||
|
message=f"Data source for '{result_type.value}' not found or query error. Details: {str(e)}",
|
||||||
|
)
|
||||||
|
except duckdb.Error as e: # Catch other DuckDB specific errors
|
||||||
|
print(f"DuckDB Error: {e}")
|
||||||
|
return Message(
|
||||||
|
type="error",
|
||||||
|
message=f"Database query error for '{result_type.value}'. Details: {str(e)}",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# Catch any other unexpected errors
|
||||||
|
print(f"Unexpected Error: {e}")
|
||||||
|
return Message(
|
||||||
|
type="error",
|
||||||
|
message=f"An unexpected error occurred while fetching results for '{result_type.value}'.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/global", response_model=list[Result])
|
||||||
|
async def list_global_results():
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
df = conn.execute("SELECT * FROM ReviewSummaryGlobal").fetchdf()
|
df = conn.execute("SELECT * FROM ReviewSummaryGlobal").fetchdf()
|
||||||
if df.empty:
|
if df.empty:
|
||||||
@@ -16,6 +113,18 @@ async def list_results():
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/user/{user_id}", response_model=list[Result])
|
||||||
|
async def list_user_results(user_id: int):
|
||||||
|
with get_connection() as conn:
|
||||||
|
df = conn.execute(
|
||||||
|
"SELECT * FROM ReviewSummaryByUser WHERE user_id = ?", (user_id,)
|
||||||
|
).fetchdf()
|
||||||
|
if df.empty:
|
||||||
|
return []
|
||||||
|
results = [Result(**row) for row in df.to_dict(orient="records")]
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
@router.get("/team/{team_id}", response_model=list[TeamResult])
|
@router.get("/team/{team_id}", response_model=list[TeamResult])
|
||||||
async def list_team_results(team_id: int):
|
async def list_team_results(team_id: int):
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eurovision-25-backend"
|
name = "eurovision-25-backend"
|
||||||
version = "1.0rc7"
|
version = "1.0rc8"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiofiles" },
|
{ name = "aiofiles" },
|
||||||
|
|||||||
Reference in New Issue
Block a user