Migrate to spectree for pydantic integration
							parent
							
								
									b76d2786e5
								
							
						
					
					
						commit
						283624706e
					
				| 
						 | 
				
			
			@ -1,11 +1,11 @@
 | 
			
		|||
from flask import Blueprint, Flask, make_response, request
 | 
			
		||||
from flask_sqlalchemy import SQLAlchemy
 | 
			
		||||
from flask_cors import CORS
 | 
			
		||||
 | 
			
		||||
import login
 | 
			
		||||
import schedule
 | 
			
		||||
import team
 | 
			
		||||
from models import init_db
 | 
			
		||||
from spec import spec
 | 
			
		||||
 | 
			
		||||
app = Flask(__name__)
 | 
			
		||||
CORS(app, origins=["http://localhost:5173"], supports_credentials=True)
 | 
			
		||||
| 
						 | 
				
			
			@ -28,3 +28,4 @@ def debug_set_cookie():
 | 
			
		|||
    return res, 200
 | 
			
		||||
 | 
			
		||||
app.register_blueprint(api)
 | 
			
		||||
spec.register(app)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,43 +15,6 @@ STEAM_OPENID_URL = "https://steamcommunity.com/openid/login"
 | 
			
		|||
def index():
 | 
			
		||||
    return "test"
 | 
			
		||||
 | 
			
		||||
def get_steam_login_url(return_to):
 | 
			
		||||
    """Build the Steam OpenID URL for login"""
 | 
			
		||||
    params = {
 | 
			
		||||
        "openid.ns": "http://specs.openid.net/auth/2.0",
 | 
			
		||||
        "openid.mode": "checkid_setup",
 | 
			
		||||
        "openid.return_to": return_to,
 | 
			
		||||
        "openid.identity": "http://specs.openid.net/auth/2.0/identifier_select",
 | 
			
		||||
        "openid.claimed_id": "http://specs.openid.net/auth/2.0/identifier_select",
 | 
			
		||||
    }
 | 
			
		||||
    return f"{STEAM_OPENID_URL}?{urllib.parse.urlencode(params)}"
 | 
			
		||||
 | 
			
		||||
#@api_login.get("/steam/")
 | 
			
		||||
#def steam_login():
 | 
			
		||||
#    return_to = url_for("api.login.steam_login_callback", _external=True)
 | 
			
		||||
#    steam_login_url = get_steam_login_url(return_to)
 | 
			
		||||
#    return redirect(steam_login_url)
 | 
			
		||||
#
 | 
			
		||||
#@api_login.get("/steam/callback/")
 | 
			
		||||
#def steam_login_callback():
 | 
			
		||||
#    params = request.args.to_dict()
 | 
			
		||||
#    params["openid.mode"] = "check_authentication"
 | 
			
		||||
#    response = requests.post(STEAM_OPENID_URL, data=params)
 | 
			
		||||
#
 | 
			
		||||
#    # Check if authentication was successful
 | 
			
		||||
#    if "is_valid:true" in response.text:
 | 
			
		||||
#        claimed_id = request.args.get("openid.claimed_id")
 | 
			
		||||
#        steam_id = extract_steam_id_from_response(claimed_id)
 | 
			
		||||
#        print("User logged in as", steam_id)
 | 
			
		||||
#
 | 
			
		||||
#        player = create_or_get_user_from_steam_id(int(steam_id))
 | 
			
		||||
#        auth_session = create_auth_session_for_player(player)
 | 
			
		||||
#
 | 
			
		||||
#        resp = make_response("Logged in")
 | 
			
		||||
#        resp.set_cookie("auth", auth_session.key, secure=True, httponly=True)
 | 
			
		||||
#        return resp
 | 
			
		||||
#    return "no"
 | 
			
		||||
 | 
			
		||||
@api_login.post("/authenticate")
 | 
			
		||||
def steam_authenticate():
 | 
			
		||||
    params = request.get_json()
 | 
			
		||||
| 
						 | 
				
			
			@ -64,7 +27,6 @@ def steam_authenticate():
 | 
			
		|||
        steam_id = int(extract_steam_id_from_response(claimed_id))
 | 
			
		||||
        print("User logged in as", steam_id)
 | 
			
		||||
 | 
			
		||||
        #player = create_or_get_user_from_steam_id(int(steam_id))
 | 
			
		||||
        player = db.session.query(
 | 
			
		||||
            Player
 | 
			
		||||
        ).where(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,8 @@ from sqlalchemy import TIMESTAMP, BigInteger, Boolean, Enum, ForeignKey, Foreign
 | 
			
		|||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
 | 
			
		||||
from sqlalchemy_utc import UtcDateTime
 | 
			
		||||
 | 
			
		||||
import spec
 | 
			
		||||
 | 
			
		||||
class Base(DeclarativeBase):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -34,6 +36,11 @@ class Player(db.Model):
 | 
			
		|||
 | 
			
		||||
    created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
 | 
			
		||||
 | 
			
		||||
class PlayerSpec(spec.BaseModel):
 | 
			
		||||
    steam_id: str
 | 
			
		||||
    username: str
 | 
			
		||||
    #teams: list["PlayerTeamSpec"]
 | 
			
		||||
 | 
			
		||||
class Team(db.Model):
 | 
			
		||||
    __tablename__ = "teams"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +52,12 @@ class Team(db.Model):
 | 
			
		|||
 | 
			
		||||
    created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
 | 
			
		||||
 | 
			
		||||
class TeamSpec(spec.BaseModel):
 | 
			
		||||
    id: int
 | 
			
		||||
    team_name: str
 | 
			
		||||
    discord_webhook_url: str | None
 | 
			
		||||
    #players: list[PlayerTeamSpec] | None
 | 
			
		||||
 | 
			
		||||
class PlayerTeam(db.Model):
 | 
			
		||||
    __tablename__ = "players_teams"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,19 @@
 | 
			
		|||
flask
 | 
			
		||||
 | 
			
		||||
# CORS
 | 
			
		||||
Flask-CORS
 | 
			
		||||
 | 
			
		||||
# ORM
 | 
			
		||||
sqlalchemy
 | 
			
		||||
Flask-SQLAlchemy
 | 
			
		||||
SQLAlchemy-Utc
 | 
			
		||||
 | 
			
		||||
# form/data validation
 | 
			
		||||
pydantic
 | 
			
		||||
Flask-Pydantic
 | 
			
		||||
spectree  # generates OpenAPI documents for us to make TypeScript API clients
 | 
			
		||||
          # based on our pydantic models
 | 
			
		||||
 | 
			
		||||
# DB migrations
 | 
			
		||||
alembic
 | 
			
		||||
Flask-Migrate
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,28 +1,25 @@
 | 
			
		|||
import datetime
 | 
			
		||||
from typing import cast
 | 
			
		||||
from flask import Blueprint, abort, jsonify, make_response, request
 | 
			
		||||
import pydantic
 | 
			
		||||
from flask_pydantic import validate
 | 
			
		||||
from models import Player, PlayerTeam, PlayerTeamAvailability, PlayerTeamRole, db
 | 
			
		||||
 | 
			
		||||
from middleware import requires_authentication
 | 
			
		||||
import models
 | 
			
		||||
import utc
 | 
			
		||||
from spec import spec, BaseModel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
api_schedule = Blueprint("schedule", __name__, url_prefix="/schedule")
 | 
			
		||||
 | 
			
		||||
class ViewScheduleForm(pydantic.BaseModel):
 | 
			
		||||
class ViewScheduleForm(BaseModel):
 | 
			
		||||
    window_start: datetime.datetime
 | 
			
		||||
    team_id: int
 | 
			
		||||
    window_size_days: int = 7
 | 
			
		||||
 | 
			
		||||
@api_schedule.get("/")
 | 
			
		||||
@validate(query=ViewScheduleForm)
 | 
			
		||||
@spec.validate()
 | 
			
		||||
@requires_authentication
 | 
			
		||||
def get(query: ViewScheduleForm, *args, **kwargs):
 | 
			
		||||
def get(query: ViewScheduleForm, player: Player, **kwargs):
 | 
			
		||||
    window_start = query.window_start
 | 
			
		||||
    window_end = window_start + datetime.timedelta(days=query.window_size_days)
 | 
			
		||||
    player: Player = kwargs["player"]
 | 
			
		||||
 | 
			
		||||
    availability_regions = db.session.query(
 | 
			
		||||
        PlayerTeamAvailability
 | 
			
		||||
| 
						 | 
				
			
			@ -65,7 +62,7 @@ def get(query: ViewScheduleForm, *args, **kwargs):
 | 
			
		|||
        "availability": availability
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
class PutScheduleForm(pydantic.BaseModel):
 | 
			
		||||
class PutScheduleForm(BaseModel):
 | 
			
		||||
    window_start: datetime.datetime
 | 
			
		||||
    window_size_days: int = 7
 | 
			
		||||
    team_id: int
 | 
			
		||||
| 
						 | 
				
			
			@ -91,17 +88,14 @@ def find_consecutive_blocks(arr: list[int]) -> list[tuple[int, int, int]]:
 | 
			
		|||
    return blocks
 | 
			
		||||
 | 
			
		||||
@api_schedule.put("/")
 | 
			
		||||
@validate(body=PutScheduleForm, get_json_params={})
 | 
			
		||||
@spec.validate()
 | 
			
		||||
@requires_authentication
 | 
			
		||||
def put(body: PutScheduleForm, **kwargs):
 | 
			
		||||
    window_start = body.window_start.replace(tzinfo=utc.utc)
 | 
			
		||||
    window_end = window_start + datetime.timedelta(days=body.window_size_days)
 | 
			
		||||
    player: Player = kwargs["player"]
 | 
			
		||||
    if not player:
 | 
			
		||||
        abort(400)
 | 
			
		||||
def put(json: PutScheduleForm, player: Player, **kwargs):
 | 
			
		||||
    window_start = json.window_start
 | 
			
		||||
    window_end = window_start + datetime.timedelta(days=json.window_size_days)
 | 
			
		||||
 | 
			
		||||
    # TODO: add error message
 | 
			
		||||
    if len(body.availability) != 168:
 | 
			
		||||
    if len(json.availability) != 168:
 | 
			
		||||
        abort(400, {
 | 
			
		||||
            "error": "Availability must be length " + str(168)
 | 
			
		||||
        })
 | 
			
		||||
| 
						 | 
				
			
			@ -111,7 +105,7 @@ def put(body: PutScheduleForm, **kwargs):
 | 
			
		|||
    ).where(
 | 
			
		||||
        PlayerTeamAvailability.player_id == player.steam_id
 | 
			
		||||
    ).where(
 | 
			
		||||
        PlayerTeamAvailability.team_id == body.team_id
 | 
			
		||||
        PlayerTeamAvailability.team_id == json.team_id
 | 
			
		||||
    ).where(
 | 
			
		||||
        PlayerTeamAvailability.start_time.between(window_start, window_end) |
 | 
			
		||||
            PlayerTeamAvailability.end_time.between(window_start, window_end)
 | 
			
		||||
| 
						 | 
				
			
			@ -148,7 +142,7 @@ def put(body: PutScheduleForm, **kwargs):
 | 
			
		|||
    # create time regions inside our window based on the availability array
 | 
			
		||||
    availability_blocks = []
 | 
			
		||||
 | 
			
		||||
    for block in find_consecutive_blocks(body.availability):
 | 
			
		||||
    for block in find_consecutive_blocks(json.availability):
 | 
			
		||||
        availability_value = block[0]
 | 
			
		||||
        hour_start = block[1]
 | 
			
		||||
        hour_end = block[2]
 | 
			
		||||
| 
						 | 
				
			
			@ -163,7 +157,7 @@ def put(body: PutScheduleForm, **kwargs):
 | 
			
		|||
        new_availability.start_time = abs_start
 | 
			
		||||
        new_availability.end_time = abs_end
 | 
			
		||||
        new_availability.player_id = player.steam_id
 | 
			
		||||
        new_availability.team_id = body.team_id
 | 
			
		||||
        new_availability.team_id = json.team_id
 | 
			
		||||
 | 
			
		||||
        availability_blocks.append(new_availability)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -182,28 +176,15 @@ def put(body: PutScheduleForm, **kwargs):
 | 
			
		|||
    db.session.commit()
 | 
			
		||||
    return make_response({ }, 300)
 | 
			
		||||
 | 
			
		||||
class ViewAvailablePlayersForm(pydantic.BaseModel):
 | 
			
		||||
class ViewAvailablePlayersForm(BaseModel):
 | 
			
		||||
    start_time: datetime.datetime
 | 
			
		||||
    team_id: int
 | 
			
		||||
 | 
			
		||||
@api_schedule.get("/view-available")
 | 
			
		||||
@validate()
 | 
			
		||||
@spec.validate()
 | 
			
		||||
@requires_authentication
 | 
			
		||||
def view_available(query: ViewAvailablePlayersForm, **kwargs):
 | 
			
		||||
    start_time = query.start_time.replace(tzinfo=utc.utc)
 | 
			
		||||
    player: Player = kwargs["player"]
 | 
			
		||||
 | 
			
		||||
    #q = (
 | 
			
		||||
    #    db.select(PlayerTeamAvailability)
 | 
			
		||||
    #    .filter(
 | 
			
		||||
    #        (PlayerTeamAvailability.player_id == player.steam_id) &
 | 
			
		||||
    #        (PlayerTeamAvailability.team_id == query.team_id) &
 | 
			
		||||
    #        (PlayerTeamAvailability.start_time == start_time)
 | 
			
		||||
    #    )
 | 
			
		||||
    #)
 | 
			
		||||
 | 
			
		||||
    #availability: Sequence[PlayerTeamAvailability] = \
 | 
			
		||||
    #    db.session.execute(q).scalars().all()
 | 
			
		||||
def view_available(query: ViewAvailablePlayersForm, player: Player, **kwargs):
 | 
			
		||||
    start_time = query.start_time
 | 
			
		||||
 | 
			
		||||
    availability = db.session.query(
 | 
			
		||||
        PlayerTeamAvailability
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,53 +1,89 @@
 | 
			
		|||
import datetime
 | 
			
		||||
from typing import List
 | 
			
		||||
from flask import Blueprint, jsonify, request
 | 
			
		||||
from flask import Blueprint, abort, jsonify, make_response, request
 | 
			
		||||
import pydantic
 | 
			
		||||
from flask_pydantic import validate
 | 
			
		||||
from models import Player, PlayerTeam, Team, db
 | 
			
		||||
from spectree import Response
 | 
			
		||||
from models import Player, PlayerTeam, Team, TeamSpec, db
 | 
			
		||||
from middleware import requires_authentication
 | 
			
		||||
import models
 | 
			
		||||
from spec import spec, BaseModel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
api_team = Blueprint("team", __name__, url_prefix="/team")
 | 
			
		||||
 | 
			
		||||
@api_team.get("/view/")
 | 
			
		||||
@api_team.get("/view/<team_id>/")
 | 
			
		||||
class CreateTeamJson(BaseModel):
 | 
			
		||||
    team_name: str
 | 
			
		||||
    webhook_url: str
 | 
			
		||||
    timezone: str
 | 
			
		||||
 | 
			
		||||
class ViewTeamResponse(BaseModel):
 | 
			
		||||
    team: models.TeamSpec
 | 
			
		||||
 | 
			
		||||
class ViewTeamsResponse(BaseModel):
 | 
			
		||||
    teams: list[models.TeamSpec]
 | 
			
		||||
 | 
			
		||||
@api_team.get("/all/")
 | 
			
		||||
@spec.validate(
 | 
			
		||||
    resp=Response(
 | 
			
		||||
        HTTP_200=ViewTeamsResponse,
 | 
			
		||||
        HTTP_403=None,
 | 
			
		||||
        HTTP_404=None,
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
@requires_authentication
 | 
			
		||||
def view(team_id = None, **kwargs):
 | 
			
		||||
def view_teams(**kwargs):
 | 
			
		||||
    player: Player = kwargs["player"]
 | 
			
		||||
    response = fetch_teams_for_player(player, None)
 | 
			
		||||
    if isinstance(response, ViewTeamsResponse):
 | 
			
		||||
        return jsonify(response.dict())
 | 
			
		||||
    abort(404)
 | 
			
		||||
 | 
			
		||||
    q_filter = PlayerTeam.player_id == player.steam_id
 | 
			
		||||
    if team_id is not None:
 | 
			
		||||
        q_filter = q_filter & (PlayerTeam.team_id == team_id)
 | 
			
		||||
@api_team.get("/id/<team_id>/")
 | 
			
		||||
@spec.validate(
 | 
			
		||||
    resp=Response(
 | 
			
		||||
        HTTP_200=ViewTeamResponse,
 | 
			
		||||
        HTTP_403=None,
 | 
			
		||||
        HTTP_404=None,
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
@requires_authentication
 | 
			
		||||
def view_team(team_id: int, **kwargs):
 | 
			
		||||
    player: Player = kwargs["player"]
 | 
			
		||||
    response = fetch_teams_for_player(player, team_id)
 | 
			
		||||
    if isinstance(response, ViewTeamResponse):
 | 
			
		||||
        return jsonify(response.dict())
 | 
			
		||||
    abort(404)
 | 
			
		||||
 | 
			
		||||
def fetch_teams_for_player(player: Player, team_id: int | None):
 | 
			
		||||
    q = db.session.query(
 | 
			
		||||
        Team
 | 
			
		||||
    ).join(
 | 
			
		||||
        PlayerTeam
 | 
			
		||||
    ).join(
 | 
			
		||||
        Player
 | 
			
		||||
    ).filter(
 | 
			
		||||
    ).where(
 | 
			
		||||
        PlayerTeam.player_id == player.steam_id
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def map_player_team_to_player_json(player_team: PlayerTeam):
 | 
			
		||||
        return {
 | 
			
		||||
            "steamId": player_team.player.steam_id,
 | 
			
		||||
            "username": player_team.player.username,
 | 
			
		||||
        }
 | 
			
		||||
    if team_id is not None:
 | 
			
		||||
        q = q.where(PlayerTeam.team_id == team_id)
 | 
			
		||||
 | 
			
		||||
    def map_team_to_json(team: Team):
 | 
			
		||||
        return {
 | 
			
		||||
            "teamName": team.team_name,
 | 
			
		||||
            "id": team.id,
 | 
			
		||||
            "players": list(map(map_player_team_to_player_json, team.players)),
 | 
			
		||||
        }
 | 
			
		||||
    def map_team_to_spec(team: Team) -> TeamSpec:
 | 
			
		||||
        return TeamSpec(
 | 
			
		||||
            id=team.id,
 | 
			
		||||
            team_name=team.team_name,
 | 
			
		||||
            discord_webhook_url=None
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if team_id is None:
 | 
			
		||||
        teams = q.all()
 | 
			
		||||
        return jsonify(list(map(map_team_to_json, teams)))
 | 
			
		||||
        return ViewTeamsResponse(
 | 
			
		||||
            teams=list(map(map_team_to_spec, teams))
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        team = q.one_or_none()
 | 
			
		||||
        if team:
 | 
			
		||||
            return jsonify(map_team_to_json(team))
 | 
			
		||||
        return jsonify(), 404
 | 
			
		||||
            return ViewTeamResponse(
 | 
			
		||||
                team=map_team_to_spec(team)
 | 
			
		||||
            )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue