Migrate to spectree for pydantic integration
parent
b76d2786e5
commit
283624706e
|
@ -1,11 +1,11 @@
|
||||||
from flask import Blueprint, Flask, make_response, request
|
from flask import Blueprint, Flask, make_response, request
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
|
|
||||||
import login
|
import login
|
||||||
import schedule
|
import schedule
|
||||||
import team
|
import team
|
||||||
from models import init_db
|
from models import init_db
|
||||||
|
from spec import spec
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app, origins=["http://localhost:5173"], supports_credentials=True)
|
CORS(app, origins=["http://localhost:5173"], supports_credentials=True)
|
||||||
|
@ -28,3 +28,4 @@ def debug_set_cookie():
|
||||||
return res, 200
|
return res, 200
|
||||||
|
|
||||||
app.register_blueprint(api)
|
app.register_blueprint(api)
|
||||||
|
spec.register(app)
|
||||||
|
|
|
@ -15,43 +15,6 @@ STEAM_OPENID_URL = "https://steamcommunity.com/openid/login"
|
||||||
def index():
|
def index():
|
||||||
return "test"
|
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")
|
@api_login.post("/authenticate")
|
||||||
def steam_authenticate():
|
def steam_authenticate():
|
||||||
params = request.get_json()
|
params = request.get_json()
|
||||||
|
@ -64,7 +27,6 @@ def steam_authenticate():
|
||||||
steam_id = int(extract_steam_id_from_response(claimed_id))
|
steam_id = int(extract_steam_id_from_response(claimed_id))
|
||||||
print("User logged in as", steam_id)
|
print("User logged in as", steam_id)
|
||||||
|
|
||||||
#player = create_or_get_user_from_steam_id(int(steam_id))
|
|
||||||
player = db.session.query(
|
player = db.session.query(
|
||||||
Player
|
Player
|
||||||
).where(
|
).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.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||||
from sqlalchemy_utc import UtcDateTime
|
from sqlalchemy_utc import UtcDateTime
|
||||||
|
|
||||||
|
import spec
|
||||||
|
|
||||||
class Base(DeclarativeBase):
|
class Base(DeclarativeBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -34,6 +36,11 @@ class Player(db.Model):
|
||||||
|
|
||||||
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
|
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):
|
class Team(db.Model):
|
||||||
__tablename__ = "teams"
|
__tablename__ = "teams"
|
||||||
|
|
||||||
|
@ -45,6 +52,12 @@ class Team(db.Model):
|
||||||
|
|
||||||
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
|
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):
|
class PlayerTeam(db.Model):
|
||||||
__tablename__ = "players_teams"
|
__tablename__ = "players_teams"
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
flask
|
flask
|
||||||
|
|
||||||
|
# CORS
|
||||||
Flask-CORS
|
Flask-CORS
|
||||||
|
|
||||||
|
# ORM
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
Flask-SQLAlchemy
|
Flask-SQLAlchemy
|
||||||
SQLAlchemy-Utc
|
SQLAlchemy-Utc
|
||||||
|
|
||||||
|
# form/data validation
|
||||||
pydantic
|
pydantic
|
||||||
Flask-Pydantic
|
spectree # generates OpenAPI documents for us to make TypeScript API clients
|
||||||
|
# based on our pydantic models
|
||||||
|
|
||||||
|
# DB migrations
|
||||||
alembic
|
alembic
|
||||||
Flask-Migrate
|
Flask-Migrate
|
||||||
|
|
||||||
|
|
|
@ -1,28 +1,25 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
from typing import cast
|
||||||
from flask import Blueprint, abort, jsonify, make_response, request
|
from flask import Blueprint, abort, jsonify, make_response, request
|
||||||
import pydantic
|
|
||||||
from flask_pydantic import validate
|
from flask_pydantic import validate
|
||||||
from models import Player, PlayerTeam, PlayerTeamAvailability, PlayerTeamRole, db
|
from models import Player, PlayerTeam, PlayerTeamAvailability, PlayerTeamRole, db
|
||||||
|
|
||||||
from middleware import requires_authentication
|
from middleware import requires_authentication
|
||||||
import models
|
from spec import spec, BaseModel
|
||||||
import utc
|
|
||||||
|
|
||||||
|
|
||||||
api_schedule = Blueprint("schedule", __name__, url_prefix="/schedule")
|
api_schedule = Blueprint("schedule", __name__, url_prefix="/schedule")
|
||||||
|
|
||||||
class ViewScheduleForm(pydantic.BaseModel):
|
class ViewScheduleForm(BaseModel):
|
||||||
window_start: datetime.datetime
|
window_start: datetime.datetime
|
||||||
team_id: int
|
team_id: int
|
||||||
window_size_days: int = 7
|
window_size_days: int = 7
|
||||||
|
|
||||||
@api_schedule.get("/")
|
@api_schedule.get("/")
|
||||||
@validate(query=ViewScheduleForm)
|
@spec.validate()
|
||||||
@requires_authentication
|
@requires_authentication
|
||||||
def get(query: ViewScheduleForm, *args, **kwargs):
|
def get(query: ViewScheduleForm, player: Player, **kwargs):
|
||||||
window_start = query.window_start
|
window_start = query.window_start
|
||||||
window_end = window_start + datetime.timedelta(days=query.window_size_days)
|
window_end = window_start + datetime.timedelta(days=query.window_size_days)
|
||||||
player: Player = kwargs["player"]
|
|
||||||
|
|
||||||
availability_regions = db.session.query(
|
availability_regions = db.session.query(
|
||||||
PlayerTeamAvailability
|
PlayerTeamAvailability
|
||||||
|
@ -65,7 +62,7 @@ def get(query: ViewScheduleForm, *args, **kwargs):
|
||||||
"availability": availability
|
"availability": availability
|
||||||
}
|
}
|
||||||
|
|
||||||
class PutScheduleForm(pydantic.BaseModel):
|
class PutScheduleForm(BaseModel):
|
||||||
window_start: datetime.datetime
|
window_start: datetime.datetime
|
||||||
window_size_days: int = 7
|
window_size_days: int = 7
|
||||||
team_id: int
|
team_id: int
|
||||||
|
@ -91,17 +88,14 @@ def find_consecutive_blocks(arr: list[int]) -> list[tuple[int, int, int]]:
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
@api_schedule.put("/")
|
@api_schedule.put("/")
|
||||||
@validate(body=PutScheduleForm, get_json_params={})
|
@spec.validate()
|
||||||
@requires_authentication
|
@requires_authentication
|
||||||
def put(body: PutScheduleForm, **kwargs):
|
def put(json: PutScheduleForm, player: Player, **kwargs):
|
||||||
window_start = body.window_start.replace(tzinfo=utc.utc)
|
window_start = json.window_start
|
||||||
window_end = window_start + datetime.timedelta(days=body.window_size_days)
|
window_end = window_start + datetime.timedelta(days=json.window_size_days)
|
||||||
player: Player = kwargs["player"]
|
|
||||||
if not player:
|
|
||||||
abort(400)
|
|
||||||
|
|
||||||
# TODO: add error message
|
# TODO: add error message
|
||||||
if len(body.availability) != 168:
|
if len(json.availability) != 168:
|
||||||
abort(400, {
|
abort(400, {
|
||||||
"error": "Availability must be length " + str(168)
|
"error": "Availability must be length " + str(168)
|
||||||
})
|
})
|
||||||
|
@ -111,7 +105,7 @@ def put(body: PutScheduleForm, **kwargs):
|
||||||
).where(
|
).where(
|
||||||
PlayerTeamAvailability.player_id == player.steam_id
|
PlayerTeamAvailability.player_id == player.steam_id
|
||||||
).where(
|
).where(
|
||||||
PlayerTeamAvailability.team_id == body.team_id
|
PlayerTeamAvailability.team_id == json.team_id
|
||||||
).where(
|
).where(
|
||||||
PlayerTeamAvailability.start_time.between(window_start, window_end) |
|
PlayerTeamAvailability.start_time.between(window_start, window_end) |
|
||||||
PlayerTeamAvailability.end_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
|
# create time regions inside our window based on the availability array
|
||||||
availability_blocks = []
|
availability_blocks = []
|
||||||
|
|
||||||
for block in find_consecutive_blocks(body.availability):
|
for block in find_consecutive_blocks(json.availability):
|
||||||
availability_value = block[0]
|
availability_value = block[0]
|
||||||
hour_start = block[1]
|
hour_start = block[1]
|
||||||
hour_end = block[2]
|
hour_end = block[2]
|
||||||
|
@ -163,7 +157,7 @@ def put(body: PutScheduleForm, **kwargs):
|
||||||
new_availability.start_time = abs_start
|
new_availability.start_time = abs_start
|
||||||
new_availability.end_time = abs_end
|
new_availability.end_time = abs_end
|
||||||
new_availability.player_id = player.steam_id
|
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)
|
availability_blocks.append(new_availability)
|
||||||
|
|
||||||
|
@ -182,28 +176,15 @@ def put(body: PutScheduleForm, **kwargs):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return make_response({ }, 300)
|
return make_response({ }, 300)
|
||||||
|
|
||||||
class ViewAvailablePlayersForm(pydantic.BaseModel):
|
class ViewAvailablePlayersForm(BaseModel):
|
||||||
start_time: datetime.datetime
|
start_time: datetime.datetime
|
||||||
team_id: int
|
team_id: int
|
||||||
|
|
||||||
@api_schedule.get("/view-available")
|
@api_schedule.get("/view-available")
|
||||||
@validate()
|
@spec.validate()
|
||||||
@requires_authentication
|
@requires_authentication
|
||||||
def view_available(query: ViewAvailablePlayersForm, **kwargs):
|
def view_available(query: ViewAvailablePlayersForm, player: Player, **kwargs):
|
||||||
start_time = query.start_time.replace(tzinfo=utc.utc)
|
start_time = query.start_time
|
||||||
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()
|
|
||||||
|
|
||||||
availability = db.session.query(
|
availability = db.session.query(
|
||||||
PlayerTeamAvailability
|
PlayerTeamAvailability
|
||||||
|
|
|
@ -1,53 +1,89 @@
|
||||||
import datetime
|
import datetime
|
||||||
from typing import List
|
from typing import List
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, abort, jsonify, make_response, request
|
||||||
import pydantic
|
import pydantic
|
||||||
from flask_pydantic import validate
|
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
|
from middleware import requires_authentication
|
||||||
import models
|
import models
|
||||||
|
from spec import spec, BaseModel
|
||||||
|
|
||||||
|
|
||||||
api_team = Blueprint("team", __name__, url_prefix="/team")
|
api_team = Blueprint("team", __name__, url_prefix="/team")
|
||||||
|
|
||||||
@api_team.get("/view/")
|
class CreateTeamJson(BaseModel):
|
||||||
@api_team.get("/view/<team_id>/")
|
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
|
@requires_authentication
|
||||||
def view(team_id = None, **kwargs):
|
def view_teams(**kwargs):
|
||||||
player: Player = kwargs["player"]
|
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
|
@api_team.get("/id/<team_id>/")
|
||||||
if team_id is not None:
|
@spec.validate(
|
||||||
q_filter = q_filter & (PlayerTeam.team_id == team_id)
|
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(
|
q = db.session.query(
|
||||||
Team
|
Team
|
||||||
).join(
|
).join(
|
||||||
PlayerTeam
|
PlayerTeam
|
||||||
).join(
|
).join(
|
||||||
Player
|
Player
|
||||||
).filter(
|
).where(
|
||||||
PlayerTeam.player_id == player.steam_id
|
PlayerTeam.player_id == player.steam_id
|
||||||
)
|
)
|
||||||
|
|
||||||
def map_player_team_to_player_json(player_team: PlayerTeam):
|
if team_id is not None:
|
||||||
return {
|
q = q.where(PlayerTeam.team_id == team_id)
|
||||||
"steamId": player_team.player.steam_id,
|
|
||||||
"username": player_team.player.username,
|
|
||||||
}
|
|
||||||
|
|
||||||
def map_team_to_json(team: Team):
|
def map_team_to_spec(team: Team) -> TeamSpec:
|
||||||
return {
|
return TeamSpec(
|
||||||
"teamName": team.team_name,
|
id=team.id,
|
||||||
"id": team.id,
|
team_name=team.team_name,
|
||||||
"players": list(map(map_player_team_to_player_json, team.players)),
|
discord_webhook_url=None
|
||||||
}
|
)
|
||||||
|
|
||||||
if team_id is None:
|
if team_id is None:
|
||||||
teams = q.all()
|
teams = q.all()
|
||||||
return jsonify(list(map(map_team_to_json, teams)))
|
return ViewTeamsResponse(
|
||||||
|
teams=list(map(map_team_to_spec, teams))
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
team = q.one_or_none()
|
team = q.one_or_none()
|
||||||
if team:
|
if team:
|
||||||
return jsonify(map_team_to_json(team))
|
return ViewTeamResponse(
|
||||||
return jsonify(), 404
|
team=map_team_to_spec(team)
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue