Conworld Codex 2018-05-17

By Max Woerner Chase

In this post:


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.