Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 162 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ uvicorn = "^0.34.0"
sqlmodel = "^0.0.24"
psycopg2-binary = "^2.9.10"
hashids = "^1.3.1"
arrow = "^1.3.0"
rapidfuzz = "^3.13.0"


[tool.poetry.group.dev.dependencies]
Expand Down
20 changes: 20 additions & 0 deletions sweepy-ui/src/components/SweepstakeTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ function SweepstakeTable({ data }) {
setExpandedRow(expandedRow === index ? null : index);
};

const stringifyScore = (score) => {
if (score === null || score === undefined) {
return "-";
} else if (score == 0) {
return "E";
} else if (score > 0) {
return `+${score}`;
} else {
return `${score}`;
}
};

const sortedParticipants = [...data.participants].sort((a, b) => {
return parseFloat(b.equity) - parseFloat(a.equity);
});
Expand Down Expand Up @@ -39,6 +51,9 @@ function SweepstakeTable({ data }) {
<tr>
<th className="px-4 py-2 text-left">Runner</th>
<th className="px-4 py-2 text-left">Probability</th>
{data.tournament_id && (
<th className="px-4 py-2 text-left">Score</th>
)}
</tr>
</thead>
<tbody>
Expand All @@ -51,6 +66,11 @@ function SweepstakeTable({ data }) {
).toFixed(2)}
%
</td>
{data.tournament_id && (
<td className="px-4 py-2">
{stringifyScore(assignment.score)}
</td>
)}
</tr>
))}
</tbody>
Expand Down
50 changes: 44 additions & 6 deletions sweepy/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from sweepy.database import get_session, init_db
from sweepy import db_models, tasks
from sweepy.integrations.betfair import BetfairClient
from sweepy.integrations.live_golf import LiveGolfClient
from sweepy import matchmaker
from sweepy.models import (
SweepstakesRequest,
Sweepstakes,
Expand All @@ -29,11 +31,12 @@
logging.basicConfig(level=logging.INFO)

__bf_client = None
__live_golf_client = None


@asynccontextmanager
async def lifespan(app: FastAPI):
global __bf_client
global __bf_client, __live_golf_client

logging.info("Starting up the FastAPI application.")

Expand All @@ -45,12 +48,19 @@ async def lifespan(app: FastAPI):

logging.info("Betfair client initialized.")

__live_golf_client = LiveGolfClient(api_key=os.getenv("LIVE_GOLF_API_KEY"))

logging.info("Live Golf client initialized.")

recreate_db = os.getenv("RECREATE_DB", "false").lower() == "true"
init_db(recreate=recreate_db)
logging.info("Database initialized.")

logging.info("Starting the sweepstakes refresh task.")
asyncio.create_task(tasks.refresh_all_sweepstakes_task(__bf_client))
logging.info("Starting the sweepstakes refresh odds task.")
asyncio.create_task(tasks.refresh_all_odds_task(__bf_client))

logging.info("Starting the sweepstakes refresh scores task.")
asyncio.create_task(tasks.refresh_all_scores_task(__live_golf_client))

yield

Expand Down Expand Up @@ -100,8 +110,9 @@ def create_sweepstakes(

try:
sweepstakes_db = generate_sweepstakes.generate_sweepstakes(
__bf_client, request, session
__bf_client, __live_golf_client, request, session
)

except (
MarketNotFoundException,
NotEnoughSelectionsException,
Expand All @@ -110,6 +121,33 @@ def create_sweepstakes(
logging.error(f"Error generating sweepstake: {e}")
raise HTTPException(status_code=400, detail=str(e))

if sweepstakes_db.has_leaderboard:
logging.info("Sweepstake has leaderboard data, updating Live Golf matching.")
try:
matchmaker.match_runners_to_live_golf(
runners=sweepstakes_db.runners,
lg_tournament_id=sweepstakes_db.tournament_id,
season=sweepstakes_db.start_date.year,
lg_client=__live_golf_client,
session=session,
)

generate_sweepstakes.refresh_sweepstake_leaderboard(
client=__live_golf_client,
sweepstake_db=sweepstakes_db,
session=session,
)
except Exception as e:
logging.error(f"Error updating Live Golf matching: {e}")
session.rollback()
raise HTTPException(
status_code=500, detail="Failed to update Live Golf matching."
)
else:
logging.info(
"Sweepstake does not have a leaderboard, skipping Live Golf matching."
)

session.refresh(sweepstakes_db)

response = generate_sweepstakes.convert_db_model_to_response(sweepstakes_db)
Expand Down Expand Up @@ -195,7 +233,7 @@ def refresh_sweepstake(

logging.info(f"Refreshing sweepstake: {sweepstake.stringified_id}")

updated_sweepstake = generate_sweepstakes.refresh_sweepstake(
updated_sweepstake = generate_sweepstakes.refresh_sweepstake_odds(
__bf_client, sweepstake, session
)

Expand Down Expand Up @@ -268,7 +306,7 @@ def get_outright_markets(event_type_id: str):

logging.info(f"Found {len(markets)} markets for event type {event_type_id}")

deduplicated_markets = list(set([MarketInfo(**market) for market in markets]))
deduplicated_markets = list(set(market for market in markets))
deduplicated_markets.sort(key=lambda x: (x.event_name, x.market_name))

logging.info(f"Deduplicated markets count: {len(deduplicated_markets)}")
Expand Down
29 changes: 29 additions & 0 deletions sweepy/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,32 @@ def wrapper(self):
return wrapper

return decorator


def timed_cache(ttl_seconds: int):
"""
Decorator to cache the result of a function for a specified time-to-live (TTL) in seconds.
After the TTL expires, the function will be recomputed.
"""

if ttl_seconds <= 0 or not isinstance(ttl_seconds, int):
raise ValueError("TTL must be an integer greater than 0 seconds")

def decorator(func):
cache = {}
last_update = {}

@wraps(func)
def wrapper(*args, **kwargs):
current_time = time.time()
key = (args, frozenset(kwargs.items()))

if key not in cache or (current_time - last_update[key]) > ttl_seconds:
cache[key] = func(*args, **kwargs)
last_update[key] = current_time

return cache[key]

return wrapper

return decorator
56 changes: 0 additions & 56 deletions sweepy/cli.py

This file was deleted.

37 changes: 31 additions & 6 deletions sweepy/database.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from contextlib import contextmanager
from sqlmodel import SQLModel, create_engine, Session
import os

from dotenv import load_dotenv


load_dotenv()

DATABASE_URL = os.getenv("DATABASE_URL")
DEV_MODE = os.getenv("ENVIRONMENT") == "development"

if os.getenv("ENVIRONMENT") == "development":
if DEV_MODE:
connect_args = {}
else:
connect_args = {"sslmode": "require"}
Expand All @@ -20,16 +23,38 @@


def init_db(recreate: bool = False):
if recreate:
SQLModel.metadata.drop_all(engine)
print(f"Initialising database: {DEV_MODE=} {DATABASE_URL=}")

# Needed for db refresh

try:
if recreate:
SQLModel.metadata.drop_all(engine)
print("Dropped existing database tables.")

SQLModel.metadata.create_all(engine)
print("Created database tables.")

except Exception as e:
print(f"Error initializing database: {e}")

else:
print("Database recreated.")


SQLModel.metadata.create_all(engine)
@contextmanager
def get_session_context():
session = Session(engine)
try:
yield session
finally:
session.close()


def get_session():
return Session(engine)


if __name__ == "__main__":
init_db(recreate=True)
print("Database initialized.")
recreate_db = os.getenv("RECREATE_DB", "false").lower() == "true"
init_db(recreate=recreate_db)
26 changes: 25 additions & 1 deletion sweepy/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ class Sweepstakes(SQLModel, table=True):
competition: str
method: str
active: bool
start_date: datetime.datetime = Field(
sa_column=Column(DateTime(timezone=True)),
)
updated_at: datetime.datetime = Field(
sa_column=Column(DateTime(timezone=True)),
default_factory=lambda: datetime.datetime.now(datetime.timezone.utc),
)
participants: List["Participant"] = Relationship(back_populates="sweepstake")
tournament_id: Optional[str] = None

@property
def stringified_id(self) -> Optional[str]:
Expand All @@ -41,6 +45,24 @@ def decode_stringified_id(stringified_id: str) -> Optional[int]:
except IndexError:
return None

@property
def runners(self) -> List["Runner"]:
"""
Returns the list of runners associated with the sweepstake.
"""
return [
runner
for participant in self.participants
for runner in participant.runners
]

@property
def has_leaderboard(self) -> bool:
"""
Returns whether the sweepstake has score data.
"""
return self.tournament_id is not None


class Participant(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
Expand Down Expand Up @@ -79,7 +101,9 @@ class ParticipantOdds(SQLModel, table=True):
class Runner(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
provider_id: str
market_provider_id: str
score_provider_id: Optional[str] = None
score: Optional[int] = None
participant_id: Optional[int] = Field(default=None, foreign_key="participant.id")

participant: Optional["Participant"] = Relationship(back_populates="runners")
Expand Down
Loading