From c0f17da5ec8477a07c6425e28b783c8f6f70b5f3 Mon Sep 17 00:00:00 2001
From: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
Date: Thu, 25 Apr 2024 21:47:40 -0400
Subject: [PATCH 01/13] orbit: db: generate schema from peewee orm classes

instead of manually specifying schemata in init-db.sql, we can describe
the fields and their properties as members of classes within a hierarchy
of classes inheriting from peewee orm's base model.

Obviously peewee orm can do a lot more than generate schemata, but this
is a nice (and fully backwards compatible) start on the work towards
fully embracing peewee for all database operations.

The existing queries continue to work because the schemata generated are
fully compatible with the existing ones (albeit not identical, so the
binary blob needed updating).

This means that the existing db code can be slowly phased out in the next
commits and orbit will remain fully functional throughout.
---
 orbit/Containerfile    |  4 +--
 orbit/db.py            | 41 +++++++++++++++++++++
 orbit/init-db.sql      | 15 --------
 orbit/requirements.txt |  1 +
 test.sh                | 82 ++++++++++++++++++++++--------------------
 5 files changed, 88 insertions(+), 55 deletions(-)
 mode change 100644 => 100755 orbit/db.py
 delete mode 100644 orbit/init-db.sql

diff --git a/orbit/Containerfile b/orbit/Containerfile
index 4492f935..856ff967 100644
--- a/orbit/Containerfile
+++ b/orbit/Containerfile
@@ -3,7 +3,6 @@ FROM alpine:3.19 AS build
 RUN apk update && apk upgrade && apk add \
 	python3-dev \
 	py3-pip \
-	sqlite \
 	build-base \
 	libffi-dev \
 	envsubst \
@@ -19,7 +18,8 @@ COPY . /orbit
 WORKDIR /orbit
 
 RUN mkdir -p /var/orbit/ && \
-	sqlite3 /var/orbit/orbit.db ".read init-db.sql" && \
+	source /radius-venv/bin/activate && \
+	./db.py \
 	:
 
 ARG orbit_version_info
diff --git a/orbit/db.py b/orbit/db.py
old mode 100644
new mode 100755
index ab5a6475..5cbf5dcd
--- a/orbit/db.py
+++ b/orbit/db.py
@@ -1,4 +1,6 @@
+#!/usr/bin/env python3
 import sqlite3
+import peewee
 import config
 # nickname  table name
 # USR => users
@@ -6,6 +8,45 @@
 # REG => newusers
 import sys
 
+DB = peewee.SqliteDatabase(config.database)
+
+
+class BaseModel(peewee.Model):
+    class Meta:
+        database = DB
+        strict_tables = True
+
+
+class User(BaseModel):
+    username = peewee.TextField(unique=True)
+    pwdhash = peewee.TextField()
+    student_id = peewee.TextField(unique=True, null=True)
+
+    class Meta:
+        table_name = 'users'
+
+
+class Session(BaseModel):
+    token = peewee.TextField(primary_key=True)
+    username = peewee.TextField(unique=True)
+    expiry = peewee.FloatField()
+
+    class Meta:
+        table_name = 'sessions'
+
+
+class Registration(BaseModel):
+    student_id = peewee.TextField(unique=True)
+    username = peewee.TextField(unique=True)
+    password = peewee.TextField()
+
+    class Meta:
+        table_name = 'newusers'
+
+
+if __name__ == '__main__':
+    DB.create_tables(BaseModel.__subclasses__())
+
 
 def _do(cmd, reps=(), set_=False, get_=False):
     if config.sql_verbose:
diff --git a/orbit/init-db.sql b/orbit/init-db.sql
deleted file mode 100644
index c1ea03e7..00000000
--- a/orbit/init-db.sql
+++ /dev/null
@@ -1,15 +0,0 @@
-PRAGMA foreign_keys=OFF;
-BEGIN TRANSACTION;
-CREATE TABLE users (
-	username text UNIQUE NOT NULL,
-	pwdhash text NOT NULL,
-	student_id text UNIQUE) STRICT;
-CREATE TABLE sessions (
-        token text PRIMARY KEY,
-        username text UNIQUE NOT NULL,
-        expiry real NOT NULL) STRICT;
-CREATE TABLE newusers (
-	student_id text UNIQUE NOT NULL,
-	username text UNIQUE NOT NULL,
-	password text NOT NULL) STRICT;
-COMMIT;
diff --git a/orbit/requirements.txt b/orbit/requirements.txt
index 5c876b11..a1543266 100644
--- a/orbit/requirements.txt
+++ b/orbit/requirements.txt
@@ -4,3 +4,4 @@ Markdown ~= 3.3.7
 urllib3 ~= 1.26.16
 uWSGI ~= 2.0.21
 uwsgi-tools ~= 1.1.1
+peewee ~= 3.17.1
diff --git a/test.sh b/test.sh
index 314fcacc..c052fdbe 100755
--- a/test.sh
+++ b/test.sh
@@ -80,44 +80,50 @@ ${DOCKER} volume export singularity_orbit-db > test/orbit_orig.tar
 
 # Import an empty orbit db with no users or sessions
 xxd -r <<- 'EOF' | gunzip | ${DOCKER} volume import singularity_orbit-db -
-00000000: 1f8b 0800 0000 0000 0003 edda c14e db30  .............N.0
-00000010: 18c0 7107 4a4b 2b85 224d 5a2e 20f9 3824  ..q.JK+."MZ. .8$
-00000020: 4493 8c72 9b34 56f5 80d6 b151 ca81 5395  D..r.4V....Q..S.
-00000030: aa9e 8806 0d8b 5395 9da6 ee51 7887 5df6  ......S....Qx.].
-00000040: 0e7b 8d5d f602 3b2e cd30 a29b 4a91 36b4  .{.]..;..0..J.6.
-00000050: 2dfa ffa4 d84e 6cb7 feec e460 2551 dc0b  -....Nl....`%Q..
-00000060: 93ad 7e4f dc23 37b5 b3bd 9de5 de4f b9eb  ..~O.#7......O..
-00000070: f993 5478 db3b 9eef d7fd ba5b 17ae e7ba  ..Tx.;.....[....
-00000080: 754f 48f7 3e07 650c 7512 c452 8af4 3f6f  uOH.>.e.u..R..?o
-00000090: 6d37 affe 3f75 78d0 0a13 255f 47f1 5990  m7..?ux...%_G.Y.
-000000a0: c8c7 6255 5896 789a ce87 1056 7a54 6e34  ..bUX.x....VzTn4
-000000b0: 5d4c 8fc2 8d73 eb0e 3f6f 89ad f8d2 ae7e  ]L...s..?o.....~
-000000c0: 1365 7b4d 54d7 aa9f ab97 2bce cad7 954f  .e{MT.....+....O
-000000d0: f69a fdc5 fef8 0703 0100 0000 80df 307e  ..............0~
-000000e0: 582a 39eb ebd6 f87d 12f4 4ed5 408d 865a  X*9....}..N.@..Z
-000000f0: c5da e4cb 8d76 73b7 d394 9ddd 67ad a634  .....vs.....g..4
-00000100: 57e5 a34a 5927 c3be 1a24 ddb0 2f13 7591  W..JY'...$../.u.
-00000110: c8a3 fdbd 83a3 a6dc 7fd9 91fb 47ad d666  ............G..f
-00000120: a53c 693a 08ce d4ac faf3 40eb 5114 5ff5  .<i:......@.Q._.
-00000130: 3715 1bf2 b0d3 de6b 746a e5a2 d358 b744  7......ktj...X.D
-00000140: 38e8 ab0b fdf6 34dd c175 8361 1265 e75d  8.....4..u.a.e.]
-00000150: 3392 ae6f 4a95 daf2 9d3a 78a6 541e cb42  3..oJ....:x.T..B
-00000160: 16fa 8707 59e8 5a69 1d46 036d f2a5 a9d0  ....Y.Zi.F.m....
-00000170: cdd5 3474 7925 89de a8c1 8fc1 bf6a efbd  ..4ty%.......j..
-00000180: d86d 1fcb e7cd e3cd ebfa 39f1 9b66 eae2  .m........9..f..
-00000190: 3c8c dfc9 5805 a7bf 4e42 f1b6 98cc 98ba  <...X...NB......
-000001a0: be29 956a 4b77 eae0 9952 71bc 6895 1cc7  .).jKw...Rq.h...
-000001b0: b1c6 2a9b 846c 66b2 6461 2afc eb65 9fbb  ..*..lf.da*..e..
-000001c0: a8a3 fe49 a04f a6d7 7473 d6ed 62e2 dc58  ...I.O..ts..b..X
-000001d0: 2c3a 4f9c 59c3 be5a e92c 2b6c 2ccc 6fea  ,:O.Y..Z.,+l,.o.
-000001e0: 65d9 642f bf3c b9c7 ed49 b2fa 571f 3300  e.d/.<...I..W.3.
-000001f0: 0000 0000 70cf b217 fcec ff01 0000 0000  ....p...........
-00000200: c835 f6ff 0000 0000 00e4 1fdf ff03 0000  .5..............
-00000210: 0000 907f bcff 0700 0000 0020 ffd8 ff03  ........... ....
-00000220: 0000 0000 907f 7cff 0f00 0000 0040 fef1  ......|......@..
-00000230: fe1f 0000 0000 80fc 63ff 0f00 0000 0000  ........c.......
-00000240: 0000 0000 0000 0000 0000 0000 0000 ffa6  ................
-00000250: ef58 8985 d900 c800 00                   .X.......
+00000000: 1f8b 0800 0000 0000 0003 edda db4f d350  .............O.P
+00000010: 1cc0 f196 5bc7 6503 9f1a 1349 8e7b 5182  ....[.e....I.{Q.
+00000020: c2a6 0b3c f822 6263 1667 9151 1230 26b3  ...<."bc.g.Q.0&.
+00000030: 6445 1a61 c39e 4ef0 71fc 05be f9ee 9fe4  dE.a..N.q.......
+00000040: 837f 8fbd d051 0a2a 8941 49f3 fd24 6b7b  .....Q.*.AI..$k{
+00000050: 7a6e 3dbf d32e 3969 bbde 8eeb 2fb4 7794  zn=...9i..../.w.
+00000060: 6b54 092c d56a d1be 9ad9 57aa 8fc2 6d7c  kT.,.j....W...m|
+00000070: 1c0b 8e97 6b95 2545 54ae f3a2 123d e9db  ....k.%ET....=..
+00000080: 5ed0 e5bf e8eb 06da 586f b8be 2376 bbde  ^.......Xo..#v..
+00000090: 81ed 8bc7 ca8c a2aa ca53 2182 ac42 f09b  .........S!..B..
+000000a0: 4815 0dd3 23a9 b47a 85e6 0bca 82f7 ad58  H...#..z.......X
+000000b0: 7aab 8c4f 7e57 a6cd d28f 52bb f8b5 345f  z..O~W....R...4_
+000000c0: 5c9c fa32 550b 4e01 0000 00f8 2bef c635  \..2U.N.....+..5
+000000d0: 7d5e 57fb 936e a7ed 1cf7 a4e3 b5a4 df6b  }^W..n.........k
+000000e0: 3b1d bfe5 b6c3 a49c 586d 1a2b 9621 36cd  ;.......Xm.+.!6.
+000000f0: fafa a621 eae6 7363 4b94 3325 cb62 cd8c  ...!..scK.3%.b..
+00000100: 4fca b2b8 5f4e 65cc bd29 68fa bda0 87e1  O..._Ne..)h.....
+00000110: b31e c24d c73e 70a2 f2e3 bf6e 3f29 9769  ...M.>p....n?).i
+00000120: 7d70 7aae 7f47 d374 5d57 4f74 dfde d98f  }pz..G.t]WOt....
+00000130: db8b 3685 d346 ad95 670d 2355 35bc d2ba  ..6..F..g.#U5...
+00000140: 6919 2f8c a630 d72c 616e 361a e275 b3fe  i./..0.,an6..u..
+00000150: 6aa5 b92d 5e1a db0f c459 ebc2 32b6 ac41  j..-^....Y..2..A
+00000160: a920 e7f0 a8bd 67cb bd8b 19e9 3884 7973  . ....g.....8.ys
+00000170: 62c3 6ad6 57ad dd31 4d7f 38ab f6a7 a3a1  b.j.W..1M.8.....
+00000180: 4b47 4ab7 db19 8cea 342d b54b 0390 2d1d  KGJ.....4-.K..-.
+00000190: c720 a993 09c3 ad11 4d9f 0d3a ea45 6148  . ......M..:.EaH
+000001a0: 0a25 fbd1 f3c1 48b7 e177 3f38 9dcc 80ae  .%....H..w?8....
+000001b0: 1c0f e7f8 d0f5 3e97 45d0 7a63 703e 19fb  ......>.E.zcp>..
+000001c0: e2e8 98be 3aab 2af1 d03f ee07 ebc6 96dd  ....:.*..?......
+000001d0: f3bb 51ba 955c 43ab 9a1c 8d29 f19a 5139  ..Q..\C....)..Q9
+000001e0: 1cd6 f4e5 602c b7a3 929e f3de 95be 67fb  ....`,........g.
+000001f0: e958 749c a368 4647 2e8d dca5 55e2 f025  .Xt..hFG....U..%
+00000200: 15cf 87ef d390 a63f 097a bc7b b1c7 b399  .......?.z.{....
+00000210: 4daa 0eff b9cf ec63 91ee 35fd 64f4 17d5  M......c..5.d...
+00000220: 68da 4eaa d1b4 25c5 92fd d0f9 694b b772  h.N...%.....iK.r
+00000230: 85db 387b 4fa6 27ee 37b7 b82d e551 d7cb  ..8{O.'.7..-.Q..
+00000240: d649 26b5 184e d0cc 7ffd c302 0000 0000  .I&..N..........
+00000250: 00d7 2c7a c1cf fa1f 0000 0000 805c 63fd  ..,z.........\c.
+00000260: 0f00 0000 0040 fef1 fd3f 0000 0000 00f9  .....@...?......
+00000270: c7fb 7f00 0000 0000 f28f f53f 0000 0000  ...........?....
+00000280: 00f9 c7f7 ff00 0000 0000 e41f efff 0100  ................
+00000290: 0000 00c8 3fd6 ff00 0000 0000 0000 0000  ....?...........
+000002a0: 0000 0000 0000 0000 0000 7033 fd04 006b  ..........p3...k
+000002b0: 73c3 00c8 0000                           s.....
 EOF
 
 # Restore the old orbit db after testing completes

From d375f968b79bab63993cef571b232e29857a24e8 Mon Sep 17 00:00:00 2001
From: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
Date: Thu, 25 Apr 2024 23:24:11 -0400
Subject: [PATCH 02/13] orbit: radius: use peewee for session queries

All calls to the `db.ses_*` functions are replaced by corresponding
code that performs equivalent queries using peewee orm functionality.

The resulting code is both clearer: no need for random [0] on the result,
able to use field names instead of [0], [1], [2] etc; and more expressive:
the logic used is spelled out using normal python syntax like `field ==
value` instead of hidden away in a helper function while not being longer.
---
 orbit/radius.py | 21 +++++++++++----------
 1 file changed, 11 insertions(+), 10 deletions(-)

diff --git a/orbit/radius.py b/orbit/radius.py
index f2d152f8..3f7e6c09 100644
--- a/orbit/radius.py
+++ b/orbit/radius.py
@@ -105,10 +105,12 @@ def __init__(self, env=None, username=None):
             self.username = username
             self.token = self.mk_hash(username)
             self.expiry = datetime.utcnow() + timedelta(minutes=min_per_ses)
-
-            if db.ses_getby_username(username):
-                db.ses_delby_username(username)
-            db.ses_ins((self.token, self.username, self.expiry_ts()))
+            # creates a new session if one does not exist
+            (db.Session
+             .replace(username=self.username,
+                      token=self.token,
+                      expiry=self.expiry_ts())
+             .execute())
 
         # try to load active session from database using user token
         else:
@@ -117,17 +119,16 @@ def __init__(self, env=None, username=None):
                 cok.load(raw)
                 res = cok.get('auth', cookies.Morsel()).value
 
-                if (ses_found := db.ses_getby_token(res)[0]):
-                    self.token = ses_found[0]
-                    self.username = ses_found[1]
-                    self.expiry = datetime.fromtimestamp(ses_found[2])
+                if (ses_found := db.Session.get_or_none(db.Session.token == res)):  # NOQA: E501
+                    self.token = ses_found.token
+                    self.username = ses_found.username
+                    self.expiry = datetime.fromtimestamp(ses_found.expiry)
 
     def end(self):
-        res = db.ses_delby_token(self.token)
+        db.Session.delete().where(db.Session.token == self.token).execute()
         self.token = None
         self.username = None
         self.expiry = None
-        return res
 
     def valid(self):
         if not self.expired():

From 6752ddc193a857ea57bbec7313a0a71ee3acf6ce Mon Sep 17 00:00:00 2001
From: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
Date: Thu, 25 Apr 2024 23:40:11 -0400
Subject: [PATCH 03/13] orbit: hyperspace: use peewee for session queries

All calls to the `db.ses_*` functions are replaced by corresponding
code that performs equivalent queries using peewee orm functionality.

Also includes a minor cleanup on the code that prints the list of
sessions. Printing a header and then no values is as clear or clearer
than printing `(no sessions)` imo, and the code is substantially more
simple as a result.
---
 orbit/hyperspace.py | 30 +++++++++++++-----------------
 1 file changed, 13 insertions(+), 17 deletions(-)

diff --git a/orbit/hyperspace.py b/orbit/hyperspace.py
index bb0a6dd1..4eca96bc 100755
--- a/orbit/hyperspace.py
+++ b/orbit/hyperspace.py
@@ -47,18 +47,22 @@ def do_query_username(args):
 def do_validate_token(args):
     need(args, t=True)
 
-    ses = db.ses_getby_token(args.token)[0]
+    ses = db.Session.get_or_none(db.Session.token == args.token)
     if ses:
-        print(ses[1])
+        print(ses.username)
     else:
         print('null')
 
 
 def do_drop_session(args):
     need(args, u=True)
-    dropped = db.ses_delby_username(args.username)[0]
-    if dropped:
-        print(dropped[0])
+    query = (db.Session
+             .delete()
+             .where(db.Session.username == args.username)
+             .returning(db.Session))
+
+    if ses := next(iter(query.execute()), None):
+        print(ses.username)
     else:
         print('null')
 
@@ -129,19 +133,11 @@ def do_roster(args):
     print(r)
 
 
-SES_FMT = """
-{} until {}: {}
-""".strip()
-
-
 def do_list_sessions(args):
-    raw_list = db.ses_get()
-    if raw_list[0] is None:
-        print("(no sessions)")
-    else:
-        print('\n'.join([SES_FMT.format(session[1],
-                                        datetime.fromtimestamp(session[2]),
-                                        session[0]) for session in raw_list]))
+    print('Sessions:')
+    for s in db.Session.select():
+        expiry = datetime.fromtimestamp(s.expiry)
+        print(f'{s.username} until {expiry}: {s.token}')
 
 
 def hyperspace_main(raw_args):

From aab610dabc0bc43775ad2bb562a76ec1faacf43b Mon Sep 17 00:00:00 2001
From: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
Date: Thu, 25 Apr 2024 23:45:20 -0400
Subject: [PATCH 04/13] orbit: db: remove functions/queries for old session
 table interface

Now that hyperspace and radius are using peewee orm exclusively for
accessing the sessions table, these functions are unused and can be
removed.
---
 orbit/db.py | 49 -------------------------------------------------
 1 file changed, 49 deletions(-)

diff --git a/orbit/db.py b/orbit/db.py
index 5cbf5dcd..0ba1f3bf 100755
--- a/orbit/db.py
+++ b/orbit/db.py
@@ -73,55 +73,6 @@ def _set(cmd, reps=()): return _do(cmd, reps, set_=True, get_=True)
 def _get(cmd, reps=()): return _do(cmd, reps, get_=True)
 
 
-# session table interface
-
-SES_GETBY_TOKEN = """
-SELECT token, username, expiry
-FROM sessions
-WHERE token = ?;
-""".strip()
-def ses_getby_token(tok): return _get(SES_GETBY_TOKEN, tok)
-
-
-SES_GETBY_USERNAME = """
-SELECT token, username, expiry
-FROM sessions
-WHERE username = ?;
-""".strip()
-def ses_getby_username(usn): return _get(SES_GETBY_USERNAME, usn)
-
-
-SES_INS = """
-INSERT INTO sessions (token, username, expiry)
-VALUES (?, ?, ?)
-RETURNING username;
-""".strip()
-def ses_ins(tpl): return _set(SES_INS, tpl)
-
-
-SES_DELBY_TOKEN = """
-DELETE FROM sessions
-WHERE token = ?
-RETURNING username;
-""".strip()
-def ses_delby_token(tok): return _set(SES_DELBY_TOKEN, tok)
-
-
-SES_DELBY_USERNAME = """
-DELETE FROM sessions
-WHERE username = ?
-RETURNING username;
-""".strip()
-def ses_delby_username(usn): return _set(SES_DELBY_USERNAME, usn)
-
-
-SES_GET = """
-SELECT token, username, expiry
-FROM sessions;
-""".strip()
-def ses_get(): return _get(SES_GET)
-
-
 # users table interface
 
 USR_PWDHASHFOR_USERNAME = """

From 8712cebf4c34f67fc34255a929be5d4de5886217 Mon Sep 17 00:00:00 2001
From: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
Date: Fri, 26 Apr 2024 14:41:09 -0400
Subject: [PATCH 05/13] orbit: radius: use peewee for registration queries

All calls to the `db.reg_*` functions are replaced by corresponding
code that performs equivalent queries using peewee orm functionality.
---
 orbit/radius.py | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/orbit/radius.py b/orbit/radius.py
index 3f7e6c09..84c40843 100644
--- a/orbit/radius.py
+++ b/orbit/radius.py
@@ -462,14 +462,17 @@ def form_respond():
     if not (student_id := rocket.body_args_query('student_id')):
         rocket.msg('you must provide a student id')
         return form_respond()
-    if not (registration_data := db.reg_get_and_del_by_stuid(student_id)[0]):
+    query = (db.Registration
+             .delete()
+             .where(db.Registration.student_id == student_id)
+             .returning(db.Registration))
+    if not (registration := next(iter(query.execute()), None)):
         rocket.msg('no such student')
         return form_respond()
-    (username, password) = registration_data
     rocket.msg('welcome to the classroom')
     return rocket.respond((register_response % {
-        'username': username,
-        'password': password,
+        'username': registration.username,
+        'password': registration.password,
     }))
 
 

From a61f26e388a748e3c54f1f78ea2aa10e1d54c702 Mon Sep 17 00:00:00 2001
From: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
Date: Fri, 26 Apr 2024 14:42:20 -0400
Subject: [PATCH 06/13] orbit: hyperspace: use peewee for registration queries

All calls to the `db.reg_*` functions are replaced by corresponding
code that performs equivalent queries using peewee orm functionality.
---
 orbit/hyperspace.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/orbit/hyperspace.py b/orbit/hyperspace.py
index 4eca96bc..f284958c 100755
--- a/orbit/hyperspace.py
+++ b/orbit/hyperspace.py
@@ -124,7 +124,8 @@ def do_newuser(args):
         db.usr_ins((args.username, do_bcrypt_hash(args, get=True),
                     args.studentid or 0))
     if args.studentid:
-        db.reg_ins((args.username, args.password, args.studentid))
+        db.Registration.create(username=args.username, password=args.password,
+                               student_id=args.studentid)
     do_validate_creds(args)
 
 

From 353e1786e05de0d553d6765fcd974e7a13f55f44 Mon Sep 17 00:00:00 2001
From: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
Date: Fri, 26 Apr 2024 14:43:02 -0400
Subject: [PATCH 07/13] orbit: db: remove functions/queries for old
 registration table interface

Now that hyperspace and radius are using peewee orm exclusively for
accessing the registration table, these functions and their associated
queries are unused and can be removed.
---
 orbit/db.py | 17 -----------------
 1 file changed, 17 deletions(-)

diff --git a/orbit/db.py b/orbit/db.py
index 0ba1f3bf..0ecd4a5a 100755
--- a/orbit/db.py
+++ b/orbit/db.py
@@ -119,20 +119,3 @@ def usr_get(): return _get(USR_GET)
 WHERE username = ?;
 """.strip()
 def usr_getby_username(usn): return _get(USR_GETBY_USERNAME, usn)
-
-
-# registration table inferface
-
-REG_INS = """
-INSERT INTO newusers (username, password, student_id)
-VALUES (?,?,?);
-""".strip()
-def reg_ins(tpl): return _set(REG_INS, tpl)
-
-
-REG_GETDEL_BY_STUID = """
-DELETE FROM newusers
-WHERE student_id = ?
-RETURNING username, password;
-"""
-def reg_get_and_del_by_stuid(sid): return _set(REG_GETDEL_BY_STUID, sid)

From 33f1b8b6314122301768336a1ac639f3de643f12 Mon Sep 17 00:00:00 2001
From: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
Date: Fri, 26 Apr 2024 14:50:33 -0400
Subject: [PATCH 08/13] orbit: radius: factor credential checking logic into
 helper

The credentials were being checked very similarly in two places
in the code. Moving the check to a helper means that the code can
be more expressive (early return in function instead of short circuit
that is too long to fit on one line, and self documenting at call sites)
and it means there is one source of truth about how to perform this
critical operation.
---
 orbit/radius.py | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/orbit/radius.py b/orbit/radius.py
index 84c40843..115b8f8d 100644
--- a/orbit/radius.py
+++ b/orbit/radius.py
@@ -51,6 +51,12 @@ def indenter(adjustment): return '\t' * (indentation_level + adjustment)
     return output
 
 
+def check_credentials(username, password):
+    if not (pwdhash := db.usr_pwdhashfor_username(username)[0]):
+        return False
+    return bcrypt.checkpw(encode(password), encode(pwdhash[0]))
+
+
 # === user session handling ===
 
 class Session:
@@ -281,8 +287,7 @@ def launch(self):
         if self.method == "POST":
             username = self.body_args_query('username')
             password = self.body_args_query('password')
-            if (pwdhash := db.usr_pwdhashfor_username(username)[0]) and \
-                    bcrypt.checkpw(encode(password), encode(pwdhash[0])):
+            if (check_credentials(username, password)):
                 new_ses = Session(username=username)
             if new_ses:
                 self._session = new_ses
@@ -411,8 +416,7 @@ def handle_mail_auth(rocket):
             or method != 'plain':
         return rocket.raw_respond(HTTPStatus.BAD_REQUEST)
 
-    if (pwdhash := db.usr_pwdhashfor_username(username)[0]) is None \
-            or not bcrypt.checkpw(encode(password), encode(pwdhash[0])):
+    if not check_credentials(username, password):
         return rocket.raw_respond(HTTPStatus.UNAUTHORIZED)
 
     return rocket.raw_respond(HTTPStatus.OK)

From d07d086ac917603bc41209303b9c4d949fe48b1a Mon Sep 17 00:00:00 2001
From: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
Date: Fri, 26 Apr 2024 14:58:54 -0400
Subject: [PATCH 09/13] orbit: radius: use peewee for user queries

All calls to the `db.usr_*` functions are replaced by corresponding
code that performs equivalent queries using peewee orm functionality.
---
 orbit/radius.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/orbit/radius.py b/orbit/radius.py
index 115b8f8d..5c54cb8f 100644
--- a/orbit/radius.py
+++ b/orbit/radius.py
@@ -52,9 +52,9 @@ def indenter(adjustment): return '\t' * (indentation_level + adjustment)
 
 
 def check_credentials(username, password):
-    if not (pwdhash := db.usr_pwdhashfor_username(username)[0]):
+    if not (user := db.User.get_or_none(db.User.username == username)):
         return False
-    return bcrypt.checkpw(encode(password), encode(pwdhash[0]))
+    return bcrypt.checkpw(encode(password), encode(user.pwdhash))
 
 
 # === user session handling ===

From 4416ffaf630ff043658de0e557b1075208beaef5 Mon Sep 17 00:00:00 2001
From: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
Date: Fri, 26 Apr 2024 16:31:18 -0400
Subject: [PATCH 10/13] orbit: hyperspace: use peewee for user queries

All calls to the `db.usr_*` functions are replaced by corresponding
code that performs equivalent queries using peewee orm functionality.
---
 orbit/hyperspace.py | 78 +++++++++++++++++++++------------------------
 1 file changed, 37 insertions(+), 41 deletions(-)

diff --git a/orbit/hyperspace.py b/orbit/hyperspace.py
index f284958c..f8f39260 100755
--- a/orbit/hyperspace.py
+++ b/orbit/hyperspace.py
@@ -29,19 +29,13 @@ def nou(u):
     errx(f'no such user "{u}". Bye.')
 
 
-USR_FMT = """
-Username        : {}
-Hashed Password : {}
-Student ID      : {}
-""".strip()
-
-
 def do_query_username(args):
     need(args, u=True)
-    u = db.usr_getby_username(args.username)[0]
-    if u is None:
+    if not (user := db.User.get_or_none(db.User.username == args.username)):
         nou(args.username)
-    print(USR_FMT.format(*u))
+    print(f'Username        : {user.username}\n'
+          f'Hashed Password : {user.pwdhash}\n'
+          f'Student ID      : {user.student_id}')
 
 
 def do_validate_token(args):
@@ -75,35 +69,34 @@ def do_create_session(args):
 
 def do_validate_creds(args):
     need(args, u=True, p=True)
-    u, p = args.username, args.password
-    pwdhash = db.usr_pwdhashfor_username(u)[0]
-    if pwdhash is None:
-        nou(u)
-    pwdhash = pwdhash[0]
-
-    if bcrypt.checkpw(bytes(p, "UTF-8"), bytes(pwdhash, "UTF-8")):
-        print('credentials(username: {}, password:{})'.format(u, p))
-    else:
+    if not (user := db.User.get_or_none(db.User.username == args.username)):
+        nou(args.username)
+    if not bcrypt.checkpw(args.password.encode('utf-8'),
+                          user.pwdhash.encode('utf-8')):
         print('null')
+        return
+    print(f'credentials(username: {args.username}, password:{args.password})')
 
 
 def do_change_password(args):
     need(args, u=True, p=True)
-    u, _ = args.username, args.password
-    if db.usr_getby_username(u)[0]:
-        db.usr_setpwdhash_username((do_bcrypt_hash(args, get=True), u))
-        do_validate_creds(args)
-    else:
-        nou(u)
+    new_hash = do_bcrypt_hash(args, get=True)
+    query = (db.User
+             .update({db.User.pwdhash: new_hash})
+             .where(db.User.username == args.username))
+    if query.execute() < 1:
+        nou(args.username)
+    print(f'credentials(username: {args.username}, password:{args.password})')
 
 
 def do_delete_user(args):
     need(args, u=True)
-    deleted = db.usr_delby_username(args.username)[0]
-    if deleted:
-        print(deleted[0])
-    else:
-        print('null')
+    query = (db.User
+             .delete()
+             .where(db.User.username == args.username))
+    if query.execute() < 1:
+        nou(args.username)
+    print(args.username)
 
 
 def do_bcrypt_hash(args, get=False):
@@ -118,20 +111,23 @@ def do_bcrypt_hash(args, get=False):
 
 def do_newuser(args):
     need(args, u=True, p=True)
-    if db.usr_getby_username(args.username)[0]:
-        errx(f'cannot create duplicate user "{args.username}"')
-    else:
-        db.usr_ins((args.username, do_bcrypt_hash(args, get=True),
-                    args.studentid or 0))
-    if args.studentid:
-        db.Registration.create(username=args.username, password=args.password,
-                               student_id=args.studentid)
-    do_validate_creds(args)
+    new_hash = do_bcrypt_hash(args, get=True)
+    try:
+        db.User.create(username=args.username, pwdhash=new_hash,
+                       student_id=args.studentid)
+        if args.studentid:
+            db.Registration.create(username=args.username,
+                                   password=args.password,
+                                   student_id=args.studentid)
+        do_validate_creds(args)
+    except db.peewee.IntegrityError as e:
+        errx(f'cannot create user with duplicate field: "{e}"')
 
 
 def do_roster(args):
-    r = db.usr_get()
-    print(r)
+    print('Users:')
+    for u in db.User.select():
+        print(f'{u.username}, {u.pwdhash}, {u.student_id}')
 
 
 def do_list_sessions(args):

From a47433474c5a3b8bcd92ce3858783b13f77c8ba2 Mon Sep 17 00:00:00 2001
From: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
Date: Fri, 26 Apr 2024 16:35:18 -0400
Subject: [PATCH 11/13] orbit: db: remove functions/queries for old user table
 interface

Now that hyperspace and radius are using peewee orm exclusively for
accessing the user table, these functions and their associated
queries are unused and can be removed.
---
 orbit/db.py | 48 ------------------------------------------------
 1 file changed, 48 deletions(-)

diff --git a/orbit/db.py b/orbit/db.py
index 0ecd4a5a..df806762 100755
--- a/orbit/db.py
+++ b/orbit/db.py
@@ -71,51 +71,3 @@ def _do(cmd, reps=(), set_=False, get_=False):
 
 def _set(cmd, reps=()): return _do(cmd, reps, set_=True, get_=True)
 def _get(cmd, reps=()): return _do(cmd, reps, get_=True)
-
-
-# users table interface
-
-USR_PWDHASHFOR_USERNAME = """
-SELECT pwdhash
-FROM users
-WHERE username = ?;
-""".strip()
-def usr_pwdhashfor_username(usn): return _get(USR_PWDHASHFOR_USERNAME, usn)
-
-
-USR_INS = """
-INSERT INTO users (username, pwdhash, student_id)
-VALUES (?, ?, ?);
-""".strip()
-def usr_ins(usr): return _set(USR_INS, usr)
-
-
-USR_DELBY_USERNAME = """
-DELETE FROM users
-WHERE username = ?
-RETURNING username;
-""".strip()
-def usr_delby_username(usn): return _set(USR_DELBY_USERNAME, usn)
-
-
-USR_SETPWDHASH_USERNAME = """
-UPDATE users
-SET pwdhash = ?
-WHERE username = ?;
-""".strip()
-def usr_setpwdhash_username(usr): return _set(USR_SETPWDHASH_USERNAME, usr)
-
-
-USR_GET = """
-SELECT username, pwdhash, student_id
-FROM users;
-""".strip()
-def usr_get(): return _get(USR_GET)
-
-
-USR_GETBY_USERNAME = """
-SELECT username, pwdhash, student_id
-FROM users
-WHERE username = ?;
-""".strip()
-def usr_getby_username(usn): return _get(USR_GETBY_USERNAME, usn)

From 10bb0c1fec3d0b44ff5b587b0f768895bb3c2e4c Mon Sep 17 00:00:00 2001
From: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
Date: Fri, 26 Apr 2024 16:38:40 -0400
Subject: [PATCH 12/13] orbit: db: remove old code and imports

Now that all db queries are executed using peewee, there is no
need for the old functions for accessing the database or for
the imports that they required.
---
 orbit/config.py |  2 --
 orbit/db.py     | 31 -------------------------------
 2 files changed, 33 deletions(-)

diff --git a/orbit/config.py b/orbit/config.py
index e4d4b7b9..67b781d1 100644
--- a/orbit/config.py
+++ b/orbit/config.py
@@ -8,5 +8,3 @@
 
 # duration of authentication token validity period
 minutes_each_session_token_is_valid = 180
-
-sql_verbose = False
diff --git a/orbit/db.py b/orbit/db.py
index df806762..fbe776e5 100755
--- a/orbit/db.py
+++ b/orbit/db.py
@@ -1,12 +1,6 @@
 #!/usr/bin/env python3
-import sqlite3
 import peewee
 import config
-# nickname  table name
-# USR => users
-# SES => sessions
-# REG => newusers
-import sys
 
 DB = peewee.SqliteDatabase(config.database)
 
@@ -46,28 +40,3 @@ class Meta:
 
 if __name__ == '__main__':
     DB.create_tables(BaseModel.__subclasses__())
-
-
-def _do(cmd, reps=(), set_=False, get_=False):
-    if config.sql_verbose:
-        print("SQL", cmd, file=sys.stderr)
-    reps = (lambda x: x if type(x) is tuple else (x,))(reps)
-    dat = None
-    con = sqlite3.connect(config.database)
-    new = con.cursor()
-    ret = new.execute(cmd, reps)
-    if get_:
-        dat = ret.fetchall()
-        if len(dat) < 1:
-            dat = [None]
-        if config.sql_verbose:
-            print("SQLRET", dat, file=sys.stderr)
-        # works when get lookup fails
-    if set_:
-        ret.execute("COMMIT;")
-    con.close()
-    return dat
-
-
-def _set(cmd, reps=()): return _do(cmd, reps, set_=True, get_=True)
-def _get(cmd, reps=()): return _do(cmd, reps, get_=True)

From 14ecdceb48b28ef4e64446cd1806cba061d66808 Mon Sep 17 00:00:00 2001
From: charliemirabile <46761267+charliemirabile@users.noreply.github.com>
Date: Fri, 26 Apr 2024 16:41:38 -0400
Subject: [PATCH 13/13] orbit: db: remove old database names

Now that there are no legacy queries to be backwards compatible with
we do not need to specify the table names explicitly and can let peewee
generate them automatically.

This required updating the binary blob to reflect the new names.
---
 orbit/db.py |  9 ------
 test.sh     | 88 ++++++++++++++++++++++++++---------------------------
 2 files changed, 44 insertions(+), 53 deletions(-)

diff --git a/orbit/db.py b/orbit/db.py
index fbe776e5..516fe738 100755
--- a/orbit/db.py
+++ b/orbit/db.py
@@ -16,27 +16,18 @@ class User(BaseModel):
     pwdhash = peewee.TextField()
     student_id = peewee.TextField(unique=True, null=True)
 
-    class Meta:
-        table_name = 'users'
-
 
 class Session(BaseModel):
     token = peewee.TextField(primary_key=True)
     username = peewee.TextField(unique=True)
     expiry = peewee.FloatField()
 
-    class Meta:
-        table_name = 'sessions'
-
 
 class Registration(BaseModel):
     student_id = peewee.TextField(unique=True)
     username = peewee.TextField(unique=True)
     password = peewee.TextField()
 
-    class Meta:
-        table_name = 'newusers'
-
 
 if __name__ == '__main__':
     DB.create_tables(BaseModel.__subclasses__())
diff --git a/test.sh b/test.sh
index c052fdbe..7fb34037 100755
--- a/test.sh
+++ b/test.sh
@@ -80,50 +80,50 @@ ${DOCKER} volume export singularity_orbit-db > test/orbit_orig.tar
 
 # Import an empty orbit db with no users or sessions
 xxd -r <<- 'EOF' | gunzip | ${DOCKER} volume import singularity_orbit-db -
-00000000: 1f8b 0800 0000 0000 0003 edda db4f d350  .............O.P
-00000010: 1cc0 f196 5bc7 6503 9f1a 1349 8e7b 5182  ....[.e....I.{Q.
-00000020: c2a6 0b3c f822 6263 1667 9151 1230 26b3  ...<."bc.g.Q.0&.
-00000030: 6445 1a61 c39e 4ef0 71fc 05be f9ee 9fe4  dE.a..N.q.......
-00000040: 837f 8fbd d051 0a2a 8941 49f3 fd24 6b7b  .....Q.*.AI..$k{
-00000050: 7a6e 3dbf d32e 3969 bbde 8eeb 2fb4 7794  zn=...9i..../.w.
-00000060: 6b54 092c d56a d1be 9ad9 57aa 8fc2 6d7c  kT.,.j....W...m|
-00000070: 1c0b 8e97 6b95 2545 54ae f3a2 123d e9db  ....k.%ET....=..
-00000080: 5ed0 e5bf e8eb 06da 586f b8be 2376 bbde  ^.......Xo..#v..
-00000090: 81ed 8bc7 ca8c a2aa ca53 2182 ac42 f09b  .........S!..B..
-000000a0: 4815 0dd3 23a9 b47a 85e6 0bca 82f7 ad58  H...#..z.......X
-000000b0: 7aab 8c4f 7e57 a6cd d28f 52bb f8b5 345f  z..O~W....R...4_
-000000c0: 5c9c fa32 550b 4e01 0000 00f8 2bef c635  \..2U.N.....+..5
-000000d0: 7d5e 57fb 936e a7ed 1cf7 a4e3 b5a4 df6b  }^W..n.........k
-000000e0: 3b1d bfe5 b6c3 a49c 586d 1a2b 9621 36cd  ;.......Xm.+.!6.
-000000f0: fafa a621 eae6 7363 4b94 3325 cb62 cd8c  ...!..scK.3%.b..
-00000100: 4fca b2b8 5f4e 65cc bd29 68fa bda0 87e1  O..._Ne..)h.....
-00000110: b31e c24d c73e 70a2 f2e3 bf6e 3f29 9769  ...M.>p....n?).i
-00000120: 7d70 7aae 7f47 d374 5d57 4f74 dfde d98f  }pz..G.t]WOt....
-00000130: db8b 3685 d346 ad95 670d 2355 35bc d2ba  ..6..F..g.#U5...
-00000140: 6919 2f8c a630 d72c 616e 361a e275 b3fe  i./..0.,an6..u..
-00000150: 6aa5 b92d 5e1a db0f c459 ebc2 32b6 ac41  j..-^....Y..2..A
-00000160: a920 e7f0 a8bd 67cb bd8b 19e9 3884 7973  . ....g.....8.ys
-00000170: 62c3 6ad6 57ad dd31 4d7f 38ab f6a7 a3a1  b.j.W..1M.8.....
-00000180: 4b47 4ab7 db19 8cea 342d b54b 0390 2d1d  KGJ.....4-.K..-.
-00000190: c720 a993 09c3 ad11 4d9f 0d3a ea45 6148  . ......M..:.EaH
-000001a0: 0a25 fbd1 f3c1 48b7 e177 3f38 9dcc 80ae  .%....H..w?8....
-000001b0: 1c0f e7f8 d0f5 3e97 45d0 7a63 703e 19fb  ......>.E.zcp>..
-000001c0: e2e8 98be 3aab 2af1 d03f ee07 ebc6 96dd  ....:.*..?......
-000001d0: f3bb 51ba 955c 43ab 9a1c 8d29 f19a 5139  ..Q..\C....)..Q9
-000001e0: 1cd6 f4e5 602c b7a3 929e f3de 95be 67fb  ....`,........g.
-000001f0: e958 749c a368 4647 2e8d dca5 55e2 f025  .Xt..hFG....U..%
-00000200: 15cf 87ef d390 a63f 097a bc7b b1c7 b399  .......?.z.{....
-00000210: 4daa 0eff b9cf ec63 91ee 35fd 64f4 17d5  M......c..5.d...
-00000220: 68da 4eaa d1b4 25c5 92fd d0f9 694b b772  h.N...%.....iK.r
-00000230: 85db 387b 4fa6 27ee 37b7 b82d e551 d7cb  ..8{O.'.7..-.Q..
-00000240: d649 26b5 184e d0cc 7ffd c302 0000 0000  .I&..N..........
-00000250: 00d7 2c7a c1cf fa1f 0000 0000 805c 63fd  ..,z.........\c.
-00000260: 0f00 0000 0040 fef1 fd3f 0000 0000 00f9  .....@...?......
-00000270: c7fb 7f00 0000 0000 f28f f53f 0000 0000  ...........?....
-00000280: 00f9 c7f7 ff00 0000 0000 e41f efff 0100  ................
-00000290: 0000 00c8 3fd6 ff00 0000 0000 0000 0000  ....?...........
-000002a0: 0000 0000 0000 0000 0000 7033 fd04 006b  ..........p3...k
-000002b0: 73c3 00c8 0000                           s.....
+00000000: 1f8b 0800 0000 0000 0003 edda 5d4f d350  ............]O.P
+00000010: 18c0 f196 b76e c826 5e2c 8d72 735c 24ba  .....n.&^,.rs\$.
+00000020: 2064 2881 186e 18d8 e8e2 1c32 ba04 6e5c   d(..n.....2..n\
+00000030: 4a56 a511 366c bb80 37c4 f151 fc28 c60f  JV..6l..7..Q.(..
+00000040: 665f f6d2 1554 bc40 49f3 ff25 5b7b 5efa  f_...T.@I..%[{^.
+00000050: 9c9d e774 4b4e bab6 7d60 b94b cd03 e906  ...tKN..}`.K....
+00000060: 153d ab2b 2bc1 7139 762c 2e3f f3df c3f3  .=.++.q9v,.?....
+00000070: 9077 beb6 525c 9544 f126 3f54 5fc7 710d  .w..R\.D.&?T_.q.
+00000080: db1b f25f 8c75 0bed ee54 2cd7 141f daf6  ..._.u...T,.....
+00000090: b1e1 8ae7 d2ac 24cb d286 105e 53ca 7b4d  ......$....^S.{M
+000000a0: 47ba fae5 8948 59be 46f8 94b4 647f cb64  G....HY.F...d..d
+000000b0: 37a4 f49d 1fd2 ddcd ecf7 eceb ccd7 ecbd  7...............
+000000c0: cc83 99f3 9947 5e15 0000 0080 bff4 3ead  .....G^.......>.
+000000d0: a80b 39b9 9bb6 5a4d f3ac e398 76c3 713b  ..9...ZM....v.q;
+000000e0: 4db3 e536 aca6 5f9c deaa 6925 5d13 f56a  M..6.._...i%]..j
+000000f0: 79a7 ae89 72f5 a5b6 27f2 b18e 79b1 5d0d  y...r...'...y.].
+00000100: 2bf3 e249 3e52 5fd8 4b29 ea63 2fbe 3c8c  +..I>R_.K).c/.<.
+00000110: efbf b58c 63d3 3fa6 7f1d bddf 6d34 f6a0  ....c.?.....m4..
+00000120: b6d0 bdaf 286a 2e27 5fe4 5ce3 e028 88e6  ....(j.'_.\..(..
+00000130: bf52 bd88 7a69 b3a2 0daf f33f 63b9 aa6b  .R..zi.....?c..k
+00000140: afb4 9aa8 6eeb a25a af54 c4bb 5af9 6da9  ....n..Z.T..Z.m.
+00000150: b62f de68 fb4f c530 b4d0 b53d 7dd0 cb6b  ./.h.O.0...=}..k
+00000160: 3939 6d1e 1ace e1e5 8668 06fc b682 d8d5  99m......h......
+00000170: 6be5 2dbd 39a5 a88b 7372 3713 ccda 311d  k.-.9...sr7...1.
+00000180: c76a b706 33ea 9595 2be7 1eef 1c4e bf57  .j..3...+....N.W
+00000190: 1bcb 4076 4251 e7bc 619c 2003 bd3e bdc3  ..@vBQ..a. ..>..
+000001a0: e468 1e22 01dc f627 b315 9bcb b553 619e  .h."...'.....Sa.
+000001b0: 9d58 f697 bcf0 8257 06f5 fd69 2f4e 4ea9  .X.....W...i/NN.
+000001c0: a539 590a 67fd f9c8 db1d 368c 8edb 0eca  .9Y.g.....6.....
+000001d0: 8dfe cc96 7b27 5352 b82f 94ce c615 756d  ....{'SR./....um
+000001e0: 5eee 3e0c fad9 e647 cb71 6dc3 8d66 215a  ^.>....G.qm..f!Z
+000001f0: 3971 65de aebc 2c4c 5eb4 6934 83e7 638a  9qe...,L^.i4..c.
+00000200: baee 8d5c b83c f270 69a3 d5e3 7f1e 3bfe  ...\.<.pi.....;.
+00000210: ad88 8f1e fd76 74d7 6545 9d9f 972f 5e04  .....vt.eE.../^.
+00000220: 2b18 ed1a 3d1f 1b5d cb78 c46b dcdb f11b  +...=..].x.k....
+00000230: 35ba a4bf b9ef 0dc7 396d dbf1 6bfa cb9d  5.......9m..k...
+00000240: f117 6ff6 bffe 7c01 0000 0000 801b 163c  ..o...|........<
+00000250: e067 ff0f 0000 0000 40a2 b1ff 0700 0000  .g......@.......
+00000260: 0020 f9f8 ff3f 0000 0000 00c9 c7f3 7f00  . ...?..........
+00000270: 0000 0000 928f fd3f 0000 0000 00c9 c7ff  .......?........
+00000280: ff01 0000 0000 483e 9eff 0300 0000 0090  ......H>........
+00000290: 7cec ff01 0000 0000 0000 0000 0000 0000  |...............
+000002a0: 0000 0000 0000 e076 fa09 3ff3 0562 00c8  .......v..?..b..
+000002b0: 0000                                     ..
 EOF
 
 # Restore the old orbit db after testing completes
