Source code for rootski.schemas.breakdown

from datetime import datetime
from typing import Any, Dict, List, Optional, Union

from pydantic import BaseModel, EmailStr, Field, constr, root_validator
from rootski.errors import BadBreakdownItemError
from rootski.schemas.morpheme import MORPHEME_TYPE_ENUM, MORPHEME_WORD_POS_ENUM, Morpheme


[docs]class NullMorphemeBreakdownItem(BaseModel): """ Use this type to include a piece of text in a breakdown that does not have a morpheme id. """ morpheme: constr(max_length=256) position: int # these values should always be none morpheme_id: None = None @staticmethod def from_morpheme(morpheme: Morpheme, position: int): return NullMorphemeBreakdownItem(morpheme=morpheme.morpheme, position=position)
[docs]class MorphemeBreakdownItemInRequest(BaseModel): morpheme_id: int position: int
[docs]class MorphemeBreakdownItemInResponse(MorphemeBreakdownItemInRequest): """ Use this type to include the morpheme associated with the given morpheme_id in the breakdown. Each of these fields are in BreakdownItem """ # these fields are not necessary in the request body but should be present otherwise type: Optional[MORPHEME_TYPE_ENUM] word_pos: Optional[MORPHEME_WORD_POS_ENUM] morpheme: Optional[str] family_id: Optional[int] family_meanings: Optional[List[str]] level: Optional[int] family: Optional[str] class Config: use_enum_values = True @staticmethod def from_morpheme(morpheme: Morpheme, position: int): return MorphemeBreakdownItemInResponse( morpheme_id=morpheme.morpheme_id, position=position, word_pos=morpheme.word_pos, family_id=morpheme.family_id, type=morpheme.type, )
[docs]class BreakdownItemCommon(BaseModel): morpheme: Optional[ constr( max_length=256, ) ] position: int family_id: Optional[int] family_meanings: Optional[List[str]] morpheme_id: Optional[int] type: Optional[MORPHEME_TYPE_ENUM] @staticmethod def from_null_morpheme_breakdown_item(item: NullMorphemeBreakdownItem): return BreakdownItem(type=None, morpheme=item.morpheme, position=item.position) @staticmethod def from_morpheme_breakdown_item(item: MorphemeBreakdownItemInResponse): return BreakdownItem(position=item.position, **MorphemeBreakdownItemInResponse.dict())
[docs] @staticmethod def from_morpheme(morpheme: Morpheme, position: int): """ If ``type`` is ``None``, this is a "null" morpheme. It is not a morpheme, but we need to store the text. Otherwise, this is a real morpheme. In this case, we only need the ``morpheme_id``. The rest of the morpheme attributes can be looked up in the database. In both cases, the morpheme position is required. """ if not morpheme.type: return BreakdownItem( position=position, morpheme=morpheme.morpheme, morpheme_id=morpheme.morpheme_id, ) else: return BreakdownItem(position=position, morpheme_id=morpheme.morpheme_id)
[docs]class BreakdownItem(BreakdownItemCommon): """Contains attributes that are not in BreakdownItemInDb""" word_pos: Optional[MORPHEME_WORD_POS_ENUM] level: Optional[int] # level of difficulty family: Optional[str] # string of the actual family corresponding to the family id
[docs] @root_validator def enforce_non_null_morpheme_data_is_set(cls, values: Dict[str, Any]): """If the morpheme ID is set, this is a non-null morpheme breakdown item.""" if values.get("morpheme_id") is not None: required_morpheme_non_null_b_item_fields = ["word_pos", "family_id", "type", "level"] for field in required_morpheme_non_null_b_item_fields: if values.get(field) is None: raise BadBreakdownItemError( f'Field "{field}" is not set in non-null morpheme breakdown item.' ) required_nullable_fields = ["family_meanings"] for field in required_nullable_fields: if field not in values.keys(): raise BadBreakdownItemError( f'Field "{field}" is unset. Is can be None, but it must be explicitly set to None.' ) return values
[docs] def to_null_or_morpheme_breakdown_item( self, ) -> Union[NullMorphemeBreakdownItem, MorphemeBreakdownItemInResponse]: """Cast this BreakdownItem to the correct sub-type. This is useful for creating an API response.""" if self.morpheme_id is not None: return MorphemeBreakdownItemInResponse(**self.dict()) else: return NullMorphemeBreakdownItem(**self.dict())
[docs]class BreakdownItemInDb(BreakdownItemCommon): # If this is a null_breakdown, expect breakdown_id to be none. # formerly, breakdown_id: Optional[int] = None, when using SQLAlchemy breakdown_id: str = "deprecated"
[docs]class BreakdownCommon(BaseModel): word_id: int word: constr(max_length=256) is_verified: bool is_inference: bool date_submitted: datetime date_verified: Optional[datetime] breakdown_items: List[BreakdownItemCommon]
[docs]class Breakdown(BreakdownCommon): breakdown_items: List[BreakdownItem] submitted_by_current_user: bool = False
[docs]class BreakdownInDB(BreakdownCommon): submitted_by_user_email: Optional[EmailStr] verified_by_user_email: Optional[EmailStr] id: Optional[int] = None
################################## # --- HTTP Request/Responses --- # ##################################
[docs]class GetBreakdownResponse(Breakdown): # all fields should be the same as Breakdown, but the datatypes of # breakdown_items should be more specific breakdown_items: List[Union[NullMorphemeBreakdownItem, MorphemeBreakdownItemInResponse]] @staticmethod def from_breakdown(breakdown: Breakdown): breakdown_items: List[Union[NullMorphemeBreakdownItem, MorphemeBreakdownItemInResponse]] = [ b_item.to_null_or_morpheme_breakdown_item() for b_item in breakdown.breakdown_items ] to_return = GetBreakdownResponse(**breakdown.dict()) to_return.breakdown_items = breakdown_items return to_return
[docs]class BreakdownUpsert(BaseModel): word_id: int = Field(description="ID of the word the breakdown is for.") breakdown_items: List[Union[NullMorphemeBreakdownItem, MorphemeBreakdownItemInRequest]]
[docs]class SubmitBreakdownResponse(BaseModel): word_id: int is_verified: bool breakdown_id: int = Field(-1, description="Always `-1` since this field is deprecated.")
############################ # --- Helper functions --- # ############################ # This is used to create request payloads in unit tests
[docs]def make_specific_breakdown_item( morpheme: Morpheme, position: int ) -> Union[NullMorphemeBreakdownItem, MorphemeBreakdownItemInResponse]: if not morpheme.morpheme_id: return NullMorphemeBreakdownItem.from_morpheme(morpheme=morpheme, position=position) else: return MorphemeBreakdownItemInResponse.from_morpheme(morpheme=morpheme, position=position)