Conworld Codex 2018-06-14
In this post:
- I try to nail down the interface concepts.
- Then rework the backend a bunch of times.
My basic idea of how I want to structure Conworld Codex is slowly shaping up.
A "Codex" is a SQLite database. It's handled in memory normally, and written to disk all at once, possibly in some kind of versioned format.
Through SQLAlchemy, each in-memory database has a corresponding session. Therefore, I can say that a loaded Codex has an associated session.
The sessions have associated models, which are currently data-only. I suspect that "fat models" in Django come from trying to put the business logic near the parts of the code responsible for persistence.
For the other levels of code, access to the Codex data is mediated through an "App" (should be "Codex") object, which functions as a root to a tree-based interface, and maintains canonical copies of everything visible to consumers of that interface. Taking some terminology from Wx, we have ideas like:
- There can be arbitrarily many Frames, but this current idea allows only one Frame pointing to a particular top-level resource. This could be changed if the Abstract Widgets were converted from NamedTuples to dataclasses, and given weakrefs to their parent objects.
- I think what I actually want is that each Frame handles all of its descendants, so possibly the change I want to make for now is to move the canonicalization dict onto the Frame... but then the Codex would still be responsible for synchronizing the model objects, if SQLAlchemy can't handle that. And SQLAlchemy doesn't know what I'm doing here.
Okay, this train of thought is unpleasant enough that I'm going to just start searching for stuff like "declarative GUI" and see if I find anything applicable to this.
Enaml looks interesting, though parts of the docs do seem worryingly anemic.
What's got me down right now is, I'm trying to teach myself how to put together UIs, and the corollary there is, I don't know what I'm doing. I think it would be easier with a completely fixed, single-window setup... which is not what I'm doing.
I think what I need to do if I want to get anywhere with this is, step away from the higher levels of code, and explicitly write out every transformation of data I want to model. They all need to be handled, so I should have a checklist made.
First, the data:
- Events (name)
- Tellers (name)
- Topics (name)
and relationships:
- Let us say for now that every Teller is a Topic. But any kind of relation there implies that perhaps there should be an overall list of Topic/Teller names, and the Topic and Teller tables just foreign key into them?
- A Teller can have commentary relating an Event to a Topic, and my feeling is that the Topic-Event relation is closer than the other relations (what Events a Teller has commented on in relation to a Topic, and what Topics a Teller has commented on in relation to an Event)
- There are two relations that can hold between events: contribution and composition. These are binary relations between two Events, and are associated with commentary by a Teller.
It occurs to me that a database in which the tables were sum types and the indexes were explictly of functions, would be able to express these ideas in a very concise way. But, since I don't feel like digging up the relevant old projects (... maybe if I ever feel like getting better with profiling), I'll look into joined table inheritance.
I went and rewrote all the models underlying this with all the stuff I just said in mind. Somehow, the tests all pass after I did that. I think it might be because there's no __slots__ attribute on any of the classes I wrote? Anyway.
I may have rewritten things a few more times.
For now, the most primitive types are Events, Topics, and secondarily Tellers.
To create an Event, provide its name, which must be unique.
To create a Topic, provide its name, which must be unique.
To create a Teller, provide either the name of a Topic which is not already associated with a Teller, or a name for a new Topic.
I believe relations are made just with the appropriate constructor, and then Commentary composes a Relation and a Teller.
To generalize this, I think I need a big matrix, where one side is CRUD and the other side is Event, Topic, Teller, EventRelation, EventTopic, EventContribution, EventConstitution, and Commentary.
Have a code dump:
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 | """SQLAlchemy ORM classes for modeling codex data.""" from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint 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__ = 'event' id = Column(Integer, primary_key=True) name = Column(String, unique=True, nullable=False) class Topic(Base): # type: ignore """A person, place, or thing.""" __tablename__ = 'topic' 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__ = 'teller' id = Column(Integer, ForeignKey('topic.id'), primary_key=True) class EventRelation(Base): # type: ignore """Base class for relations.""" __tablename__ = 'event_relation' id = Column(Integer, primary_key=True) type = Column(String(50)) __mapper_args__ = { 'polymorphic_identity': 'event_relation', 'polymorphic_on': type, } class EventTopic(EventRelation): # type: ignore """How a topic relates to an event.""" __tablename__ = 'event_topic' __table_args__ = ( UniqueConstraint('topic_id', 'event_id'), ) __mapper_args__ = { 'polymorphic_identity': 'event_topic', } id = Column(Integer, ForeignKey('event_relation.id'), primary_key=True) topic_id = Column(Integer, ForeignKey('topic.id'), nullable=False) event_id = Column(Integer, ForeignKey('event.id'), nullable=False) class EventContribution(EventRelation): """How one event contributed to another.""" __tablename__ = 'event_contribution' __table_args__ = ( UniqueConstraint('cause', 'effect'), ) __mapper_args__ = { 'polymorphic_identity': 'event_contribution', } id = Column(Integer, ForeignKey('event_relation.id'), primary_key=True) cause = Column(Integer, ForeignKey('event.id'), nullable=False) effect = Column(Integer, ForeignKey('event.id'), nullable=False) class EventConstitution(EventRelation): """How one event was part of another.""" __tablename__ = 'event_constitution' __table_args__ = ( UniqueConstraint('part', 'whole'), ) __mapper_args__ = { 'polymorphic_identity': 'event_constitution' } id = Column(Integer, ForeignKey('event_relation.id'), primary_key=True) part = Column(Integer, ForeignKey('event.id'), nullable=False) whole = Column(Integer, ForeignKey('event.id'), nullable=False) class Commentary(Base): """Commentary on a relation by a Teller.""" __tablename__ = 'commentary' __table_args__ = ( UniqueConstraint('event_relation_id', 'teller_id'), ) id = Column(Integer, primary_key=True) event_relation_id = Column(Integer, ForeignKey('event_relation.id'), nullable=False) teller_id = Column(Integer, ForeignKey('teller.id'), nullable=False) text = Column(String) |
I'm very unsure whether some of this makes sense, but I'm feeling a little better about using inheritance rather than manually monomorphizing relationships like I was before.
PS: You know what category of feature would be great to think about with all of this stuff? Robust undo/redo support.
Next week, I get that matrix filled in.