+ return sorted(list(items.values()))
+
+ @classmethod
+ def by_date_range_with_limits(cls: type[BaseModelInstance],
+ db_conn: DatabaseConnection,
+ date_range: tuple[str, str],
+ date_col: str = 'day'
+ ) -> tuple[list[BaseModelInstance], str,
+ str]:
+ """Return list of items in DB within (closed) date_range interval.
+
+ If no range values provided, defaults them to 'yesterday' and
+ 'tomorrow'. Knows to properly interpret these and 'today' as value.
+ """
+ start_str = date_range[0] if date_range[0] else 'yesterday'
+ end_str = date_range[1] if date_range[1] else 'tomorrow'
+ start_date = valid_date(start_str)
+ end_date = valid_date(end_str)
+ items = []
+ sql = f'SELECT id FROM {cls.table_name} '
+ sql += f'WHERE {date_col} >= ? AND {date_col} <= ?'
+ for row in db_conn.exec(sql, (start_date, end_date)):
+ items += [cls.by_id(db_conn, row[0])]
+ return items, start_date, end_date
+
+ @classmethod
+ def matching(cls: type[BaseModelInstance], db_conn: DatabaseConnection,
+ pattern: str) -> list[BaseModelInstance]:
+ """Return all objects whose .to_search match pattern."""
+ items = cls.all(db_conn)
+ if pattern:
+ filtered = []
+ for item in items:
+ for attr_name in cls.to_search:
+ toks = attr_name.split('.')
+ parent = item
+ for tok in toks:
+ attr = getattr(parent, tok)
+ parent = attr
+ if pattern in attr:
+ filtered += [item]
+ break
+ return filtered
+ return items
+
+ # database writing
+
+ def save(self, db_conn: DatabaseConnection) -> None:
+ """Write self to DB and cache and ensure .id_.
+
+ Write both to DB, and to cache. To DB, write .id_ and attributes
+ listed in cls.to_save_[simples|versioned|_relations].
+
+ Ensure self.id_ by setting it to what the DB command returns as the
+ last saved row's ID (cursor.lastrowid), EXCEPT if self.id_ already
+ exists as a 'str', which implies we do our own ID creation (so far
+ only the case with the Day class, where it's to be a date string.
+ """
+ values = tuple([self.id_] + [getattr(self, key)
+ for key in self.to_save_simples])
+ table_name = self.table_name
+ cursor = db_conn.exec_on_vals(f'REPLACE INTO {table_name} VALUES',
+ values)
+ if not isinstance(self.id_, str):
+ self.id_ = cursor.lastrowid # type: ignore[assignment]
+ self.cache()
+ for attr_name in self.to_save_versioned():
+ getattr(self, attr_name).save(db_conn)
+ for table, column, attr_name, key_index in self.to_save_relations:
+ assert isinstance(self.id_, (int, str))
+ db_conn.rewrite_relations(table, column, self.id_,
+ [[i.id_] for i
+ in getattr(self, attr_name)], key_index)
+
+ def remove(self, db_conn: DatabaseConnection) -> None:
+ """Remove from DB and cache, including dependencies."""
+ if self.id_ is None or self._get_cached(self.id_) is None:
+ raise HandledException('cannot remove unsaved item')
+ for attr_name in self.to_save_versioned():
+ getattr(self, attr_name).remove(db_conn)
+ for table, column, attr_name, _ in self.to_save_relations:
+ db_conn.delete_where(table, column, self.id_)
+ self._uncache()
+ db_conn.delete_where(self.table_name, 'id', self.id_)
+ self._disappear()