Example #1
0
class Well(Base, Id):
    __tablename__ = "wells"
    id = Column(Integer, primary_key=True)
    volume = Column(Numeric)
    address = Column(String)
    plate_id = Column(Integer, ForeignKey('plates.id'))
    solvent = Column(EnumType(WellSolvent), nullable=True)

    logs = relationship("Log", backref="wells")

    general_state = Column(EnumType(GeneralState), nullable=False)

    @transition(source='stocked', target='terminated')
    def terminate(self):
        """This function terminates a well"""
        pass

    well_state = Column(EnumType(WellState), nullable=False)

    @transition(source='dried', target='liquid')
    def resuspend(self):
        """This function resuspends a well"""
        pass

    resident_type = Column(EnumType(ResidentState))

    part_id = Column(Integer, ForeignKey('parts.id'), nullable=True)
    part = relationship("Part", back_populates="wells")

    vector_id = Column(Integer, ForeignKey('parts.id'), nullable=True)
    vector = relationship("Part", back_populates="wells")

    organism_id = Column(Integer, ForeignKey('organisms.id'), nullable=True)
    organism = relationship("Organism", back_populates="wells")
    # TODO all cannot be false

    # DNA data
    ng_quantity = Column(Numeric)
    quality = Column(Numeric)

    def fmol_quantity(self):
        """This function calculates fmol per ul from volume and ng_quantity"""
        pass

    sequence_state = Column(EnumType(SequenceState), nullable=False)

    @transition(source='not_sequenced', target='sequence_confirmed')
    def sequence_dna(self):
        """This function analyzes sequence data. Or something."""
        pass

    # A well can come from many wells
    provenance_wells = relationship('Well',
                                    secondary=wells_wells,
                                    backref='progeny_wells')
class File(Base):
    __tablename__ = 'files'

    id = Column(Integer, primary_key=True)
    category = Column(EnumType(FileCategory, name='file_category'))
    group = Column(EnumType(FileGroup, name='file_group'))
    name = Column(String)

    def __init__(self, category: FileCategory, group: FileGroup, name: str):
        self.category = category
        self.group = group
        self.name = name
Example #3
0
class Part(Base, Virtual):
    __tablename__ = "parts"
    hrid = Column(String)
    seq = Column(String)
    part_type = Column(EnumType(PartType))
    part_status = Column(EnumType(PartType))  # TODO transitions here

    # A part can have many parts
    provenance_parts = relationship('Part',
                                    secondary=parts_parts,
                                    backref='progeny_parts')

    provenance_type = Column(EnumType(ProvenanceType))

    dna_samples = relationship('dna_samples', backref='parts')
    wells = relationship("Well", back_populates="parts")
Example #4
0
class Ending(Base):
    __tablename__ = 'endings'

    id = Column(Integer, primary_key=True)

    ending_id: int = Column(String, unique=True)
    title: Optional[str] = Column(String, nullable=True)
    description: str = Column(String)
    image: str = Column(String)
    flavour: EndingFlavour = Column(EnumType(EndingFlavour, name='flavour'))
    animation: str = Column(String)
    achievement: Optional[str] = Column(String, nullable=True)

    @classmethod
    def from_data(cls, file: File, data: Dict[str, Any],
                  translations: Dict[str, Dict[str, Any]],
                  game_contents: GameContents) -> 'Ending':
        e = game_contents.get_ending(get(data, 'id'))
        e.file = file
        e.title = get(data, 'label', translations=translations)
        e.description = get(data, 'description', translations=translations)
        e.image = get(data, 'image')
        flavour = get(data, 'flavour', 'None')
        e.flavour = EndingFlavour(flavour[0].upper() +
                                  flavour[1:]  # Workaround for a broken ending
                                  )
        e.animation = get(data, 'anim')
        e.achievement = get(data, 'achievement')
        return e

    @classmethod
    def get_by_ending_id(cls, session: Session, ending_id: str) -> 'Ending':
        return session.query(cls).filter(cls.ending_id == ending_id).one()
Example #5
0
class GroupSessionWaitingList(Base):
    id = Column(Integer, primary_key=True, index=True)
    student_id = Column(ForeignKey("user.id"), unique=True)
    english_proficiency = Column(EnumType(ProficiencyLevel), nullable=False)

    updated = Column(DateTime, onupdate=func.now())
    created = Column(DateTime, server_default=func.now())
Example #6
0
class Condition(Base, Id):
    __tablename__ = "conditions"
    condition = Column(EnumType(ConditionStates))
    part = relationship("Part",
                        secondary=part_conditions,
                        backref="conditions")
    organism = relationship("Organism",
                            secondary=organism_conditions,
                            backref="conditions")
class Challenge(Base):
    __tablename__ = "challenges"

    id = Column(Integer, primary_key=True)
    game_id = Column(Integer, nullable=False)
    category = Column(String(1023), nullable=False)
    name = Column(String(1023), nullable=False)
    description = Column(String(65535), nullable=False)
    image = Column(String(1023), nullable=False)
    target = Column(Integer, nullable=False)
    nature = Column(EnumType(ChallengeNature, length=127), nullable=False)
    type = Column(EnumType(ChallengeType, length=127), nullable=False)

    branch_observation_id = Column(Integer,
                                   ForeignKey("branches_observations.id"),
                                   nullable=False)
    branch_observation = relationship("BranchObservation",
                                      back_populates="challenges")
Example #8
0
class Quality(GameEntity):
    __tablename__ = "qualities"

    name = Column(String(1023), nullable=False)
    description = Column(String(65535))
    category = Column(String(1023), nullable=False)
    nature = Column(EnumType(QualityNature, length=127), nullable=False)
    storylet_id = Column(Integer, ForeignKey("storylets.id"))
    storylet = relationship("Storylet", back_populates="thing")
Example #9
0
class MessageQueue(Base, Timestamp):
    __tablename__ = "message_queue"
    __table_args__ = (PrimaryKeyConstraint("id"), )

    id = Column(UUID(as_uuid=True), nullable=False, default=uuid4)
    to = Column(UnicodeText, nullable=False)
    subject = Column(UnicodeText, nullable=False)
    contents = Column(UnicodeText, nullable=False)
    status = Column(EnumType(MessageStatus), nullable=False)
    message = Column(UnicodeText, nullable=False)
Example #10
0
class User(Base):
    id = Column(Integer, primary_key=True, index=True)
    full_name = Column(String, index=True)
    email = Column(String, unique=True, index=True, nullable=False)
    hashed_password = Column(String, nullable=False)
    is_active = Column(Boolean(), default=True, nullable=False)
    is_superuser = Column(Boolean(), default=False, nullable=False)

    type_ = Column(EnumType(UserRole), nullable=False)
    updated = Column(DateTime, onupdate=func.now())
    created = Column(DateTime, server_default=func.now())
    items = relationship("Item", back_populates="owner")
Example #11
0
class Article(Base, Timestamp):
    __tablename__ = "article"
    __table_args__ = (
        PrimaryKeyConstraint("id"),
        Index("article__created__idx", "created"),
        Index("article__external_id__idx", "external_id", unique=True),
    )

    id = Column(UUID(as_uuid=True), nullable=False, default=uuid4)
    external_id = Column(UnicodeText, nullable=False)
    body = Column(UnicodeText, nullable=False)
    status = Column(EnumType(ArticleStatus), nullable=False)
    message = Column(UnicodeText, nullable=False)
Example #12
0
class OutcomeMessage(Base):
    __tablename__ = "outcome_messages"

    id = Column(Integer, primary_key=True)
    type = Column(EnumType(OutcomeMessageType, length=127), nullable=False)
    text = Column(String(65535), nullable=False)
    image = Column(String(127))
    change = Column(Integer)

    quality_id = Column(Integer, ForeignKey("qualities.id"))
    quality = relationship("Quality", lazy="selectin")
    outcome_observation_id = Column(
        Integer, ForeignKey("outcome_observations.id")
    )
    outcome_observation = relationship(
        "OutcomeObservation", back_populates="messages"
    )
Example #13
0
class Area(GameEntity):
    __tablename__ = "areas"

    name = Column(String(1023))
    description = Column(String(65535))
    image = Column(String(1023))
    type = Column(EnumType(AreaType, length=127))
    storylets = relationship("Storylet",
                             secondary=areas_storylets,
                             back_populates="areas")
    settings = relationship("Setting",
                            secondary=areas_settings,
                            back_populates="areas")
    outcome_redirects = relationship("OutcomeObservation",
                                     back_populates="redirect_area")

    @property
    def url(self):
        return f"/area/{self.id}"
Example #14
0
class Virtual(Base, Id):
    __tablename__ = 'virtuals'  # added by juul

    date_created = Column(DateTime(timezone=True), server_default=func.now())
    genbank_file = Column(
        String)  # TODO this is a file, so let's dump to str for now
    name = Column(
        String)  # Human readable name. Different than human readable id.

    # added by juul
    tags = Column(String)
    description = Column(
        String)  # Human readable name. Different than human readable id.
    bionet_id = Column(String)
    submitter_name = Column(String)
    submitter_email = Column(String)
    submitted_part_type = Column(
        EnumType(SubmittedPartType))  # TODO actually a subset? (vector?)
    submitted_codon_modify_ok = Column(Boolean)
    submitted_url = Column(String)

    def as_dict(self):
        return {c.name: getattr(self, c.name) for c in self.__table__.columns}
Example #15
0
user = Table(
    'user',
    metadata,
    Column('uuid',
           UUID,
           nullable=False,
           primary_key=True,
           default=lambda _: str(uuid4())),
    Column('login', String, nullable=True, unique=True),
    Column('password', String, nullable=True),
    Column('phone', String, unique=True, nullable=True),
    Column('email', String, nullable=True, unique=True),
    Column('name', String),
    Column('surname', String),
    Column('middle_name', String),
    Column('type', EnumType(UserType), nullable=False),
    Column('active', Boolean, default=True, server_default="True"),
    Column('deleted', Boolean, default=False, server_default="False"),

    # access_level_id = Column(Integer, ForeignKey("access_level.id"))
    # access_level = relation("AccessLevel")
    Column(
        'created',
        DateTime,
        nullable=False,
        default=func.now(),
        server_default=func.now(),
    ),
    Column(
        'updated',
        DateTime,
Example #16
0
class Plate(Base, Id):
    __tablename__ = "plates"
    plate_type = Column(EnumType(PlateTypes))
    plate_location = Column(
        String)  # Human readable sentence of where to find the plate
    wells = relationship("Well", backref="plates")
Example #17
0
class Recipe(Base, GameContentMixin):
    __tablename__ = 'recipes'

    id = Column(Integer, primary_key=True)
    recipe_id: str = Column(String, unique=True)

    label: str = Column(String)
    start_description: Optional[str] = Column(String, nullable=True)
    description: Optional[str] = Column(String, nullable=True)
    action_id: Optional[int] = Column(
        Integer,
        ForeignKey('verbs.id'),
        nullable=True
    )
    action = relationship(
        'Verb',
        foreign_keys=action_id,
        back_populates='recipes'
    )
    requirements: List['RecipeRequirement'] = relationship('RecipeRequirement')
    effects: List['RecipeEffect'] = relationship('RecipeEffect')
    aspects: List['RecipeAspect'] = relationship('RecipeAspect')
    mutation_effects: List[MutationEffect] = relationship(
        MutationEffect, back_populates='recipe'
    )
    signal_ending_flavour: EndingFlavour = Column(
        EnumType(EndingFlavour, name='ending_flavour')
    )
    craftable: bool = Column(Boolean)
    hint_only: bool = Column(Boolean)
    warmup: int = Column(Integer)
    deck_effect: List['RecipeDeckEffect'] = relationship('RecipeDeckEffect')
    alternative_recipes: List['RecipeAlternativeRecipeDetails'] = relationship(
        'RecipeAlternativeRecipeDetails',
        back_populates='source_recipe',
        foreign_keys='RecipeAlternativeRecipeDetails.source_recipe_id'
    )
    linked_recipes: List['RecipeLinkedRecipeDetails'] = relationship(
        'RecipeLinkedRecipeDetails',
        back_populates='source_recipe',
        foreign_keys='RecipeLinkedRecipeDetails.source_recipe_id'
    )
    ending_flag: Optional[str] = Column(String, nullable=True)
    max_executions: int = Column(Integer)
    burn_image: Optional[str] = Column(String, nullable=True)
    portal_effect: PortalEffect = Column(
        EnumType(PortalEffect, name='portal_effect')
    )
    slot_specifications: List['RecipeSlotSpecification'] = relationship(
        'RecipeSlotSpecification', back_populates='recipe'
    )
    signal_important_loop: bool = Column(Boolean)
    comments: Optional[str] = Column(String, nullable=True)

    @classmethod
    def from_data(
            cls,
            file: File,
            data: Dict[str, Any],
            game_contents: GameContents
    ) -> 'Recipe':
        r = game_contents.get_recipe(data['id'])
        r.file = file
        r.label = get(data, 'label', data['id'])
        r.start_description = get(data, 'startdescription')
        r.description = get(data, 'description')
        r.action = game_contents.get_verb(get(data, 'actionId'))
        r.requirements = RecipeRequirement.from_data(
            get(data, 'requirements', {}), game_contents
        )
        r.effects = RecipeEffect.from_data(
            get(data, 'effects', {}), game_contents
        )
        if 'aspects' in data:
            # TODO Remove this when fixed
            if isinstance(data['aspects'], str):
                logging.error('Invalid value for aspects for recipe {}'.format(
                    data['id']
                ))
            else:
                r.aspects = RecipeAspect.from_data(
                    get(data, 'aspects', {}), game_contents
                )
        r.mutation_effects = MutationEffect.from_data(
            get(data, 'mutations', []), game_contents
        )
        r.signal_ending_flavour = EndingFlavour(get(
            data, 'signalEndingFlavour', 'none'
        ).lower())
        r.craftable = get(data, 'craftable', False, to_bool)
        r.hint_only = get(data, 'hintonly', False, to_bool)
        r.warmup = get(data, 'warmup', 0, int)
        r.deck_effect = RecipeDeckEffect.from_data(
            get(data, 'deckeffect', {}), game_contents
        )
        r.alternative_recipes = [
            RecipeAlternativeRecipeDetails.from_data(lrd, game_contents)
            for lrd in get(data, 'alternativerecipes', [])
        ]
        r.linked_recipes = [
            RecipeLinkedRecipeDetails.from_data(lrd, game_contents)
            for lrd in get(data, 'linked', [])
        ]
        r.ending_flag = get(data, 'ending')
        r.max_executions = get(data, 'maxexecutions', 0, int)
        r.burn_image = get(data, 'burnimage')
        r.portal_effect = PortalEffect(
            get(data, 'portaleffect', 'none').lower()
        )
        r.slot_specifications = [
            RecipeSlotSpecification.from_data(v, game_contents)
            for v in get(data, 'slots', [])]
        r.signal_important_loop = get(
            data, 'signalimportantloop', False, to_bool
        )
        return r

    @classmethod
    def get_by_recipe_id(cls, session: Session, recipe_id: str) -> 'Recipe':
        return session.query(cls).filter(cls.recipe_id == recipe_id).one()
class Schedule(Base):  # type: ignore
    __tablename__ = "schedule"

    id = Column(
        UUIDType(binary=False), primary_key=True, default=uuid.uuid4)
    user_id = Column(UUIDType(binary=False), nullable=False, index=True)
    org_id = Column(
        UUIDType(binary=False), nullable=False, index=True)
    workspace_id = Column(
        UUIDType(binary=False), nullable=False, index=True)
    experiment_id = Column(
        UUIDType(binary=False), nullable=False, index=True)
    token_id = Column(UUIDType(binary=False), nullable=False)
    job_id = Column(UUIDType(binary=False), nullable=True)
    scheduled = Column(DateTime(), nullable=False)
    repeat = Column(Integer, nullable=True)
    interval = Column(Integer, nullable=True)
    cron = Column(String, nullable=True)
    status = Column(EnumType(ScheduleStatus), default=ScheduleStatus.unknown)
    results = Column(EncryptedType(JSONB, get_secret_key, AesEngine, 'pkcs5'))
    settings = Column(EncryptedType(JSONB, get_secret_key, AesEngine, 'pkcs5'))
    configuration = Column(
        EncryptedType(JSONB, get_secret_key, AesEngine, 'pkcs5'))
    secrets = Column(EncryptedType(JSONB, get_secret_key, AesEngine, 'pkcs5'))

    @staticmethod
    def load(user_id: Union[UUID, str], schedule_id: Union[UUID, str],
             session: Session) -> 'Schedule':
        return session.query(Schedule).\
            filter_by(id=schedule_id).\
            filter_by(user_id=user_id).first()

    @staticmethod
    def load_by_user(user_id: Union[UUID, str],
                     session: Session) -> List['Schedule']:
        return session.query(Schedule).\
            filter_by(user_id=user_id).all()

    @staticmethod
    def create(user_id: Union[UUID, str], org_id: Union[UUID, str],
               workspace_id: Union[UUID, str], experiment_id: Union[UUID, str],
               token_id: Union[UUID, str], scheduled_at: str, repeat: int,
               interval: int, cron: str, settings: Any, configuration: Any,
               secrets: Any, session: Session) -> 'Schedule':

        scheduled = parse(scheduled_at)
        schedule = Schedule(
            user_id=user_id,
            org_id=org_id,
            workspace_id=workspace_id,
            experiment_id=experiment_id,
            token_id=token_id,
            status=ScheduleStatus.created,
            scheduled=scheduled,
            repeat=repeat or None,
            interval=interval or None,
            cron=cron or None,
            settings=settings,
            configuration=configuration,
            secrets=secrets
        )
        session.add(schedule)
        return schedule

    @staticmethod
    def delete(schedule_id: Union[UUID, str], session: Session) -> NoReturn:
        schedule = session.query(Schedule).filter_by(
            id=schedule_id).first()

        if schedule:
            session.delete(schedule)

    @staticmethod
    def set_job_id(schedule_id: Union[UUID, str], job_id: Union[UUID, str],
                   session: Session) -> NoReturn:
        schedule = session.query(Schedule).filter_by(
            id=schedule_id).first()
        schedule.job_id = job_id

    @staticmethod
    def set_status(schedule_id: Union[UUID, str], status: ScheduleStatus,
                   session: Session) -> NoReturn:
        schedule = session.query(Schedule).filter_by(
            id=schedule_id).first()
        schedule.status = status

    @staticmethod
    def list_by_state(status: ScheduleStatus,
                      session: Session) -> List['Schedule']:
        return session.query(Schedule).filter_by(status=status).all()
Example #19
0
class Recipe(Base, GameContentMixin):
    __tablename__ = 'recipes'

    id = Column(Integer, primary_key=True)
    recipe_id: str = Column(String, unique=True)

    label: str = Column(String)
    start_description: Optional[str] = Column(String, nullable=True)
    description: Optional[str] = Column(String, nullable=True)
    action_id: Optional[int] = Column(Integer,
                                      ForeignKey('verbs.id'),
                                      nullable=True)
    action = relationship('Verb',
                          foreign_keys=action_id,
                          back_populates='recipes')
    requirements: List['RecipeRequirement'] = relationship('RecipeRequirement')
    table_requirements: List['RecipeTableRequirement'] = relationship(
        'RecipeTableRequirement')
    extant_requirements: List['RecipeExtantRequirement'] = relationship(
        'RecipeExtantRequirement')
    effects: List['RecipeEffect'] = relationship('RecipeEffect')
    aspects: List['RecipeAspect'] = relationship('RecipeAspect')
    mutation_effects: List[MutationEffect] = relationship(
        MutationEffect, back_populates='recipe')
    purge: List['RecipePurge'] = relationship('RecipePurge')
    halt_verb: List['RecipeHaltVerb'] = relationship('RecipeHaltVerb')
    delete_verb: List['RecipeDeleteVerb'] = relationship('RecipeDeleteVerb')
    signal_ending_flavour: EndingFlavour = Column(
        EnumType(EndingFlavour, name='ending_flavour'))
    craftable: bool = Column(Boolean)
    hint_only: bool = Column(Boolean)
    warmup: int = Column(Integer)
    deck_effect: List['RecipeDeckEffect'] = relationship('RecipeDeckEffect')
    internal_deck_id: int = Column(Integer,
                                   ForeignKey('decks.id'),
                                   nullable=True)
    internal_deck: Optional['Deck'] = relationship('Deck')
    alternative_recipes: List['RecipeAlternativeRecipeDetails'] = relationship(
        'RecipeAlternativeRecipeDetails',
        back_populates='source_recipe',
        foreign_keys='RecipeAlternativeRecipeDetails.source_recipe_id')
    linked_recipes: List['RecipeLinkedRecipeDetails'] = relationship(
        'RecipeLinkedRecipeDetails',
        back_populates='source_recipe',
        foreign_keys='RecipeLinkedRecipeDetails.source_recipe_id')
    from_alternative_recipes: List['RecipeAlternativeRecipeDetails'] = \
        relationship(
            'RecipeAlternativeRecipeDetails',
            foreign_keys='RecipeAlternativeRecipeDetails.recipe_id'
        )
    from_linked_recipes: List['RecipeLinkedRecipeDetails'] = relationship(
        'RecipeLinkedRecipeDetails',
        foreign_keys='RecipeLinkedRecipeDetails.recipe_id')
    ending_flag: Optional[str] = Column(String, nullable=True)
    max_executions: int = Column(Integer)
    burn_image: Optional[str] = Column(String, nullable=True)
    portal_effect: PortalEffect = Column(
        EnumType(PortalEffect, name='portal_effect'))
    slot_specifications: List['RecipeSlotSpecification'] = relationship(
        'RecipeSlotSpecification', back_populates='recipe')
    signal_important_loop: bool = Column(Boolean)
    comments: Optional[str] = Column(String, nullable=True)

    @property
    def from_recipes(self) -> List['Recipe']:
        return sorted(
            set([d.source_recipe for d in self.from_alternative_recipes] +
                [d.source_recipe for d in self.from_linked_recipes]),
            key=lambda r: r.recipe_id)

    @classmethod
    def from_data(cls, file: File, data: Dict[str, Any],
                  translations: Dict[str, Dict[str, Any]],
                  game_contents: GameContents) -> 'Recipe':
        r = game_contents.get_recipe(data['id'])
        r.file = file
        r.label = get(data, 'label', data['id'], translations=translations)
        r.start_description = get(data,
                                  'startdescription',
                                  translations=translations)
        r.description = get(data, 'description', translations=translations)
        r.action = game_contents.get_verb(get(data, 'actionId'))
        r.requirements = RecipeRequirement.from_data(
            get(data, 'requirements', {}), game_contents)
        r.table_requirements = RecipeTableRequirement.from_data(
            get(data, 'tablereqs', {}), game_contents)
        r.extant_requirements = RecipeExtantRequirement.from_data(
            get(data, 'extantreqs', {}), game_contents)
        r.effects = RecipeEffect.from_data(get(data, 'effects', {}),
                                           game_contents)
        if 'aspects' in data:
            # TODO Remove this when fixed
            if isinstance(data['aspects'], str):
                logging.error('Invalid value for aspects for recipe {}'.format(
                    data['id']))
            else:
                r.aspects = RecipeAspect.from_data(get(data, 'aspects', {}),
                                                   game_contents)
        r.mutation_effects = MutationEffect.from_data(
            get(data, 'mutations', []), game_contents)
        r.purge = RecipePurge.from_data(get(data, 'purge', {}), game_contents)
        r.halt_verb = RecipeHaltVerb.from_data(get(data, 'haltverb', {}),
                                               game_contents)
        r.delete_verb = RecipeDeleteVerb.from_data(get(data, 'deleteverb', {}),
                                                   game_contents)
        r.signal_ending_flavour = EndingFlavour(
            get(data, 'signalEndingFlavour', 'None'))
        r.craftable = get(data, 'craftable', False, to_bool)
        r.hint_only = get(data, 'hintonly', False, to_bool)
        r.warmup = get(data, 'warmup', 0, int)
        r.deck_effect = RecipeDeckEffect.from_data(get(data, 'deckeffect', {}),
                                                   game_contents)
        internal_deck = get(data, 'internaldeck')
        if internal_deck:
            internal_deck['id'] = "internal:" + r.recipe_id
            r.internal_deck = Deck.from_data(file, internal_deck, {},
                                             game_contents)
        alternative_recipes = get(data, 'alternativerecipes', [])
        if not alternative_recipes:
            alternative_recipes = get(data, 'alt', [])
        r.alternative_recipes = [
            RecipeAlternativeRecipeDetails.from_data(
                lrd, RecipeAlternativeRecipeDetailsChallengeRequirement,
                game_contents) for lrd in alternative_recipes
        ]
        r.linked_recipes = [
            RecipeLinkedRecipeDetails.from_data(
                lrd, RecipeLinkedRecipeDetailsChallengeRequirement,
                game_contents) for lrd in get(data, 'linked', [])
        ]
        r.ending_flag = get(data, 'ending')
        r.max_executions = get(data, 'maxexecutions', 0, int)
        r.burn_image = get(data, 'burnimage')
        r.portal_effect = PortalEffect(
            get(data, 'portaleffect', 'none').lower())
        r.slot_specifications = [
            RecipeSlotSpecification.from_data(
                v, {
                    c: c_transformation["slots"][i]
                    for c, c_transformation in translations.items()
                    if "slots" in c_transformation
                }, game_contents) for i, v in enumerate(get(data, 'slots', []))
        ]
        r.signal_important_loop = get(data, 'signalimportantloop', False,
                                      to_bool)
        r.comments = get(data, 'comments', None)
        if not r.comments:
            r.comments = get(data, 'comment', None)
        return r

    @classmethod
    def get_by_recipe_id(cls, session: Session, recipe_id: str) -> 'Recipe':
        return session.query(cls).filter(cls.recipe_id == recipe_id).one()
class Storylet(GameEntity):
    __tablename__ = "storylets"

    can_go_back = Column(Boolean)
    category = Column(EnumType(StoryletCategory, length=127))
    distribution = Column(EnumType(StoryletDistribution, length=127))
    frequency = Column(EnumType(StoryletFrequency, length=127))
    stickiness = Column(EnumType(StoryletStickiness, length=127))
    image = Column(String(1023))
    urgency = Column(EnumType(StoryletUrgency, length=127))
    is_autofire = Column(Boolean, default=False)
    is_card = Column(Boolean, default=False)
    is_top_level = Column(Boolean, default=False)

    observations: InstrumentedList[StoryletObservation] = relationship(
        "StoryletObservation",
        back_populates="storylet",
        cascade="all, delete, delete-orphan",
        lazy="selectin",
        order_by="desc(StoryletObservation.last_modified)"
    )
    outcome_redirects = relationship(
        "OutcomeObservation", back_populates="redirect"
    )
    areas = relationship(
        "Area", secondary=areas_storylets, back_populates="storylets"
    )
    settings = relationship(
        "Setting", secondary=settings_storylets, back_populates="storylets"
    )
    branches = relationship(
        "Branch", back_populates="storylet", order_by="desc(Branch.ordering)"
    )
    thing = relationship(
        "Quality", back_populates="storylet", uselist=False
    )

    before = relationship(
        "Storylet",
        secondary=storylets_order,
        primaryjoin="storylets_order.c.after_id == Storylet.id",
        secondaryjoin="storylets_order.c.before_id == Storylet.id",
        backref="after"
    )

    def url(self, area_id: int) -> str:
        return f"/storylet/{area_id}/{self.id}"

    @property
    def color(self) -> str:
        if self.category == StoryletCategory.SINISTER:
            return "black"
        elif self.category == StoryletCategory.GOLD:
            return "gold"
        elif self.category in (
                StoryletCategory.AMBITION, StoryletCategory.SEASONAL
        ):
            return "silver"
        elif self.category in (
                StoryletCategory.QUESTICLE_START,
                StoryletCategory.QUESTICLE_STEP,
                StoryletCategory.EPISODIC
        ):
            return "bronze"
        return "white"

    @property
    def name(self) -> str:
        return latest_observation_property(self.observations, "name")

    @property
    def teaser(self) -> str:
        return latest_observation_property(self.observations, "teaser")

    @property
    def description(self) -> str:
        return latest_observation_property(self.observations, "description")

    @property
    def quality_requirements(self) -> List:
        return latest_observation_property(
            self.observations, "quality_requirements"
        )

    def __repr__(self) -> str:
        return f"<Storylet id={self.id} name={self.name}>"