Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mmeyer/update fastapi #279

Merged
merged 7 commits into from
Aug 15, 2023
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
2 changes: 1 addition & 1 deletion requirements.dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ flake8==6.0.0
pytest==7.2.1
httpx==0.23.3
coverage==7.2.3
polyfactory==2.0.0
polyfactory==2.7.2
7 changes: 4 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
redis==4.4.4
fastapi==0.97.0
uvicorn[standard]==0.20.0
fastapi==0.101.0
uvicorn[standard]==0.23.2
pyjwt[crypto]==2.6.0
fastapi_mail==1.2.5
cfenv==0.5.3
SQLAlchemy==2.0.5.post1
psycopg2==2.9.5
alembic==1.10.2
PyMuPDF==1.21.1
pydantic-settings==2.0.2
email-validator==2.0.0.post2
python-multipart==0.0.6
7 changes: 5 additions & 2 deletions training-front-end/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 15 additions & 15 deletions training-front-end/src/components/QuizResults.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,21 @@
You got <b>{{ result_string }}</b> questions correct, for a total score of <b>{{ percentage }}%</b>, which meets the 75% or higher requirement to pass.
</p>
<form
:action=quiz_certificate_url
method="post"
>
<input
type="hidden"
name="jwtToken"
:value="user.jwt"
>
<button
class="usa-button usa-button--outline margin-bottom-3"
type="submit"
>
<FileDownLoad /> Download your certificate of completion
</button>
</form>
:action="quiz_certificate_url"
method="post"
>
<input
type="hidden"
name="jwtToken"
:value="user.jwt"
>
<button
class="usa-button usa-button--outline margin-bottom-3"
type="submit"
>
<FileDownLoad /> Download your certificate of completion
</button>
</form>
</div>
<div v-else>
<div class="usa-prose">
Expand Down
6 changes: 3 additions & 3 deletions training/api/api_v1/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ def auth_exchange(
detail="Invalid user."
)

user = User.from_orm(db_user)
user = User.model_validate(db_user)
if not user.is_admin():
logging.info(f"UAA authenticated, but not an admin: {uaa_user['email']}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to login."
)

jwt_user = UserJWT.from_orm(db_user)
encoded_jwt = jwt.encode(jwt_user.dict(), settings.JWT_SECRET, algorithm="HS256")
jwt_user = UserJWT.model_validate(db_user)
encoded_jwt = jwt.encode(jwt_user.model_dump(), settings.JWT_SECRET, algorithm="HS256")
logging.info(f"Token exchange success for {db_user.email}")
return {'user': jwt_user, 'jwt': encoded_jwt}
6 changes: 3 additions & 3 deletions training/api/api_v1/loginless_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def send_link(
detail="Unauthorized"
)

user = TempUser.parse_obj({
user = TempUser.model_validate({
"name": user_from_db.name,
"email": user_from_db.email,
"agency_id": user_from_db.agency_id,
Expand Down Expand Up @@ -118,7 +118,7 @@ async def get_user(
db_user = repo.find_by_email(user.email)
if not db_user:
db_user = repo.create(user)
user_return = UserJWT.from_orm(db_user)
user_return = UserJWT.model_validate(db_user)
logging.info(f"Confirmed email token for {user.email}")
encoded_jwt = jwt.encode(user_return.dict(), settings.JWT_SECRET, algorithm="HS256")
encoded_jwt = jwt.encode(user_return.model_dump(), settings.JWT_SECRET, algorithm="HS256")
return {'user': user_return, 'jwt': encoded_jwt}
45 changes: 29 additions & 16 deletions training/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from pydantic import BaseSettings, EmailStr
from typing import Tuple, Type
from pydantic import EmailStr
from typing import Dict, Any
from cfenv import AppEnv
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict


def vcap_services_settings(settings: BaseSettings) -> Dict[str, Any]:
Expand Down Expand Up @@ -52,16 +54,19 @@ class Settings(BaseSettings):

# for local dev, email setting should be added to .env
# see .env_example for example
SMTP_USER: str | None
SMTP_USER: str | None = None
SMTP_SERVER: str
SMTP_PORT: int
EMAIL_FROM: EmailStr = EmailStr("[email protected]")
SMTP_STARTTLS: bool | None = None
SMTP_SSL_TLS: bool | None = None

EMAIL_FROM: EmailStr = "[email protected]"
EMAIL_FROM_NAME: str = "GSA SmartPay"
EMAIL_SUBJECT: str = "GSA SmartPay Training"

# These are normally parsed from VCAP_SERVICES in Cloud Foundry, but can
# be overridden locally by using the .env file.
SMTP_PASSWORD: str | None
SMTP_PASSWORD: str | None = None
REDIS_HOST: str
REDIS_PORT: int
REDIS_PASSWORD: str
Expand All @@ -74,18 +79,26 @@ class Settings(BaseSettings):
AUTH_CLIENT_ID: str
AUTH_AUTHORITY_URL: str

class Config:
env_file = '.env'
env_file_encoding = 'utf-8'

@classmethod
def customise_sources(cls, init_settings, env_settings, file_secret_settings):
return (
init_settings,
env_settings,
file_secret_settings,
vcap_services_settings,
)
model_config = SettingsConfigDict(
env_file='.env',
env_file_encoding='utf-8'
)

@classmethod
def customise_sources(
cls,
settings_cls: Type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource
) -> Tuple[PydanticBaseSettingsSource, ...]:
return (
init_settings,
env_settings,
file_secret_settings,
vcap_services_settings,
)


settings = Settings() # type: ignore
4 changes: 2 additions & 2 deletions training/data/user_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ def get(self, token: str) -> Optional[UserCreate]:
user = redis.get(token)
if user:
user = json.loads(user)
return UserCreate(**user)
return UserCreate.model_validate(user)

def set(self, user: TempUser) -> str:
token = str(uuid4())
user_str = json.dumps(user.dict())
user_str = user.model_dump_json()
# try/except here
redis.set(token, user_str)
redis.expire(token, self.CACHE_TTL)
Expand Down
2 changes: 1 addition & 1 deletion training/repositories/quiz.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def __init__(self, session: Session):
super().__init__(session, models.Quiz)

def create(self, quiz: schemas.QuizCreate) -> models.Quiz:
content_dict = quiz.content.dict()
content_dict = quiz.content.model_dump()

# Assign IDs to questions and choices
for qindex, question in enumerate(content_dict.get("questions", [])):
Expand Down
10 changes: 4 additions & 6 deletions training/schemas/agency.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from pydantic import BaseModel
from pydantic.schema import Optional
from typing import Optional
from pydantic import ConfigDict, BaseModel


class AgencyBase(BaseModel):
name: str
bureau: Optional[str]
bureau: Optional[str] = None


class AgencyCreate(AgencyBase):
Expand All @@ -13,9 +13,7 @@ class AgencyCreate(AgencyBase):

class Agency(AgencyBase):
id: int

class Config:
orm_mode = True
model_config = ConfigDict(from_attributes=True)


class Bureau(BaseModel):
Expand Down
10 changes: 3 additions & 7 deletions training/schemas/quiz.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from enum import Enum
from pydantic import BaseModel
from pydantic import ConfigDict, BaseModel
from training.schemas import QuizContent, QuizContentCreate, QuizContentPublic


Expand Down Expand Up @@ -29,13 +29,9 @@ class QuizCreate(QuizBase):
class QuizPublic(QuizBase):
id: int
content: QuizContentPublic

class Config:
orm_mode = True
model_config = ConfigDict(from_attributes=True)


class Quiz(QuizBase):
id: int

class Config:
orm_mode = True
model_config = ConfigDict(from_attributes=True)
6 changes: 2 additions & 4 deletions training/schemas/quiz_choice.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import ConfigDict, BaseModel


class QuizChoiceBase(BaseModel):
Expand All @@ -16,6 +16,4 @@ class QuizChoicePublic(QuizChoiceBase):
class QuizChoice(QuizChoiceBase):
id: int
correct: bool

class Config:
orm_mode = True
model_config = ConfigDict(from_attributes=True)
6 changes: 2 additions & 4 deletions training/schemas/quiz_completion.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import datetime
from pydantic import BaseModel
from pydantic import ConfigDict, BaseModel


class QuizCompletionBase(BaseModel):
Expand All @@ -15,6 +15,4 @@ class QuizCompletionCreate(QuizCompletionBase):
class QuizCompletion(QuizCompletionBase):
id: int
submit_ts: datetime

class Config:
orm_mode = True
model_config = ConfigDict(from_attributes=True)
2 changes: 1 addition & 1 deletion training/schemas/quiz_grade.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class QuizGradeQuestion(BaseModel):


class QuizGrade(BaseModel):
quiz_completion_id: int | None
quiz_completion_id: int | None = None
quiz_id: int
correct_count: int
question_count: int
Expand Down
6 changes: 2 additions & 4 deletions training/schemas/quiz_question.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from enum import Enum
from pydantic import BaseModel
from pydantic import ConfigDict, BaseModel
from training.schemas import QuizChoice, QuizChoicePublic, QuizChoiceCreate


Expand All @@ -25,6 +25,4 @@ class QuizQuestionPublic(QuizQuestionBase):

class QuizQuestion(QuizQuestionBase):
id: int

class Config:
orm_mode = True
model_config = ConfigDict(from_attributes=True)
6 changes: 2 additions & 4 deletions training/schemas/report_user_x_agency.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from pydantic import BaseModel
from pydantic import ConfigDict, BaseModel


class ReportUserXAgency(BaseModel):
user_id: int
agency_id: int

class Config:
orm_mode = True
model_config = ConfigDict(from_attributes=True)
6 changes: 2 additions & 4 deletions training/schemas/role.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import ConfigDict, BaseModel


class RoleCreate(BaseModel):
Expand All @@ -7,6 +7,4 @@ class RoleCreate(BaseModel):

class Role(RoleCreate):
id: int

class Config:
orm_mode = True
model_config = ConfigDict(from_attributes=True)
18 changes: 15 additions & 3 deletions training/schemas/temp_user.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel, EmailStr
from pydantic import ConfigDict, BaseModel, EmailStr, field_validator


class TempUser(BaseModel):
Expand All @@ -9,9 +9,21 @@ class TempUser(BaseModel):
email: EmailStr
name: str
agency_id: int
model_config = ConfigDict(from_attributes=True)

class Config:
orm_mode = True
@field_validator("agency_id", mode="before")
@classmethod
def to_int(cls, value: str | int) -> int:
'''
This addresses a bug in pydantic that does not correctly choose
the right value from the union[TempUser, IncompleteTempUser]
when agency_id is a string. Related:
https://docs.pydantic.dev/dev-v2/migration/#unions
'''
if isinstance(value, str):
value = int(value)

return value


class IncompleteTempUser(BaseModel):
Expand Down
Loading
Loading