home · contact · privacy
Coordinate all tables in new BricksDb super-structure.
authorPlom Heller <plom@plomlompom.com>
Fri, 1 May 2026 17:59:23 +0000 (19:59 +0200)
committerPlom Heller <plom@plomlompom.com>
Fri, 1 May 2026 17:59:23 +0000 (19:59 +0200)
bricksplom.py

index 6a8fc7314684f0d8ea884eef1cf73487f0c21b29..58c846dfde27dfc61e690f016e37dd139b428631 100755 (executable)
@@ -68,11 +68,23 @@ class Textfiled(ABC):
     @abstractmethod
     def from_textfile(
             cls,
-            path: tuple[str, str]
+            path: tuple[str, str],
+            **kwargs
             ) -> dict[str, Self]:
         'Build from file at path.'
 
 
+class WithDb(ABC):
+
+    def __init__(
+            self,
+            db: Optional['BricksDb'] = None,
+            **kwargs
+            ) -> None:
+        self._db = db
+        super().__init__(**kwargs)
+
+
 class Color(Textfiled):
     'Color incl. solidness/transparency field.'
 
@@ -89,7 +101,8 @@ class Color(Textfiled):
     @classmethod
     def from_textfile(
             cls,
-            path: tuple[str, str]
+            path: tuple[str, str],
+            **_
             ) -> dict[str, Self]:
         collected = {}
         for id_, desc in [cls.tokify(line, 2) for line in cls.lines_of(path)]:
@@ -120,7 +133,8 @@ class Design(Textfiled):
     @classmethod
     def from_textfile(
             cls,
-            path: tuple[str, str]
+            path: tuple[str, str],
+            **_
             ) -> dict[str, Self]:
         collected = {}
         alts: dict[str, set[str]] = {}
@@ -177,7 +191,8 @@ class Piece(Textfiled):
     @classmethod
     def from_textfile(
             cls,
-            path: tuple[str, str]
+            path: tuple[str, str],
+            **_
             ) -> dict[str, Self]:
         collected = {}
         for toks in [cls.tokify(line, 3) for line in cls.lines_of(path)]:
@@ -194,7 +209,7 @@ class Piece(Textfiled):
                 f'{self.color_id:>3} {self.comment}').rstrip()
 
 
-class Collection(Textfiled):
+class Collection(Textfiled, WithDb):
     'Named collection of pieces in order of pages of columns of counts.'
 
     def __init__(
@@ -202,17 +217,21 @@ class Collection(Textfiled):
             id_: str,
             is_in: Optional[bool],
             description: str,
-            piece_listings: tuple[Page, ...]
+            piece_listings: tuple[Page, ...],
+            **kwargs
             ) -> None:
         self.id_ = id_
         self.is_in = is_in
         self.description = description
         self.piece_listings = piece_listings
+        super().__init__(**kwargs)
 
     @classmethod
     def from_textfile(
             cls,
-            path: tuple[str, str]
+            path: tuple[str, str],
+            db: Optional['BricksDb'] = None,
+            **_
             ) -> dict[str, Self]:
         collected: dict[str, tuple[Optional[bool],
                                    str,
@@ -240,8 +259,12 @@ class Collection(Textfiled):
                 assert len(id_) > 0
                 i_listings[-1][-1] += [(int(count), id_, comment)]
         return {
-            k: cls(k, v[0], v[1], tuple(tuple(tuple(column) for column in page)
-                                        for page in v[2]))
+            k: cls(id_=k,
+                   is_in=v[0],
+                   description=v[1],
+                   piece_listings=tuple(tuple(tuple(column) for column in page)
+                                        for page in v[2]),
+                   db=db)
             for k, v in collected.items()}
 
     def __str__(
@@ -276,86 +299,44 @@ class Collection(Textfiled):
                 collected += list(column)
         return tuple(collected)
 
-    def _piece_to_box_idx_and_description(
-            self,
-            piece: 'Piece',
-            designs: dict[str, 'Design'],
-            boxes: dict[str, 'Box']
-            ) -> tuple[str, str]:
-        design = Design.possibly_by_alt(piece.design_id, designs)
-        assert design is not None
-        description = design.description
-        box = [b for b in boxes.values() if design.id_ in b.designs][0]
-        idx_in_box = box.designs.index(design.id_) + 1
-        return f'{box.id_:>2}:{idx_in_box:>2}', description
-
     def print_packing_instructions(
-            self,
-            pieces: dict[str, 'Piece'],
-            designs: dict[str, 'Design'],
-            boxes: dict[str, 'Box'],
+            self
             ) -> None:
         'Print helper for putting into boxes all pieces of self.'
         def format_line(_, piece_id, __) -> str:
-            box_listing, description = self._piece_to_box_idx_and_description(
-                    pieces[piece_id], designs, boxes)
+            assert self._db
+            box_listing, description = self._db.piece_to_box_listing(piece_id)
             return f'{piece_id:>7} {box_listing} {description}'
 
         print(self._format_paginated(format_line))
 
     def print_unpacking_instructions(
-            self,
-            pieces: dict[str, 'Piece'],
-            designs: dict[str, 'Design'],
-            boxes: dict[str, 'Box'],
-            colors: dict[str, 'Color']
+            self
             ) -> None:
         'Print helper for collecting from boxes all pieces of self.'
+        assert self._db
         lines = []
         for count, piece_id, _ in self.piece_listings_flat():
-            piece = pieces[piece_id]
-            box_listing, description = self._piece_to_box_idx_and_description(
-                    piece, designs, boxes)
-            color = str(colors[piece.color_id]).lstrip().split(CHAR_SPACE,
-                                                               maxsplit=1)[1]
+            box_listing, description = self._db.piece_to_box_listing(piece_id)
+            color = str(self._db.colors[self._db.pieces[piece_id].color_id]
+                        ).lstrip().split(CHAR_SPACE, maxsplit=1)[1]
             lines += [f'{box_listing} {count}× {color} {description}']
         for line in sorted(lines):
             print(line)
 
 
-class Box:
+class Box(WithDb):
     'Order of designs.'
 
     def __init__(
             self,
             id_: str,
-            designs: tuple[str, ...]
+            designs: tuple[str, ...],
+            **kwargs
             ) -> None:
         self.id_ = id_
         self.designs = designs
-
-    @classmethod
-    def from_collections(
-            cls,
-            collections: dict[str, 'Collection'],
-            pieces: dict[str, 'Piece'],
-            designs: dict[str, 'Design']
-            ) -> dict[str, Self]:
-        'Parse from "box:"-prefixed entries in collections.'
-        collected = {}
-        for coll in [c for c in collections.values()
-                     if c.id_.startswith(BOX_PREFIX)]:
-            box_id = coll.id_[len(BOX_PREFIX):]
-            assert box_id
-            boxed_designs: list[str] = []
-            for piece_id in [t[1] for t in coll.piece_listings_flat()]:
-                design = Design.possibly_by_alt(pieces[piece_id].design_id,
-                                                designs)
-                assert design
-                if [design.id_] != boxed_designs[-1:]:
-                    boxed_designs += [design.id_]
-            collected[box_id] = cls(box_id, tuple(boxed_designs))
-        return collected
+        super().__init__(**kwargs)
 
     def __str__(
             self
@@ -364,94 +345,134 @@ class Box:
 
     def print_pieces(
             self,
-            pieces: dict[str, 'Piece'],
-            designs: dict[str, 'Design'],
-            colors: dict[str, 'Color'],
-            collections: dict[str, 'Collection']
             ) -> None:
         'Print explanatory listings of pieces expected by .designs.'
-        print(f'box:{self.id_} - storage box')
+        assert self._db
+        print(f'box:{self.id_}')
         for design_id in self.designs:
-            design = designs[design_id]
+            design = self._db.designs[design_id]
             print(f'=== {design_id:>6}: {design.description} ===')
-            for piece in [p for p in pieces.values()
+            for piece in [p for p in self._db.pieces.values()
                           if p.design_id in {design.id_} | design.alternates]:
                 count_pieces = 0
-                color = colors[piece.color_id]
+                color = self._db.colors[piece.color_id]
                 for listings in [c.piece_listings_flat()
-                                 for c in collections.values()]:
+                                 for c in self._db.colls.values()]:
                     for count in [t[0] for t in listings if t[1] == piece.id_]:
                         count_pieces += count
                 print(f'{count_pieces:>2}× {piece.id_:>7} / {color}')
 
 
-def check_consistencies_between_tables(
-        colors: dict[str, Color],
-        pieces: dict[str, Piece],
-        collections: dict[str, Collection],
-        designs: dict[str, Design],
-        boxes: dict[str, Box]
-        ) -> None:
-    'Ensure intra-table consistencies between inputs.'
-
-    # check all items listed in collections recorded in pieces
-    for coll in collections.values():
-        for _, piece_id, _ in coll.piece_listings_flat():
-            assert piece_id in pieces, piece_id
-
-    # check all designs listed in boxes recorded in designs
-    for design_ids in [b.designs for b in boxes.values()]:
-        for design_id in design_ids:
-            assert design_id in designs, design_id
-
-    # check all pieces' designs recorded in designs
-    for design_id in [piece.design_id for piece in pieces.values()]:
-        assert Design.possibly_by_alt(design_id, designs), design_id
-
-    # check all recorded designs have matching pieces (at least via alts)
-    for design_id, alts in [(k, v.alternates) for k, v in designs.items()]:
-        pieces_found = False
-        for id_ in [id_ for id_ in {design_id} | alts
-                    if id_ in [piece.design_id for piece in pieces.values()]]:
-            pieces_found = True
-            break
-        assert pieces_found, design_id
-
-    # check all pieces' colors are recorded
-    for color_id in [v.color_id for v in pieces.values()]:
-        assert color_id in colors, color_id
-
-    # check collections in-out directions even out
-    counts: dict[str, int] = {}
-    for coll in collections.values():
-        if coll.is_in is None:
-            continue
-        for count, piece_id, _ in coll.piece_listings_flat():
-            counts[piece_id] = (counts.get(piece_id, 0)
-                                + ((1 if coll.is_in else (-1)) * count))
-    for piece_id, count in counts.items():
-        assert count == 0, (piece_id, count)
+class BricksDb:
+    'Collection of all the tables enabling their combined processing.'
+
+    def __init__(
+            self,
+            path_tables: str
+            ) -> None:
+        self.colors = Color.from_textfile((path_tables, PATH_COLORS))
+        self.pieces = Piece.from_textfile((path_tables, PATH_PIECES))
+        self.colls = Collection.from_textfile((path_tables, PATH_COLLECTIONS),
+                                              db=self)
+        self.designs = Design.from_textfile((path_tables, PATH_DESIGNS))
+        self._check_consistencies_between_tables()
+
+    def boxes(
+            self
+            ) -> dict[str, Box]:
+        'Parsed from "box:"-prefixed entries in .colls.'
+        collected = {}
+        for coll in [c for c in self.colls.values()
+                     if c.id_.startswith(BOX_PREFIX)]:
+            box_id = coll.id_[len(BOX_PREFIX):]
+            assert box_id
+            boxed_designs: list[str] = []
+            for piece_id in [t[1] for t in coll.piece_listings_flat()]:
+                design = Design.possibly_by_alt(
+                        self.pieces[piece_id].design_id,
+                        self.designs)
+                assert design
+                if [design.id_] != boxed_designs[-1:]:
+                    boxed_designs += [design.id_]
+            collected[box_id] = Box(box_id, tuple(boxed_designs), db=self)
+        return collected
+
+    def _check_consistencies_between_tables(
+            self
+            ) -> None:
+
+        # check all items listed in collections recorded in pieces
+        for coll in self.colls.values():
+            for _, piece_id, _ in coll.piece_listings_flat():
+                assert piece_id in self.pieces, piece_id
+
+        # check all designs listed in boxes recorded in designs
+        for design_ids in [b.designs for b in self.boxes().values()]:
+            for design_id in design_ids:
+                assert design_id in self.designs, design_id
+
+        # check all pieces' designs recorded in designs
+        for design_id in [piece.design_id for piece in self.pieces.values()]:
+            assert Design.possibly_by_alt(design_id, self.designs), design_id
+
+        # check all recorded designs have matching pieces (at least via alts)
+        for design_id, alts in [(k, v.alternates)
+                                for k, v in self.designs.items()]:
+            pieces_found = False
+            for id_ in [id_ for id_ in {design_id} | alts
+                        if id_ in [piece.design_id
+                                   for piece in self.pieces.values()]]:
+                pieces_found = True
+                break
+            assert pieces_found, design_id
+
+        # check all pieces' colors are recorded
+        for color_id in [v.color_id for v in self.pieces.values()]:
+            assert color_id in self.colors, color_id
+
+        # check collections in-out directions even out
+        counts: dict[str, int] = {}
+        for coll in self.colls.values():
+            if coll.is_in is None:
+                continue
+            for count, piece_id, _ in coll.piece_listings_flat():
+                counts[piece_id] = (counts.get(piece_id, 0)
+                                    + ((1 if coll.is_in else (-1)) * count))
+        for piece_id, count in counts.items():
+            assert count == 0, (piece_id, count)
+
+    def piece_to_box_listing(
+            self,
+            piece_id: str,
+            ) -> tuple[str, str]:
+        'For Piece of piece_id, print bosition in .boxes() and description.'
+        design = Design.possibly_by_alt(self.pieces[piece_id].design_id,
+                                        self.designs)
+        assert design is not None
+        description = design.description
+        box = [b for b in self.boxes().values() if design.id_ in b.designs][0]
+        idx_in_box = box.designs.index(design.id_) + 1
+        return f'{box.id_:>2}:{idx_in_box:>2}', description
+
+    def print(
+            self
+            ) -> None:
+        'Show all we know.'
+        for title, items in (('COLORS', self.colors),
+                             ('PIECES', self.pieces),
+                             ('DESIGNS', self.designs),
+                             ('BOXES', self.boxes()),
+                             ('COLLECTIONS', self.colls)):
+            print(title)
+            for item in items.values():
+                print(item)
 
 
 def main(
         ) -> None:
     'Load tables from BRICKSPLOM_DIR and put out in reproducible listings.'
-    dir_tables = environ.get(NAME_ENV_DIRNAME, '.')
-    colors = Color.from_textfile((dir_tables, PATH_COLORS))
-    pieces = Piece.from_textfile((dir_tables, PATH_PIECES))
-    collections = Collection.from_textfile((dir_tables, PATH_COLLECTIONS))
-    designs = Design.from_textfile((dir_tables, PATH_DESIGNS))
-    boxes = Box.from_collections(collections, pieces, designs)
-    check_consistencies_between_tables(colors, pieces, collections, designs,
-                                       boxes)
-    for title, items in (('COLORS', colors),
-                         ('PIECES', pieces),
-                         ('DESIGNS', designs),
-                         ('BOXES', boxes),
-                         ('COLLECTIONS', collections)):
-        print(title)
-        for item in items.values():
-            print(item)
+    db = BricksDb(environ.get(NAME_ENV_DIRNAME, '.'))
+    db.print()
 
 
 main()