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.