Conworld Codex 2018-05-17
In this post:
- Step one proves to be naïvely optimistic.
- I do, though, manage to outline the adapter code between the models and the GUI (not yet implemented)... and then promptly rewrite both models and adapter.
Okay, let's see. Step one here, is to put together some GUI code.
I should note here and now that a lot of my attempts to "do" GUI code are going to be kind of flailing and confused.
Anyway, what I have so far is the idea that "getting to the meat" of an event actually requires some somewhat deep nesting. An event has four overall categories of events it relates to, and within those, the events that it relates to in such a way, and within those relationships, how the event is talked about. Also, events relate to topics.
I think I'm going to have to try to sketch this out, because using words is a mess.
I had a revelation on Mastodon:
I wish I could write GUI code as, like, a bunch of high-level relationships between components, and then apply styling to actually instantiate concrete components to implement the design.
Basically, I have been spoiled by the fact that CSS lets me toss down layout that looks completely asinine without styling, and then style the heck out of it.
I wonder if I'd get my wish if I made some "container" types, then wrote visitor code to process them...
The basic idea is, what if I wrote code so as to defer the decisions that I, personally, do not feel qualified to make? This feels like it would be kind of janky in a collaborative context, but that's not this code. Basically, instead of wireframing the layout, I want to "wireframe the interactions". Like, "here's the data you need to fulfill such and such hypothetical GUI action".
I took that idea and ran with it. Now the "GUI" code is full of stub methods, some of which have some API in mind. I'll try documenting the code first, so I know what it's supposed to do.
Hm. Here's where I am now:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | """Main GUI code for Conworld Codex. The model handler classes assume they have exclusive access to the database. """ import enum import typing from sqlalchemy.orm import session from .. import models class Relation(enum.Enum): """The four ways events can relate.""" CONTAINED_BY = enum.auto() PRECEDED_BY = enum.auto() FOLLOWED_BY = enum.auto() CONTAINS = enum.auto() class App(typing.NamedTuple): """High-level abstraction.""" session: session.Session class EventWindow(typing.NamedTuple): """Container for rendering the window associated with an Event.""" event: models.Event app: App @property def session(self) -> session.Session: """Return the associated SQLAlchemy session.""" return self.app.session @property def contained_by(self) -> 'RelationTab': """Return the RelationTab for Events that contain this one.""" return RelationTab(self, Relation.CONTAINED_BY) @property def preceded_by(self) -> 'RelationTab': """Return the RelationTab for Events that precede this one.""" return RelationTab(self, Relation.PRECEDED_BY) @property def followed_by(self) -> 'RelationTab': """Return the RelationTab for Events that follow this one.""" return RelationTab(self, Relation.FOLLOWED_BY) @property def contains(self) -> 'RelationTab': """Return the RelationTab for Events that are contained by this one.""" return RelationTab(self, Relation.CONTAINS) @property def topics(self) -> 'TopicsTab': """Return the TopicsTab for this Event.""" return TopicsTab(self) class RelationTab(typing.NamedTuple): """Container for each tab within an EventWindow.""" event_window: EventWindow relation: Relation @property def session(self) -> session.Session: """Return the associated SQLAlchemy session.""" return self.event_window.session @property def events(self) -> typing.List['EventTab']: """Return the list of EventTabs associated with this tab.""" # TODO @property def unlinked_events(self) -> typing.List[models.Event]: """Return the list of events with no associated EventTab.""" # TODO def add_event(self, event: models.Event) -> 'EventTab': """Add a tab for the given Event, and return the EventTab. Raises an exception if the Event already has a tab. """ # TODO class EventTab(typing.NamedTuple): """Container for each event within a RelationTab.""" relation_tab: RelationTab event: models.Event @property def session(self) -> session.Session: """Return the associated SQLAlchemy session.""" return self.relation_tab.session @property def tellers(self) -> typing.List['EventTellerTab']: """Return the list of EventTellerTabs associated with this tab.""" # TODO @property def unlinked_tellers(self) -> typing.List[models.Teller]: """Return the list of Tellers with no associated EventTellerTab.""" # TODO def add_teller(self, teller: models.Teller) -> 'EventTellerTab': """Add a tab for the given Teller, and return the EventTellerTab. Raises an exception if the Teller already has a tab. """ # TODO def drop(self) -> None: """Remove this tab from the UI model, and the database if necessary.""" # TODO class EventTellerTab(typing.NamedTuple): """Container for each Teller within an EventTab.""" event_tab: EventTab teller: models.Teller @property def session(self) -> session.Session: """Return the associated SQLAlchemy session.""" return self.event_tab.session def drop(self) -> None: """Remove this tab from the UI model, and the database if necessary.""" # TODO class TopicsTab(typing.NamedTuple): """Container for related topics.""" event_window: EventWindow @property def session(self) -> session.Session: """Return the associated SQLAlchemy session.""" return self.event_window.session @property def topics(self) -> typing.List['TopicTab']: """Return the list of TopicTabs associated with this tab.""" # TODO @property def unlinked_topics(self) -> typing.List[models.Topic]: """Return the list of topics with no associated TopicTab.""" # TODO def add_topic(self, topic: models.Topic) -> 'TopicTab': """Add a tab for the given Topic, and return the TopicTab. Raises an exception if the Topic already has a tab. """ # TODO class TopicTab(typing.NamedTuple): """Container for a specific Topic.""" topics_tab: TopicsTab topic: models.Topic @property def session(self) -> session.Session: """Return the associated SQLAlchemy session.""" return self.topics_tab.session @property def tellers(self) -> typing.List['TopicTellerTab']: """Return the list of TopicTellerTabs associated with this tab.""" # TODO @property def unlinked_tellers(self) -> typing.List[models.Teller]: """Return the list of Tellers with no associated TopicTellerTab.""" # TODO def add_teller(self, teller: models.Teller) -> 'TopicTellerTab': """Add a tab for the given Teller, and return the TopicTellerTab. Raises an exception if the Teller already has a tab. """ # TODO def drop(self) -> None: """Remove this tab from the UI model, and the database if necessary.""" # TODO class TopicTellerTab(typing.NamedTuple): """Container for each Teller within an TopicTab.""" topic_tab: TopicTab teller: models.Teller @property def session(self) -> session.Session: """Return the associated SQLAlchemy session.""" return self.topic_tab.session def drop(self) -> None: """Remove this tab from the UI model, and the database if necessary.""" # TODO |
My big concern with this is that my current conception of this design attempts to track state that doesn't currently fit in the database. I think I ought to rewrite the database layout some to normalize it more, and enforce the constraints I want with some kind of policy, rather than through the database structure.
I made the changes. Here's where things ended up:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | """SQLAlchemy ORM classes for modeling codex data.""" from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() # pylint: disable=invalid-name class Event(Base): # type: ignore """The events within the history of the codex.""" __tablename__ = 'events' id = Column(Integer, primary_key=True) name = Column(String, unique=True, nullable=False) class Teller(Base): # type: ignore """The people talking about the events.""" __tablename__ = 'tellers' id = Column(Integer, primary_key=True) name = Column(String, unique=True, nullable=False) topic_id = Column(Integer, ForeignKey('topics.id'), unique=True) topic = relationship('Topic', back_populates='teller') class Topic(Base): # type: ignore """A person, place, or thing.""" __tablename__ = 'topics' id = Column(Integer, primary_key=True) name = Column(String, unique=True, nullable=False) teller = relationship('Teller', back_populates='topic', uselist=False) class EventTopic(Base): # type: ignore """How a topic relates to an event.""" __tablename__ = 'event_topics' __table_args__ = ( UniqueConstraint('topic_id', 'event_id'), ) id = Column(Integer, primary_key=True) topic_id = Column(Integer, ForeignKey('topics.id'), nullable=False) event_id = Column(Integer, ForeignKey('events.id'), nullable=False) class TopicRelation(Base): # type: ignore """How a topic relates to an event, according to a teller.""" __tablename__ = 'topic_relations' __table_args__ = ( UniqueConstraint('event_topic_id', 'teller_id'), ) id = Column(Integer, primary_key=True) event_topic_id = Column( Integer, ForeignKey('event_topics.id'), nullable=False) teller_id = Column(Integer, ForeignKey('tellers.id'), nullable=False) text = Column(String) class Contribution(Base): # type: ignore """How one event contributed to another.""" __tablename__ = 'contributions' __table_args__ = ( UniqueConstraint('cause', 'effect'), ) id = Column(Integer, primary_key=True) cause = Column(Integer, ForeignKey('events.id'), nullable=False) effect = Column(Integer, ForeignKey('events.id'), nullable=False) class ContributionTeller(Base): # type: ignore """How one event contributed to another, according to a teller.""" __tablename__ = 'contribution_tellers' __table_args__ = ( UniqueConstraint('contribution_id', 'teller_id'), ) id = Column(Integer, primary_key=True) contribution_id = Column( Integer, ForeignKey('contributions.id'), nullable=False) teller_id = Column(Integer, ForeignKey('tellers.id'), nullable=False) text = Column(String) class PartOf(Base): # type: ignore """How one event was part of another.""" __tablename__ = 'parts_of' __table_args__ = ( UniqueConstraint('part', 'whole'), ) id = Column(Integer, primary_key=True) part = Column(Integer, ForeignKey('events.id'), nullable=False) whole = Column(Integer, ForeignKey('events.id'), nullable=False) class PartOfTeller(Base): # type: ignore """How one event was part of another, according to a teller.""" __tablename__ = 'part_of_tellers' __table_args__ = ( UniqueConstraint('part_of_id', 'teller_id'), ) id = Column(Integer, primary_key=True) part_of_id = Column(Integer, ForeignKey('parts_of.id'), nullable=False) teller_id = Column(Integer, ForeignKey('tellers.id'), nullable=False) text = Column(String) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | """Main GUI code for Conworld Codex. The model handler classes assume they have exclusive access to the database. """ import enum import typing import sqlalchemy.orm.session from .. import models class Relation(enum.Enum): """The four ways events can relate.""" CONTAINED_BY = enum.auto() PRECEDED_BY = enum.auto() FOLLOWED_BY = enum.auto() CONTAINS = enum.auto() class App(typing.NamedTuple): """High-level abstraction.""" session: sqlalchemy.orm.session.Session class EventWindow(typing.NamedTuple): """Container for rendering the window associated with an Event.""" event: models.Event app: App @property def session(self) -> sqlalchemy.orm.session.Session: """Return the associated SQLAlchemy session.""" return self.app.session @property def contained_by(self) -> 'RelationTab': """Return the RelationTab for Events that contain this one.""" return RelationTab(self, Relation.CONTAINED_BY) @property def preceded_by(self) -> 'RelationTab': """Return the RelationTab for Events that precede this one.""" return RelationTab(self, Relation.PRECEDED_BY) @property def followed_by(self) -> 'RelationTab': """Return the RelationTab for Events that follow this one.""" return RelationTab(self, Relation.FOLLOWED_BY) @property def contains(self) -> 'RelationTab': """Return the RelationTab for Events that are contained by this one.""" return RelationTab(self, Relation.CONTAINS) @property def topics(self) -> 'TopicsTab': """Return the TopicsTab for this Event.""" return TopicsTab(self) class RelationTab(typing.NamedTuple): """Container for each tab within an EventWindow.""" event_window: EventWindow relation: Relation @property def session(self) -> sqlalchemy.orm.session.Session: """Return the associated SQLAlchemy session.""" return self.event_window.session @property def events(self) -> typing.Tuple['EventTab', ...]: """Return the list of EventTabs associated with this tab.""" # TODO @property def unlinked_events(self) -> typing.Tuple[models.Event, ...]: """Return the list of events with no associated EventTab.""" # TODO def add_event(self, event: models.Event) -> 'EventTab': """Add a tab for the given Event, and return the EventTab. Raises an exception if the Event already has a tab. """ # TODO class EventTab(typing.NamedTuple): """Container for each event within a RelationTab.""" relation_tab: RelationTab event: models.Event @property def session(self) -> sqlalchemy.orm.session.Session: """Return the associated SQLAlchemy session.""" return self.relation_tab.session @property def tellers(self) -> typing.Tuple['EventTellerTab', ...]: """Return the list of EventTellerTabs associated with this tab.""" # TODO @property def unlinked_tellers(self) -> typing.Tuple[models.Teller, ...]: """Return the list of Tellers with no associated EventTellerTab.""" # TODO def add_teller(self, teller: models.Teller) -> 'EventTellerTab': """Add a tab for the given Teller, and return the EventTellerTab. Raises an exception if the Teller already has a tab. """ # TODO def drop(self) -> None: """Remove this tab from the UI model.""" # TODO class EventTellerTab(typing.NamedTuple): """Container for each Teller within an EventTab.""" event_tab: EventTab teller: models.Teller @property def session(self) -> sqlalchemy.orm.session.Session: """Return the associated SQLAlchemy session.""" return self.event_tab.session def drop(self) -> None: """Remove this tab from the UI model.""" # TODO class TopicsTab(typing.NamedTuple): """Container for related topics.""" event_window: EventWindow @property def session(self) -> sqlalchemy.orm.session.Session: """Return the associated SQLAlchemy session.""" return self.event_window.session @property def topics(self) -> typing.Tuple['TopicTab', ...]: """Return the list of TopicTabs associated with this tab.""" # TODO @property def unlinked_topics(self) -> typing.Tuple[models.Topic, ...]: """Return the list of topics with no associated TopicTab.""" # TODO def add_topic(self, topic: models.Topic) -> 'TopicTab': """Add a tab for the given Topic, and return the TopicTab. Raises an exception if the Topic already has a tab. """ # TODO class TopicTab(typing.NamedTuple): """Container for a specific Topic.""" topics_tab: TopicsTab topic: models.Topic @property def session(self) -> sqlalchemy.orm.session.Session: """Return the associated SQLAlchemy session.""" return self.topics_tab.session @property def tellers(self) -> typing.Tuple['TopicTellerTab', ...]: """Return the list of TopicTellerTabs associated with this tab.""" # TODO @property def unlinked_tellers(self) -> typing.Tuple[models.Teller, ...]: """Return the list of Tellers with no associated TopicTellerTab.""" # TODO def add_teller(self, teller: models.Teller) -> 'TopicTellerTab': """Add a tab for the given Teller, and return the TopicTellerTab. Raises an exception if the Teller already has a tab. """ # TODO def drop(self) -> None: """Remove this tab from the UI model.""" # TODO class TopicTellerTab(typing.NamedTuple): """Container for each Teller within an TopicTab.""" topic_tab: TopicTab teller: models.Teller @property def session(self) -> sqlalchemy.orm.session.Session: """Return the associated SQLAlchemy session.""" return self.topic_tab.session def drop(self) -> None: """Remove this tab from the UI model.""" # TODO |
(Pictured: why I don't like the idea of changing code after I paste it in. It's full of random quality-of-life changes I realized I wanted after I wrote it.) Anyway, I can definitely see some places this might change, but I think the interface is almost correct, which is good enough for now, since nobody else depends on this code. What I'm hitting now is, I'm actually not so familiar with queries in SQLAlchemy, so I'll have to learn to use them before I can implement most of this.
Next week, I should learn how to use SQLAlchemy queries.