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
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")
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()
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())
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")
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")
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)
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")
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)
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" )
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}"
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}
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,
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")
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()
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}>"