diff --git a/bin/__init__.py b/bin/__init__.py
index 0db276a14a80711ddadb4bcc3ab70e8635a73094..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/bin/__init__.py
+++ b/bin/__init__.py
@@ -1,8 +0,0 @@
-"""Module for bin
-"""
-
-from __future__ import absolute_import
-
-from . import geomag
-
-__all__ = ["geomag"]
diff --git a/create_db.py b/create_db.py
index eed829480b247c4aa73f76e7e9d988a19025d983..9d2c1873c318b839168ed613408b0847991d46d3 100644
--- a/create_db.py
+++ b/create_db.py
@@ -1,3 +1,4 @@
-from geomagio.api.db.create import create_db
+from sqlmodel import SQLModel
+from geomagio.api.db.database import engine
 
-create_db()
+SQLModel.metadata.create_all(engine)
diff --git a/docs/working_agreements.md b/docs/working_agreements.md
new file mode 100644
index 0000000000000000000000000000000000000000..6841cbc2ef5057721b1595d10a860452d75fe866
--- /dev/null
+++ b/docs/working_agreements.md
@@ -0,0 +1,9 @@
+Developer Working Agreements
+=====================
+
+These are agreements among the development team to facilitate smooth workflows and readable, efficient code. This is a living document that should be revisited regularly and these are meant to be guidelines with the understanding that exceptions can and do happen.
+
+- We will do our best not to push to production on Friday afternoons.
+- We try to not push commented out code to production.
+- Each directory should have an empty `__init__.py` file to allow python 3 to turn that directory into a module. We will avoid adding initialization code to these files including using the `__all__` variable.
+- Imports belong at the top of python files and should be specific (ie don't use `import * from package`). We will do our best to use absolute imports for external directories and relative imports to import modules in the same directory.
diff --git a/geomagio/Controller.py b/geomagio/Controller.py
index 817598fedd5877e88977e7d59ea0f82891e921ac..09935932e8b3c60b0e96decf9bb13203e6d9c003 100644
--- a/geomagio/Controller.py
+++ b/geomagio/Controller.py
@@ -1,32 +1,57 @@
 """Controller class for geomag algorithms"""
 
+import sys
 import argparse
+
 from io import StringIO
-import sys
 from typing import List, Optional, Tuple, Union
-
 from obspy.core import Stream, UTCDateTime
 
-from .algorithm import Algorithm, algorithms, AlgorithmException, FilterAlgorithm
-from .DerivedTimeseriesFactory import DerivedTimeseriesFactory
-from .PlotTimeseriesFactory import PlotTimeseriesFactory
-from .StreamTimeseriesFactory import StreamTimeseriesFactory
-from . import TimeseriesUtility, Util
-
-# factory packages
-from . import binlog
-from . import edge
-from . import iaga2002
-from . import imfjson
-from . import pcdcp
-from . import imfv122
-from . import imfv283
-from . import temperature
-from . import vbf
-from . import xml
-from . import covjson
-from . import netcdf
-from . import imagcdf
+from geomagio import TimeseriesUtility, Util
+from geomagio.algorithm.Algorithm import Algorithm
+from geomagio.algorithm.AlgorithmException import AlgorithmException
+from geomagio.algorithm.FilterAlgorithm import FilterAlgorithm
+from geomagio.algorithm.AdjustedAlgorithm import AdjustedAlgorithm
+from geomagio.algorithm.AverageAlgorithm import AverageAlgorithm
+from geomagio.algorithm.DbDtAlgorithm import DbDtAlgorithm
+from geomagio.algorithm.DeltaFAlgorithm import DeltaFAlgorithm
+from geomagio.algorithm.SqDistAlgorithm import SqDistAlgorithm
+from geomagio.algorithm.XYZAlgorithm import XYZAlgorithm
+
+from geomagio.DerivedTimeseriesFactory import DerivedTimeseriesFactory
+from geomagio.PlotTimeseriesFactory import PlotTimeseriesFactory
+from geomagio.StreamTimeseriesFactory import StreamTimeseriesFactory
+
+from geomagio.binlog.BinLogFactory import BinLogFactory
+from geomagio.edge.EdgeFactory import EdgeFactory
+from geomagio.edge.IRISFactory import IRISFactory
+from geomagio.edge.MiniSeedFactory import MiniSeedFactory
+from geomagio.edge.FDSNFactory import FDSNFactory
+from geomagio.edge.LocationCode import LocationCode
+from geomagio.iaga2002.IAGA2002Factory import IAGA2002Factory
+from geomagio.imfjson.IMFJSONFactory import IMFJSONFactory
+from geomagio.pcdcp.PCDCPFactory import PCDCPFactory
+from geomagio.imfv122.IMFV122Factory import IMFV122Factory
+from geomagio.imfv283.IMFV283Factory import IMFV283Factory
+from geomagio.imfv283.GOESIMFV283Factory import GOESIMFV283Factory
+from geomagio.temperature.TEMPFactory import TEMPFactory
+from geomagio.vbf.VBFFactory import VBFFactory
+from geomagio.xml.XMLFactory import XMLFactory
+from geomagio.covjson.CovJSONFactory import CovJSONFactory
+from geomagio.netcdf.NetCDFFactory import NetCDFFactory
+from geomagio.imagcdf.ImagCDFFactory import ImagCDFFactory
+
+
+algorithms = {
+    "identity": Algorithm,
+    "adjusted": AdjustedAlgorithm,
+    "average": AverageAlgorithm,
+    "dbdt": DbDtAlgorithm,
+    "deltaf": DeltaFAlgorithm,
+    "filter": FilterAlgorithm,
+    "sqdist": SqDistAlgorithm,
+    "xyz": XYZAlgorithm,
+}
 
 
 class Controller(object):
@@ -516,7 +541,7 @@ def get_input_factory(args):
         else:
             input_stream = StringIO(Util.read_url(args.input_url))
     if input_type == "edge":
-        input_factory = edge.EdgeFactory(
+        input_factory = EdgeFactory(
             host=args.input_host,
             port=args.input_port,
             locationCode=args.locationcode,
@@ -526,7 +551,7 @@ def get_input_factory(args):
         )
     elif input_type == "goes":
         # TODO: deal with other goes arguments
-        input_factory = imfv283.GOESIMFV283Factory(
+        input_factory = GOESIMFV283Factory(
             directory=args.input_goes_directory,
             getdcpmessages=args.input_goes_getdcpmessages,
             password=args.input_goes_password,
@@ -535,7 +560,7 @@ def get_input_factory(args):
             **input_factory_args,
         )
     elif input_type == "iris":
-        input_factory = edge.IRISFactory(
+        input_factory = IRISFactory(
             base_url=args.iris_url,
             network=args.iris_network,
             locationCode=args.locationcode,
@@ -543,7 +568,7 @@ def get_input_factory(args):
             **input_factory_args,
         )
     elif input_type == "fdsn":
-        input_factory = edge.FDSNFactory(
+        input_factory = FDSNFactory(
             base_url=args.fdsn_url,
             network=args.network,
             locationCode=args.locationcode,
@@ -552,17 +577,17 @@ def get_input_factory(args):
     else:
         # stream compatible factories
         if input_type == "iaga2002":
-            input_factory = iaga2002.IAGA2002Factory(**input_factory_args)
+            input_factory = IAGA2002Factory(**input_factory_args)
         if input_type == "netcdf":
-            input_factory = netcdf.NetCDFFactory(**input_factory_args)
+            input_factory = NetCDFFactory(**input_factory_args)
         elif input_type == "imfv122":
-            input_factory = imfv122.IMFV122Factory(**input_factory_args)
+            input_factory = IMFV122Factory(**input_factory_args)
         elif input_type == "imfv283":
-            input_factory = imfv283.IMFV283Factory(**input_factory_args)
+            input_factory = IMFV283Factory(**input_factory_args)
         elif input_type == "pcdcp":
-            input_factory = pcdcp.PCDCPFactory(**input_factory_args)
+            input_factory = PCDCPFactory(**input_factory_args)
         elif input_type == "miniseed":
-            input_factory = edge.MiniSeedFactory(
+            input_factory = MiniSeedFactory(
                 host=args.input_host,
                 port=args.input_port,
                 locationCode=args.locationcode,
@@ -572,12 +597,12 @@ def get_input_factory(args):
                 **input_factory_args,
             )
         elif input_type == "xml":
-            input_factory = xml.XMLFactory(**input_factory_args)
+            input_factory = XMLFactory(**input_factory_args)
         elif input_type == "covjson":
-            input_factory = covjson.CovJSONFactory(**input_factory_args)
+            input_factory = CovJSONFactory(**input_factory_args)
         elif input_type == "imagcdf":
             input_factory_args["output"] = args.output
-            input_factory = imagcdf.ImagCDFFactory(**input_factory_args)
+            input_factory = ImagCDFFactory(**input_factory_args)
         # wrap stream
         if input_stream is not None:
             input_factory = StreamTimeseriesFactory(
@@ -628,7 +653,7 @@ def get_output_factory(args):
     if output_type == "edge":
         # TODO: deal with other edge arguments
         locationcode = args.outlocationcode or args.locationcode or None
-        output_factory = edge.EdgeFactory(
+        output_factory = EdgeFactory(
             host=args.output_host,
             port=args.output_read_port,
             write_port=args.output_port,
@@ -644,25 +669,25 @@ def get_output_factory(args):
     else:
         # stream compatible factories
         if output_type == "binlog":
-            output_factory = binlog.BinLogFactory(**output_factory_args)
+            output_factory = BinLogFactory(**output_factory_args)
         elif output_type == "iaga2002":
-            output_factory = iaga2002.IAGA2002Factory(**output_factory_args)
+            output_factory = IAGA2002Factory(**output_factory_args)
         elif output_type == "netcdf":
-            output_factory = netcdf.NetCDFFactory(**output_factory_args)
+            output_factory = NetCDFFactory(**output_factory_args)
         elif output_type == "imfjson":
-            output_factory = imfjson.IMFJSONFactory(**output_factory_args)
+            output_factory = IMFJSONFactory(**output_factory_args)
         elif output_type == "covjson":
-            output_factory = covjson.CovJSONFactory(**output_factory_args)
+            output_factory = CovJSONFactory(**output_factory_args)
         elif output_type == "pcdcp":
-            output_factory = pcdcp.PCDCPFactory(**output_factory_args)
+            output_factory = PCDCPFactory(**output_factory_args)
         elif output_type == "temperature":
-            output_factory = temperature.TEMPFactory(**output_factory_args)
+            output_factory = TEMPFactory(**output_factory_args)
         elif output_type == "vbf":
-            output_factory = vbf.VBFFactory(**output_factory_args)
+            output_factory = VBFFactory(**output_factory_args)
         elif output_type == "miniseed":
             # TODO: deal with other miniseed arguments
             locationcode = args.outlocationcode or args.locationcode or None
-            output_factory = edge.MiniSeedFactory(
+            output_factory = MiniSeedFactory(
                 host=args.output_host,
                 port=args.output_read_port,
                 write_port=args.output_port,
@@ -672,9 +697,9 @@ def get_output_factory(args):
                 **output_factory_args,
             )
         elif output_type == "xml":
-            output_factory = xml.XMLFactory(**output_factory_args)
+            output_factory = XMLFactory(**output_factory_args)
         elif output_type == "imagcdf":
-            output_factory = imagcdf.ImagCDFFactory(**output_factory_args)
+            output_factory = ImagCDFFactory(**output_factory_args)
         # wrap stream
         if output_stream is not None:
             output_factory = StreamTimeseriesFactory(
@@ -958,7 +983,7 @@ def parse_args(args):
                 instead of "--type"
                 """,
         metavar="CODE",
-        type=edge.LocationCode,
+        type=LocationCode,
     )
     input_group.add_argument(
         "--observatory",
@@ -1173,7 +1198,7 @@ def parse_args(args):
         "--outlocationcode",
         help="Defaults to --locationcode",
         metavar="CODE",
-        type=edge.LocationCode,
+        type=LocationCode,
     )
     output_group.add_argument(
         "--output-edge-forceout",
diff --git a/geomagio/DerivedTimeseriesFactory.py b/geomagio/DerivedTimeseriesFactory.py
index 273b18dd25a8d8409c45e69cf0274c03bd003647..a2c6990b20043349aaa535a55e565e93e9934352 100644
--- a/geomagio/DerivedTimeseriesFactory.py
+++ b/geomagio/DerivedTimeseriesFactory.py
@@ -2,8 +2,10 @@ from typing import List, Optional
 
 from obspy import Stream, Trace, UTCDateTime
 
-from .algorithm import Algorithm, DeltaFAlgorithm, XYZAlgorithm
-from .TimeseriesFactory import TimeseriesFactory, TimeseriesUtility
+from geomagio.algorithm.DeltaFAlgorithm import DeltaFAlgorithm
+from geomagio.algorithm.XYZAlgorithm import XYZAlgorithm
+from geomagio.TimeseriesFactory import TimeseriesFactory
+from geomagio import TimeseriesUtility
 
 
 class DerivedTimeseriesFactory(TimeseriesFactory):
diff --git a/geomagio/__init__.py b/geomagio/__init__.py
index 1887216cb25d8ac23a5afa614c2ad4c9a018939a..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/__init__.py
+++ b/geomagio/__init__.py
@@ -1,31 +0,0 @@
-"""
-Geomag Algorithm Module
-"""
-
-from . import ChannelConverter
-from . import StreamConverter
-from . import TimeseriesUtility
-from . import Util
-
-from .Controller import Controller
-from .DerivedTimeseriesFactory import DerivedTimeseriesFactory
-from .ObservatoryMetadata import ObservatoryMetadata
-from .VariometerMetadata import VariometerMetadata
-from .PlotTimeseriesFactory import PlotTimeseriesFactory
-from .TimeseriesFactory import TimeseriesFactory
-from .TimeseriesFactoryException import TimeseriesFactoryException
-
-__all__ = [
-    "ChannelConverter",
-    "Controller",
-    "DeltaFAlgorithm",
-    "DerivedTimeseriesFactory",
-    "ObservatoryMetadata",
-    "VariometerMetadata",
-    "PlotTimeseriesFactory",
-    "StreamConverter",
-    "TimeseriesFactory",
-    "TimeseriesFactoryException",
-    "TimeseriesUtility",
-    "Util",
-]
diff --git a/geomagio/adjusted/AdjustedMatrix.py b/geomagio/adjusted/AdjustedMatrix.py
index 7b35f765588a5f82142d869d6af0d49d4a0b113b..41eecd800084a56fcf425c02aa27b7f47f29067a 100644
--- a/geomagio/adjusted/AdjustedMatrix.py
+++ b/geomagio/adjusted/AdjustedMatrix.py
@@ -3,8 +3,8 @@ from obspy import Stream, UTCDateTime
 from pydantic import BaseModel
 from typing import Any, List, Optional
 
-from ..pydantic_utcdatetime import CustomUTCDateTimeType
-from ..residual.Reading import Reading, get_absolutes_xyz, get_ordinates
+from geomagio.pydantic_utcdatetime import CustomUTCDateTimeType
+from geomagio.residual.Reading import Reading, get_absolutes_xyz, get_ordinates
 from .. import ChannelConverter
 from .Metric import Metric, get_metric
 
diff --git a/geomagio/adjusted/Affine.py b/geomagio/adjusted/Affine.py
index a4157d3e193cf667bf406b1f5c691eae70886f2b..33bbf04b45d662ba5053c6afa2a742859daabd7e 100644
--- a/geomagio/adjusted/Affine.py
+++ b/geomagio/adjusted/Affine.py
@@ -4,14 +4,16 @@ from obspy import UTCDateTime
 from pydantic import BaseModel, Field
 from typing import List, Optional, Tuple
 
-from ..residual.Reading import (
+from geomagio.residual.Reading import (
     Reading,
     get_absolutes_xyz,
     get_ordinates,
 )
 from .AdjustedMatrix import AdjustedMatrix
-from .transform import RotationTranslationXY, TranslateOrigins, Transform
-from ..pydantic_utcdatetime import CustomUTCDateTimeType
+from .transform.RotationTranslationXY import RotationTranslationXY
+from .transform.TranslateOrigins import TranslateOrigins
+from .transform.Transform import Transform
+from geomagio.pydantic_utcdatetime import CustomUTCDateTimeType
 
 
 class Affine(BaseModel):
diff --git a/geomagio/adjusted/__init__.py b/geomagio/adjusted/__init__.py
index b86d5d92707e5d9bb6fd5b3395d88fa59261c661..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/adjusted/__init__.py
+++ b/geomagio/adjusted/__init__.py
@@ -1,9 +0,0 @@
-from .AdjustedMatrix import AdjustedMatrix
-from .Affine import Affine
-from .Metric import Metric
-
-__all__ = [
-    "AdjustedMatrix",
-    "Affine",
-    "Metric",
-]
diff --git a/geomagio/adjusted/transform/__init__.py b/geomagio/adjusted/transform/__init__.py
index 0fa99ab9aca8f355ff40cd866cc7f3b515e2a7f6..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/adjusted/transform/__init__.py
+++ b/geomagio/adjusted/transform/__init__.py
@@ -1,26 +0,0 @@
-from .LeastSq import LeastSq
-from .QRFactorization import QRFactorization
-from .Rescale3D import Rescale3D
-from .RotationTranslationXY import RotationTranslationXY
-from .ShearYZ import ShearYZ
-from .Transform import Transform
-from .TranslateOrigins import TranslateOrigins
-from .SVD import SVD
-from .ZRotationHScale import ZRotationHscale
-from .ZRotationHScaleZBaseline import ZRotationHscaleZbaseline
-from .ZRotationShear import ZRotationShear
-
-__all__ = [
-    "LeastSq",
-    "QRFactorization",
-    "Rescale3D",
-    "RotationTranslation3D",
-    "RotationTranslationXY",
-    "ShearYZ",
-    "Transform",
-    "TranslateOrigins",
-    "SVD",
-    "ZRotationHscale",
-    "ZRotationHscaleZbaseline",
-    "ZRotationShear",
-]
diff --git a/geomagio/algorithm/AdjustedAlgorithm.py b/geomagio/algorithm/AdjustedAlgorithm.py
index 3e2d1c2c71a89c676153036125340a186f3b8361..f835fdbbdc27a961afcdce2ea0715fb69c09d57e 100644
--- a/geomagio/algorithm/AdjustedAlgorithm.py
+++ b/geomagio/algorithm/AdjustedAlgorithm.py
@@ -4,7 +4,7 @@ import json
 import numpy as np
 from obspy.core import Stream, Stats
 
-from ..adjusted import AdjustedMatrix
+from geomagio.adjusted.AdjustedMatrix import AdjustedMatrix
 from .Algorithm import Algorithm
 
 
diff --git a/geomagio/algorithm/__init__.py b/geomagio/algorithm/__init__.py
index 0d56bbcdd630f13aa06df62b94497594cbb3f159..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/algorithm/__init__.py
+++ b/geomagio/algorithm/__init__.py
@@ -1,46 +0,0 @@
-"""
-Geomag Algorithms module
-"""
-
-from __future__ import absolute_import
-
-# base classes
-from .Algorithm import Algorithm
-from .AlgorithmException import AlgorithmException
-
-# algorithms
-from .AdjustedAlgorithm import AdjustedAlgorithm
-from .AverageAlgorithm import AverageAlgorithm
-from .DbDtAlgorithm import DbDtAlgorithm
-from .DeltaFAlgorithm import DeltaFAlgorithm
-from .FilterAlgorithm import FilterAlgorithm
-from .SqDistAlgorithm import SqDistAlgorithm
-from .XYZAlgorithm import XYZAlgorithm
-
-
-# algorithms is used by Controller to auto generate arguments
-algorithms = {
-    "identity": Algorithm,
-    "adjusted": AdjustedAlgorithm,
-    "average": AverageAlgorithm,
-    "dbdt": DbDtAlgorithm,
-    "deltaf": DeltaFAlgorithm,
-    "filter": FilterAlgorithm,
-    "sqdist": SqDistAlgorithm,
-    "xyz": XYZAlgorithm,
-}
-
-
-__all__ = [
-    # base classes
-    "Algorithm",
-    "AlgorithmException",
-    # algorithms
-    "AdjustedAlgorithm",
-    "AverageAlgorithm",
-    "DbDtAlgorithm",
-    "DeltaFAlgorithm",
-    "FilterAlgorithm",
-    "SqDistAlgorithm",
-    "XYZAlgorithm",
-]
diff --git a/geomagio/api/__init__.py b/geomagio/api/__init__.py
index ee21241f2ddd1b9529d7ae775a3e96d3b1e267ee..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/api/__init__.py
+++ b/geomagio/api/__init__.py
@@ -1,4 +0,0 @@
-from .app import app
-from .db.create import create_db
-
-__all__ = ["app", "create_db"]
diff --git a/geomagio/api/app.py b/geomagio/api/app.py
index d13f8b92be9a37c8547dd66ab9498568f98442c9..07ee0c3497806c3d55f9614eee9e4ca3ae2b940c 100644
--- a/geomagio/api/app.py
+++ b/geomagio/api/app.py
@@ -10,12 +10,11 @@ and can be run using uvicorn, or any other ASGI server:
 import logging
 import os
 
-from contextlib import asynccontextmanager
 from fastapi import FastAPI
 from starlette.responses import RedirectResponse
 
-from . import secure, ws
-from .db import database
+from geomagio.api.secure.secure_app import app as secure_app
+from geomagio.api.ws.ws_app import app as ws_app
 
 LOG_BASIC_CONFIG = os.getenv("LOG_BASIC_CONFIG", "false")
 LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
@@ -23,21 +22,12 @@ if LOG_BASIC_CONFIG == "true":
     logging.basicConfig(level=LOG_LEVEL)
 
 
-@asynccontextmanager
-async def lifespan(app: FastAPI):
-    # on startup
-    await database.connect()
-    yield
-    # on shutdown
-    await database.disconnect()
+ws_app = FastAPI()
 
+ws_app.mount("/ws/secure", secure_app)
+ws_app.mount("/ws", ws_app)
 
-app = FastAPI(lifespan=lifespan)
 
-app.mount("/ws/secure", secure.app)
-app.mount("/ws", ws.app)
-
-
-@app.get("/", include_in_schema=False)
+@ws_app.get("/", include_in_schema=False)
 async def redirect_to_ws():
     return RedirectResponse("/ws")
diff --git a/geomagio/api/db/MetadataDatabaseFactory.py b/geomagio/api/db/MetadataDatabaseFactory.py
deleted file mode 100644
index 7a92a91d52b072e7522f8e4db2aff577cc39d276..0000000000000000000000000000000000000000
--- a/geomagio/api/db/MetadataDatabaseFactory.py
+++ /dev/null
@@ -1,132 +0,0 @@
-from datetime import datetime
-from typing import List, Optional
-
-from databases import Database
-from obspy import UTCDateTime
-from sqlalchemy import or_
-
-from ...metadata import Metadata, MetadataQuery
-from .metadata_history_table import metadata_history
-from .metadata_table import metadata as metadata_table
-
-
-class MetadataDatabaseFactory(object):
-    def __init__(self, database: Database):
-        self.database = database
-
-    async def create_metadata(self, meta: Metadata) -> Metadata:
-        query = metadata_table.insert()
-        meta.status = meta.status or "new"
-        values = meta.model_dump(exclude={"id", "metadata_id"}, exclude_none=True)
-        query = query.values(**values)
-        meta.id = await self.database.execute(query)
-        return meta
-
-    async def get_metadata(
-        self,
-        params: MetadataQuery,
-        history: bool = False,
-    ) -> List[Metadata]:
-        table = metadata_table
-        if history:
-            table = metadata_history
-        query = table.select()
-        (
-            id,
-            category,
-            starttime,
-            endtime,
-            created_after,
-            created_before,
-            network,
-            station,
-            channel,
-            location,
-            data_valid,
-            metadata,
-            status,
-        ) = params.model_dump().values()
-        if id:
-            query = query.where(table.c.id == id)
-        if category:
-            query = query.where(table.c.category == category)
-        if network:
-            query = query.where(table.c.network == network)
-        if station:
-            query = query.where(table.c.station == station)
-        if channel:
-            query = query.where(table.c.channel.like(channel))
-        if location:
-            query = query.where(table.c.location.like(location))
-        if starttime:
-            query = query.where(
-                or_(
-                    table.c.endtime == None,
-                    table.c.endtime > starttime,
-                )
-            )
-        if endtime:
-            query = query.where(
-                or_(
-                    table.c.starttime == None,
-                    table.c.starttime < endtime,
-                )
-            )
-        if created_after:
-            query = query.where(table.c.created_time > created_after)
-        if created_before:
-            query = query.where(table.c.created_time < created_before)
-        if data_valid is not None:
-            query = query.where(table.c.data_valid == data_valid)
-        if status is not None:
-            query = query.where(table.c.status.in_(status))
-        rows = await self.database.fetch_all(query)
-        return [Metadata(**row) for row in rows]
-
-    async def get_metadata_by_id(self, id: int):
-        meta = await self.get_metadata(MetadataQuery(id=id))
-        if len(meta) != 1:
-            raise ValueError(f"{len(meta)} records found")
-        return meta[0]
-
-    async def get_metadata_history_by_id(self, id: int) -> Optional[Metadata]:
-        query = metadata_history.select()
-        query = query.where(metadata_history.c.id == id)
-        meta = await self.database.fetch_one(query)
-        if meta is None:
-            return meta
-        return Metadata(**meta)
-
-    async def get_metadata_history_by_metadata_id(
-        self, metadata_id: int
-    ) -> List[Metadata]:
-        async with self.database.transaction() as transaction:
-            query = metadata_history.select()
-            query = query.where(metadata_history.c.metadata_id == metadata_id).order_by(
-                metadata_history.c.updated_time
-            )
-            rows = await self.database.fetch_all(query)
-            metadata = [Metadata(**row) for row in rows]
-            current_metadata = await self.get_metadata_by_id(id=metadata_id)
-            metadata.append(current_metadata)
-            # return records in order of age(newest first)
-            metadata.reverse()
-            return metadata
-
-    async def update_metadata(self, meta: Metadata, updated_by: str) -> Metadata:
-        async with self.database.transaction() as transaction:
-            # write current record to metadata history table
-            original_metadata = await self.get_metadata_by_id(id=meta.id)
-            original_metadata.metadata_id = original_metadata.id
-            values = original_metadata.model_dump(exclude={"id"}, exclude_none=True)
-            query = metadata_history.insert()
-            query = query.values(**values)
-            original_metadata.id = await self.database.execute(query)
-            # update record in metadata table
-            meta.updated_by = updated_by
-            meta.updated_time = UTCDateTime()
-            query = metadata_table.update().where(metadata_table.c.id == meta.id)
-            values = meta.model_dump(exclude={"id", "metadata_id"})
-            query = query.values(**values)
-            await self.database.execute(query)
-            return await self.get_metadata_by_id(id=meta.id)
diff --git a/geomagio/api/db/__init__.py b/geomagio/api/db/__init__.py
index dfb22968ecc34f0a19bcc60648f7efae772bf834..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/api/db/__init__.py
+++ b/geomagio/api/db/__init__.py
@@ -1,16 +0,0 @@
-"""geomagio.api.db package.
-
-This package manages the database connection, data models,
-and provides methods for data access from other parts of the api.
-
-Modules outside the api should not access the database directly.
-"""
-
-from .common import database, sqlalchemy_metadata
-from .MetadataDatabaseFactory import MetadataDatabaseFactory
-
-__all__ = [
-    "database",
-    "sqlalchemy_metadata",
-    "MetadataDatabaseFactory",
-]
diff --git a/geomagio/api/db/common.py b/geomagio/api/db/common.py
deleted file mode 100644
index d5646f1cc6825d445f2c6d63fd71b5df66dcd100..0000000000000000000000000000000000000000
--- a/geomagio/api/db/common.py
+++ /dev/null
@@ -1,47 +0,0 @@
-"""
-Define the database connection and sqlalchemy metadata objects.
-
-
-Configuration:
-    uses environment variables:
-
-    DATABASE_URL  - url to connect to database.
-                    Default is "sqlite:///./api_database.db"
-
-
-Database models:
-
-    Register with metadata.
-
-        class DatabaseModel(orm.Model):
-            __database__ = database
-            __metadata__ = sqlalchemy_metadata
-
-    And import in create.py, so scripts can manage the database schema.
-
-
-Applications must manage the database connections:
-
-    @app.on_event("startup")
-    async def on_startup():
-        await database.connect()
-
-
-    @app.on_event("shutdown")
-    async def on_shutdown():
-        await database.disconnect()
-"""
-
-import os
-
-from databases import Database
-from sqlalchemy import MetaData
-
-
-# database connection
-database_url = os.getenv("DATABASE_URL", None)
-database_url = database_url or "sqlite:///./api_database.db"
-database = Database(database_url)
-
-# metadata used to manage database schema
-sqlalchemy_metadata = MetaData()
diff --git a/geomagio/api/db/create.py b/geomagio/api/db/create.py
deleted file mode 100644
index 531b79a9b00212624cfe1dbb566da6db08ab75aa..0000000000000000000000000000000000000000
--- a/geomagio/api/db/create.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import sqlalchemy
-
-from .common import database, sqlalchemy_metadata
-
-# register models with sqlalchemy_metadata by importing
-from .metadata_history_table import metadata_history
-from .metadata_table import metadata
-from .session_table import session
-
-
-def create_db():
-    """Create the database using sqlalchemy."""
-    database_url = str(database.url).replace("mysql://", "mysql+pymysql://")
-    engine = sqlalchemy.create_engine(database_url)
-    sqlalchemy_metadata.create_all(engine)
-
-
-if __name__ == "__main__":
-    create_db()
diff --git a/geomagio/api/db/database.md b/geomagio/api/db/database.md
new file mode 100644
index 0000000000000000000000000000000000000000..ebfb7024adc2d4c16cd7de80d3df4a0f12dcd3d3
--- /dev/null
+++ b/geomagio/api/db/database.md
@@ -0,0 +1,3 @@
+# Database
+
+All interactions with the metadata database should be through this db package. The db package consists of the engine, which manages the database connnections, the models, which define the sqlmodel table classes and pydantic models, and the factories that handle the actual reading and writing. Because of the limitation of sqlmodel table classes not allowing "metadata" to be a field as noted [here](https://github.com/fastapi/sqlmodel/issues/290), we have to translate pydantic models to sqlmdodel classes and back. This contains the issue to just the factories and the rest of the code base and users don't need to be aware of the translation.
\ No newline at end of file
diff --git a/geomagio/api/db/database.py b/geomagio/api/db/database.py
new file mode 100644
index 0000000000000000000000000000000000000000..74fb3211b2e94bc9f556c62b9c39c497efb7bc0a
--- /dev/null
+++ b/geomagio/api/db/database.py
@@ -0,0 +1,16 @@
+import os
+
+from enum import Enum
+from sqlmodel import create_engine
+
+from geomagio.api.db.models.metadata import MetadataTable
+from geomagio.api.db.models.metadata_history import MetadataHistoryTable
+
+
+class Tables(str, Enum):
+    METADATA = MetadataTable
+    METADATAHISTORYTABLE = MetadataHistoryTable
+
+
+database_url = os.getenv("DATABASE_URL", "sqlite:///./api_database.db")
+engine = create_engine(database_url)
diff --git a/geomagio/api/db/factories/MetadataDatabaseFactory.py b/geomagio/api/db/factories/MetadataDatabaseFactory.py
new file mode 100644
index 0000000000000000000000000000000000000000..d5816eb7d627548ad62da2f2248fec6dbe7e38cd
--- /dev/null
+++ b/geomagio/api/db/factories/MetadataDatabaseFactory.py
@@ -0,0 +1,153 @@
+from typing import List
+from sqlmodel import Session
+from datetime import datetime, timezone
+from obspy import UTCDateTime
+
+from geomagio.api.db.models.metadata import (
+    MetadataTable,
+    Metadata,
+    MetadataQuery,
+    MetadataUpdate,
+)
+from geomagio.api.db.models.metadata_history import MetadataHistoryTable
+from geomagio.api.db.factories import db_helpers
+
+
+class MetadataDatabaseFactory:
+    def __init__(self, engine):
+        self.engine = engine
+
+    async def create_metadata(self, new_metadata: Metadata) -> Metadata:
+        with Session(self.engine) as db_session:
+            # convert Metadata input into MetadataTable object
+            new_metadata_values = new_metadata.model_dump(by_alias=True)
+            new_metadata_table_model = MetadataTable(**new_metadata_values)
+
+            new_metadata_table_model = MetadataTable.model_validate(
+                new_metadata_table_model
+            )
+            new_metadata_table_model.id = None
+            if new_metadata_table_model.created_time is None:
+                new_metadata_table_model.created_time = UTCDateTime.now()
+
+            # convert UTCDateTime to aware datetime for sqlalchemy
+            new_metadata_table_model = db_helpers.convert_utcdatetime_to_datetime(
+                metadata=new_metadata_table_model
+            )
+
+            db_session.add(new_metadata_table_model)
+            db_session.commit()
+            db_session.refresh(new_metadata_table_model)
+
+            # convert newly created metadata row into Metadata object
+            newly_created_metadata_values = new_metadata_table_model.model_dump(
+                by_alias=True
+            )
+            returned_metadata = Metadata(**newly_created_metadata_values)
+
+            return returned_metadata
+
+    async def get_metadata(self, query: MetadataQuery) -> List[Metadata]:
+        with Session(self.engine) as db_session:
+            query = MetadataQuery.model_validate(query)
+            statement = db_helpers.build_metadata_search_query(
+                query=query, table=MetadataTable
+            )
+
+            results = db_session.exec(statement)
+
+            returned_metadata = []
+
+            for result in results:
+                found_metadata_values = result.model_dump(by_alias=True)
+                found_metadata = Metadata(**found_metadata_values)
+                returned_metadata.append(found_metadata)
+
+            return returned_metadata
+
+    async def get_metadata_by_id(self, id=int) -> Metadata:
+        if type(id) != int:
+            raise ValueError("expected an int")
+
+        with Session(self.engine) as db_session:
+            result = db_session.get(MetadataTable, id)
+            if result != None:
+                result_values = result.model_dump(by_alias=True)
+                return Metadata(**result_values)
+            else:
+                return None
+
+    async def update_metadata(
+        self, updated_metadata: MetadataUpdate, updated_by: str
+    ) -> Metadata:
+        with Session(self.engine) as db_session:
+            # convert any times on updated metadata to aware datetime for sqlalchemy
+            updated_metadata = db_helpers.convert_utcdatetime_to_datetime(
+                updated_metadata
+            )
+
+            # get original metadata
+            original_metadata = db_session.get(MetadataTable, updated_metadata.id)
+            if original_metadata is None:
+                raise ValueError("metadata not found")
+
+            # save original meetadata as a new metadata_history record
+            new_metadata_history_values = original_metadata.model_dump()
+            new_metadata_history = MetadataHistoryTable(**new_metadata_history_values)
+
+            # remove the id and set metadata_id to the original_metadata id
+            new_metadata_history.id = None
+            new_metadata_history.metadata_id = original_metadata.id
+
+            # copy over the time fields by hand to prevent them from being serialized into strings
+            new_metadata_history.created_time = original_metadata.created_time
+            if original_metadata.updated_time:
+                new_metadata_history.updated_time = original_metadata.updated_time
+            if original_metadata.starttime:
+                new_metadata_history.starttime = original_metadata.starttime
+            if original_metadata.endtime:
+                new_metadata_history.endtime = original_metadata.endtime
+
+            db_session.add(new_metadata_history)
+            db_session.flush()
+
+            # set updated values on original_metadata
+            # do not model_validate the inputs otherwise the defaults will all be set to None.
+            # sqlmodel_update removes any values set to None
+            update_values = updated_metadata.model_dump(
+                exclude_unset=True, by_alias=True
+            )
+            original_metadata.sqlmodel_update(update_values)
+
+            original_metadata.updated_by = updated_by
+            original_metadata.updated_time = datetime.now(timezone.utc)
+            db_session.add(original_metadata)
+            db_session.commit()
+            db_session.refresh(original_metadata)
+
+            # convert newly updated metadata row to Metadata type
+            return_metadata_values = original_metadata.model_dump(by_alias=True)
+            return Metadata(**return_metadata_values)
+
+    async def batch_create_metadata(self, new_metadatas: List[Metadata]):
+        with Session(self.engine) as db_session:
+            for new_metadata in new_metadatas:
+                new_metadata_values = new_metadata.model_dump(by_alias=True)
+                new_metadata_table_model = MetadataTable(**new_metadata_values)
+
+                new_metadata_table_model = MetadataTable.model_validate(
+                    new_metadata_table_model
+                )
+                new_metadata_table_model.id = None
+                if new_metadata_table_model.created_time is None:
+                    new_metadata_table_model.created_time = UTCDateTime.now()
+
+                # convert UTCDateTime to aware datetime for sqlalchemy
+                new_metadata_table_model = db_helpers.convert_utcdatetime_to_datetime(
+                    metadata=new_metadata_table_model
+                )
+
+                db_session.add(new_metadata_table_model)
+            db_session.commit()
+            db_session.refresh(new_metadata_table_model)
+        return new_metadatas
diff --git a/geomagio/api/db/factories/MetadataHistoryDatabaseFactory.py b/geomagio/api/db/factories/MetadataHistoryDatabaseFactory.py
new file mode 100644
index 0000000000000000000000000000000000000000..fdb5b725f4e4462458895741b3bd08665c46516a
--- /dev/null
+++ b/geomagio/api/db/factories/MetadataHistoryDatabaseFactory.py
@@ -0,0 +1,102 @@
+from typing import List
+from sqlmodel import Session, select
+
+from geomagio.api.db.factories import db_helpers
+from geomagio.api.db.models.metadata import Metadata, MetadataTable, MetadataQuery
+from geomagio.api.db.models.metadata_history import (
+    MetadataHistoryTable,
+    MetadataHistory,
+)
+
+
+class MetadataHistoryDatabaseFactory:
+    def __init__(self, engine):
+        self.engine = engine
+
+    async def get_metadata_history_by_id(self, id=int) -> MetadataHistory:
+        if type(id) != int:
+            raise ValueError("expected an int")
+
+        with Session(self.engine) as db_session:
+            result = db_session.get(MetadataHistoryTable, id)
+            if result != None:
+                result_values = result.model_dump(by_alias=True)
+                return MetadataHistory(**result_values)
+            else:
+                return None
+
+    async def get_metadata_history(self, query: MetadataQuery) -> List[MetadataHistory]:
+        with Session(self.engine) as db_session:
+            query = MetadataQuery.model_validate(query)
+            statement = db_helpers.build_metadata_search_query(
+                query=query, table=MetadataHistoryTable
+            )
+
+            results = db_session.exec(statement)
+
+            returned_results = []
+            for result in results:
+                result_values = result.model_dump(by_alias=True)
+                returned_results.append(MetadataHistory(**result_values))
+
+            return returned_results
+
+    async def get_metadata_history_by_metadata_id(self, metadata_id: int):
+        if type(metadata_id) != int:
+            raise ValueError("expected an int")
+
+        returned_list = []
+        with Session(self.engine) as db_session:
+            statement = (
+                select(MetadataHistoryTable, MetadataTable)
+                .join(MetadataTable)
+                .where(MetadataTable.id == metadata_id)
+            )
+            results = db_session.exec(statement)
+            history = results.all()
+
+            # if there's no metadata_history associated with the metdata object, just return the metadata ojbect
+            if len(history) == 0:
+                metadata = db_session.get(MetadataTable, metadata_id)
+                if metadata == None:
+                    return None
+                else:
+                    result_values = metadata.model_dump(by_alias=True)
+                    return [Metadata(**result_values)]
+
+            metadata_list = []
+            for metadata_history, metadata in history:
+                metadata_history_values = metadata_history.model_dump(by_alias=True)
+                formatted_metadata_history = MetadataHistory(**metadata_history_values)
+                returned_list.insert(0, formatted_metadata_history)
+
+                # make sure only 1 metadata is appended. if not, raise a big error
+                if len(metadata_list) == 0:
+                    metadata_list.append(metadata)
+                else:
+                    if metadata_list[0] != metadata:
+                        raise SystemError(
+                            "get_metadata_history_by_metadata_id returned more than one metadata for id: ",
+                            metadata_id,
+                        )
+
+            # insert Metadata object to the front of the list
+            formatted_metadata_values = metadata_list[0].model_dump(by_alias=True)
+            formatted_metadata = Metadata(**formatted_metadata_values)
+            returned_list.insert(0, formatted_metadata)
+
+            return returned_list
+
+    async def batch_create_metadata_history(self, new_metadatas: List[MetadataHistory]):
+        with Session(self.engine) as db_session:
+            for new_metadata in new_metadatas:
+                new_metadata = MetadataHistoryTable.model_validate(new_metadata)
+                new_metadata.id = None
+
+                # convert UTCDateTime to aware datetime for sqlalchemy
+                new_metadata = db_helpers.convert_utcdatetime_to_datetime(
+                    metadata=new_metadata
+                )
+
+                db_session.add(new_metadata)
+            db_session.commit()
diff --git a/geomagio/api/db/factories/SessionDatabaseFactory.py b/geomagio/api/db/factories/SessionDatabaseFactory.py
new file mode 100644
index 0000000000000000000000000000000000000000..bd83645f40b21aff6ee40599b8ea31b321cb545b
--- /dev/null
+++ b/geomagio/api/db/factories/SessionDatabaseFactory.py
@@ -0,0 +1,62 @@
+import os
+import json
+import uuid
+
+from typing import Mapping
+from datetime import datetime, timezone
+from sqlmodel import Session, select
+
+from geomagio.api.db.models.session import session
+from geomagio.api.secure.encryption import get_fernet
+
+
+class SessionDatabaseFactory:
+    def __init__(self, engine):
+        self.encryption = get_fernet()
+        self.engine = engine
+
+    async def get_session(self, session_id: str) -> session:
+        with Session(self.engine) as db_session:
+            statement = select(session).where(session.session_id == session_id)
+            results = db_session.exec(statement)
+            existing_session = results.one()
+
+            # if existing_session is None:
+            #     return None
+
+            data = self.encryption.decrypt(existing_session.data.encode("utf8"))
+            return json.loads(data)
+
+    async def save_session(self, session_id: str, data: Mapping) -> None:
+        updated = datetime.now(timezone.utc)
+
+        data = json.dumps(data)
+        data = self.encryption.encrypt(data.encode("utf8")).decode("utf8")
+
+        with Session(self.engine) as db_session:
+            # check if session exists before updating
+            statement = select(session).where(session.session_id == session_id)
+            results = db_session.exec(statement)
+            existing_session = results.first()
+
+            if not existing_session:
+                new_session = session(session_id=session_id, data=data, updated=updated)
+                db_session.add(new_session)
+            else:
+                existing_session.updated = updated
+                existing_session.data = data
+                db_session.add(existing_session)
+
+            db_session.commit()
+
+    async def delete_session(self, session_id: str) -> None:
+        with Session(self.engine) as db_session:
+            statement = select(session).where(session.session_id == session_id)
+            results = db_session.exec(statement)
+            existing_session = results.first()
+
+            if existing_session is None:
+                return None
+
+            db_session.delete(existing_session)
+            db_session.commit()
diff --git a/geomagio/api/db/factories/__init__.py b/geomagio/api/db/factories/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/geomagio/api/db/factories/db_helpers.py b/geomagio/api/db/factories/db_helpers.py
new file mode 100644
index 0000000000000000000000000000000000000000..e45caf93c69cb7cb2ddfbeecd8316f792c198842
--- /dev/null
+++ b/geomagio/api/db/factories/db_helpers.py
@@ -0,0 +1,80 @@
+from datetime import timezone
+from typing import Any
+from sqlmodel import select, or_
+
+from geomagio.api.db.models.metadata import MetadataQuery
+from geomagio.api.db.database import Tables
+
+
+def convert_utcdatetime_to_datetime(metadata: Any):
+    if metadata.created_time is not None:
+        metadata.created_time = metadata.created_time.datetime.replace(
+            tzinfo=timezone.utc
+        )
+    if metadata.updated_time is not None:
+        metadata.updated_time = metadata.updated_time.datetime.replace(
+            tzinfo=timezone.utc
+        )
+    if metadata.starttime is not None:
+        metadata.starttime = metadata.starttime.datetime.replace(tzinfo=timezone.utc)
+    if metadata.endtime is not None:
+        metadata.endtime = metadata.endtime.datetime.replace(tzinfo=timezone.utc)
+    return metadata
+
+
+def build_metadata_search_query(query: MetadataQuery, table: Tables):
+    # convert UTCDateTime to aware datetime for sqlalchemy
+    if query.created_before is not None:
+        query.created_before = query.created_before.datetime.replace(
+            tzinfo=timezone.utc
+        )
+    if query.created_after is not None:
+        query.created_after = query.created_after.datetime.replace(tzinfo=timezone.utc)
+    if query.starttime is not None:
+        query.starttime = query.starttime.datetime.replace(tzinfo=timezone.utc)
+    if query.endtime is not None:
+        query.endtime = query.endtime.datetime.replace(tzinfo=timezone.utc)
+
+    # build select statement
+    statement = select(table)
+    if query.id:
+        statement = statement.where(table.id == query.id)
+    if query.category:
+        statement = statement.where(table.category == query.category)
+    if query.network:
+        statement = statement.where(table.network == query.network)
+    if query.station:
+        statement = statement.where(table.station == query.station)
+
+    # using "like" to search for channels and locations to account for potential inconsistencies over time
+    if query.channel:
+        statement = statement.where(table.channel.like(query.channel))
+    if query.location:
+        statement = statement.where(table.location.like(query.location))
+
+    if query.starttime:
+        statement = statement.where(
+            or_(
+                table.endtime == None,
+                table.endtime > query.starttime,
+            )
+        )
+    if query.endtime:
+        statement = statement.where(
+            or_(
+                table.starttime == None,
+                table.starttime < query.endtime,
+            )
+        )
+    if query.created_after:
+        statement = statement.where(table.created_time > query.created_after)
+    if query.created_before:
+        statement = statement.where(table.created_time < query.created_before)
+    if query.data_valid is not None:
+        statement = statement.where(table.data_valid == query.data_valid)
+
+    # users can search for multiple statuses
+    if query.status is not None:
+        statement = statement.where(table.status.in_(query.status))
+
+    return statement
diff --git a/geomagio/api/db/metadata_history_table.py b/geomagio/api/db/metadata_history_table.py
deleted file mode 100644
index 8fbd785c108d7cebbfa7657f8da6b3bc06cc43c8..0000000000000000000000000000000000000000
--- a/geomagio/api/db/metadata_history_table.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from sqlalchemy import Column, ForeignKey, Integer
-
-from .common import sqlalchemy_metadata
-from .metadata_table import metadata
-
-# create copy of original metadata table and add to sqlalchemy metadata
-metadata_history = metadata.to_metadata(
-    metadata=sqlalchemy_metadata, name="metadata_history"
-)
-metadata_history.indexes.clear()
-metadata_history.append_column(
-    Column(
-        "metadata_id",
-        Integer,
-        ForeignKey("metadata.id"),
-        nullable=False,
-    ),
-)
diff --git a/geomagio/api/db/metadata_table.py b/geomagio/api/db/metadata_table.py
deleted file mode 100644
index 6b5716db50b2e96b9102f0c16f61a021884e6bee..0000000000000000000000000000000000000000
--- a/geomagio/api/db/metadata_table.py
+++ /dev/null
@@ -1,74 +0,0 @@
-from sqlalchemy import Boolean, Column, Index, Integer, JSON, String, Table, Text
-import sqlalchemy_utc
-
-from .common import sqlalchemy_metadata
-
-
-"""Metadata database model.
-
-See pydantic model geomagio.metadata.Metadata
-"""
-metadata = Table(
-    "metadata",
-    sqlalchemy_metadata,
-    ## COLUMNS
-    Column("id", Integer, primary_key=True),
-    # author
-    Column("created_by", String(length=255), index=True),
-    Column(
-        "created_time",
-        sqlalchemy_utc.UtcDateTime,
-        default=sqlalchemy_utc.utcnow(),
-        index=True,
-    ),
-    # editor
-    Column("updated_by", String(length=255), index=True, nullable=True),
-    Column("updated_time", sqlalchemy_utc.UtcDateTime, index=True, nullable=True),
-    # time range
-    Column("starttime", sqlalchemy_utc.UtcDateTime, index=True, nullable=True),
-    Column("endtime", sqlalchemy_utc.UtcDateTime, index=True, nullable=True),
-    # what data metadata references, null for wildcard
-    Column("network", String(length=255), nullable=True),  # indexed below
-    Column("station", String(length=255), nullable=True),  # indexed below
-    Column("channel", String(length=255), nullable=True),  # indexed below
-    Column("location", String(length=255), nullable=True),  # indexed below
-    # category (flag, matrix, etc)
-    Column("category", String(length=255)),  # indexed below
-    # higher priority overrides lower priority
-    Column("priority", Integer, default=1),
-    # whether data is valid (primarily for flags)
-    Column("data_valid", Boolean, default=True, index=True),
-    # deletion status indicator
-    Column("status", String(length=255), nullable=True),
-    # metadata json blob
-    Column("metadata", JSON, nullable=True),
-    # general comment
-    Column("comment", Text, nullable=True),
-    # review specific comment
-    Column("review_comment", Text, nullable=True),
-    ## INDICES
-    Index(
-        "index_station_metadata",
-        # sncl
-        "network",
-        "station",
-        "channel",
-        "location",
-        # type
-        "category",
-        # date
-        "starttime",
-        "endtime",
-        # valid
-        "data_valid",
-        "status",
-    ),
-    Index(
-        "index_category_time",
-        # type
-        "category",
-        # date
-        "starttime",
-        "endtime",
-    ),
-)
diff --git a/geomagio/api/db/models/__init__.py b/geomagio/api/db/models/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/geomagio/api/db/models/metadata.py b/geomagio/api/db/models/metadata.py
new file mode 100644
index 0000000000000000000000000000000000000000..2bfa3b4be6fb69721e83e6721e194e8b5481a557
--- /dev/null
+++ b/geomagio/api/db/models/metadata.py
@@ -0,0 +1,147 @@
+import sqlalchemy_utc
+
+from enum import Enum
+from obspy import UTCDateTime
+from typing import Dict, List, Any
+from pydantic import BaseModel
+from sqlmodel import SQLModel, Field, Column, Text, JSON, Index
+
+from geomagio.pydantic_utcdatetime import CustomUTCDateTimeType
+
+
+class MetadataCategory(str, Enum):
+    ADJUSTED_MATRIX = "adjusted-matrix"
+    FLAG = "flag"
+    INSTRUMENT = "instrument"
+    OBSERVATORY = "observatory"
+    READING = "reading"
+
+
+class MetadataTable(SQLModel, table=True):
+    __tablename__ = "metadata"
+
+    id: int | None = Field(primary_key=True)
+    created_by: str | None = Field(index=True)
+    created_time: CustomUTCDateTimeType | None = Field(
+        default=UTCDateTime.now(),
+        sa_column=Column(sqlalchemy_utc.UtcDateTime, index=True),
+    )
+    updated_by: str | None = Field(index=True)
+    updated_time: CustomUTCDateTimeType | None = Field(
+        sa_column=Column(sqlalchemy_utc.UtcDateTime, index=True)
+    )
+    starttime: CustomUTCDateTimeType | None = Field(
+        sa_column=Column(sqlalchemy_utc.UtcDateTime, index=True)
+    )
+    endtime: CustomUTCDateTimeType | None = Field(
+        sa_column=Column(sqlalchemy_utc.UtcDateTime, index=True)
+    )
+    network: str | None = None
+    station: str | None = None
+    channel: str | None = None
+    location: str | None = None
+    category: str | None = None
+    priority: int | None = Field(default=1)
+    data_valid: bool | None = Field(default=True, index=True)
+    status: str | None = "new"
+    # metadata field is already being used by sqlmodel and therefore needs a different name and alias
+    metadata_: Dict[str, Any] | None = Field(
+        sa_column=Column("metadata", JSON),
+        alias="metadata",
+        schema_extra={"serialization_alias": "metadata"},
+    )
+    comment: str | None = Field(sa_column=Column(Text))
+    review_comment: str | None = Field(sa_column=Column(Text))
+
+    __table_args__ = (
+        Index(
+            "index_station_metadata",
+            # sncl
+            "network",
+            "station",
+            "channel",
+            "location",
+            # type
+            "category",
+            # date
+            "starttime",
+            "endtime",
+            # valid
+            "data_valid",
+            "status",
+        ),
+        Index(
+            "index_category_time",
+            # type
+            "category",
+            # date
+            "starttime",
+            "endtime",
+        ),
+    )
+
+
+# this pydantic model exists as an abstraction layer between the code and the database for 2 reasons
+# 1. sqlmodel table validation is not powerful
+# 2. this allows the metadata_ name conflict to only be an issue when interacting directly with the database in the factory
+class Metadata(BaseModel):
+    id: int | None = None
+    created_by: str | None = None
+    created_time: CustomUTCDateTimeType | None = None
+    updated_by: str | None = None
+    updated_time: CustomUTCDateTimeType | None = None
+    starttime: CustomUTCDateTimeType | None = None
+    endtime: CustomUTCDateTimeType | None = None
+    network: str | None = None
+    station: str | None = None
+    channel: str | None = None
+    location: str | None = None
+    category: MetadataCategory | None = None
+    priority: int | None = 1
+    data_valid: bool | None = True
+    status: str | None = "new"
+    metadata: Dict[str, Any] = Field(default=None, alias="metadata_")
+    comment: str | None = None
+    review_comment: str | None = None
+
+
+class MetadataQuery(BaseModel):
+    id: int | None = None
+    category: MetadataCategory | None = None
+    starttime: CustomUTCDateTimeType | None = None
+    endtime: CustomUTCDateTimeType | None = None
+    created_after: CustomUTCDateTimeType | None = None
+    created_before: CustomUTCDateTimeType | None = None
+    network: str | None = None
+    station: str | None = None
+    channel: str | None = None
+    location: str | None = None
+    data_valid: bool | None = None
+    metadata: Dict[str, Any] | None = None
+    status: List[str] | None = None
+
+
+# this class validates the required id field
+class MetadataUpdate(BaseModel):
+    id: int
+    created_by: str | None = None
+    created_time: CustomUTCDateTimeType | None = None
+    updated_by: str | None = None
+    updated_time: CustomUTCDateTimeType | None = None
+    starttime: CustomUTCDateTimeType | None = None
+    endtime: CustomUTCDateTimeType | None = None
+    network: str | None = None
+    station: str | None = None
+    channel: str | None = None
+    location: str | None = None
+    category: MetadataCategory | None = None
+    priority: int | None = None
+    data_valid: bool | None = None
+    status: str | None = None
+    metadata: Dict[str, Any] = Field(
+        default=None,
+        alias="metadata_",
+        schema_extra={"serialization_alias": "metadata_"},
+    )
+    comment: str | None = None
+    review_comment: str | None = None
diff --git a/geomagio/api/db/models/metadata_history.py b/geomagio/api/db/models/metadata_history.py
new file mode 100644
index 0000000000000000000000000000000000000000..86eb9dd92f4d5ff4b9f9b7e53e7ed02dd4f00786
--- /dev/null
+++ b/geomagio/api/db/models/metadata_history.py
@@ -0,0 +1,70 @@
+import sqlalchemy_utc
+
+from datetime import datetime, timezone
+from typing import Dict, Any
+from sqlmodel import SQLModel, Field, Column, Text, JSON
+from pydantic import BaseModel
+
+from geomagio.pydantic_utcdatetime import CustomUTCDateTimeType
+from geomagio.api.db.models.metadata import MetadataCategory
+
+
+class MetadataHistoryTable(SQLModel, table=True):
+    __tablename__ = "metadata_history"
+
+    id: int | None = Field(primary_key=True)
+    metadata_id: int = Field(foreign_key="metadata.id")
+    created_by: str | None = Field(index=True)
+    created_time: CustomUTCDateTimeType | None = Field(
+        default=datetime.now(timezone.utc),
+        sa_column=Column(sqlalchemy_utc.UtcDateTime, index=True),
+    )
+    updated_by: str | None = Field(index=True)
+    updated_time: CustomUTCDateTimeType | None = Field(
+        sa_column=Column(sqlalchemy_utc.UtcDateTime, index=True)
+    )
+    starttime: CustomUTCDateTimeType | None = Field(
+        sa_column=Column(sqlalchemy_utc.UtcDateTime, index=True)
+    )
+    endtime: CustomUTCDateTimeType | None = Field(
+        sa_column=Column(sqlalchemy_utc.UtcDateTime, index=True)
+    )
+    network: str | None = None
+    station: str | None = None
+    channel: str | None = None
+    location: str | None = None
+    category: str | None = None
+    priority: int | None = Field(default=1)
+    data_valid: bool | None = Field(default=True, index=True)
+    status: str | None = None
+    metadata_: Dict[str, Any] | None = Field(
+        sa_column=Column("metadata", JSON),
+        alias="metadata",
+        schema_extra={"serialization_alias": "metadata"},
+    )
+    comment: str | None = Field(sa_column=Column(Text))
+    review_comment: str | None = Field(sa_column=Column(Text))
+
+
+class MetadataHistory(BaseModel):
+    id: int | None = None
+    metadata_id: int
+    created_by: str | None = None
+    created_time: CustomUTCDateTimeType | None = None
+    updated_by: str | None = None
+    updated_time: CustomUTCDateTimeType | None = None
+    starttime: CustomUTCDateTimeType | None = None
+    endtime: CustomUTCDateTimeType | None = None
+    network: str | None = None
+    station: str | None = None
+    channel: str | None = None
+    location: str | None = None
+    category: MetadataCategory | None = None
+    priority: int | None = None
+    data_valid: bool | None = None
+    status: str | None = None
+    metadata: Dict | None = Field(
+        alias="metadata_",
+    )
+    comment: str | None = None
+    review_comment: str | None = None
diff --git a/geomagio/api/db/models/session.py b/geomagio/api/db/models/session.py
new file mode 100644
index 0000000000000000000000000000000000000000..5b71910af196f117088177dcfdd6d4446e6523ab
--- /dev/null
+++ b/geomagio/api/db/models/session.py
@@ -0,0 +1,10 @@
+import sqlalchemy_utc
+
+from sqlmodel import SQLModel, Field, Column, String, Text
+
+
+class session(SQLModel, table=True):
+    id: int = Field(primary_key=True)
+    session_id: str = Field(sa_column=Column(String(100), index=True))
+    data: str = Field(sa_column=Column(Text))
+    updated: str = Field(sa_column=Column(sqlalchemy_utc.UtcDateTime, index=True))
diff --git a/geomagio/api/db/session_table.py b/geomagio/api/db/session_table.py
deleted file mode 100644
index 61f4b589a0217b5c6ade5f729f8deddd23a33a17..0000000000000000000000000000000000000000
--- a/geomagio/api/db/session_table.py
+++ /dev/null
@@ -1,50 +0,0 @@
-from datetime import datetime, timedelta, timezone
-
-import sqlalchemy
-import sqlalchemy_utc
-
-from .common import database, sqlalchemy_metadata
-
-
-session = sqlalchemy.Table(
-    "session",
-    sqlalchemy_metadata,
-    sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
-    sqlalchemy.Column("session_id", sqlalchemy.String(length=100), index=True),
-    sqlalchemy.Column("data", sqlalchemy.Text),
-    sqlalchemy.Column("updated", sqlalchemy_utc.UtcDateTime, index=True),
-)
-
-
-async def delete_session(session_id: str) -> None:
-    query = session.delete().where(session.c.session_id == session_id)
-    await database.execute(query)
-
-
-async def get_session(session_id: str) -> str:
-    query = session.select().where(session.c.session_id == session_id)
-    row = await database.fetch_one(query)
-    return row.data
-
-
-async def remove_expired_sessions(max_age: timedelta) -> None:
-    threshold = datetime.now(tz=timezone.utc) - max_age
-    query = session.delete().where(session.c.updated < threshold)
-    await database.execute(query)
-
-
-async def save_session(session_id: str, data: str) -> None:
-    updated = datetime.now(tz=timezone.utc)
-    # try update first
-    query = (
-        session.update()
-        .where(session.c.session_id == session_id)
-        .values(data=data, updated=updated)
-    )
-    count = await database.execute(query)
-    if count == 0:
-        # no matching session, insert
-        query = session.insert().values(
-            session_id=session_id, data=data, updated=updated
-        )
-        await database.execute(query)
diff --git a/geomagio/api/secure/SessionMiddleware.py b/geomagio/api/secure/SessionMiddleware.py
index 3d455bad5c24849b4d73984850957eeaf6d9c767..84715e7c2e6766234b8e00415d0cdfced8a67ef4 100644
--- a/geomagio/api/secure/SessionMiddleware.py
+++ b/geomagio/api/secure/SessionMiddleware.py
@@ -1,13 +1,13 @@
-import json
 import logging
 import uuid
-from typing import Callable, Dict, Mapping
 
-from cryptography.fernet import Fernet
 from starlette.datastructures import MutableHeaders
 from starlette.requests import HTTPConnection
 from starlette.types import ASGIApp, Message, Receive, Scope, Send
 
+from geomagio.api.db.database import engine
+from geomagio.api.db.factories.SessionDatabaseFactory import SessionDatabaseFactory
+
 
 class SessionMiddleware:
     """Based on Starlette SessionMiddleware.
@@ -22,11 +22,6 @@ class SessionMiddleware:
     ----------
     app: the ASGI application
 
-    delete_session_callback(session_id): callback to delete stored session data.
-    get_session_callback(session_id): callback to get stored session data.
-    save_session_callback(session_id): callback to update stored session data.
-    encryption: encrypt session data before storage if provided
-
     session_cookie: name of session cookie
     path: path for session cookie
     max_age: how long session cookies last
@@ -37,10 +32,6 @@ class SessionMiddleware:
     def __init__(
         self,
         app: ASGIApp,
-        delete_session_callback: Callable[[str], None],
-        get_session_callback: Callable[[str], str],
-        save_session_callback: Callable[[str, str], None],
-        encryption: Fernet = None,
         session_cookie: str = "session",
         path: str = "/",
         max_age: int = 14 * 24 * 60 * 60,  # 14 days, in seconds
@@ -48,10 +39,6 @@ class SessionMiddleware:
         https_only: bool = False,
     ) -> None:
         self.app = app
-        self.encryption = encryption
-        self.delete_session_callback = delete_session_callback
-        self.get_session_callback = get_session_callback
-        self.save_session_callback = save_session_callback
         self.session_cookie = session_cookie
         self.path = path
         self.max_age = max_age
@@ -71,10 +58,11 @@ class SessionMiddleware:
         if self.session_cookie in connection.cookies:
             session_id = connection.cookies[self.session_cookie]
             try:
-                scope["session"] = await self.get_session(session_id)
+                scope["session"] = await SessionDatabaseFactory(
+                    engine=engine
+                ).get_session(session_id=session_id)
                 initial_session_was_empty = False
             except Exception:
-                logging.exception(f"Error loading session {session_id}")
                 scope["session"] = {}
         else:
             scope["session"] = {}
@@ -85,31 +73,20 @@ class SessionMiddleware:
                 if scope["session"]:
                     session_id = session_id or uuid.uuid4().hex
                     # Persist session
-                    await self.save_session(session_id, scope["session"])
+                    await SessionDatabaseFactory(engine=engine).save_session(
+                        session_id=session_id, data=scope["session"]
+                    )
                     self.set_cookie(message=message, value=session_id)
                 elif not initial_session_was_empty:
                     # Clear session
-                    await self.delete_session(session_id)
+                    await SessionDatabaseFactory(engine=engine).delete_session(
+                        session_id=session_id
+                    )
                     self.set_cookie(message=message, value="null", max_age=-1)
             await send(message)
 
         await self.app(scope, receive, send_wrapper)
 
-    async def delete_session(self, session_id: str):
-        await self.delete_session_callback(session_id)
-
-    async def get_session(self, session_id: str) -> Dict:
-        data = await self.get_session_callback(session_id)
-        if self.encryption:
-            data = self.encryption.decrypt(data.encode("utf8"))
-        return json.loads(data)
-
-    async def save_session(self, session_id: str, data: Mapping):
-        data = json.dumps(data)
-        if self.encryption:
-            data = self.encryption.encrypt(data.encode("utf8")).decode("utf8")
-        await self.save_session_callback(session_id, data)
-
     def set_cookie(
         self,
         message: Message,
diff --git a/geomagio/api/secure/__init__.py b/geomagio/api/secure/__init__.py
index 6684194cefef11ec9bea87f6f72ab6b92e4bce2d..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/api/secure/__init__.py
+++ b/geomagio/api/secure/__init__.py
@@ -1,6 +0,0 @@
-"""Module with application for "/ws/secure" endpoints.
-"""
-
-from .app import app
-
-__all__ = ["app"]
diff --git a/geomagio/api/secure/encryption.py b/geomagio/api/secure/encryption.py
index 8f405df6f3a52782739f30a6a538d7658b80c8eb..30eb19f3cc1bc9266dc8be7e29626f4ce27689cf 100644
--- a/geomagio/api/secure/encryption.py
+++ b/geomagio/api/secure/encryption.py
@@ -1,11 +1,15 @@
+import os
 import base64
+
 from cryptography.fernet import Fernet
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives import hashes
 from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
 
 
-def generate_key(password: str, salt: str) -> str:
+def generate_key() -> str:
+    password = os.getenv("SECRET_KEY", "secret_key")
+    salt = os.getenv("SECRET_SALT", "secret_salt")
     kdf = PBKDF2HMAC(
         algorithm=hashes.SHA256(),
         length=32,
@@ -17,5 +21,5 @@ def generate_key(password: str, salt: str) -> str:
     return key
 
 
-def get_fernet(password: str, salt: str):
-    return Fernet(generate_key(password, salt))
+def get_fernet():
+    return Fernet(generate_key())
diff --git a/geomagio/api/secure/login.py b/geomagio/api/secure/login_routes.py
similarity index 100%
rename from geomagio/api/secure/login.py
rename to geomagio/api/secure/login_routes.py
diff --git a/geomagio/api/secure/metadata.py b/geomagio/api/secure/metadata_routes.py
similarity index 54%
rename from geomagio/api/secure/metadata.py
rename to geomagio/api/secure/metadata_routes.py
index ee5a8328aee818777b9dd86f58d1144943bd63c2..2fbe11c133e7c04ac5813aae308b82eb827af52e 100644
--- a/geomagio/api/secure/metadata.py
+++ b/geomagio/api/secure/metadata_routes.py
@@ -16,17 +16,24 @@ Configuration:
 import os
 from typing import List
 
-from fastapi import APIRouter, Body, Depends, Request, Response, Query
-from obspy import UTCDateTime
+from fastapi import APIRouter, Body, Depends, Response, Request, Query, HTTPException
 
-from ...metadata import Metadata, MetadataCategory, MetadataQuery
-from ..db.common import database
-from ..db import MetadataDatabaseFactory
-from .login import require_user, User
+from geomagio.api.db.models.metadata import (
+    Metadata,
+    MetadataUpdate,
+    MetadataCategory,
+    MetadataQuery,
+)
+from geomagio.api.db.models.metadata_history import MetadataHistory
+from geomagio.api.db.factories.MetadataDatabaseFactory import MetadataDatabaseFactory
+from geomagio.api.db.factories.MetadataHistoryDatabaseFactory import (
+    MetadataHistoryDatabaseFactory,
+)
+from geomagio.api.db.database import engine
+from .login_routes import require_user, User
 from ...pydantic_utcdatetime import CustomUTCDateTimeType
 
 
-# routes for login/logout
 router = APIRouter()
 
 
@@ -62,17 +69,15 @@ def get_metadata_query(
     "/metadata",
     description="Save metadata in database",
     response_model=Metadata,
+    status_code=201,
 )
 async def create_metadata(
     request: Request,
     metadata: Metadata,
     user: User = Depends(require_user()),
 ):
-    metadata = await MetadataDatabaseFactory(database=database).create_metadata(
-        meta=metadata
-    )
-    return Response(
-        metadata.model_dump_json(), status_code=201, media_type="application/json"
+    return await MetadataDatabaseFactory(engine=engine).create_metadata(
+        new_metadata=metadata
     )
 
 
@@ -82,21 +87,25 @@ async def create_metadata(
     name="Request metadata",
     response_model=List[Metadata],
 )
-async def get_metadata(query: MetadataQuery = Depends(get_metadata_query)):
-    metas = await MetadataDatabaseFactory(database=database).get_metadata(params=query)
-    return metas
+async def get_metadata(
+    # each field needs to be handled separately otherwise FastAPI thinks it should expect a body
+    query: MetadataQuery = Depends(get_metadata_query),
+):
+    return await MetadataDatabaseFactory(engine=engine).get_metadata(query=query)
 
 
 @router.get(
     "/metadata/history",
     description="Search historical metadata by query parameters",
-    response_model=List[Metadata],
+    response_model=List[MetadataHistory],
 )
-async def get_metadata_history(query: MetadataQuery = Depends(get_metadata_query)):
-    metas = await MetadataDatabaseFactory(database=database).get_metadata(
-        params=query, history=True
+async def get_metadata_history(
+    # each field needs to be handled separately otherwise FastAPI thinks it should expect a body
+    query: MetadataQuery = Depends(get_metadata_query),
+):
+    return await MetadataHistoryDatabaseFactory(engine=engine).get_metadata_history(
+        query=query
     )
-    return metas
 
 
 @router.get(
@@ -105,36 +114,43 @@ async def get_metadata_history(query: MetadataQuery = Depends(get_metadata_query
     response_model=Metadata,
 )
 async def get_metadata_by_id(id: int):
-    return await MetadataDatabaseFactory(database=database).get_metadata_by_id(id=id)
+    metadata = await MetadataDatabaseFactory(engine=engine).get_metadata_by_id(id=id)
+    if metadata is None:
+        raise HTTPException(status_code=404, detail="metadata not found")
+    else:
+        return metadata
 
 
 @router.get(
     "/metadata/{metadata_id}/history",
     description="Search metadata version history by database id",
-    response_model=List[Metadata],
 )
 async def get_metadata_history_by_metadata_id(
     metadata_id: int,
 ):
-    return await MetadataDatabaseFactory(
-        database=database
+    metadata_history = await MetadataHistoryDatabaseFactory(
+        engine=engine
     ).get_metadata_history_by_metadata_id(
         metadata_id=metadata_id,
     )
+    if metadata_history is None:
+        raise HTTPException(status_code=404, detail="metadata not found")
+    else:
+        return metadata_history
 
 
 @router.get(
     "/metadata/history/{id}",
     description="Search historical metadata by database id",
-    response_model=Metadata,
+    response_model=MetadataHistory,
 )
 async def get_metadata_history_by_id(id: int):
-    metadata = await MetadataDatabaseFactory(
-        database=database
+    metadata_history = await MetadataHistoryDatabaseFactory(
+        engine=engine
     ).get_metadata_history_by_id(id=id)
-    if metadata is None:
-        return Response(status_code=404)
-    return metadata
+    if metadata_history is None:
+        raise HTTPException(status_code=404, detail="metadata_history not found")
+    return metadata_history
 
 
 @router.put(
@@ -144,14 +160,18 @@ async def get_metadata_history_by_id(id: int):
 )
 async def update_metadata(
     id: int,
-    metadata: Metadata = Body(...),
+    metadata: MetadataUpdate,
     user: User = Depends(
         require_user(
             [os.getenv("REVIEWER_GROUP", "ghsc/geomag/operations/roles/reviewer")]
         )
     ),
 ):
-    return await MetadataDatabaseFactory(database=database).update_metadata(
-        meta=metadata,
-        updated_by=user.nickname,
-    )
+    try:
+        return await MetadataDatabaseFactory(engine=engine).update_metadata(
+            updated_metadata=metadata,
+            updated_by=user.nickname,
+        )
+    except Exception as e:
+        if str(e) == "metadata not found":
+            raise HTTPException(status_code=404, detail="metadata not found")
diff --git a/geomagio/api/secure/app.py b/geomagio/api/secure/secure_app.py
similarity index 53%
rename from geomagio/api/secure/app.py
rename to geomagio/api/secure/secure_app.py
index 79e3f8c6a66a79fe96afdecda79f816c6cc03e11..e8e52a3d8b9367a5eaa254a8f6652bb90dd4b98d 100644
--- a/geomagio/api/secure/app.py
+++ b/geomagio/api/secure/secure_app.py
@@ -1,14 +1,10 @@
 import json
-import os
-import uuid
 
-from fastapi import Depends, FastAPI, Request, Response
+from fastapi import FastAPI, Request, Response, Depends
 
-from ..db.session_table import delete_session, get_session, save_session
-from .encryption import get_fernet
-from .login import current_user, router as login_router, User
-from .metadata import router as metadata_router
-from .SessionMiddleware import SessionMiddleware
+from geomagio.api.secure.login_routes import current_user, router as login_router, User
+from geomagio.api.secure.metadata_routes import router as metadata_router
+from geomagio.api.secure.SessionMiddleware import SessionMiddleware
 
 
 app = FastAPI(
@@ -17,22 +13,12 @@ app = FastAPI(
     title="Geomagnetism Metadata Web Service",
 )
 
-# NOTE: database used for sessions is started by ..app.app,
-# which mounts this application at /ws/secure
 app.add_middleware(
-    middleware_class=SessionMiddleware,
-    delete_session_callback=delete_session,
-    get_session_callback=get_session,
-    save_session_callback=save_session,
-    encryption=get_fernet(
-        os.getenv("SECRET_KEY", uuid.uuid4().hex),
-        os.getenv("SECRET_SALT", "secret_salt"),
-    ),
+    SessionMiddleware,
     path="/ws/secure",
     session_cookie="PHPSESSID",
 )
 
-# include login routes to manage user
 app.include_router(login_router)
 app.include_router(metadata_router)
 
diff --git a/geomagio/api/ws/__init__.py b/geomagio/api/ws/__init__.py
index cd87bf0b86e36cdc340c2f81f47a61e217966af5..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/api/ws/__init__.py
+++ b/geomagio/api/ws/__init__.py
@@ -1,6 +0,0 @@
-"""Module with application for "/ws" endpoints.
-"""
-
-from .app import app
-
-__all__ = ["app"]
diff --git a/geomagio/api/ws/algorithms.py b/geomagio/api/ws/algorithms.py
index c4696a4c5323115c7f715eb325006ae1394cccf8..53fef06cb69e8205efd6d7e4bba0b426a2274987 100644
--- a/geomagio/api/ws/algorithms.py
+++ b/geomagio/api/ws/algorithms.py
@@ -4,11 +4,10 @@ from fastapi import APIRouter, Depends, HTTPException, Query
 from starlette.responses import Response
 
 
-from ...algorithm import DbDtAlgorithm
-from ...residual import (
-    calculate,
-    Reading,
-)
+from geomagio.algorithm.DbDtAlgorithm import DbDtAlgorithm
+from geomagio.residual.Calculation import calculate
+from geomagio.residual.Reading import Reading
+
 from .DataApiQuery import DataApiQuery
 from .FilterApiQuery import FilterApiQuery
 from .data import format_timeseries, get_data_factory, get_data_query, get_timeseries
diff --git a/geomagio/api/ws/metadata.py b/geomagio/api/ws/metadata.py
deleted file mode 100644
index cb265e4695c03125724a547992a220b2951e6c67..0000000000000000000000000000000000000000
--- a/geomagio/api/ws/metadata.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from typing import List
-
-from fastapi import APIRouter, Query
-from obspy import UTCDateTime
-
-from ...metadata import Metadata, MetadataCategory, MetadataQuery
-from ..db.common import database
-from ..db import MetadataDatabaseFactory
-from ...pydantic_utcdatetime import CustomUTCDateTimeType
-
-
-router = APIRouter()
-
-
-@router.get(
-    "/metadata",
-    description="Search metadata records with query parameters(excludes id and metadata id)",
-    response_model=List[Metadata],
-)
-async def get_metadata(
-    category: MetadataCategory = None,
-    starttime: CustomUTCDateTimeType = None,
-    endtime: CustomUTCDateTimeType = None,
-    network: str = None,
-    station: str = None,
-    channel: str = None,
-    location: str = None,
-    data_valid: bool = None,
-    status: List[str] = Query(None),
-):
-    query = MetadataQuery(
-        category=category,
-        starttime=starttime,
-        endtime=endtime,
-        network=network,
-        station=station,
-        channel=channel,
-        location=location,
-        data_valid=data_valid,
-        status=status,
-    )
-    metas = await MetadataDatabaseFactory(database=database).get_metadata(
-        **query.datetime_dict(exclude={"id", "metadata_id"})
-    )
-    return metas
diff --git a/geomagio/api/ws/app.py b/geomagio/api/ws/ws_app.py
similarity index 96%
rename from geomagio/api/ws/app.py
rename to geomagio/api/ws/ws_app.py
index 3018c39e1fc2510ed6be18394538245db8ab48a9..85a5cab3286305251934a2631adaf8b1adae2fc3 100644
--- a/geomagio/api/ws/app.py
+++ b/geomagio/api/ws/ws_app.py
@@ -5,7 +5,7 @@ from fastapi.exceptions import RequestValidationError
 from fastapi.responses import JSONResponse, PlainTextResponse, RedirectResponse
 from obspy import UTCDateTime
 
-from . import algorithms, data, elements, metadata, observatories, variometers
+from . import algorithms, data, elements, observatories, variometers
 
 
 ERROR_CODE_MESSAGES = {
@@ -39,9 +39,6 @@ app.include_router(elements.router)
 app.include_router(observatories.router)
 app.include_router(variometers.router)
 
-if METADATA_ENDPOINT:
-    app.include_router(metadata.router)
-
 
 @app.middleware("http")
 async def add_headers(request: Request, call_next):
diff --git a/geomagio/binlog/__init__.py b/geomagio/binlog/__init__.py
index 5928f63f9c89336564b18342e10e1a3ce477644b..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/binlog/__init__.py
+++ b/geomagio/binlog/__init__.py
@@ -1,11 +0,0 @@
-"""IO Module for BinLog Format
-"""
-
-from __future__ import absolute_import
-
-from .BinLogFactory import BinLogFactory
-from .StreamBinLogFactory import StreamBinLogFactory
-from .BinLogWriter import BinLogWriter
-
-
-__all__ = ["BinLogFactory", "StreamBinLogFactory", "BinLogWriter"]
diff --git a/geomagio/covjson/__init__.py b/geomagio/covjson/__init__.py
index 11211bd390c2c419a5bc84e363059e5376d5c703..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/covjson/__init__.py
+++ b/geomagio/covjson/__init__.py
@@ -1,7 +0,0 @@
-"""IO Module for CovJSON Format"""
-
-from __future__ import absolute_import
-
-from .CovJSONFactory import CovJSONFactory
-
-__all__ = ["CovJSONFactory"]
diff --git a/geomagio/edge/__init__.py b/geomagio/edge/__init__.py
index e8a4117e3743f8d424246750a2f1f84eeeb41aba..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/edge/__init__.py
+++ b/geomagio/edge/__init__.py
@@ -1,30 +0,0 @@
-"""IO Module for Edge Format
-"""
-
-from __future__ import absolute_import
-
-from .EdgeFactory import EdgeFactory
-from .IRISFactory import IRISFactory
-from .FDSNFactory import FDSNFactory
-from .LocationCode import LocationCode
-from .MiniSeedFactory import MiniSeedFactory
-from .MiniSeedInputClient import MiniSeedInputClient
-from .RawInputClient import RawInputClient
-from .IRISSNCL import IRISSNCL
-from .FDSNSNCL import FDSNSNCL
-from .SNCL import SNCL
-from .LegacySNCL import LegacySNCL
-
-__all__ = [
-    "EdgeFactory",
-    "IRISFactory",
-    "FDSNFactory",
-    "LocationCode",
-    "MiniSeedFactory",
-    "MiniSeedInputClient",
-    "RawInputClient",
-    "IRISSNCL",
-    "FDSNSNCL",
-    "SNCL",
-    "LegacySNCL",
-]
diff --git a/geomagio/iaga2002/__init__.py b/geomagio/iaga2002/__init__.py
index 0a9617effa3de9b4fac7a0bdf81db913f1b26bc8..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/iaga2002/__init__.py
+++ b/geomagio/iaga2002/__init__.py
@@ -1,20 +0,0 @@
-"""IO Module for IAGA 2002 Format
-
-Based on documentation at:
-  https://www.ngdc.noaa.gov/IAGA/vdat/IAGA2002/iaga2002format.html
-"""
-
-from __future__ import absolute_import
-
-from .IAGA2002Factory import IAGA2002Factory
-from .StreamIAGA2002Factory import StreamIAGA2002Factory
-from .IAGA2002Parser import IAGA2002Parser
-from .IAGA2002Writer import IAGA2002Writer
-
-
-__all__ = [
-    "IAGA2002Factory",
-    "StreamIAGA2002Factory",
-    "IAGA2002Parser",
-    "IAGA2002Writer",
-]
diff --git a/geomagio/imagcdf/__init__.py b/geomagio/imagcdf/__init__.py
index e7611ab4cdd29eb6c6eea8426606b749ed3675ec..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/imagcdf/__init__.py
+++ b/geomagio/imagcdf/__init__.py
@@ -1,10 +0,0 @@
-"""IO Module for ImagCDF Format
-"""
-
-from __future__ import absolute_import
-
-from .ImagCDFFactory import ImagCDFFactory
-from .IMCDFPublication import IMCDFPublicationLevel
-
-
-__all__ = ["ImagCDFFactory", "IMCDFPublicationLevel"]
diff --git a/geomagio/imfjson/__init__.py b/geomagio/imfjson/__init__.py
index 8cec78d55922a7234c9180ebba7913d6986e300d..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/imfjson/__init__.py
+++ b/geomagio/imfjson/__init__.py
@@ -1,10 +0,0 @@
-"""IO Module for IMFSONFactory Format
-"""
-
-from __future__ import absolute_import
-
-from .IMFJSONFactory import IMFJSONFactory
-from .IMFJSONWriter import IMFJSONWriter
-
-
-__all__ = ["IMFJSONWriter", "IMFJSONFactory"]
diff --git a/geomagio/imfv122/__init__.py b/geomagio/imfv122/__init__.py
index dc6129d4f7ebc04018e77e57f264882b85db4a3c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/imfv122/__init__.py
+++ b/geomagio/imfv122/__init__.py
@@ -1,14 +0,0 @@
-"""IO Module for IMFV122 Format
-
-Based on documentation at:
-  http://www.intermagnet.org/data-donnee/formats/imfv122-eng.php
-"""
-
-from __future__ import absolute_import
-
-from .IMFV122Factory import IMFV122Factory
-from .IMFV122Parser import IMFV122Parser
-from .StreamIMFV122Factory import StreamIMFV122Factory
-
-
-__all__ = ["IMFV122Factory", "IMFV122Parser", "StreamIMFV122Factory"]
diff --git a/geomagio/imfv283/__init__.py b/geomagio/imfv283/__init__.py
index 9a38dcfdaff4e20f6e96374ed1c521b45f2d142d..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/imfv283/__init__.py
+++ b/geomagio/imfv283/__init__.py
@@ -1,20 +0,0 @@
-"""IO Module for IMFV283Factory Format
-
-Based on documentation at:
-  http://http://www.intermagnet.org/data-donnee/formats/imfv283e-eng.php
-"""
-
-from __future__ import absolute_import
-
-from .GOESIMFV283Factory import GOESIMFV283Factory
-from .IMFV283Factory import IMFV283Factory
-from .StreamIMFV283Factory import StreamIMFV283Factory
-from .IMFV283Parser import IMFV283Parser
-
-
-__all__ = [
-    "GOESIMFV283Factory",
-    "IMFV283Factory",
-    "StreamIMFV283Factory",
-    "IMFV283Parser",
-]
diff --git a/geomagio/metadata/Metadata.py b/geomagio/metadata/Metadata.py
deleted file mode 100644
index bfc6a35f9cd3a6a3b9fdbfb04085a0d0ce691127..0000000000000000000000000000000000000000
--- a/geomagio/metadata/Metadata.py
+++ /dev/null
@@ -1,111 +0,0 @@
-from datetime import timezone
-from typing import Dict, Optional
-
-from obspy import UTCDateTime
-from pydantic import field_validator, field_serializer, BaseModel, Field
-
-from .MetadataCategory import MetadataCategory
-from ..pydantic_utcdatetime import CustomUTCDateTimeType
-
-
-class Metadata(BaseModel):
-    """
-    This class is used for Data flagging and other Metadata.
-
-    Flag example:
-    ```
-    automatic_flag = Metadata(
-        created_by = 'algorithm/version',
-        start_time = UTCDateTime('2020-01-02T00:17:00.1Z'),
-        end_time = UTCDateTime('2020-01-02T00:17:00.1Z'),
-        network = 'NT',
-        station = 'BOU',
-        channel = 'BEU',
-        category = MetadataCategory.FLAG,
-        comment = "spike detected",
-        priority = 1,
-        data_valid = False)
-    ```
-
-    Adjusted Matrix example:
-    ```
-    adjusted_matrix = Metadata(
-        created_by = 'algorithm/version',
-        start_time = UTCDateTime('2020-01-02T00:17:00Z'),
-        end_time = None,
-        network = 'NT',
-        station = 'BOU',
-        category = MetadataCategory.ADJUSTED_MATRIX,
-        comment = 'automatic adjusted matrix',
-        priority = 1,
-        value = {
-            'parameters': {'x': 1, 'y': 2, 'z': 3}
-            'matrix': [ ... ]
-        }
-    )
-    ```
-    """
-
-    # database id
-    id: Optional[int] = None
-    # metadata history id referencing database id
-    metadata_id: Optional[int] = None
-    # author
-    created_by: Optional[str] = None
-    created_time: Optional[CustomUTCDateTimeType] = None
-    # editor
-    updated_by: Optional[str] = None
-    updated_time: Optional[CustomUTCDateTimeType] = None
-    # time range
-    starttime: Optional[CustomUTCDateTimeType] = None
-    endtime: Optional[CustomUTCDateTimeType] = None
-    # what data metadata references, null for wildcard
-    network: Optional[str] = None
-    station: Optional[str] = None
-    channel: Optional[str] = None
-    location: Optional[str] = None
-    # category (flag, matrix, etc)
-    category: Optional[MetadataCategory] = None
-    # higher priority overrides lower priority
-    priority: Optional[int] = 1
-    # whether data is valid (primarily for flags)
-    data_valid: Optional[bool] = True
-    # metadata json blob
-    metadata: Optional[Dict] = None
-    # general comment
-    comment: Optional[str] = None
-    # review specific comment
-    review_comment: Optional[str] = None
-    # metadata status indicator
-    status: Optional[str] = None
-
-    # instructions for model_dump() to serialize pydantic CustomUTCDateTimeType into aware datetime.datetime type
-    # sqlalchemy is expecting aware datetime.datetime, not the string model_dump() creates by default
-    @field_serializer("created_time")
-    def serialize_created_time(self, created_time: UTCDateTime):
-        if created_time is not None:
-            created_time = created_time.datetime.replace(tzinfo=timezone.utc)
-        return created_time
-
-    @field_serializer("updated_time")
-    def serialize_updated_time(self, updated_time: UTCDateTime):
-        if updated_time is not None:
-            updated_time = updated_time.datetime.replace(tzinfo=timezone.utc)
-        return updated_time
-
-    @field_serializer("starttime")
-    def serialize_starttime(self, starttime: UTCDateTime):
-        if starttime is not None:
-            starttime = starttime.datetime.replace(tzinfo=timezone.utc)
-        return starttime
-
-    @field_serializer("endtime")
-    def serialize_endtime(self, endtime: UTCDateTime):
-        if endtime is not None:
-            endtime = endtime.datetime.replace(tzinfo=timezone.utc)
-        return endtime
-
-    @field_validator("created_time")
-    @classmethod
-    def set_default_created_time(cls, created_time: UTCDateTime = None) -> UTCDateTime:
-        return created_time or UTCDateTime()
diff --git a/geomagio/metadata/MetadataCategory.py b/geomagio/metadata/MetadataCategory.py
deleted file mode 100644
index bb33eda18439ff3d7233a3e96af6b2a55fe91bc1..0000000000000000000000000000000000000000
--- a/geomagio/metadata/MetadataCategory.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from enum import Enum
-
-
-# known category values as enumeration
-class MetadataCategory(str, Enum):
-    ADJUSTED_MATRIX = "adjusted-matrix"
-    FLAG = "flag"
-    INSTRUMENT = "instrument"
-    OBSERVATORY = "observatory"
-    READING = "reading"
diff --git a/geomagio/metadata/MetadataFactory.py b/geomagio/metadata/MetadataFactory.py
index 3e3dce3f7af86a0a434ff5755c4ee85354ee1279..f8efd5a270be044c3fb87492916db2ee0d5681a8 100644
--- a/geomagio/metadata/MetadataFactory.py
+++ b/geomagio/metadata/MetadataFactory.py
@@ -7,8 +7,7 @@ import json
 from obspy import UTCDateTime
 from pydantic import TypeAdapter
 
-from .Metadata import Metadata
-from .MetadataQuery import MetadataQuery
+from geomagio.api.db.models.metadata import Metadata, MetadataQuery
 
 # Force IPV4 in requests package
 requests.packages.urllib3.util.connection.HAS_IPV6 = False
diff --git a/geomagio/metadata/MetadataQuery.py b/geomagio/metadata/MetadataQuery.py
deleted file mode 100644
index fbf74a008d6d9573c73b850d83bb802010cddd44..0000000000000000000000000000000000000000
--- a/geomagio/metadata/MetadataQuery.py
+++ /dev/null
@@ -1,50 +0,0 @@
-from datetime import timezone
-
-from obspy import UTCDateTime
-from pydantic import field_serializer, BaseModel
-from typing import List, Optional, Dict, Any
-
-from .MetadataCategory import MetadataCategory
-from ..pydantic_utcdatetime import CustomUTCDateTimeType
-
-
-class MetadataQuery(BaseModel):
-    id: Optional[int] = None
-    category: Optional[MetadataCategory] = None
-    starttime: Optional[CustomUTCDateTimeType] = None
-    endtime: Optional[CustomUTCDateTimeType] = None
-    created_after: Optional[CustomUTCDateTimeType] = None
-    created_before: Optional[CustomUTCDateTimeType] = None
-    network: Optional[str] = None
-    station: Optional[str] = None
-    channel: Optional[str] = None
-    location: Optional[str] = None
-    data_valid: Optional[bool] = None
-    metadata: Optional[Dict[str, Any]] = None
-    status: Optional[List[str]] = None
-
-    # instructions for model_dump() to serialize pydantic CustomUTCDateTimeType into aware datetime.datetime type
-    # sqlalchemy is expecting aware datetime.datetime, not the string model_dump() creates by default
-    @field_serializer("created_after")
-    def serialize_created_after(self, created_after: UTCDateTime):
-        if created_after is not None:
-            created_after = created_after.datetime.replace(tzinfo=timezone.utc)
-        return created_after
-
-    @field_serializer("created_before")
-    def serialize_created_before(self, created_before: UTCDateTime):
-        if created_before is not None:
-            created_before = created_before.datetime.replace(tzinfo=timezone.utc)
-        return created_before
-
-    @field_serializer("starttime")
-    def serialize_starttime(self, starttime: UTCDateTime):
-        if starttime is not None:
-            starttime = starttime.datetime.replace(tzinfo=timezone.utc)
-        return starttime
-
-    @field_serializer("endtime")
-    def serialize_endtime(self, endtime: UTCDateTime):
-        if endtime is not None:
-            endtime = endtime.datetime.replace(tzinfo=timezone.utc)
-        return endtime
diff --git a/geomagio/metadata/__init__.py b/geomagio/metadata/__init__.py
index 4a348bb4793b409f7cb9afb1be8261731341922a..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/metadata/__init__.py
+++ b/geomagio/metadata/__init__.py
@@ -1,13 +0,0 @@
-from .Metadata import Metadata
-from .MetadataCategory import MetadataCategory
-from .MetadataFactory import MetadataFactory, GEOMAG_API_URL
-from .MetadataQuery import MetadataQuery
-
-
-__all__ = [
-    "GEOMAG_API_URL",
-    "Metadata",
-    "MetadataCategory",
-    "MetadataFactory",
-    "MetadataQuery",
-]
diff --git a/geomagio/metadata/instrument/InstrumentCalibrations.py b/geomagio/metadata/instrument/InstrumentCalibrations.py
index 7a2374cd8d018b489b4db632b4b68397b6fc5ac8..c016e1d4e8002f5783773aa06d1e7620fec8baba 100644
--- a/geomagio/metadata/instrument/InstrumentCalibrations.py
+++ b/geomagio/metadata/instrument/InstrumentCalibrations.py
@@ -2,7 +2,8 @@ from obspy import UTCDateTime
 
 from requests.exceptions import JSONDecodeError, ConnectTimeout
 
-from geomagio.metadata import Metadata, MetadataFactory, MetadataCategory
+from geomagio.api.db.models.metadata import Metadata, MetadataCategory, MetadataQuery
+from geomagio.metadata.MetadataFactory import MetadataFactory
 from geomagio.Util import write_state_file, read_state_file
 
 
@@ -320,11 +321,10 @@ def get_instrument_calibrations(
     if not calibrations:
         state_filename = f"{observatory}_instrument_cals.json"
         metadata = []
-
         factory = MetadataFactory(
             url=metadata_url or "https://geomag.usgs.gov/ws/secure/metadata",
         )
-        query = Metadata(
+        query = MetadataQuery(
             category=MetadataCategory.INSTRUMENT,
             starttime=start_time,
             endtime=end_time,
@@ -333,9 +333,10 @@ def get_instrument_calibrations(
         )
         try:
             metadata = factory.get_metadata(query=query, timeout=28)
-        except:
+        except Exception as e:
             print(
-                "Warning: An error occurred while trying to pull metadata from the metadata server!"
+                "Warning: An error occurred while trying to pull metadata from the metadata server: e",
+                e,
             )
 
         if not metadata:
diff --git a/geomagio/metadata/instrument/__init__.py b/geomagio/metadata/instrument/__init__.py
index c470473d1dcbd0aab8e8985c965267b10585d82f..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/metadata/instrument/__init__.py
+++ b/geomagio/metadata/instrument/__init__.py
@@ -1,6 +0,0 @@
-from .InstrumentCalibrations import InstrumentCalibrations
-
-
-__all__ = [
-    "InstrumentCalibrations",
-]
diff --git a/geomagio/metadata/main.py b/geomagio/metadata/main.py
index cf1cd9adca76d198c839739beae0d80bae37d836..9304c1d6a7269f15c002f8e72e992277b6bbf88d 100644
--- a/geomagio/metadata/main.py
+++ b/geomagio/metadata/main.py
@@ -1,15 +1,13 @@
 import sys
 import json
 import os
-from typing import Dict, List, Optional
+import typer
 
+from typing import Dict, List, Optional
 from obspy import UTCDateTime
-import typer
 
-from .Metadata import Metadata
-from .MetadataCategory import MetadataCategory
+from geomagio.api.db.models.metadata import Metadata, MetadataCategory, MetadataQuery
 from .MetadataFactory import MetadataFactory
-from .MetadataQuery import MetadataQuery
 
 
 GEOMAG_API_HOST = os.getenv("GEOMAG_API_HOST", "geomag.usgs.gov")
diff --git a/geomagio/netcdf/NetCDFFactory.py b/geomagio/netcdf/NetCDFFactory.py
index 3a92d580e495879690a6eea008383d2d7176bd93..21eff7d5a23831f6fe33a78dc7bc045ac231f406 100644
--- a/geomagio/netcdf/NetCDFFactory.py
+++ b/geomagio/netcdf/NetCDFFactory.py
@@ -1,11 +1,12 @@
 import netCDF4
 import numpy as np
-from obspy import Stream, Trace, UTCDateTime
-from datetime import datetime, timezone
 import tempfile
 import shutil
 import os
 
+from obspy import Stream, Trace, UTCDateTime
+from datetime import datetime, timezone
+
 from geomagio import ChannelConverter
 from geomagio.TimeseriesFactory import TimeseriesFactory
 from geomagio.api.ws.Element import ELEMENT_INDEX
diff --git a/geomagio/netcdf/__init__.py b/geomagio/netcdf/__init__.py
index f2ccd864e41c948fc29ac6050d2961232284dc1e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/netcdf/__init__.py
+++ b/geomagio/netcdf/__init__.py
@@ -1,8 +0,0 @@
-"""IO Module for NetCDF Format
-"""
-
-from __future__ import absolute_import
-
-from .NetCDFFactory import NetCDFFactory
-
-__all__ = ["NetCDFFactory"]
diff --git a/geomagio/pcdcp/__init__.py b/geomagio/pcdcp/__init__.py
index 308618e29aca59a353638e5f2d3537645bdccbde..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/pcdcp/__init__.py
+++ b/geomagio/pcdcp/__init__.py
@@ -1,18 +0,0 @@
-"""IO Module for PCDCP Format
-"""
-
-from __future__ import absolute_import
-
-from .PCDCPFactory import PCDCPFactory, PCDCP_FILE_PATTERN
-from .StreamPCDCPFactory import StreamPCDCPFactory
-from .PCDCPParser import PCDCPParser
-from .PCDCPWriter import PCDCPWriter
-
-
-__all__ = [
-    "PCDCPFactory",
-    "PCDCP_FILE_PATTERN",
-    "PCDCPParser",
-    "PCDCPWriter",
-    "StreamPCDCPFactory",
-]
diff --git a/geomagio/processing/__init__.py b/geomagio/processing/__init__.py
index a31e13c3b30dec782b9d94936a0d9ac45413d5d1..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/processing/__init__.py
+++ b/geomagio/processing/__init__.py
@@ -1,20 +0,0 @@
-"""Package with near-real time processing configurations.
-
-Note that these implementations are subject to change,
-and should be considered less stable than other packages in the library.
-"""
-
-from .factory import get_edge_factory, get_miniseed_factory
-from .derived import adjusted, average, sqdist_minute
-from .filters import minute_filter, second_filter
-
-
-__all__ = [
-    "adjusted",
-    "average",
-    "get_edge_factory",
-    "get_miniseed_factory",
-    "minute_filter",
-    "second_filter",
-    "sqdist_minute",
-]
diff --git a/geomagio/residual/__init__.py b/geomagio/residual/__init__.py
index ec2de5e8adb9e2855ba2675a368f5a358f525154..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/residual/__init__.py
+++ b/geomagio/residual/__init__.py
@@ -1,46 +0,0 @@
-# residual module
-from __future__ import absolute_import
-
-from . import Angle
-from .Absolute import Absolute
-from .Calculation import (
-    calculate,
-    calculate_D_absolute,
-    calculate_HZ_absolutes,
-    calculate_I,
-    calculate_scale_value,
-)
-from .CalFileFactory import CalFileFactory
-from .Measurement import Measurement, AverageMeasurement, average_measurement
-from .MeasurementType import (
-    MeasurementType,
-    DECLINATION_TYPES,
-    INCLINATION_TYPES,
-    MARK_TYPES,
-)
-from .Reading import Reading
-from .SpreadsheetAbsolutesFactory import SpreadsheetAbsolutesFactory
-from .SpreadsheetSummaryFactory import SpreadsheetSummaryFactory
-from .WebAbsolutesFactory import WebAbsolutesFactory
-
-__all__ = [
-    "Absolute",
-    "Angle",
-    "AverageMeasurement",
-    "average_measurement",
-    "CalFileFactory",
-    "calculate",
-    "calculate_D_absolute",
-    "calculate_HZ_absolutes",
-    "calculate_I",
-    "calculate_scale_value",
-    "DECLINATION_TYPES",
-    "INCLINATION_TYPES",
-    "MARK_TYPES",
-    "Measurement",
-    "MeasurementType",
-    "Reading",
-    "SpreadsheetAbsolutesFactory",
-    "SpreadsheetSummaryFactory",
-    "WebAbsolutesFactory",
-]
diff --git a/geomagio/temperature/__init__.py b/geomagio/temperature/__init__.py
index 7b64a57f7d29694a55d73b8dab306356d83efe08..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/temperature/__init__.py
+++ b/geomagio/temperature/__init__.py
@@ -1,11 +0,0 @@
-"""IO Module for TEMP Format
-"""
-
-from __future__ import absolute_import
-
-from .TEMPFactory import TEMPFactory
-from .StreamTEMPFactory import StreamTEMPFactory
-from .TEMPWriter import TEMPWriter
-
-
-__all__ = ["TEMPFactory", "StreamTEMPFactory", "TEMPWriter"]
diff --git a/geomagio/vbf/__init__.py b/geomagio/vbf/__init__.py
index f0b694de1daf3294404f77f659df4eefe998bece..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/vbf/__init__.py
+++ b/geomagio/vbf/__init__.py
@@ -1,11 +0,0 @@
-"""IO Module for VBF Format
-"""
-
-from __future__ import absolute_import
-
-from .VBFFactory import VBFFactory
-from .StreamVBFFactory import StreamVBFFactory
-from .VBFWriter import VBFWriter
-
-
-__all__ = ["VBFFactory", "StreamVBFFactory", "VBFWriter"]
diff --git a/geomagio/xml/__init__.py b/geomagio/xml/__init__.py
index 5253c8cabd56cc790afe6ee2c97aa845145321bf..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/geomagio/xml/__init__.py
+++ b/geomagio/xml/__init__.py
@@ -1,8 +0,0 @@
-"""IO Module for Edge Format
-"""
-
-from __future__ import absolute_import
-
-from .XMLFactory import XMLFactory
-
-__all__ = ["XMLFactory"]
diff --git a/localdev/docker-compose.yml b/localdev/docker-compose.yml
index d68cee8c99cf01a018fdae066b256b769254b083..4957687ffceae803e0a5be9834c19e29fef24fb2 100644
--- a/localdev/docker-compose.yml
+++ b/localdev/docker-compose.yml
@@ -34,7 +34,7 @@ services:
       - DATA_HOST=host.docker.internal
       - OPENID_CLIENT_ID=$OPENID_CLIENT_ID
       - OPENID_CLIENT_SECRET=$OPENID_CLIENT_SECRET
-      - DATABASE_URL=mysql://root:password@host.docker.internal:3306/geomag_operations
+      - DATABASE_URL=mysql+pymysql://root:password@host.docker.internal:3306/geomag_operations
       - OPENID_METADATA_URL=https://code.usgs.gov/.well-known/openid-configuration
       - SECRET_KEY=$SECRET_KEY
       - SECRET_SALT=$SECRET_SALT
diff --git a/migrations/versions/2280fe551e60_initialize_database.py b/migrations/versions/2280fe551e60_initialize_database.py
index 0e2fe978e5db08b35986f2f7dd1973f60f7df46a..89d25141a0744a42f6ca281b1b53f2210889fc6c 100644
--- a/migrations/versions/2280fe551e60_initialize_database.py
+++ b/migrations/versions/2280fe551e60_initialize_database.py
@@ -7,8 +7,9 @@ Create Date: 2021-04-22 13:06:28.852803
 """
 
 from alembic import op
+from sqlmodel import SQLModel
 
-from geomagio.api.db.create import create_db
+from geomagio.api.db.database import engine
 
 
 # revision identifiers, used by Alembic.
@@ -19,7 +20,7 @@ depends_on = None
 
 
 def upgrade():
-    create_db()
+    SQLModel.metadata.create_all(engine)
 
 
 def downgrade():
diff --git a/poetry.lock b/poetry.lock
index f97cf23eb9060488612f02f1131b9ce886eba4cb..b50927e308a179cf7a5a3a688685a357695e0d83 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,59 +1,23 @@
-# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
-
-[[package]]
-name = "aiomysql"
-version = "0.2.0"
-description = "MySQL driver for asyncio."
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "aiomysql-0.2.0-py3-none-any.whl", hash = "sha256:b7c26da0daf23a5ec5e0b133c03d20657276e4eae9b73e040b72787f6f6ade0a"},
-    {file = "aiomysql-0.2.0.tar.gz", hash = "sha256:558b9c26d580d08b8c5fd1be23c5231ce3aeff2dadad989540fee740253deb67"},
-]
-
-[package.dependencies]
-PyMySQL = ">=1.0"
-
-[package.extras]
-rsa = ["PyMySQL[rsa] (>=1.0)"]
-sa = ["sqlalchemy (>=1.3,<1.4)"]
-
-[[package]]
-name = "aiosqlite"
-version = "0.21.0"
-description = "asyncio bridge to the standard sqlite3 module"
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0"},
-    {file = "aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3"},
-]
-
-[package.dependencies]
-typing_extensions = ">=4.0"
-
-[package.extras]
-dev = ["attribution (==1.7.1)", "black (==24.3.0)", "build (>=1.2)", "coverage[toml] (==7.6.10)", "flake8 (==7.0.0)", "flake8-bugbear (==24.12.12)", "flit (==3.10.1)", "mypy (==1.14.1)", "ufmt (==2.5.1)", "usort (==1.0.8.post1)"]
-docs = ["sphinx (==8.1.3)", "sphinx-mdinclude (==0.6.1)"]
+# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
 
 [[package]]
 name = "alembic"
-version = "1.14.1"
+version = "1.15.1"
 description = "A database migration tool for SQLAlchemy."
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "alembic-1.14.1-py3-none-any.whl", hash = "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5"},
-    {file = "alembic-1.14.1.tar.gz", hash = "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213"},
+    {file = "alembic-1.15.1-py3-none-any.whl", hash = "sha256:197de710da4b3e91cf66a826a5b31b5d59a127ab41bd0fc42863e2902ce2bbbe"},
+    {file = "alembic-1.15.1.tar.gz", hash = "sha256:e1a1c738577bca1f27e68728c910cd389b9a92152ff91d902da649c192e30c49"},
 ]
 
 [package.dependencies]
 Mako = "*"
-SQLAlchemy = ">=1.3.0"
-typing-extensions = ">=4"
+SQLAlchemy = ">=1.4.0"
+typing-extensions = ">=4.12"
 
 [package.extras]
-tz = ["backports.zoneinfo", "tzdata"]
+tz = ["tzdata"]
 
 [[package]]
 name = "annotated-types"
@@ -68,13 +32,13 @@ files = [
 
 [[package]]
 name = "anyio"
-version = "4.8.0"
+version = "4.9.0"
 description = "High level compatibility layer for multiple asynchronous event loop implementations"
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"},
-    {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"},
+    {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"},
+    {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"},
 ]
 
 [package.dependencies]
@@ -84,19 +48,19 @@ sniffio = ">=1.1"
 typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
 
 [package.extras]
-doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
-test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
+doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
+test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
 trio = ["trio (>=0.26.1)"]
 
 [[package]]
 name = "authlib"
-version = "1.4.1"
+version = "1.5.1"
 description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients."
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "Authlib-1.4.1-py2.py3-none-any.whl", hash = "sha256:edc29c3f6a3e72cd9e9f45fff67fc663a2c364022eb0371c003f22d5405915c1"},
-    {file = "authlib-1.4.1.tar.gz", hash = "sha256:30ead9ea4993cdbab821dc6e01e818362f92da290c04c7f6a1940f86507a790d"},
+    {file = "authlib-1.5.1-py2.py3-none-any.whl", hash = "sha256:8408861cbd9b4ea2ff759b00b6f02fd7d81ac5a56d0b2b22c08606c6049aae11"},
+    {file = "authlib-1.5.1.tar.gz", hash = "sha256:5cbc85ecb0667312c1cdc2f9095680bb735883b123fb509fde1e65b1c5df972e"},
 ]
 
 [package.dependencies]
@@ -520,74 +484,74 @@ test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist"
 
 [[package]]
 name = "coverage"
-version = "7.6.12"
+version = "7.7.0"
 description = "Code coverage measurement for Python"
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"},
-    {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"},
-    {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"},
-    {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"},
-    {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"},
-    {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"},
-    {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"},
-    {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"},
-    {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"},
-    {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"},
-    {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"},
-    {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"},
-    {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"},
-    {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"},
-    {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"},
-    {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"},
-    {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"},
-    {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"},
-    {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"},
-    {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"},
-    {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"},
-    {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"},
-    {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"},
-    {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"},
-    {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"},
-    {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"},
-    {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"},
-    {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"},
-    {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"},
-    {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"},
-    {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"},
-    {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"},
-    {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"},
-    {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"},
-    {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"},
-    {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"},
-    {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"},
-    {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"},
-    {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"},
-    {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"},
-    {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"},
-    {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"},
-    {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"},
-    {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"},
-    {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"},
-    {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"},
-    {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"},
-    {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"},
-    {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"},
-    {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"},
-    {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"},
-    {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"},
-    {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"},
-    {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"},
-    {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"},
-    {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"},
-    {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"},
-    {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"},
-    {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"},
-    {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"},
-    {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"},
-    {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"},
-    {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"},
+    {file = "coverage-7.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a538a23119d1e2e2ce077e902d02ea3d8e0641786ef6e0faf11ce82324743944"},
+    {file = "coverage-7.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1586ad158523f4133499a4f322b230e2cfef9cc724820dbd58595a5a236186f4"},
+    {file = "coverage-7.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b6c96d69928a3a6767fab8dc1ce8a02cf0156836ccb1e820c7f45a423570d98"},
+    {file = "coverage-7.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f18d47641282664276977c604b5a261e51fefc2980f5271d547d706b06a837f"},
+    {file = "coverage-7.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a1e18a85bd066c7c556d85277a7adf4651f259b2579113844835ba1a74aafd"},
+    {file = "coverage-7.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:70f0925c4e2bfc965369f417e7cc72538fd1ba91639cf1e4ef4b1a6b50439b3b"},
+    {file = "coverage-7.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b0fac2088ec4aaeb5468b814bd3ff5e5978364bfbce5e567c44c9e2854469f6c"},
+    {file = "coverage-7.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3e212a894d8ae07fde2ca8b43d666a6d49bbbddb10da0f6a74ca7bd31f20054"},
+    {file = "coverage-7.7.0-cp310-cp310-win32.whl", hash = "sha256:f32b165bf6dfea0846a9c9c38b7e1d68f313956d60a15cde5d1709fddcaf3bee"},
+    {file = "coverage-7.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:a2454b12a3f12cc4698f3508912e6225ec63682e2ca5a96f80a2b93cef9e63f3"},
+    {file = "coverage-7.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a0a207c87a9f743c8072d059b4711f8d13c456eb42dac778a7d2e5d4f3c253a7"},
+    {file = "coverage-7.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d673e3add00048215c2cc507f1228a7523fd8bf34f279ac98334c9b07bd2656"},
+    {file = "coverage-7.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f81fe93dc1b8e5673f33443c0786c14b77e36f1025973b85e07c70353e46882b"},
+    {file = "coverage-7.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8c7524779003d59948c51b4fcbf1ca4e27c26a7d75984f63488f3625c328b9b"},
+    {file = "coverage-7.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c124025430249118d018dcedc8b7426f39373527c845093132196f2a483b6dd"},
+    {file = "coverage-7.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f559c36d5cdc448ee13e7e56ed7b6b5d44a40a511d584d388a0f5d940977ba"},
+    {file = "coverage-7.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:37cbc7b0d93dfd133e33c7ec01123fbb90401dce174c3b6661d8d36fb1e30608"},
+    {file = "coverage-7.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7d2a65876274acf544703e943c010b60bd79404e3623a1e5d52b64a6e2728de5"},
+    {file = "coverage-7.7.0-cp311-cp311-win32.whl", hash = "sha256:f5a2f71d6a91238e7628f23538c26aa464d390cbdedf12ee2a7a0fb92a24482a"},
+    {file = "coverage-7.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:ae8006772c6b0fa53c33747913473e064985dac4d65f77fd2fdc6474e7cd54e4"},
+    {file = "coverage-7.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:056d3017ed67e7ddf266e6f57378ece543755a4c9231e997789ab3bd11392c94"},
+    {file = "coverage-7.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33c1394d8407e2771547583b66a85d07ed441ff8fae5a4adb4237ad39ece60db"},
+    {file = "coverage-7.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fbb7a0c3c21908520149d7751cf5b74eb9b38b54d62997b1e9b3ac19a8ee2fe"},
+    {file = "coverage-7.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb356e7ae7c2da13f404bf8f75be90f743c6df8d4607022e759f5d7d89fe83f8"},
+    {file = "coverage-7.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce730d484038e97f27ea2dbe5d392ec5c2261f28c319a3bb266f6b213650135"},
+    {file = "coverage-7.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa4dff57fc21a575672176d5ab0ef15a927199e775c5e8a3d75162ab2b0c7705"},
+    {file = "coverage-7.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b667b91f4f714b17af2a18e220015c941d1cf8b07c17f2160033dbe1e64149f0"},
+    {file = "coverage-7.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:693d921621a0c8043bfdc61f7d4df5ea6d22165fe8b807cac21eb80dd94e4bbd"},
+    {file = "coverage-7.7.0-cp312-cp312-win32.whl", hash = "sha256:52fc89602cde411a4196c8c6894afb384f2125f34c031774f82a4f2608c59d7d"},
+    {file = "coverage-7.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ce8cf59e09d31a4915ff4c3b94c6514af4c84b22c4cc8ad7c3c546a86150a92"},
+    {file = "coverage-7.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4545485fef7a8a2d8f30e6f79ce719eb154aab7e44217eb444c1d38239af2072"},
+    {file = "coverage-7.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1393e5aa9441dafb0162c36c8506c648b89aea9565b31f6bfa351e66c11bcd82"},
+    {file = "coverage-7.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:316f29cc3392fa3912493ee4c83afa4a0e2db04ff69600711f8c03997c39baaa"},
+    {file = "coverage-7.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1ffde1d6bc2a92f9c9207d1ad808550873748ac2d4d923c815b866baa343b3f"},
+    {file = "coverage-7.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:416e2a8845eaff288f97eaf76ab40367deafb9073ffc47bf2a583f26b05e5265"},
+    {file = "coverage-7.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5efdeff5f353ed3352c04e6b318ab05c6ce9249c25ed3c2090c6e9cadda1e3b2"},
+    {file = "coverage-7.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:57f3bd0d29bf2bd9325c0ff9cc532a175110c4bf8f412c05b2405fd35745266d"},
+    {file = "coverage-7.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ab7090f04b12dc6469882ce81244572779d3a4b67eea1c96fb9ecc8c607ef39"},
+    {file = "coverage-7.7.0-cp313-cp313-win32.whl", hash = "sha256:180e3fc68ee4dc5af8b33b6ca4e3bb8aa1abe25eedcb958ba5cff7123071af68"},
+    {file = "coverage-7.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:55143aa13c49491f5606f05b49ed88663446dce3a4d3c5d77baa4e36a16d3573"},
+    {file = "coverage-7.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc41374d2f27d81d6558f8a24e5c114580ffefc197fd43eabd7058182f743322"},
+    {file = "coverage-7.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:89078312f06237417adda7c021c33f80f7a6d2db8572a5f6c330d89b080061ce"},
+    {file = "coverage-7.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b2f144444879363ea8834cd7b6869d79ac796cb8f864b0cfdde50296cd95816"},
+    {file = "coverage-7.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60e6347d1ed882b1159ffea172cb8466ee46c665af4ca397edbf10ff53e9ffaf"},
+    {file = "coverage-7.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb203c0afffaf1a8f5b9659a013f8f16a1b2cad3a80a8733ceedc968c0cf4c57"},
+    {file = "coverage-7.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ad0edaa97cb983d9f2ff48cadddc3e1fb09f24aa558abeb4dc9a0dbacd12cbb4"},
+    {file = "coverage-7.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c5f8a5364fc37b2f172c26a038bc7ec4885f429de4a05fc10fdcb53fb5834c5c"},
+    {file = "coverage-7.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4e09534037933bf6eb31d804e72c52ec23219b32c1730f9152feabbd7499463"},
+    {file = "coverage-7.7.0-cp313-cp313t-win32.whl", hash = "sha256:1b336d06af14f8da5b1f391e8dec03634daf54dfcb4d1c4fb6d04c09d83cef90"},
+    {file = "coverage-7.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b54a1ee4c6f1905a436cbaa04b26626d27925a41cbc3a337e2d3ff7038187f07"},
+    {file = "coverage-7.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c8fbce80b2b8bf135d105aa8f5b36eae0c57d702a1cc3ebdea2a6f03f6cdde5"},
+    {file = "coverage-7.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d9710521f07f526de30ccdead67e6b236fe996d214e1a7fba8b36e2ba2cd8261"},
+    {file = "coverage-7.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7789e700f33f2b133adae582c9f437523cd5db8de845774988a58c360fc88253"},
+    {file = "coverage-7.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c36093aca722db73633cf2359026ed7782a239eb1c6db2abcff876012dc4cf"},
+    {file = "coverage-7.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c075d167a6ec99b798c1fdf6e391a1d5a2d054caffe9593ba0f97e3df2c04f0e"},
+    {file = "coverage-7.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d013c07061751ae81861cae6ec3a4fe04e84781b11fd4b6b4201590234b25c7b"},
+    {file = "coverage-7.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:104bf640f408f4e115b85110047c7f27377e1a8b7ba86f7db4fa47aa49dc9a8e"},
+    {file = "coverage-7.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:39abcacd1ed54e2c33c54bdc488b310e8ef6705833f7148b6eb9a547199d375d"},
+    {file = "coverage-7.7.0-cp39-cp39-win32.whl", hash = "sha256:8e336b56301774ace6be0017ff85c3566c556d938359b61b840796a0202f805c"},
+    {file = "coverage-7.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:8c938c6ae59be67ac19a7204e079efc94b38222cd7d0269f96e45e18cddeaa59"},
+    {file = "coverage-7.7.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:3b0e6e54591ae0d7427def8a4d40fca99df6b899d10354bab73cd5609807261c"},
+    {file = "coverage-7.7.0-py3-none-any.whl", hash = "sha256:708f0a1105ef2b11c79ed54ed31f17e6325ac936501fc373f24be3e6a578146a"},
+    {file = "coverage-7.7.0.tar.gz", hash = "sha256:cd879d4646055a573775a1cec863d00c9ff8c55860f8b17f6d8eee9140c06166"},
 ]
 
 [package.dependencies]
@@ -598,42 +562,46 @@ toml = ["tomli"]
 
 [[package]]
 name = "cryptography"
-version = "44.0.1"
+version = "44.0.2"
 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
 optional = false
 python-versions = "!=3.9.0,!=3.9.1,>=3.7"
 files = [
-    {file = "cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009"},
-    {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f"},
-    {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2"},
-    {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911"},
-    {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69"},
-    {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026"},
-    {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd"},
-    {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0"},
-    {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf"},
-    {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864"},
-    {file = "cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a"},
-    {file = "cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00"},
-    {file = "cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008"},
-    {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862"},
-    {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3"},
-    {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7"},
-    {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a"},
-    {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c"},
-    {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62"},
-    {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41"},
-    {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b"},
-    {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7"},
-    {file = "cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9"},
-    {file = "cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f"},
-    {file = "cryptography-44.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183"},
-    {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12"},
-    {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83"},
-    {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420"},
-    {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4"},
-    {file = "cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7"},
-    {file = "cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14"},
+    {file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"},
+    {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1"},
+    {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb"},
+    {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843"},
+    {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5"},
+    {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c"},
+    {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a"},
+    {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308"},
+    {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688"},
+    {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7"},
+    {file = "cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79"},
+    {file = "cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa"},
+    {file = "cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3"},
+    {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639"},
+    {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd"},
+    {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181"},
+    {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea"},
+    {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699"},
+    {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9"},
+    {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23"},
+    {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922"},
+    {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4"},
+    {file = "cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5"},
+    {file = "cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6"},
+    {file = "cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb"},
+    {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41"},
+    {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562"},
+    {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5"},
+    {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa"},
+    {file = "cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d"},
+    {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d"},
+    {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471"},
+    {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615"},
+    {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390"},
+    {file = "cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0"},
 ]
 
 [package.dependencies]
@@ -646,7 +614,7 @@ nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"]
 pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
 sdist = ["build (>=1.0.0)"]
 ssh = ["bcrypt (>=3.1.5)"]
-test = ["certifi (>=2024)", "cryptography-vectors (==44.0.1)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
+test = ["certifi (>=2024)", "cryptography-vectors (==44.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
 test-randomorder = ["pytest-randomly"]
 
 [[package]]
@@ -678,41 +646,15 @@ files = [
 [package.extras]
 dev = ["black", "flake8", "flake8-pyi", "matplotlib", "mypy (==0.770)", "numpy", "pandas", "pytest"]
 
-[[package]]
-name = "databases"
-version = "0.6.2"
-description = "Async database support for Python."
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "databases-0.6.2-py3-none-any.whl", hash = "sha256:ff4010136ac2bb9da2322a2ffda4ef9185ae1c365e5891e52924dd9499d33dc4"},
-    {file = "databases-0.6.2.tar.gz", hash = "sha256:b09c370ad7c2f64c7f4316c096e265dc2e28304732639889272390decda2f893"},
-]
-
-[package.dependencies]
-aiomysql = {version = "*", optional = true, markers = "extra == \"mysql\""}
-aiosqlite = {version = "*", optional = true, markers = "extra == \"sqlite\""}
-sqlalchemy = ">=1.4,<=1.4.41"
-
-[package.extras]
-aiomysql = ["aiomysql"]
-aiopg = ["aiopg"]
-aiosqlite = ["aiosqlite"]
-asyncmy = ["asyncmy"]
-asyncpg = ["asyncpg"]
-mysql = ["aiomysql"]
-postgresql = ["asyncpg"]
-sqlite = ["aiosqlite"]
-
 [[package]]
 name = "decorator"
-version = "5.1.1"
+version = "5.2.1"
 description = "Decorators for Humans"
 optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.8"
 files = [
-    {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
-    {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
+    {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"},
+    {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"},
 ]
 
 [[package]]
@@ -763,18 +705,18 @@ test = ["pytest (>=6)"]
 
 [[package]]
 name = "fastapi"
-version = "0.115.8"
+version = "0.115.11"
 description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"},
-    {file = "fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9"},
+    {file = "fastapi-0.115.11-py3-none-any.whl", hash = "sha256:32e1541b7b74602e4ef4a0260ecaf3aadf9d4f19590bba3e1bf2ac4666aa2c64"},
+    {file = "fastapi-0.115.11.tar.gz", hash = "sha256:cc81f03f688678b92600a65a5e618b93592c65005db37157147204d8924bf94f"},
 ]
 
 [package.dependencies]
 pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
-starlette = ">=0.40.0,<0.46.0"
+starlette = ">=0.40.0,<0.47.0"
 typing-extensions = ">=4.8.0"
 
 [package.extras]
@@ -1712,27 +1654,24 @@ files = [
 
 [[package]]
 name = "obspy"
-version = "1.4.1"
+version = "1.4.0"
 description = "ObsPy - a Python framework for seismological observatories."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "obspy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d2e53224654711e32a6f808d6a7343a5277cba7ad423b94417874c6f3e80f7"},
-    {file = "obspy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09e0faecb206231d7395e975371ac66f00d564e2fea3e957274181b8d1c06548"},
-    {file = "obspy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:2355082a07b2539005364e12fe6a3ed030c7da6a4ddad11e6031eb0d549e80e4"},
-    {file = "obspy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2275962307055bfd0d6c875b12edb810a731f9ea4943e0c9d4c2eb26d45ada39"},
-    {file = "obspy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:affc60e6c93fc9d66907dab07e7e7261135e173b36324fe2dc89731adf75cf4d"},
-    {file = "obspy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:3176fdda86968478a6e9d9ffa8a3bb979356fbee29b58396cb0490b7eed02fbb"},
-    {file = "obspy-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f133ce01caa0910cc445f83c7cafa23881ae8c3dd536e0b8f07adb4cf2cc40d"},
-    {file = "obspy-1.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20fcbeb061d03f17b73235518f3372f400058245fe9a7ecfa325eb166d263076"},
-    {file = "obspy-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:eeb01ffc7b15ffc028155128a060a8fdb191cb63e3ae3b7392ddc4b0951f134b"},
-    {file = "obspy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2de4b92baf97e20792c27a0e3663219512b3048dd9acc329370c196241c3b93a"},
-    {file = "obspy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c8befbcf01f230e2102794629d1e4b43fa3950f2830d81dab62ac0f8a819abf"},
-    {file = "obspy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:05256048d3df93936992370fdd6f1d6c1e55fbb52604e47481a42a0dde1d77c0"},
-    {file = "obspy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9ac3eb3e106bfa9669c9bcfa1e57b17b7444e3d4cfb7c399ec57e919703d7bfd"},
-    {file = "obspy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:407922924b88c922f2ed411cbe33ef5bbfb657919d6cabb445eb49b12ec3185e"},
-    {file = "obspy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:770b8318f0f7c3245fcd3aac645140a170dffae984c046ede7958e8865a613f1"},
-    {file = "obspy-1.4.1.tar.gz", hash = "sha256:9cf37b0ce03de43d80398703c006bfddbd709f32e8460a9404b27df998d3f747"},
+    {file = "obspy-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab33aef9cb16059e229b5a64205c0f40a982056c2a680f78ad732207347d4308"},
+    {file = "obspy-1.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c60fc347efe27b04aa4528a873f11d9d62c3708a93b4d4b7a2aeb5751b798b2d"},
+    {file = "obspy-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:307a2f6293b29d325e67c3cd80c4eef5df8e7aa1c87154c531f373d6f3c6682d"},
+    {file = "obspy-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86321501112b2ab0de2deaf1d8229fda6d09654bd9a4cbb36d06bbbd0d6387a2"},
+    {file = "obspy-1.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9de41809bd391f36b763e4eada2dab398de9af35b0c1c3c643a042943ee2956"},
+    {file = "obspy-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:e8b2f9c93f1321af851505a65e881b090d68c0974c03cb3b19d74bb361c56e52"},
+    {file = "obspy-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ea33704863b4859a7264255f1b6f520cde002f975d9af7dcbfdf506a563c52"},
+    {file = "obspy-1.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbc566a2da7d3db4faa17662f25796274e1c55a715a2e6f480e95469776da0b"},
+    {file = "obspy-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:a6baa3610d40442da25d619776568d5a4e48530a42c63a1eb391c91a420baffe"},
+    {file = "obspy-1.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d70488ae7d9eb9ef4173e2b7ef9a535cbeede9776fdf8d7be185b3381da570bf"},
+    {file = "obspy-1.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84a87c2cf01728262020b7836c3e66ef231de0739c3c3bb288fbf7510bec6eae"},
+    {file = "obspy-1.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:2090a95b08b214575892c3d99bb3362b13a3b0f4689d4ee55f95ea4d8a2cbc26"},
+    {file = "obspy-1.4.0.tar.gz", hash = "sha256:336a6e1d9a485732b08173cb5dc1dd720a8e53f3b54c180a62bb8ceaa5fe5c06"},
 ]
 
 [package.dependencies]
@@ -1743,7 +1682,7 @@ numpy = ">=1.20"
 requests = "*"
 scipy = ">=1.7"
 setuptools = "*"
-sqlalchemy = "<2"
+sqlalchemy = "*"
 
 [package.extras]
 all = ["cartopy", "geographiclib", "packaging", "pyproj", "pyshp", "pytest", "pytest-json-report"]
@@ -1965,59 +1904,59 @@ files = [
 
 [[package]]
 name = "pycurl"
-version = "7.45.4"
+version = "7.45.6"
 description = "PycURL -- A Python Interface To The cURL library"
 optional = true
 python-versions = ">=3.5"
 files = [
-    {file = "pycurl-7.45.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:247b4af8eab7d04137a7f1a98391930e04ea93dc669b64db5625070fe15f80a3"},
-    {file = "pycurl-7.45.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:561f88697f7540634b1c750146f37bdc0da367b15f6b4ab2bb780871ee6ab005"},
-    {file = "pycurl-7.45.4-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b485fdaf78553f0b8e1c2803bb7dcbe47a7b47594f846fc7e9d3b94d794cfc89"},
-    {file = "pycurl-7.45.4-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e7ae49b88a5d57485fbabef004534225dfe04dc15716a61fae1a0c7f46f2279e"},
-    {file = "pycurl-7.45.4-cp310-cp310-win32.whl", hash = "sha256:d14f954ecd21a070038d65ef1c6d1d3ab220f952ff703d48313123222097615c"},
-    {file = "pycurl-7.45.4-cp310-cp310-win_amd64.whl", hash = "sha256:2548c3291a33c821f0f80bf9989fc43b5d90fb78b534a7015c8419b83c6f5803"},
-    {file = "pycurl-7.45.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6c0e22052946bbfa25be67f9d1d6639eff10781c89f0cf6f3ff2099273d1bad"},
-    {file = "pycurl-7.45.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acf25cfdaf914db21a2a6e9e274b6d95e3fa2b6018c38f2c58c94b5d8ac3d1b7"},
-    {file = "pycurl-7.45.4-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a39f28f031885485325034918386be352036c220ca45625c7e286d3938eb579d"},
-    {file = "pycurl-7.45.4-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:9940e3234c1ca3d30f27a2202d325dbc25291605c98e9585100a351cacd935e8"},
-    {file = "pycurl-7.45.4-cp311-cp311-win32.whl", hash = "sha256:ffd3262f98b8997ad04940061d5ebd8bab2362169b9440939c397e24a4a135b0"},
-    {file = "pycurl-7.45.4-cp311-cp311-win_amd64.whl", hash = "sha256:1324a859b50bdb0abdbd5620e42f74240d0b7daf2d5925fa303695d9fc3ece18"},
-    {file = "pycurl-7.45.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:731c46e7c0acffaab19f7c2ecc3d9e7ee337500e87b260b4e0b9fae2d90fa133"},
-    {file = "pycurl-7.45.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13eb1643ab0bf4fdc539a2cdf1021029b07095d3196c5cee5a4271af268d3d31"},
-    {file = "pycurl-7.45.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:df5f94c051c5a163fa85064559ca94979575e2da26740ff91c078c50c541c465"},
-    {file = "pycurl-7.45.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:688d09ba2c6a0d4a749d192c43422839d73c40c85143c50cc65c944258fe0ba8"},
-    {file = "pycurl-7.45.4-cp312-cp312-win32.whl", hash = "sha256:236600bfe2cd72efe47333add621286667e8fa027dadf1247349afbf30333e95"},
-    {file = "pycurl-7.45.4-cp312-cp312-win_amd64.whl", hash = "sha256:26745c6c5ebdccfe8a828ac3fd4e6da6f5d2245696604f04529eb7894a02f4db"},
-    {file = "pycurl-7.45.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9bd493ce598f1dc76c8e50043c47debec27c583fa313a836b2d3667640f875d5"},
-    {file = "pycurl-7.45.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4f25d52c97dbca6ebea786f0961b49c1998fa05178abf1964a977c825b3d8ae6"},
-    {file = "pycurl-7.45.4-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:13c4b18f44637859f34639493efd297a08670f45e4eec34ab2dcba724e3cb5fc"},
-    {file = "pycurl-7.45.4-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0470bff6cc24d8c2f63c80931aa239463800871609dafc6bcc9ca10f5a12a04e"},
-    {file = "pycurl-7.45.4-cp313-cp313-win32.whl", hash = "sha256:3452459668bd01d646385482362b021834a31c036aa1c02acd88924ddeff7d0d"},
-    {file = "pycurl-7.45.4-cp313-cp313-win_amd64.whl", hash = "sha256:fd167f73d34beb0cb8064334aee76d9bdd13167b30be6d5d36fb07d0c8223b71"},
-    {file = "pycurl-7.45.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b0e38e3eb83b0c891f391853f798fc6a97cb5a86a4a731df0b6320e539ae54ae"},
-    {file = "pycurl-7.45.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d192a48b3cec2e13ad432196b65c22e99620db92feae39c0476635354eff68c6"},
-    {file = "pycurl-7.45.4-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:57971d7215fc6fdedcfc092f880a59f04f52fcaf2fd329151b931623d7b59a9c"},
-    {file = "pycurl-7.45.4-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:73df3eb5940a7fbf4cf62f7271e9f23a8e9f80e352c838ee9a8448a70c01d3f5"},
-    {file = "pycurl-7.45.4-cp39-cp39-win32.whl", hash = "sha256:587a4891039803b5f48392066f97b7cd5e7e9a166187abb5cb4b4806fdb8fbef"},
-    {file = "pycurl-7.45.4-cp39-cp39-win_amd64.whl", hash = "sha256:caec8b634763351dd4e1b729a71542b1e2de885d39710ba8e7202817a381b453"},
-    {file = "pycurl-7.45.4.tar.gz", hash = "sha256:32c8e237069273f4260b6ae13d1e0f99daae938977016021565dc6e11050e803"},
+    {file = "pycurl-7.45.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c31b390f1e2cd4525828f1bb78c1f825c0aab5d1588228ed71b22c4784bdb593"},
+    {file = "pycurl-7.45.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:942b352b69184cb26920db48e0c5cb95af39874b57dbe27318e60f1e68564e37"},
+    {file = "pycurl-7.45.6-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3441ee77e830267aa6e2bb43b29fd5f8a6bd6122010c76a6f0bf84462e9ea9c7"},
+    {file = "pycurl-7.45.6-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:2a21e13278d7553a04b421676c458449f6c10509bebf04993f35154b06ee2b20"},
+    {file = "pycurl-7.45.6-cp310-cp310-win32.whl", hash = "sha256:d0b5501d527901369aba307354530050f56cd102410f2a3bacd192dc12c645e3"},
+    {file = "pycurl-7.45.6-cp310-cp310-win_amd64.whl", hash = "sha256:abe1b204a2f96f2eebeaf93411f03505b46d151ef6d9d89326e6dece7b3a008a"},
+    {file = "pycurl-7.45.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f57ad26d6ab390391ad5030790e3f1a831c1ee54ad3bf969eb378f5957eeb0a"},
+    {file = "pycurl-7.45.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6fd295f03c928da33a00f56c91765195155d2ac6f12878f6e467830b5dce5f5"},
+    {file = "pycurl-7.45.6-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:334721ce1ccd71ff8e405470768b3d221b4393570ccc493fcbdbef4cd62e91ed"},
+    {file = "pycurl-7.45.6-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:0cd6b7794268c17f3c660162ed6381769ce0ad260331ef49191418dfc3a2d61a"},
+    {file = "pycurl-7.45.6-cp311-cp311-win32.whl", hash = "sha256:357ea634395310085b9d5116226ac5ec218a6ceebf367c2451ebc8d63a6e9939"},
+    {file = "pycurl-7.45.6-cp311-cp311-win_amd64.whl", hash = "sha256:878ae64484db18f8f10ba99bffc83fefb4fe8f5686448754f93ec32fa4e4ee93"},
+    {file = "pycurl-7.45.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c872d4074360964697c39c1544fe8c91bfecbff27c1cdda1fee5498e5fdadcda"},
+    {file = "pycurl-7.45.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56d1197eadd5774582b259cde4364357da71542758d8e917f91cc6ed7ed5b262"},
+    {file = "pycurl-7.45.6-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8a99e56d2575aa74c48c0cd08852a65d5fc952798f76a34236256d5589bf5aa0"},
+    {file = "pycurl-7.45.6-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c04230b9e9cfdca9cf3eb09a0bec6cf2f084640f1f1ca1929cca51411af85de2"},
+    {file = "pycurl-7.45.6-cp312-cp312-win32.whl", hash = "sha256:ae893144b82d72d95c932ebdeb81fc7e9fde758e5ecd5dd10ad5b67f34a8b8ee"},
+    {file = "pycurl-7.45.6-cp312-cp312-win_amd64.whl", hash = "sha256:56f841b6f2f7a8b2d3051b9ceebd478599dbea3c8d1de8fb9333c895d0c1eea5"},
+    {file = "pycurl-7.45.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c09b7180799af70fc1d4eed580cfb1b9f34fda9081f73a3e3bc9a0e5a4c0e9b"},
+    {file = "pycurl-7.45.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:361bf94b2a057c7290f9ab84e935793ca515121fc012f4b6bef6c3b5e4ea4397"},
+    {file = "pycurl-7.45.6-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bb9eff0c7794af972da769a887c87729f1bcd8869297b1c01a2732febbb75876"},
+    {file = "pycurl-7.45.6-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:26839d43dc7fff6b80e0067f185cc1d0e9be2ae6e2e2361ae8488cead5901c04"},
+    {file = "pycurl-7.45.6-cp313-cp313-win32.whl", hash = "sha256:a721c2696a71b1aa5ecf82e6d0ade64bc7211b7317f1c9c66e82f82e2264d8b4"},
+    {file = "pycurl-7.45.6-cp313-cp313-win_amd64.whl", hash = "sha256:f0198ebcda8686b3a0c66d490a687fa5fd466f8ecc2f20a0ed0931579538ae3d"},
+    {file = "pycurl-7.45.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a554a2813d415a7bb9a996a6298f3829f57e987635dcab9f1197b2dccd0ab3b2"},
+    {file = "pycurl-7.45.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f721e3394e5bd7079802ec1819b19c5be4842012268cc45afcb3884efb31cf0"},
+    {file = "pycurl-7.45.6-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:81005c0f681d31d5af694d1d3c18bbf1bed0bc8b2bb10fb7388cb1378ba9bd6a"},
+    {file = "pycurl-7.45.6-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:3fc0b505c37c7c54d88ced27e1d9e3241130987c24bf1611d9bbd9a3e499e07c"},
+    {file = "pycurl-7.45.6-cp39-cp39-win32.whl", hash = "sha256:1309fc0f558a80ca444a3a5b0bdb1572a4d72b195233f0e65413b4d4dd78809b"},
+    {file = "pycurl-7.45.6-cp39-cp39-win_amd64.whl", hash = "sha256:2d1a49418b8b4c61f52e06d97b9c16142b425077bd997a123a2ba9ef82553203"},
+    {file = "pycurl-7.45.6.tar.gz", hash = "sha256:2b73e66b22719ea48ac08a93fc88e57ef36d46d03cb09d972063c9aa86bb74e6"},
 ]
 
 [[package]]
 name = "pydantic"
-version = "2.9.2"
+version = "2.10.6"
 description = "Data validation using Python type hints"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"},
-    {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"},
+    {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"},
+    {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"},
 ]
 
 [package.dependencies]
 annotated-types = ">=0.6.0"
-pydantic-core = "2.23.4"
-typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""}
+pydantic-core = "2.27.2"
+typing-extensions = ">=4.12.2"
 
 [package.extras]
 email = ["email-validator (>=2.0.0)"]
@@ -2025,100 +1964,111 @@ timezone = ["tzdata"]
 
 [[package]]
 name = "pydantic-core"
-version = "2.23.4"
+version = "2.27.2"
 description = "Core functionality for Pydantic validation and serialization"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"},
-    {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"},
-    {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"},
-    {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"},
-    {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"},
-    {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"},
-    {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"},
-    {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"},
-    {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"},
-    {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"},
-    {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"},
-    {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"},
-    {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"},
-    {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"},
-    {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"},
-    {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"},
-    {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"},
-    {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"},
-    {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"},
-    {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"},
-    {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"},
-    {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"},
-    {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"},
-    {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"},
-    {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"},
-    {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"},
-    {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"},
-    {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"},
-    {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"},
-    {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"},
-    {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"},
-    {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"},
-    {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"},
-    {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"},
-    {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"},
-    {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"},
-    {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"},
-    {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"},
-    {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"},
-    {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"},
-    {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"},
-    {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"},
-    {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"},
-    {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"},
-    {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"},
-    {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"},
-    {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"},
-    {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"},
-    {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"},
-    {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"},
-    {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"},
-    {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"},
-    {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"},
-    {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"},
-    {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"},
-    {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"},
-    {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"},
-    {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"},
-    {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"},
-    {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"},
-    {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"},
-    {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"},
-    {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"},
-    {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"},
-    {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"},
-    {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"},
-    {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"},
-    {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"},
-    {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"},
-    {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"},
-    {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"},
-    {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"},
-    {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"},
-    {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"},
-    {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"},
-    {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"},
-    {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"},
-    {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"},
-    {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"},
-    {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"},
-    {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"},
-    {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"},
-    {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"},
-    {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"},
-    {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"},
-    {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"},
-    {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"},
-    {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"},
-    {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"},
+    {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"},
+    {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"},
+    {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"},
+    {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"},
+    {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"},
+    {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"},
+    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"},
+    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"},
+    {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"},
 ]
 
 [package.dependencies]
@@ -2155,13 +2105,13 @@ diagrams = ["jinja2", "railroad-diagrams"]
 
 [[package]]
 name = "pytest"
-version = "8.3.4"
+version = "8.3.5"
 description = "pytest: simple powerful testing with Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
-    {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
+    {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"},
+    {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"},
 ]
 
 [package.dependencies]
@@ -2479,13 +2429,13 @@ test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "po
 
 [[package]]
 name = "setuptools"
-version = "75.8.0"
+version = "76.1.0"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"},
-    {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"},
+    {file = "setuptools-76.1.0-py3-none-any.whl", hash = "sha256:34750dcb17d046929f545dec9b8349fe42bf4ba13ddffee78428aec422dbfb73"},
+    {file = "setuptools-76.1.0.tar.gz", hash = "sha256:4959b9ad482ada2ba2320c8f1a8d8481d4d8d668908a7a1b84d987375cd7f5bd"},
 ]
 
 [package.extras]
@@ -2521,77 +2471,98 @@ files = [
 
 [[package]]
 name = "sqlalchemy"
-version = "1.4.41"
+version = "2.0.39"
 description = "Database Abstraction Library"
 optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
-files = [
-    {file = "SQLAlchemy-1.4.41-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:13e397a9371ecd25573a7b90bd037db604331cf403f5318038c46ee44908c44d"},
-    {file = "SQLAlchemy-1.4.41-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2d6495f84c4fd11584f34e62f9feec81bf373787b3942270487074e35cbe5330"},
-    {file = "SQLAlchemy-1.4.41-cp27-cp27m-win32.whl", hash = "sha256:e570cfc40a29d6ad46c9aeaddbdcee687880940a3a327f2c668dd0e4ef0a441d"},
-    {file = "SQLAlchemy-1.4.41-cp27-cp27m-win_amd64.whl", hash = "sha256:5facb7fd6fa8a7353bbe88b95695e555338fb038ad19ceb29c82d94f62775a05"},
-    {file = "SQLAlchemy-1.4.41-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f37fa70d95658763254941ddd30ecb23fc4ec0c5a788a7c21034fc2305dab7cc"},
-    {file = "SQLAlchemy-1.4.41-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:361f6b5e3f659e3c56ea3518cf85fbdae1b9e788ade0219a67eeaaea8a4e4d2a"},
-    {file = "SQLAlchemy-1.4.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0990932f7cca97fece8017414f57fdd80db506a045869d7ddf2dda1d7cf69ecc"},
-    {file = "SQLAlchemy-1.4.41-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cd767cf5d7252b1c88fcfb58426a32d7bd14a7e4942497e15b68ff5d822b41ad"},
-    {file = "SQLAlchemy-1.4.41-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5102fb9ee2c258a2218281adcb3e1918b793c51d6c2b4666ce38c35101bb940e"},
-    {file = "SQLAlchemy-1.4.41-cp310-cp310-win32.whl", hash = "sha256:2082a2d2fca363a3ce21cfa3d068c5a1ce4bf720cf6497fb3a9fc643a8ee4ddd"},
-    {file = "SQLAlchemy-1.4.41-cp310-cp310-win_amd64.whl", hash = "sha256:e4b12e3d88a8fffd0b4ca559f6d4957ed91bd4c0613a4e13846ab8729dc5c251"},
-    {file = "SQLAlchemy-1.4.41-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:90484a2b00baedad361402c257895b13faa3f01780f18f4a104a2f5c413e4536"},
-    {file = "SQLAlchemy-1.4.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b67fc780cfe2b306180e56daaa411dd3186bf979d50a6a7c2a5b5036575cbdbb"},
-    {file = "SQLAlchemy-1.4.41-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad2b727fc41c7f8757098903f85fafb4bf587ca6605f82d9bf5604bd9c7cded"},
-    {file = "SQLAlchemy-1.4.41-cp311-cp311-win32.whl", hash = "sha256:59bdc291165b6119fc6cdbc287c36f7f2859e6051dd923bdf47b4c55fd2f8bd0"},
-    {file = "SQLAlchemy-1.4.41-cp311-cp311-win_amd64.whl", hash = "sha256:d2e054aed4645f9b755db85bc69fc4ed2c9020c19c8027976f66576b906a74f1"},
-    {file = "SQLAlchemy-1.4.41-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:4ba7e122510bbc07258dc42be6ed45997efdf38129bde3e3f12649be70683546"},
-    {file = "SQLAlchemy-1.4.41-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0dcf127bb99458a9d211e6e1f0f3edb96c874dd12f2503d4d8e4f1fd103790b"},
-    {file = "SQLAlchemy-1.4.41-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e16c2be5cb19e2c08da7bd3a87fed2a0d4e90065ee553a940c4fc1a0fb1ab72b"},
-    {file = "SQLAlchemy-1.4.41-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5ebeeec5c14533221eb30bad716bc1fd32f509196318fb9caa7002c4a364e4c"},
-    {file = "SQLAlchemy-1.4.41-cp36-cp36m-win32.whl", hash = "sha256:3e2ef592ac3693c65210f8b53d0edcf9f4405925adcfc031ff495e8d18169682"},
-    {file = "SQLAlchemy-1.4.41-cp36-cp36m-win_amd64.whl", hash = "sha256:eb30cf008850c0a26b72bd1b9be6730830165ce049d239cfdccd906f2685f892"},
-    {file = "SQLAlchemy-1.4.41-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:c23d64a0b28fc78c96289ffbd0d9d1abd48d267269b27f2d34e430ea73ce4b26"},
-    {file = "SQLAlchemy-1.4.41-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8eb8897367a21b578b26f5713833836f886817ee2ffba1177d446fa3f77e67c8"},
-    {file = "SQLAlchemy-1.4.41-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:14576238a5f89bcf504c5f0a388d0ca78df61fb42cb2af0efe239dc965d4f5c9"},
-    {file = "SQLAlchemy-1.4.41-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:639e1ae8d48b3c86ffe59c0daa9a02e2bfe17ca3d2b41611b30a0073937d4497"},
-    {file = "SQLAlchemy-1.4.41-cp37-cp37m-win32.whl", hash = "sha256:0005bd73026cd239fc1e8ccdf54db58b6193be9a02b3f0c5983808f84862c767"},
-    {file = "SQLAlchemy-1.4.41-cp37-cp37m-win_amd64.whl", hash = "sha256:5323252be2bd261e0aa3f33cb3a64c45d76829989fa3ce90652838397d84197d"},
-    {file = "SQLAlchemy-1.4.41-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:05f0de3a1dc3810a776275763764bb0015a02ae0f698a794646ebc5fb06fad33"},
-    {file = "SQLAlchemy-1.4.41-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0002e829142b2af00b4eaa26c51728f3ea68235f232a2e72a9508a3116bd6ed0"},
-    {file = "SQLAlchemy-1.4.41-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22ff16cedab5b16a0db79f1bc99e46a6ddececb60c396562e50aab58ddb2871c"},
-    {file = "SQLAlchemy-1.4.41-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccfd238f766a5bb5ee5545a62dd03f316ac67966a6a658efb63eeff8158a4bbf"},
-    {file = "SQLAlchemy-1.4.41-cp38-cp38-win32.whl", hash = "sha256:58bb65b3274b0c8a02cea9f91d6f44d0da79abc993b33bdedbfec98c8440175a"},
-    {file = "SQLAlchemy-1.4.41-cp38-cp38-win_amd64.whl", hash = "sha256:ce8feaa52c1640de9541eeaaa8b5fb632d9d66249c947bb0d89dd01f87c7c288"},
-    {file = "SQLAlchemy-1.4.41-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:199a73c31ac8ea59937cc0bf3dfc04392e81afe2ec8a74f26f489d268867846c"},
-    {file = "SQLAlchemy-1.4.41-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676d51c9f6f6226ae8f26dc83ec291c088fe7633269757d333978df78d931ab"},
-    {file = "SQLAlchemy-1.4.41-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:036d8472356e1d5f096c5e0e1a7e0f9182140ada3602f8fff6b7329e9e7cfbcd"},
-    {file = "SQLAlchemy-1.4.41-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2307495d9e0ea00d0c726be97a5b96615035854972cc538f6e7eaed23a35886c"},
-    {file = "SQLAlchemy-1.4.41-cp39-cp39-win32.whl", hash = "sha256:9c56e19780cd1344fcd362fd6265a15f48aa8d365996a37fab1495cae8fcd97d"},
-    {file = "SQLAlchemy-1.4.41-cp39-cp39-win_amd64.whl", hash = "sha256:f5fa526d027d804b1f85cdda1eb091f70bde6fb7d87892f6dd5a48925bc88898"},
-    {file = "SQLAlchemy-1.4.41.tar.gz", hash = "sha256:0292f70d1797e3c54e862e6f30ae474014648bc9c723e14a2fda730adb0a9791"},
+python-versions = ">=3.7"
+files = [
+    {file = "SQLAlchemy-2.0.39-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:66a40003bc244e4ad86b72abb9965d304726d05a939e8c09ce844d27af9e6d37"},
+    {file = "SQLAlchemy-2.0.39-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67de057fbcb04a066171bd9ee6bcb58738d89378ee3cabff0bffbf343ae1c787"},
+    {file = "SQLAlchemy-2.0.39-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:533e0f66c32093a987a30df3ad6ed21170db9d581d0b38e71396c49718fbb1ca"},
+    {file = "SQLAlchemy-2.0.39-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7399d45b62d755e9ebba94eb89437f80512c08edde8c63716552a3aade61eb42"},
+    {file = "SQLAlchemy-2.0.39-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:788b6ff6728072b313802be13e88113c33696a9a1f2f6d634a97c20f7ef5ccce"},
+    {file = "SQLAlchemy-2.0.39-cp37-cp37m-win32.whl", hash = "sha256:01da15490c9df352fbc29859d3c7ba9cd1377791faeeb47c100832004c99472c"},
+    {file = "SQLAlchemy-2.0.39-cp37-cp37m-win_amd64.whl", hash = "sha256:f2bcb085faffcacf9319b1b1445a7e1cfdc6fb46c03f2dce7bc2d9a4b3c1cdc5"},
+    {file = "SQLAlchemy-2.0.39-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b761a6847f96fdc2d002e29e9e9ac2439c13b919adfd64e8ef49e75f6355c548"},
+    {file = "SQLAlchemy-2.0.39-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0d7e3866eb52d914aea50c9be74184a0feb86f9af8aaaa4daefe52b69378db0b"},
+    {file = "SQLAlchemy-2.0.39-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:995c2bacdddcb640c2ca558e6760383dcdd68830160af92b5c6e6928ffd259b4"},
+    {file = "SQLAlchemy-2.0.39-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:344cd1ec2b3c6bdd5dfde7ba7e3b879e0f8dd44181f16b895940be9b842fd2b6"},
+    {file = "SQLAlchemy-2.0.39-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5dfbc543578058c340360f851ddcecd7a1e26b0d9b5b69259b526da9edfa8875"},
+    {file = "SQLAlchemy-2.0.39-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3395e7ed89c6d264d38bea3bfb22ffe868f906a7985d03546ec7dc30221ea980"},
+    {file = "SQLAlchemy-2.0.39-cp38-cp38-win32.whl", hash = "sha256:bf555f3e25ac3a70c67807b2949bfe15f377a40df84b71ab2c58d8593a1e036e"},
+    {file = "SQLAlchemy-2.0.39-cp38-cp38-win_amd64.whl", hash = "sha256:463ecfb907b256e94bfe7bcb31a6d8c7bc96eca7cbe39803e448a58bb9fcad02"},
+    {file = "sqlalchemy-2.0.39-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6827f8c1b2f13f1420545bd6d5b3f9e0b85fe750388425be53d23c760dcf176b"},
+    {file = "sqlalchemy-2.0.39-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9f119e7736967c0ea03aff91ac7d04555ee038caf89bb855d93bbd04ae85b41"},
+    {file = "sqlalchemy-2.0.39-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4600c7a659d381146e1160235918826c50c80994e07c5b26946a3e7ec6c99249"},
+    {file = "sqlalchemy-2.0.39-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a06e6c8e31c98ddc770734c63903e39f1947c9e3e5e4bef515c5491b7737dde"},
+    {file = "sqlalchemy-2.0.39-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4c433f78c2908ae352848f56589c02b982d0e741b7905228fad628999799de4"},
+    {file = "sqlalchemy-2.0.39-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7bd5c5ee1448b6408734eaa29c0d820d061ae18cb17232ce37848376dcfa3e92"},
+    {file = "sqlalchemy-2.0.39-cp310-cp310-win32.whl", hash = "sha256:87a1ce1f5e5dc4b6f4e0aac34e7bb535cb23bd4f5d9c799ed1633b65c2bcad8c"},
+    {file = "sqlalchemy-2.0.39-cp310-cp310-win_amd64.whl", hash = "sha256:871f55e478b5a648c08dd24af44345406d0e636ffe021d64c9b57a4a11518304"},
+    {file = "sqlalchemy-2.0.39-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a28f9c238f1e143ff42ab3ba27990dfb964e5d413c0eb001b88794c5c4a528a9"},
+    {file = "sqlalchemy-2.0.39-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:08cf721bbd4391a0e765fe0fe8816e81d9f43cece54fdb5ac465c56efafecb3d"},
+    {file = "sqlalchemy-2.0.39-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a8517b6d4005facdbd7eb4e8cf54797dbca100a7df459fdaff4c5123265c1cd"},
+    {file = "sqlalchemy-2.0.39-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b2de1523d46e7016afc7e42db239bd41f2163316935de7c84d0e19af7e69538"},
+    {file = "sqlalchemy-2.0.39-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:412c6c126369ddae171c13987b38df5122cb92015cba6f9ee1193b867f3f1530"},
+    {file = "sqlalchemy-2.0.39-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b35e07f1d57b79b86a7de8ecdcefb78485dab9851b9638c2c793c50203b2ae8"},
+    {file = "sqlalchemy-2.0.39-cp311-cp311-win32.whl", hash = "sha256:3eb14ba1a9d07c88669b7faf8f589be67871d6409305e73e036321d89f1d904e"},
+    {file = "sqlalchemy-2.0.39-cp311-cp311-win_amd64.whl", hash = "sha256:78f1b79132a69fe8bd6b5d91ef433c8eb40688ba782b26f8c9f3d2d9ca23626f"},
+    {file = "sqlalchemy-2.0.39-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c457a38351fb6234781d054260c60e531047e4d07beca1889b558ff73dc2014b"},
+    {file = "sqlalchemy-2.0.39-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:018ee97c558b499b58935c5a152aeabf6d36b3d55d91656abeb6d93d663c0c4c"},
+    {file = "sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5493a8120d6fc185f60e7254fc056a6742f1db68c0f849cfc9ab46163c21df47"},
+    {file = "sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2cf5b5ddb69142511d5559c427ff00ec8c0919a1e6c09486e9c32636ea2b9dd"},
+    {file = "sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f03143f8f851dd8de6b0c10784363712058f38209e926723c80654c1b40327a"},
+    {file = "sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06205eb98cb3dd52133ca6818bf5542397f1dd1b69f7ea28aa84413897380b06"},
+    {file = "sqlalchemy-2.0.39-cp312-cp312-win32.whl", hash = "sha256:7f5243357e6da9a90c56282f64b50d29cba2ee1f745381174caacc50d501b109"},
+    {file = "sqlalchemy-2.0.39-cp312-cp312-win_amd64.whl", hash = "sha256:2ed107331d188a286611cea9022de0afc437dd2d3c168e368169f27aa0f61338"},
+    {file = "sqlalchemy-2.0.39-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fe193d3ae297c423e0e567e240b4324d6b6c280a048e64c77a3ea6886cc2aa87"},
+    {file = "sqlalchemy-2.0.39-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:79f4f502125a41b1b3b34449e747a6abfd52a709d539ea7769101696bdca6716"},
+    {file = "sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a10ca7f8a1ea0fd5630f02feb055b0f5cdfcd07bb3715fc1b6f8cb72bf114e4"},
+    {file = "sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6b0a1c7ed54a5361aaebb910c1fa864bae34273662bb4ff788a527eafd6e14d"},
+    {file = "sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52607d0ebea43cf214e2ee84a6a76bc774176f97c5a774ce33277514875a718e"},
+    {file = "sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c08a972cbac2a14810463aec3a47ff218bb00c1a607e6689b531a7c589c50723"},
+    {file = "sqlalchemy-2.0.39-cp313-cp313-win32.whl", hash = "sha256:23c5aa33c01bd898f879db158537d7e7568b503b15aad60ea0c8da8109adf3e7"},
+    {file = "sqlalchemy-2.0.39-cp313-cp313-win_amd64.whl", hash = "sha256:4dabd775fd66cf17f31f8625fc0e4cfc5765f7982f94dc09b9e5868182cb71c0"},
+    {file = "sqlalchemy-2.0.39-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2600a50d590c22d99c424c394236899ba72f849a02b10e65b4c70149606408b5"},
+    {file = "sqlalchemy-2.0.39-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4eff9c270afd23e2746e921e80182872058a7a592017b2713f33f96cc5f82e32"},
+    {file = "sqlalchemy-2.0.39-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7332868ce891eda48896131991f7f2be572d65b41a4050957242f8e935d5d7"},
+    {file = "sqlalchemy-2.0.39-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:125a7763b263218a80759ad9ae2f3610aaf2c2fbbd78fff088d584edf81f3782"},
+    {file = "sqlalchemy-2.0.39-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:04545042969833cb92e13b0a3019549d284fd2423f318b6ba10e7aa687690a3c"},
+    {file = "sqlalchemy-2.0.39-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:805cb481474e111ee3687c9047c5f3286e62496f09c0e82e8853338aaaa348f8"},
+    {file = "sqlalchemy-2.0.39-cp39-cp39-win32.whl", hash = "sha256:34d5c49f18778a3665d707e6286545a30339ad545950773d43977e504815fa70"},
+    {file = "sqlalchemy-2.0.39-cp39-cp39-win_amd64.whl", hash = "sha256:35e72518615aa5384ef4fae828e3af1b43102458b74a8c481f69af8abf7e802a"},
+    {file = "sqlalchemy-2.0.39-py3-none-any.whl", hash = "sha256:a1c6b0a5e3e326a466d809b651c63f278b1256146a377a528b6938a279da334f"},
+    {file = "sqlalchemy-2.0.39.tar.gz", hash = "sha256:5d2d1fe548def3267b4c70a8568f108d1fed7cbbeccb9cc166e05af2abc25c22"},
 ]
 
 [package.dependencies]
-greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"}
+greenlet = {version = "!=0.4.17", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"}
+typing-extensions = ">=4.6.0"
 
 [package.extras]
-aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
-aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"]
+aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"]
+aioodbc = ["aioodbc", "greenlet (!=0.4.17)"]
+aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
 asyncio = ["greenlet (!=0.4.17)"]
-asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"]
-mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"]
+asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
+mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"]
 mssql = ["pyodbc"]
 mssql-pymssql = ["pymssql"]
 mssql-pyodbc = ["pyodbc"]
-mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"]
-mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"]
+mypy = ["mypy (>=0.910)"]
+mysql = ["mysqlclient (>=1.4.0)"]
 mysql-connector = ["mysql-connector-python"]
-oracle = ["cx-oracle (>=7)", "cx-oracle (>=7,<8)"]
+oracle = ["cx_oracle (>=8)"]
+oracle-oracledb = ["oracledb (>=1.0.1)"]
 postgresql = ["psycopg2 (>=2.7)"]
 postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
-postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
+postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
+postgresql-psycopg = ["psycopg (>=3.0.7)"]
 postgresql-psycopg2binary = ["psycopg2-binary"]
 postgresql-psycopg2cffi = ["psycopg2cffi"]
-pymysql = ["pymysql", "pymysql (<1)"]
-sqlcipher = ["sqlcipher3-binary"]
+postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
+pymysql = ["pymysql"]
+sqlcipher = ["sqlcipher3_binary"]
 
 [[package]]
 name = "sqlalchemy-stubs"
@@ -2623,15 +2594,30 @@ files = [
 setuptools = "*"
 SQLAlchemy = ">=0.9.0"
 
+[[package]]
+name = "sqlmodel"
+version = "0.0.24"
+description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "sqlmodel-0.0.24-py3-none-any.whl", hash = "sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193"},
+    {file = "sqlmodel-0.0.24.tar.gz", hash = "sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423"},
+]
+
+[package.dependencies]
+pydantic = ">=1.10.13,<3.0.0"
+SQLAlchemy = ">=2.0.14,<2.1.0"
+
 [[package]]
 name = "starlette"
-version = "0.45.3"
+version = "0.46.1"
 description = "The little ASGI library that shines."
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d"},
-    {file = "starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f"},
+    {file = "starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227"},
+    {file = "starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230"},
 ]
 
 [package.dependencies]
@@ -2715,13 +2701,13 @@ files = [
 
 [[package]]
 name = "types-requests"
-version = "2.32.0.20241016"
+version = "2.32.0.20250306"
 description = "Typing stubs for requests"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"},
-    {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"},
+    {file = "types_requests-2.32.0.20250306-py3-none-any.whl", hash = "sha256:25f2cbb5c8710b2022f8bbee7b2b66f319ef14aeea2f35d80f18c9dbf3b60a0b"},
+    {file = "types_requests-2.32.0.20250306.tar.gz", hash = "sha256:0962352694ec5b2f95fda877ee60a159abdf84a0fc6fdace599f20acb41a03d1"},
 ]
 
 [package.dependencies]
@@ -2916,80 +2902,80 @@ anyio = ">=3.0.0"
 
 [[package]]
 name = "websockets"
-version = "14.2"
+version = "15.0.1"
 description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "websockets-14.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885"},
-    {file = "websockets-14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397"},
-    {file = "websockets-14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:714a9b682deb4339d39ffa674f7b674230227d981a37d5d174a4a83e3978a610"},
-    {file = "websockets-14.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e53c72052f2596fb792a7acd9704cbc549bf70fcde8a99e899311455974ca3"},
-    {file = "websockets-14.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3fbd68850c837e57373d95c8fe352203a512b6e49eaae4c2f4088ef8cf21980"},
-    {file = "websockets-14.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b27ece32f63150c268593d5fdb82819584831a83a3f5809b7521df0685cd5d8"},
-    {file = "websockets-14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4daa0faea5424d8713142b33825fff03c736f781690d90652d2c8b053345b0e7"},
-    {file = "websockets-14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bc63cee8596a6ec84d9753fd0fcfa0452ee12f317afe4beae6b157f0070c6c7f"},
-    {file = "websockets-14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a570862c325af2111343cc9b0257b7119b904823c675b22d4ac547163088d0d"},
-    {file = "websockets-14.2-cp310-cp310-win32.whl", hash = "sha256:75862126b3d2d505e895893e3deac0a9339ce750bd27b4ba515f008b5acf832d"},
-    {file = "websockets-14.2-cp310-cp310-win_amd64.whl", hash = "sha256:cc45afb9c9b2dc0852d5c8b5321759cf825f82a31bfaf506b65bf4668c96f8b2"},
-    {file = "websockets-14.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3bdc8c692c866ce5fefcaf07d2b55c91d6922ac397e031ef9b774e5b9ea42166"},
-    {file = "websockets-14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c93215fac5dadc63e51bcc6dceca72e72267c11def401d6668622b47675b097f"},
-    {file = "websockets-14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c9b6535c0e2cf8a6bf938064fb754aaceb1e6a4a51a80d884cd5db569886910"},
-    {file = "websockets-14.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a52a6d7cf6938e04e9dceb949d35fbdf58ac14deea26e685ab6368e73744e4c"},
-    {file = "websockets-14.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f05702e93203a6ff5226e21d9b40c037761b2cfb637187c9802c10f58e40473"},
-    {file = "websockets-14.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22441c81a6748a53bfcb98951d58d1af0661ab47a536af08920d129b4d1c3473"},
-    {file = "websockets-14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd9b868d78b194790e6236d9cbc46d68aba4b75b22497eb4ab64fa640c3af56"},
-    {file = "websockets-14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a5a20d5843886d34ff8c57424cc65a1deda4375729cbca4cb6b3353f3ce4142"},
-    {file = "websockets-14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:34277a29f5303d54ec6468fb525d99c99938607bc96b8d72d675dee2b9f5bf1d"},
-    {file = "websockets-14.2-cp311-cp311-win32.whl", hash = "sha256:02687db35dbc7d25fd541a602b5f8e451a238ffa033030b172ff86a93cb5dc2a"},
-    {file = "websockets-14.2-cp311-cp311-win_amd64.whl", hash = "sha256:862e9967b46c07d4dcd2532e9e8e3c2825e004ffbf91a5ef9dde519ee2effb0b"},
-    {file = "websockets-14.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c"},
-    {file = "websockets-14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967"},
-    {file = "websockets-14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990"},
-    {file = "websockets-14.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda"},
-    {file = "websockets-14.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95"},
-    {file = "websockets-14.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3"},
-    {file = "websockets-14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9"},
-    {file = "websockets-14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267"},
-    {file = "websockets-14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe"},
-    {file = "websockets-14.2-cp312-cp312-win32.whl", hash = "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205"},
-    {file = "websockets-14.2-cp312-cp312-win_amd64.whl", hash = "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce"},
-    {file = "websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e"},
-    {file = "websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad"},
-    {file = "websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03"},
-    {file = "websockets-14.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f"},
-    {file = "websockets-14.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5"},
-    {file = "websockets-14.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a"},
-    {file = "websockets-14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20"},
-    {file = "websockets-14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2"},
-    {file = "websockets-14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307"},
-    {file = "websockets-14.2-cp313-cp313-win32.whl", hash = "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc"},
-    {file = "websockets-14.2-cp313-cp313-win_amd64.whl", hash = "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f"},
-    {file = "websockets-14.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7cd5706caec1686c5d233bc76243ff64b1c0dc445339bd538f30547e787c11fe"},
-    {file = "websockets-14.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec607328ce95a2f12b595f7ae4c5d71bf502212bddcea528290b35c286932b12"},
-    {file = "websockets-14.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da85651270c6bfb630136423037dd4975199e5d4114cae6d3066641adcc9d1c7"},
-    {file = "websockets-14.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ecadc7ce90accf39903815697917643f5b7cfb73c96702318a096c00aa71f5"},
-    {file = "websockets-14.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1979bee04af6a78608024bad6dfcc0cc930ce819f9e10342a29a05b5320355d0"},
-    {file = "websockets-14.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dddacad58e2614a24938a50b85969d56f88e620e3f897b7d80ac0d8a5800258"},
-    {file = "websockets-14.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:89a71173caaf75fa71a09a5f614f450ba3ec84ad9fca47cb2422a860676716f0"},
-    {file = "websockets-14.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6af6a4b26eea4fc06c6818a6b962a952441e0e39548b44773502761ded8cc1d4"},
-    {file = "websockets-14.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:80c8efa38957f20bba0117b48737993643204645e9ec45512579132508477cfc"},
-    {file = "websockets-14.2-cp39-cp39-win32.whl", hash = "sha256:2e20c5f517e2163d76e2729104abc42639c41cf91f7b1839295be43302713661"},
-    {file = "websockets-14.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4c8cef610e8d7c70dea92e62b6814a8cd24fbd01d7103cc89308d2bfe1659ef"},
-    {file = "websockets-14.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d7d9cafbccba46e768be8a8ad4635fa3eae1ffac4c6e7cb4eb276ba41297ed29"},
-    {file = "websockets-14.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c76193c1c044bd1e9b3316dcc34b174bbf9664598791e6fb606d8d29000e070c"},
-    {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd475a974d5352390baf865309fe37dec6831aafc3014ffac1eea99e84e83fc2"},
-    {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6c0097a41968b2e2b54ed3424739aab0b762ca92af2379f152c1aef0187e1c"},
-    {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7ff794c8b36bc402f2e07c0b2ceb4a2424147ed4785ff03e2a7af03711d60a"},
-    {file = "websockets-14.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dec254fcabc7bd488dab64846f588fc5b6fe0d78f641180030f8ea27b76d72c3"},
-    {file = "websockets-14.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bbe03eb853e17fd5b15448328b4ec7fb2407d45fb0245036d06a3af251f8e48f"},
-    {file = "websockets-14.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3c4aa3428b904d5404a0ed85f3644d37e2cb25996b7f096d77caeb0e96a3b42"},
-    {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:577a4cebf1ceaf0b65ffc42c54856214165fb8ceeba3935852fc33f6b0c55e7f"},
-    {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad1c1d02357b7665e700eca43a31d52814ad9ad9b89b58118bdabc365454b574"},
-    {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f390024a47d904613577df83ba700bd189eedc09c57af0a904e5c39624621270"},
-    {file = "websockets-14.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3c1426c021c38cf92b453cdf371228d3430acd775edee6bac5a4d577efc72365"},
-    {file = "websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b"},
-    {file = "websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5"},
+    {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"},
+    {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"},
+    {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"},
+    {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"},
+    {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"},
+    {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"},
+    {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"},
+    {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"},
+    {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"},
+    {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"},
+    {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"},
+    {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"},
+    {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"},
+    {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"},
+    {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"},
+    {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"},
+    {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"},
+    {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"},
+    {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"},
+    {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"},
+    {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"},
+    {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"},
+    {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"},
+    {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"},
+    {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"},
+    {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"},
+    {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"},
+    {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"},
+    {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"},
+    {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"},
+    {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"},
+    {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"},
+    {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"},
+    {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"},
+    {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"},
+    {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"},
+    {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"},
+    {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"},
+    {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"},
+    {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"},
+    {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"},
+    {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"},
+    {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"},
+    {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"},
+    {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"},
+    {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"},
+    {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"},
+    {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"},
+    {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"},
+    {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"},
+    {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"},
+    {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"},
+    {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"},
+    {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"},
+    {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"},
+    {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"},
+    {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"},
+    {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"},
+    {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"},
+    {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"},
+    {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"},
+    {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"},
+    {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"},
+    {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"},
+    {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"},
+    {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"},
+    {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"},
+    {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"},
+    {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"},
 ]
 
 [[package]]
@@ -3017,4 +3003,4 @@ pycurl = ["pycurl"]
 [metadata]
 lock-version = "2.0"
 python-versions = ">3.9.1,<3.13"
-content-hash = "69cdd5c32d76c7b949db62d26677507f03af27a81ce79c0df8c2c107ddd318b1"
+content-hash = "dea6769b0117c7b226b39a1d011b3acf3abeb2e2cbed5d109aa11099c5ef5aaa"
diff --git a/pyproject.toml b/pyproject.toml
index 48d96e6c011770534bf2610114a77ab2d6ecdfd1..4e6d09c73229983a3ca5619adc7d093c8940910a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -27,7 +27,7 @@ numpy = "^1.25.0"
 obspy = "^1.3.1"
 openpyxl = "^3.0.10"
 pycurl = {version = "^7.45.1", optional = true}
-pydantic = "2.9.2"
+pydantic = "2.10.6"
 requests = "^2.28.1"
 scipy = "^1.9.3"
 typer = "^0.6.1"
@@ -35,18 +35,16 @@ typer = "^0.6.1"
 alembic = "^1.8.1"
 Authlib = "^1.1.0"
 cryptography = "^44.0.1"
-databases = {extras = ["mysql", "sqlite"], version = "^0.6.1"}
 fastapi = ">=0.103.0"
 gunicorn = "^23.0.0"
 httpx = "0.28.1"
-# 2022-10-24 pin sqlalchemy to 1.4.41 for now
-# 1.4.42 seems to cause issues with databases (https://stackoverflow.com/a/74102692)
-SQLAlchemy = "1.4.41"
+SQLAlchemy = "2.0.39"
 SQLAlchemy-Utc = "^0.14.0"
 uvicorn = {extras = ["standard"], version = "^0.22.0"}
 netcdf4 = "^1.7.2"
 cdflib = "^1.3.2"
-
+sqlmodel = "^0.0.24"
+PyMySQL = "^1.1.1"
 
 [tool.poetry.dev-dependencies]
 black = "^24.3.0"
diff --git a/test/Controller_test.py b/test/Controller_test.py
index bff8a8fdb44e354403f9940d8e07974f9b2369d0..209f44b030dd33fe3fbcb259542aeec8524db69d 100644
--- a/test/Controller_test.py
+++ b/test/Controller_test.py
@@ -1,9 +1,10 @@
 #! /usr/bin/env python
-from geomagio import Controller, TimeseriesFactory
-from geomagio.algorithm import Algorithm
+from geomagio.Controller import Controller
+from geomagio.TimeseriesFactory import TimeseriesFactory
+from geomagio.algorithm.Algorithm import Algorithm
 
 # needed to read outputs generated by Controller and test data
-from geomagio.iaga2002 import IAGA2002Factory
+from geomagio.iaga2002.IAGA2002Factory import IAGA2002Factory
 
 # needed to emulate geomag.py script
 from geomagio.Controller import _main, get_previous_interval, parse_args
diff --git a/test/DerivedTimeseriesFactory_test.py b/test/DerivedTimeseriesFactory_test.py
index debb0b213fa3b747e15e9f51b5540738cb5a9508..94de45831891bcd89073a469927970d183b45715 100644
--- a/test/DerivedTimeseriesFactory_test.py
+++ b/test/DerivedTimeseriesFactory_test.py
@@ -2,11 +2,12 @@ from typing import List
 
 from obspy import Stream
 
-from geomagio import TimeseriesUtility
-from geomagio.algorithm import Algorithm, DeltaFAlgorithm, XYZAlgorithm
+from geomagio.TimeseriesFactory import TimeseriesUtility
+from geomagio.algorithm.DeltaFAlgorithm import DeltaFAlgorithm
+from geomagio.algorithm.XYZAlgorithm import XYZAlgorithm
 from geomagio.DerivedTimeseriesFactory import DerivedTimeseriesFactory, get_missing
-from geomagio.iaga2002 import StreamIAGA2002Factory
-from geomagio.edge import EdgeFactory
+from geomagio.iaga2002.StreamIAGA2002Factory import StreamIAGA2002Factory
+from geomagio.edge.EdgeFactory import EdgeFactory
 
 
 def test_derive_trace():
diff --git a/test/adjusted_test/adjusted_test.py b/test/adjusted_test/adjusted_test.py
index 1395e2c61ed6c9803bc42716586af08cb2dbbe80..aed7d5e3cd5dc06adf1f2aee62003eeb74081d79 100644
--- a/test/adjusted_test/adjusted_test.py
+++ b/test/adjusted_test/adjusted_test.py
@@ -5,21 +5,20 @@ from numpy.testing import assert_equal, assert_array_almost_equal
 from obspy.core import UTCDateTime
 import pytest
 
-from geomagio.adjusted import AdjustedMatrix
 from geomagio.adjusted.Affine import Affine, get_epochs
-from geomagio.adjusted.transform import (
-    LeastSq,
-    QRFactorization,
-    Rescale3D,
-    RotationTranslationXY,
-    SVD,
-    ShearYZ,
-    TranslateOrigins,
-    ZRotationHscale,
-    TranslateOrigins,
+from geomagio.adjusted.transform.LeastSq import LeastSq
+from geomagio.adjusted.transform.QRFactorization import QRFactorization
+from geomagio.adjusted.transform.Rescale3D import Rescale3D
+from geomagio.adjusted.transform.RotationTranslationXY import RotationTranslationXY
+from geomagio.adjusted.transform.SVD import SVD
+from geomagio.adjusted.transform.ShearYZ import ShearYZ
+from geomagio.adjusted.transform.TranslateOrigins import TranslateOrigins
+from geomagio.adjusted.transform.ZRotationHScale import ZRotationHscale
+from geomagio.adjusted.transform.TranslateOrigins import TranslateOrigins
+from geomagio.adjusted.transform.ZRotationHScaleZBaseline import (
     ZRotationHscaleZbaseline,
-    ZRotationShear,
 )
+from geomagio.adjusted.transform.ZRotationShear import ZRotationShear
 from test.residual_test.residual_test import (
     get_json_readings,
     get_spreadsheet_directory_readings,
diff --git a/test/algorithm_test/AdjustedAlgorithm_test.py b/test/algorithm_test/AdjustedAlgorithm_test.py
index 38af1ffae35db7d9d8bc471f23873fd6465e4fc3..1b66ae9538296c8ed68a8eac0071896e5a70cb50 100644
--- a/test/algorithm_test/AdjustedAlgorithm_test.py
+++ b/test/algorithm_test/AdjustedAlgorithm_test.py
@@ -1,8 +1,9 @@
-from geomagio.adjusted import AdjustedMatrix
-from geomagio.algorithm import AdjustedAlgorithm
-import geomagio.iaga2002 as i2
 from numpy.testing import assert_almost_equal, assert_array_equal, assert_equal
 
+from geomagio.adjusted.AdjustedMatrix import AdjustedMatrix
+from geomagio.algorithm.AdjustedAlgorithm import AdjustedAlgorithm
+from geomagio.iaga2002.IAGA2002Factory import IAGA2002Factory
+
 
 def test_construct():
     """algorithm_test.AdjustedAlgorithm_test.test_construct()"""
@@ -61,9 +62,9 @@ def test_process_XYZF_AdjustedMatrix():
 
     # load boulder Jan 16 files from /etc/ directory
     with open("etc/adjusted/BOU201601vmin.min") as f:
-        raw = i2.IAGA2002Factory().parse_string(f.read())
+        raw = IAGA2002Factory().parse_string(f.read())
     with open("etc/adjusted/BOU201601adj.min") as f:
-        expected = i2.IAGA2002Factory().parse_string(f.read())
+        expected = IAGA2002Factory().parse_string(f.read())
 
     # process hezf (raw) channels with loaded transform
     adjusted = a.process(raw)
@@ -95,9 +96,9 @@ def test_process_reverse_polarity_AdjustedMatrix():
 
     # load boulder May 20 files from /etc/ directory
     with open("etc/adjusted/BOU202005vmin.min") as f:
-        raw = i2.IAGA2002Factory().parse_string(f.read())
+        raw = IAGA2002Factory().parse_string(f.read())
     with open("etc/adjusted/BOU202005adj.min") as f:
-        expected = i2.IAGA2002Factory().parse_string(f.read())
+        expected = IAGA2002Factory().parse_string(f.read())
 
     # process he(raw) channels with loaded transform
     adjusted = a.process(raw)
@@ -124,9 +125,9 @@ def test_process_XYZF_statefile():
 
     # load boulder Jan 16 files from /etc/ directory
     with open("etc/adjusted/BOU201601vmin.min") as f:
-        raw = i2.IAGA2002Factory().parse_string(f.read())
+        raw = IAGA2002Factory().parse_string(f.read())
     with open("etc/adjusted/BOU201601adj.min") as f:
-        expected = i2.IAGA2002Factory().parse_string(f.read())
+        expected = IAGA2002Factory().parse_string(f.read())
 
     # process hezf (raw) channels with loaded transform
     adjusted = a.process(raw)
@@ -153,9 +154,9 @@ def test_process_reverse_polarity_statefile():
 
     # load boulder May 20 files from /etc/ directory
     with open("etc/adjusted/BOU202005vmin.min") as f:
-        raw = i2.IAGA2002Factory().parse_string(f.read())
+        raw = IAGA2002Factory().parse_string(f.read())
     with open("etc/adjusted/BOU202005adj.min") as f:
-        expected = i2.IAGA2002Factory().parse_string(f.read())
+        expected = IAGA2002Factory().parse_string(f.read())
 
     # process he(raw) channels with loaded transform
     adjusted = a.process(raw)
@@ -176,7 +177,7 @@ def test_process_no_statefile():
     a = AdjustedAlgorithm(inchannels=["H", "E", "Z", "F"])
     # load boulder Jan 16 files from /etc/ directory
     with open("etc/adjusted/BOU201601vmin.min") as f:
-        raw = i2.IAGA2002Factory().parse_string(f.read())
+        raw = IAGA2002Factory().parse_string(f.read())
     # process hezf (raw) channels with identity transform
     adjusted = a.process(raw)
     for i in range(len(adjusted)):
diff --git a/test/algorithm_test/Algorithm_test.py b/test/algorithm_test/Algorithm_test.py
index 986a1512a3b061f551470c051e7ef7cb1126293a..8b89c325c8cc72b0138ee1b822012e75838c9eae 100644
--- a/test/algorithm_test/Algorithm_test.py
+++ b/test/algorithm_test/Algorithm_test.py
@@ -1,7 +1,8 @@
 #! /usr/bin/env python
 from obspy.core.stream import Stream
 from numpy.testing import assert_equal
-from geomagio.algorithm import Algorithm
+
+from geomagio.algorithm.Algorithm import Algorithm
 
 
 def test_algorithm_process():
diff --git a/test/algorithm_test/AverageAlgorithm_test.py b/test/algorithm_test/AverageAlgorithm_test.py
index bbe377e70fe89abd8e290341c8497597f78a6c23..75a723e708c0d8f91d36237014980405a6d52e98 100644
--- a/test/algorithm_test/AverageAlgorithm_test.py
+++ b/test/algorithm_test/AverageAlgorithm_test.py
@@ -1,10 +1,11 @@
-from geomagio.algorithm import AverageAlgorithm
 from obspy.core.stream import Stream
-from ..StreamConverter_test import __create_trace
 from obspy.core import UTCDateTime
 import numpy as np
 from numpy.testing import assert_array_equal, assert_equal
 
+from geomagio.algorithm.AverageAlgorithm import AverageAlgorithm
+from ..StreamConverter_test import __create_trace
+
 
 def test_process():
     """AverageAlgorithm_test.test_process()
diff --git a/test/algorithm_test/DbDtAlgorithm_test.py b/test/algorithm_test/DbDtAlgorithm_test.py
index df9fc08efc05ccffb1fa0eb5ba9a06753e71facb..64f9cef70cfd544bc762c87db65f5ca800f12e35 100644
--- a/test/algorithm_test/DbDtAlgorithm_test.py
+++ b/test/algorithm_test/DbDtAlgorithm_test.py
@@ -1,5 +1,5 @@
-from geomagio.algorithm import DbDtAlgorithm
-import geomagio.iaga2002 as i2
+from geomagio.algorithm.DbDtAlgorithm import DbDtAlgorithm
+from geomagio.iaga2002.IAGA2002Factory import IAGA2002Factory
 from numpy.testing import assert_almost_equal, assert_equal
 
 
@@ -17,7 +17,7 @@ def test_process():
     hez_iaga2002_string = hez_iaga2002_file.read()
     hez_dbdt_iaga2002_file = open("etc/dbdt/BOU202005dbdt.min")
     hez_dbdt_iaga2002_string = hez_dbdt_iaga2002_file.read()
-    factory = i2.IAGA2002Factory()
+    factory = IAGA2002Factory()
     hez = factory.parse_string(hez_iaga2002_string)
     hez_dbdt = factory.parse_string(hez_dbdt_iaga2002_string)
 
diff --git a/test/algorithm_test/FilterAlgorithm_test.py b/test/algorithm_test/FilterAlgorithm_test.py
index cd7357e60de9ff200bce5eb2b83e534686d6586a..8d0e7d8ecc4f8eab32ef18aff062b179c9fabff3 100644
--- a/test/algorithm_test/FilterAlgorithm_test.py
+++ b/test/algorithm_test/FilterAlgorithm_test.py
@@ -1,12 +1,12 @@
 import json
+import pytest
+import numpy as np
 
 from numpy.testing import assert_almost_equal, assert_equal
-import numpy as np
 from obspy import read, UTCDateTime
-import pytest
 
 from geomagio.algorithm.FilterAlgorithm import FilterAlgorithm, get_nearest_time
-import geomagio.iaga2002 as i2
+from geomagio.iaga2002.StreamIAGA2002Factory import StreamIAGA2002Factory
 
 
 def test_second():
@@ -35,7 +35,7 @@ def test_second():
     filtered = f.process(llo)
 
     with open("etc/filter/LLO20200106vsec.sec", "r") as f:
-        iaga = i2.StreamIAGA2002Factory(stream=f)
+        iaga = StreamIAGA2002Factory(stream=f)
         LLO = iaga.get_timeseries(starttime=None, endtime=None, observatory="LLO")
 
     u = LLO.select(channel="U")[0]
@@ -80,7 +80,7 @@ def test_minute():
     filtered = f.process(llo)
 
     with open("etc/filter/LLO20200106vmin.min", "r") as f:
-        iaga = i2.StreamIAGA2002Factory(stream=f)
+        iaga = StreamIAGA2002Factory(stream=f)
         LLO = iaga.get_timeseries(starttime=None, endtime=None, observatory="LLO")
 
     u = LLO.select(channel="U")[0]
@@ -123,7 +123,7 @@ def test_hour():
     filtered = f.process(bou)
 
     with open("etc/filter/BOU20200831vhor.hor", "r") as f:
-        iaga = i2.StreamIAGA2002Factory(stream=f)
+        iaga = StreamIAGA2002Factory(stream=f)
         BOU = iaga.get_timeseries(starttime=None, endtime=None, observatory="BOU")
 
     h = BOU.select(channel="H")[0]
@@ -169,7 +169,7 @@ def test_day():
     filtered = f.process(bou)
 
     with open("etc/filter/BOU20200831vday.day", "r") as f:
-        iaga = i2.StreamIAGA2002Factory(stream=f)
+        iaga = StreamIAGA2002Factory(stream=f)
         BOU = iaga.get_timeseries(starttime=None, endtime=None, observatory="BOU")
 
     h = BOU.select(channel="H")[0]
@@ -221,7 +221,7 @@ def test_custom():
     filtered = f.process(llo)
 
     with open("etc/filter/LLO20200106_custom_vsec.sec", "r") as f:
-        iaga = i2.StreamIAGA2002Factory(stream=f)
+        iaga = StreamIAGA2002Factory(stream=f)
         LLO = iaga.get_timeseries(starttime=None, endtime=None, observatory="LLO")
 
     u = LLO.select(channel="U")[0]
@@ -253,7 +253,7 @@ def test_starttime_shift():
     # with open('BOU20200101vsec.sec','wb') as file:
     #     i2w.write(out=file,timeseries=bou,channels=["H","E","Z","F"])
     with open("etc/filter/BOU20200101vsec.sec", "r") as file:
-        iaga = i2.StreamIAGA2002Factory(stream=file)
+        iaga = StreamIAGA2002Factory(stream=file)
         bou = iaga.get_timeseries(starttime=None, endtime=None, observatory="BOU")
     # check initial assumptions
     assert_equal(bou[0].stats.starttime, UTCDateTime("2020-01-01T00:00:00Z"))
diff --git a/test/algorithm_test/SQDistAlgorithm_test.py b/test/algorithm_test/SQDistAlgorithm_test.py
index 767f71c122a8acfa72cc92ccb4691fb13419f850..d3b801453064056185163f23189d463bc9bbd1f0 100644
--- a/test/algorithm_test/SQDistAlgorithm_test.py
+++ b/test/algorithm_test/SQDistAlgorithm_test.py
@@ -1,4 +1,3 @@
-from geomagio.algorithm import SqDistAlgorithm as sq
 import numpy as np
 from numpy.testing import (
     assert_allclose,
@@ -7,6 +6,8 @@ from numpy.testing import (
     assert_equal,
 )
 
+from geomagio.algorithm.SqDistAlgorithm import SqDistAlgorithm as sq
+
 
 def test_sqdistalgorithm_additive1():
     """SqDistAlgorithm_test.test_sqdistalgorithm_additive1()
diff --git a/test/algorithm_test/XYZAlgorithm_test.py b/test/algorithm_test/XYZAlgorithm_test.py
index 67be143a880a3ed11b5de59c0d711e0773c205d7..b34cbcaff1aef8b8979d06544282833e0180abce 100644
--- a/test/algorithm_test/XYZAlgorithm_test.py
+++ b/test/algorithm_test/XYZAlgorithm_test.py
@@ -1,9 +1,11 @@
 #! /usr/bin/env python
+import numpy as np
+
 from obspy.core.stream import Stream
 from numpy.testing import assert_equal
-from geomagio.algorithm import XYZAlgorithm
+
+from geomagio.algorithm.XYZAlgorithm import XYZAlgorithm
 from ..StreamConverter_test import __create_trace
-import numpy as np
 
 
 def test_xyzalgorithm_process():
diff --git a/test/api_test/conftest.py b/test/api_test/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..556849c67918de707c44f7465e52f0007f1f711b
--- /dev/null
+++ b/test/api_test/conftest.py
@@ -0,0 +1,715 @@
+import pytest
+
+from datetime import datetime, timezone
+from obspy import UTCDateTime
+
+from geomagio.api.db.models.metadata import (
+    Metadata,
+    MetadataTable,
+    MetadataCategory,
+)
+from geomagio.api.db.models.metadata_history import (
+    MetadataHistoryTable,
+    MetadataHistory,
+)
+
+
+@pytest.fixture()
+def valid_token():
+    return {
+        "access_token": "test_access_token",
+        "token_type": "Bearer",
+        "expires_in": 7200,
+        "refresh_token": "test_refresh_toekn",
+        "scope": "openid email profile",
+        "created_at": 1733244369,
+        "id_token": "test_id_token",
+        "expires_at": 1733251569,
+        "userinfo": {
+            "iss": "http://test_url",
+            "sub": "1234",
+            "aud": "test_aud",
+            "exp": 1733244489,
+            "iat": 1733244369,
+            "nonce": "test_nonce",
+            "auth_time": 1733244368,
+            "sub_legacy": "test_sub_legacy",
+            "name": "test_user",
+            "nickname": "test_user_nickname",
+            "preferred_username": "test_preferred_username",
+            "email": "test_email",
+            "email_verified": True,
+            "profile": "http://test_url/user",
+            "picture": "http://picture_url",
+            "groups_direct": ["group1", "group2"],
+        },
+    }
+
+
+@pytest.fixture()
+def valid_userinfo():
+    return {
+        "sub": "1234",
+        "sub_legacy": "test_sub_legacy",
+        "name": "test_user",
+        "nickname": "test_user_nickname",
+        "preferred_username": "test_preferred_username",
+        "email": "test_email",
+        "email_verified": True,
+        "profile": "http://test_url/user",
+        "picture": "http://picture_url",
+        "groups": ["group1", "group2"],
+    }
+
+
+@pytest.fixture()
+def valid_gitlab_user():
+    return {
+        "id": 1234,
+        "email": "test_email",
+        "name": "test_user",
+        "username": "test_user_nickname",
+        "avatar_url": "http://picture_url",
+    }
+
+
+@pytest.fixture()
+def valid_session(valid_token, valid_userinfo):
+    return {"token": valid_token, "user": valid_userinfo}
+
+
+@pytest.fixture()
+def valid_reading_metadata():
+    return {
+        "created_by": "test_user",
+        "starttime": "2024-11-07T12:12:12Z",
+        "endtime": "2024-11-07T20:20:20Z",
+        "network": "NT",
+        "station": "BOU",
+        "category": "reading",
+        "data_valid": True,
+        "metadata": {
+            "azimuth": 199.1383,
+            "metadata": {
+                "time": "2020-01-03T17:12:47Z",
+                "station": "BOU",
+                "observer": "Test Observer",
+                "reviewed": True,
+                "reviewer": "Test Reviewer",
+                "mark_name": "AZ",
+                "pier_name": "MainPCDCP",
+                "theodolite": "108449",
+                "electronics": "0110",
+                "mark_azimuth": 199.1383,
+                "pier_correction": -22,
+            },
+            "absolutes": [
+                {
+                    "shift": 0,
+                    "valid": True,
+                    "element": "D",
+                    "endtime": "2020-01-03T17:16:21.000000Z",
+                    "absolute": 8.3851056,
+                    "baseline": 8.58571,
+                    "starttime": "2020-01-03T17:12:47.000000Z",
+                },
+                {
+                    "shift": 0,
+                    "valid": True,
+                    "element": "H",
+                    "endtime": "2020-01-03T17:24:40.000000Z",
+                    "absolute": 20728.0650365,
+                    "baseline": -71.7177135,
+                    "starttime": "2020-01-03T17:20:48.000000Z",
+                },
+                {
+                    "shift": 0,
+                    "valid": True,
+                    "element": "Z",
+                    "endtime": "2020-01-03T17:24:40.000000Z",
+                    "absolute": 47450.1529433,
+                    "baseline": 578.2041933,
+                    "starttime": "2020-01-03T17:20:48.000000Z",
+                },
+            ],
+            "hemisphere": 1,
+            "diagnostics": None,
+            "scale_value": None,
+            "measurements": [
+                {
+                    "e": None,
+                    "f": None,
+                    "h": None,
+                    "z": None,
+                    "angle": 191.56666666666666,
+                    "measurement_type": "FirstMarkDown",
+                },
+                {
+                    "e": None,
+                    "f": None,
+                    "h": None,
+                    "z": None,
+                    "angle": 11.566666666666666,
+                    "measurement_type": "FirstMarkUp",
+                },
+                {
+                    "e": None,
+                    "f": None,
+                    "h": None,
+                    "z": None,
+                    "angle": 191.56666666666666,
+                    "measurement_type": "SecondMarkDown",
+                },
+                {
+                    "e": None,
+                    "f": None,
+                    "h": None,
+                    "z": None,
+                    "angle": 11.566666666666666,
+                    "measurement_type": "SecondMarkUp",
+                },
+                {
+                    "e": -72.242,
+                    "f": 51801.81,
+                    "h": 20800.329,
+                    "z": 46871.49,
+                    "angle": 270.71666666666664,
+                    "measurement_type": "WestDown",
+                    "time": "2024-11-07T12:12:12Z",
+                },
+                {
+                    "e": -72.636,
+                    "f": 51801.92,
+                    "h": 20800.259,
+                    "z": 46871.641,
+                    "angle": 90.66666666666667,
+                    "measurement_type": "EastDown",
+                    "time": "2024-11-07T12:13:14Z",
+                },
+                {
+                    "e": -72.657,
+                    "f": 51801.82,
+                    "h": 20800.259,
+                    "z": 46871.521,
+                    "angle": 90.93333333333334,
+                    "measurement_type": "WestUp",
+                    "time": "2024-11-07T13:13:11Z",
+                },
+                {
+                    "e": -72.758,
+                    "f": 51801.92,
+                    "h": 20800.086,
+                    "z": 46871.707,
+                    "angle": 270.96666666666664,
+                    "measurement_type": "EastUp",
+                    "time": "2024-11-07T13:15:12Z",
+                },
+                {
+                    "e": -72.898,
+                    "f": 51801.89,
+                    "h": 20799.796,
+                    "z": 46871.802,
+                    "angle": 246.38333333333333,
+                    "measurement_type": "SouthDown",
+                    "time": "2024-11-07T15:12:12Z",
+                },
+                {
+                    "e": -72.8,
+                    "f": 51802.01,
+                    "h": 20799.852,
+                    "z": 46871.919,
+                    "angle": 66.38333333333334,
+                    "measurement_type": "NorthUp",
+                    "time": "2024-11-07T18:12:12Z",
+                },
+                {
+                    "e": -72.775,
+                    "f": 51802.01,
+                    "h": 20799.668,
+                    "z": 46871.997,
+                    "angle": 113.58333333333333,
+                    "measurement_type": "SouthUp",
+                    "time": "2024-11-07T19:12:19Z",
+                },
+                {
+                    "e": -72.813,
+                    "f": 51802.14,
+                    "h": 20799.815,
+                    "z": 46872.077,
+                    "angle": 293.5833333333333,
+                    "measurement_type": "NorthDown",
+                    "time": "2024-11-07T20:20:20Z",
+                },
+                {"measurement_type": "Meridian", "angle": 21},
+            ],
+            "pier_correction": -22,
+        },
+    }
+
+
+@pytest.fixture()
+def instrument_metadata():
+    return Metadata(
+        created_by="test_metadata.py",
+        category=MetadataCategory.INSTRUMENT,
+        network="NT",
+        station="BDT",
+        location="R0",
+        channel="F",
+        metadata={
+            "type": "FGE",
+            "channels": {
+                "U": [{"channel": "U_Volt", "offset": 0, "scale": 313.2}],
+                "V": [{"channel": "V_Volt", "offset": 0, "scale": 312.3}],
+                "W": [{"channel": "W_Volt", "offset": 0, "scale": 312.0}],
+            },
+            "electronics": {
+                "serial": "E0542",
+                "x-scale": 313.2,
+                "y-scale": 312.3,
+                "z-scale": 312.0,
+                "temperature-scale": 0.01,
+            },
+            "sensor": {
+                "serial": "S0419",
+                "x-constant": 36958,
+                "y-constant": 36849,
+                "z-constant": 36811,
+            },
+        },
+        status="new",
+    )
+
+
+@pytest.fixture()
+def metadata_with_times():
+    return Metadata(
+        created_by="test_metadata.py",
+        created_time=UTCDateTime(2021, 1, 3, 17, 24, 40),
+        updated_time=UTCDateTime(2021, 2, 3, 17, 24, 40),
+        starttime=UTCDateTime(2020, 12, 3, 22, 43, 27),
+        endtime=UTCDateTime(2020, 12, 3, 23, 43, 27),
+        category=MetadataCategory.INSTRUMENT,
+        network="NT",
+        station="BOU",
+        channel="U",
+        metadata={
+            "type": "Narod",
+            "channels": {
+                "U": [
+                    {"channel": "U_Volt", "offset": 0, "scale": 100},
+                    {"channel": "U_Bin", "offset": 0, "scale": 500},
+                ],
+                "V": [
+                    {"channel": "V_Volt", "offset": 0, "scale": 100},
+                    {"channel": "V_Bin", "offset": 0, "scale": 500},
+                ],
+                "W": [
+                    {"channel": "W_Volt", "offset": 0, "scale": 100},
+                    {"channel": "W_Bin", "offset": 0, "scale": 500},
+                ],
+            },
+        },
+        status="updated",
+    )
+
+
+@pytest.fixture()
+def metadata_with_datetimes():
+    return Metadata(
+        created_by="test_metadata.py",
+        created_time=datetime(2021, 1, 3, 17, 24, 40, tzinfo=timezone.utc),
+        updated_time=datetime(2021, 2, 3, 17, 24, 40, tzinfo=timezone.utc),
+        starttime=datetime(2020, 12, 4, 22, 43, 27, tzinfo=timezone.utc),
+        endtime=datetime(2020, 12, 4, 23, 43, 27, tzinfo=timezone.utc),
+        category=MetadataCategory.INSTRUMENT,
+        network="NT",
+        station="BOU",
+        location="R0",
+        metadata={
+            "type": "Narod",
+            "channels": {
+                "U": [
+                    {"channel": "U_Volt", "offset": 0, "scale": 100},
+                    {"channel": "U_Bin", "offset": 0, "scale": 500},
+                ],
+                "V": [
+                    {"channel": "V_Volt", "offset": 0, "scale": 100},
+                    {"channel": "V_Bin", "offset": 0, "scale": 500},
+                ],
+                "W": [
+                    {"channel": "W_Volt", "offset": 0, "scale": 100},
+                    {"channel": "W_Bin", "offset": 0, "scale": 500},
+                ],
+            },
+        },
+        status="updated",
+    )
+
+
+@pytest.fixture()
+def metadata_with_datetime_naive():
+    return Metadata(
+        created_by="test_metadata.py",
+        created_time=datetime(2021, 1, 3, 17, 24, 40),
+        updated_time=datetime(2021, 2, 3, 17, 24, 40),
+        starttime=datetime(2020, 12, 5, 22, 43, 27),
+        endtime=datetime(2020, 12, 5, 23, 43, 27),
+        category=MetadataCategory.INSTRUMENT,
+        network="NT",
+        station="BOU",
+        metadata={
+            "type": "Narod",
+            "channels": {
+                "U": [
+                    {"channel": "U_Volt", "offset": 0, "scale": 100},
+                    {"channel": "U_Bin", "offset": 0, "scale": 500},
+                ],
+                "V": [
+                    {"channel": "V_Volt", "offset": 0, "scale": 100},
+                    {"channel": "V_Bin", "offset": 0, "scale": 500},
+                ],
+                "W": [
+                    {"channel": "W_Volt", "offset": 0, "scale": 100},
+                    {"channel": "W_Bin", "offset": 0, "scale": 500},
+                ],
+            },
+        },
+        status="updated",
+    )
+
+
+@pytest.fixture()
+def metadata_with_time_strings():
+    return Metadata(
+        created_by="test_metadata.py",
+        created_time="2021-01-03T17:24:40",
+        starttime="2020-12-03T22:43:27",
+        endtime="2020-12-03T23:43:27",
+        category=MetadataCategory.INSTRUMENT,
+        network="NT",
+        station="BOU",
+        metadata={
+            "type": "Narod",
+            "channels": {
+                "U": [
+                    {"channel": "U_Volt", "offset": 0, "scale": 100},
+                    {"channel": "U_Bin", "offset": 0, "scale": 500},
+                ],
+                "V": [
+                    {"channel": "V_Volt", "offset": 0, "scale": 100},
+                    {"channel": "V_Bin", "offset": 0, "scale": 500},
+                ],
+                "W": [
+                    {"channel": "W_Volt", "offset": 0, "scale": 100},
+                    {"channel": "W_Bin", "offset": 0, "scale": 500},
+                ],
+            },
+        },
+    )
+
+
+@pytest.fixture()
+def observatory_metadata():
+    return Metadata(
+        created_by="test_metadata.py",
+        starttime="2022-02-03T22:43:27",
+        endtime="2022-02-03T23:43:27",
+        category=MetadataCategory.OBSERVATORY,
+        network="NT",
+        station="BRW",
+        metadata={
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "12"}],
+            "agency": "USGS",
+            "latitude": 71.322,
+            "elevation": 10,
+            "longitude": 203.378,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        },
+    )
+
+
+@pytest.fixture()
+def get_all_metadata(
+    instrument_metadata,
+    metadata_with_times,
+    metadata_with_datetimes,
+    metadata_with_datetime_naive,
+    metadata_with_time_strings,
+    observatory_metadata,
+):
+    return [
+        instrument_metadata,
+        metadata_with_times,
+        metadata_with_datetimes,
+        metadata_with_datetime_naive,
+        metadata_with_time_strings,
+        observatory_metadata,
+    ]
+
+
+@pytest.fixture()
+def metadata_history_1():
+    return MetadataHistoryTable(
+        metadata_id=38742,
+        created_by="test_metadata.py",
+        starttime="2022-02-03T22:43:27",
+        endtime="2022-02-03T23:43:27",
+        category=MetadataCategory.OBSERVATORY,
+        network="NT",
+        station="BRW",
+        metadata={
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "12"}],
+            "agency": "USGS",
+            "latitude": 71.322,
+            "elevation": 10,
+            "longitude": 203.378,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        },
+    )
+
+
+@pytest.fixture()
+def metadata_history_2():
+    return MetadataHistoryTable(
+        metadata_id=38742,
+        created_by="test_metadata.py",
+        updated_by="tester",
+        updated_time="2023-01-21T03:30:43",
+        starttime="2022-02-03T22:43:27",
+        endtime="2022-02-03T23:43:27",
+        category=MetadataCategory.OBSERVATORY,
+        network="NT",
+        station="BRW",
+        metadata={
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "22"}],
+            "agency": "USGS",
+            "latitude": 71.322,
+            "elevation": 10,
+            "longitude": 203.378,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        },
+    )
+
+
+@pytest.fixture()
+def metadata_history_3():
+    return MetadataHistoryTable(
+        metadata_id=38742,
+        created_by="test_metadata.py",
+        updated_by="another_tester",
+        updated_time="2023-01-22T13:30:43",
+        starttime="2022-02-03T22:43:27",
+        endtime="2022-02-03T23:43:27",
+        category=MetadataCategory.OBSERVATORY,
+        network="NT",
+        station="BRW",
+        metadata={
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "22"}],
+            "agency": "USGS",
+            "latitude": 87.32,
+            "elevation": 10,
+            "longitude": 203.378,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        },
+    )
+
+
+@pytest.fixture()
+def current_metadata_for_related_metadata_history():
+    return MetadataTable(
+        id=38742,
+        created_by="test_metadata.py",
+        created_time=datetime(2025, 1, 22, 13, 45, 27, tzinfo=timezone.utc),
+        updated_by="another_tester",
+        updated_time=datetime(2023, 6, 1, 13, 45, 27, tzinfo=timezone.utc),
+        starttime=datetime(2022, 2, 3, 22, 43, 27, tzinfo=timezone.utc),
+        endtime=datetime(2022, 2, 3, 23, 43, 27, tzinfo=timezone.utc),
+        category=MetadataCategory.OBSERVATORY,
+        network="NT",
+        station="BRW",
+        metadata_={
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "22"}],
+            "agency": "USGS",
+            "latitude": 87.32,
+            "elevation": 10,
+            "longitude": 202.234,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        },
+    )
+
+
+@pytest.fixture()
+def get_all_related_metadata_history(
+    metadata_history_1, metadata_history_2, metadata_history_3
+):
+    return [metadata_history_1, metadata_history_2, metadata_history_3]
+
+
+@pytest.fixture()
+def formatted_metadata_history_1():
+    return MetadataHistory(
+        metadata_id=38746,
+        created_by="test_metadata.py",
+        starttime=datetime(2022, 2, 3, 22, 43, 27, tzinfo=timezone.utc),
+        endtime=datetime(2022, 2, 3, 23, 43, 27, tzinfo=timezone.utc),
+        category=MetadataCategory.OBSERVATORY,
+        network="NT",
+        station="BRW",
+        metadata={
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "12"}],
+            "agency": "USGS",
+            "latitude": 71.322,
+            "elevation": 10,
+            "longitude": 203.378,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        },
+    )
+
+
+@pytest.fixture()
+def formatted_metadata_history_2():
+    return MetadataHistory(
+        metadata_id=38746,
+        created_by="test_metadata.py",
+        updated_by="tester",
+        updated_time=datetime(2023, 1, 21, 3, 30, 43, tzinfo=timezone.utc),
+        starttime=datetime(2022, 2, 3, 22, 43, 27, tzinfo=timezone.utc),
+        endtime=datetime(2022, 2, 3, 23, 43, 27, tzinfo=timezone.utc),
+        category=MetadataCategory.OBSERVATORY,
+        network="NT",
+        station="BRW",
+        metadata={
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "22"}],
+            "agency": "USGS",
+            "latitude": 71.322,
+            "elevation": 10,
+            "longitude": 203.378,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        },
+    )
+
+
+@pytest.fixture()
+def formatted_metadata_history_3():
+    return MetadataHistory(
+        metadata_id=38746,
+        created_by="test_metadata.py",
+        updated_by="another_tester",
+        updated_time=datetime(2023, 1, 22, 3, 30, 43, tzinfo=timezone.utc),
+        starttime=datetime(2022, 2, 3, 22, 43, 27, tzinfo=timezone.utc),
+        endtime=datetime(2022, 2, 3, 23, 43, 27, tzinfo=timezone.utc),
+        category=MetadataCategory.OBSERVATORY,
+        network="NT",
+        station="BRW",
+        metadata={
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "22"}],
+            "agency": "USGS",
+            "latitude": 87.32,
+            "elevation": 10,
+            "longitude": 203.378,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        },
+    )
+
+
+@pytest.fixture()
+def formatted_metadata_for_formatted_metadata_history():
+    return Metadata(
+        id=38746,
+        created_by="test_metadata.py",
+        created_time=datetime(2025, 1, 22, 13, 45, 27, tzinfo=timezone.utc),
+        updated_by="another_tester",
+        updated_time=datetime(2023, 6, 1, 13, 45, 27, tzinfo=timezone.utc),
+        starttime=datetime(2022, 2, 3, 22, 43, 27, tzinfo=timezone.utc),
+        endtime=datetime(2022, 2, 3, 23, 43, 27, tzinfo=timezone.utc),
+        category=MetadataCategory.OBSERVATORY,
+        network="NT",
+        station="BRW",
+        metadata={
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "22"}],
+            "agency": "USGS",
+            "latitude": 87.32,
+            "elevation": 10,
+            "longitude": 202.234,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        },
+    )
+
+
+@pytest.fixture()
+def get_all_formatted_metadata_history(
+    formatted_metadata_for_formatted_metadata_history,
+    formatted_metadata_history_3,
+    formatted_metadata_history_2,
+    formatted_metadata_history_1,
+):
+    return [
+        formatted_metadata_for_formatted_metadata_history,
+        formatted_metadata_history_3,
+        formatted_metadata_history_2,
+        formatted_metadata_history_1,
+    ]
diff --git a/test/api_test/db/factories/MetadataDatabaseFactory_test.py b/test/api_test/db/factories/MetadataDatabaseFactory_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..8e1dc23a369723f6bc13832a0ab9c679d89c52de
--- /dev/null
+++ b/test/api_test/db/factories/MetadataDatabaseFactory_test.py
@@ -0,0 +1,1307 @@
+import pytest
+import pydantic_core
+
+from obspy import UTCDateTime
+from datetime import datetime, timedelta, timezone
+from sqlmodel import Session, delete, select
+
+from geomagio.api.db.models.metadata import (
+    Metadata,
+    MetadataTable,
+    MetadataCategory,
+    MetadataQuery,
+    MetadataUpdate,
+)
+from geomagio.api.db.models.metadata_history import MetadataHistoryTable
+from geomagio.api.db.factories.MetadataDatabaseFactory import MetadataDatabaseFactory
+
+
+def compare_metadata_objects(a: MetadataTable, b: Metadata) -> bool:
+    if a.id != b.id:
+        raise ValueError(
+            "ids do not match. a.id = " + str(a.id) + " and b.id = " + str(b.id)
+        )
+    if a.created_by != b.created_by:
+        raise ValueError(
+            "created_by does not match. a.created_by = "
+            + a.created_by
+            + " and b.created_by = "
+            + b.created_by
+        )
+    if a.created_time != b.created_time:
+        raise ValueError(
+            "created_time does not match. a.created_time = "
+            + a.created_time
+            + " and b.created_time = "
+            + b.created_time
+        )
+    if a.updated_by != b.updated_by:
+        raise ValueError(
+            "updated_by does not match. a.updated_by = "
+            + a.updated_by
+            + " and b.updated_by = "
+            + b.updated_by
+        )
+    if a.updated_time != b.updated_time:
+        raise ValueError(
+            "updated_time does not match. a.updated_time = "
+            + a.updated_time
+            + " and b.updated_time = "
+            + b.updated_time
+        )
+    if a.starttime != b.starttime:
+        raise ValueError(
+            "starttime does not match. a.starttime = "
+            + a.starttime
+            + " and b.starttime = "
+            + b.starttime
+        )
+    if a.endtime != b.endtime:
+        raise ValueError(
+            "endtime does not match. a.endtime = "
+            + a.endtime
+            + " and b.endtime = "
+            + b.endtime
+        )
+    if a.network != b.network:
+        raise ValueError(
+            "network does not match. a.network = "
+            + a.network
+            + " and b.network = "
+            + b.network
+        )
+    if a.station != b.station:
+        raise ValueError(
+            "station does not match. a.station = "
+            + a.station
+            + " and b.station = "
+            + b.station
+        )
+    if a.channel != b.channel:
+        raise ValueError(
+            "channel does not match. a.channel = "
+            + a.channel
+            + " and b.channel = "
+            + b.channel
+        )
+    if a.location != b.location:
+        raise ValueError(
+            "location does not match. a.location = "
+            + a.location
+            + " and b.location = "
+            + b.location
+        )
+    if a.category != b.category:
+        raise ValueError(
+            "category does not match. a.category = "
+            + a.category
+            + " and b.category = "
+            + b.category
+        )
+    if a.priority != b.priority:
+        raise ValueError(
+            "priority does not match. a.priority = "
+            + a.priority
+            + " and b.priority = "
+            + b.priority
+        )
+    if a.data_valid != b.data_valid:
+        raise ValueError(
+            "data_valid does not match. a.data_valid = "
+            + a.data_valid
+            + " and b.data_valid = "
+            + b.data_valid
+        )
+    if a.status != b.status:
+        raise ValueError(
+            "status does not match. a.status = "
+            + a.status
+            + " and b.status = "
+            + b.status
+        )
+    if a.metadata_ != b.metadata:
+        raise ValueError(
+            "metadata does not match. a.metadata_ = "
+            + a.metadata
+            + " and b.metadata = "
+            + b.metadata
+        )
+    if a.comment != b.comment:
+        raise ValueError(
+            "comment does not match. a.comment = "
+            + a.comment
+            + " and b.comment = "
+            + b.comment
+        )
+    if a.review_comment != b.review_comment:
+        raise ValueError(
+            "review_comment does not match. a.review_comment = "
+            + a.review_comment
+            + " and b.review_comment = "
+            + b.review_comment
+        )
+    return True
+
+
+@pytest.mark.asyncio
+async def test_create_metadata_defaults(test_engine, instrument_metadata):
+    instrument_metadata.id = 123456
+    returned_metadata = await MetadataDatabaseFactory(
+        engine=test_engine
+    ).create_metadata(new_metadata=instrument_metadata)
+
+    # get the newly saved record to verify it was created correctly
+    with Session(test_engine) as test_db_session:
+        saved_metadata = test_db_session.get(MetadataTable, returned_metadata.id)
+
+        # assert the saved metadata fields are set correctly
+        assert saved_metadata.created_by == "test_metadata.py"
+        assert saved_metadata.category == MetadataCategory.INSTRUMENT
+        assert saved_metadata.network == "NT"
+        assert saved_metadata.station == "BDT"
+        assert saved_metadata.metadata_ == {
+            "type": "FGE",
+            "channels": {
+                "U": [{"channel": "U_Volt", "offset": 0, "scale": 313.2}],
+                "V": [{"channel": "V_Volt", "offset": 0, "scale": 312.3}],
+                "W": [{"channel": "W_Volt", "offset": 0, "scale": 312.0}],
+            },
+            "electronics": {
+                "serial": "E0542",
+                "x-scale": 313.2,
+                "y-scale": 312.3,
+                "z-scale": 312.0,
+                "temperature-scale": 0.01,
+            },
+            "sensor": {
+                "serial": "S0419",
+                "x-constant": 36958,
+                "y-constant": 36849,
+                "z-constant": 36811,
+            },
+        }
+
+        # assert created_time is within one hour of current time
+        discard = timedelta(
+            minutes=saved_metadata.created_time.minute,
+            seconds=saved_metadata.created_time.second,
+            microseconds=saved_metadata.created_time.microsecond,
+        )
+        saved_time = saved_metadata.created_time - discard
+
+        now = UTCDateTime.now()
+        current_time = datetime(
+            year=now.year,
+            month=now.month,
+            day=now.day,
+            hour=now.hour,
+            tzinfo=timezone.utc,
+        )
+
+        assert current_time == saved_time
+
+        # assert defaults are set correctly
+        assert saved_metadata.priority == 1
+        assert saved_metadata.data_valid == True
+        assert saved_metadata.status == "new"
+
+        # assert id is the database assigned id. not the input id
+        assert saved_metadata.id != instrument_metadata.id
+
+        # assert the metadata returned is the same as the saved metadata
+        # print("returned_metadata:", returned_metadata)
+        assert compare_metadata_objects(a=saved_metadata, b=returned_metadata)
+
+
+@pytest.mark.asyncio
+async def test_create_metadata_with_starttime_and_endtime(
+    test_engine, metadata_with_times
+):
+    returned_metadata = await MetadataDatabaseFactory(
+        engine=test_engine
+    ).create_metadata(new_metadata=metadata_with_times)
+
+    # get the newly saved record to verify it was created correctly
+    with Session(test_engine) as test_db_session:
+        saved_metadata = test_db_session.get(MetadataTable, returned_metadata.id)
+
+        # assert the saved metadata fields are set correctly
+        assert saved_metadata.created_by == "test_metadata.py"
+        assert saved_metadata.created_time == datetime(
+            2021, 1, 3, 17, 24, 40, tzinfo=timezone.utc
+        )
+        assert saved_metadata.updated_time == datetime(
+            2021, 2, 3, 17, 24, 40, tzinfo=timezone.utc
+        )
+        assert saved_metadata.starttime == datetime(
+            2020, 12, 3, 22, 43, 27, tzinfo=timezone.utc
+        )
+        assert saved_metadata.endtime == datetime(
+            2020, 12, 3, 23, 43, 27, tzinfo=timezone.utc
+        )
+        assert saved_metadata.category == MetadataCategory.INSTRUMENT
+        assert saved_metadata.network == "NT"
+        assert saved_metadata.station == "BOU"
+        assert saved_metadata.metadata_ == {
+            "type": "Narod",
+            "channels": {
+                "U": [
+                    {"channel": "U_Volt", "offset": 0, "scale": 100},
+                    {"channel": "U_Bin", "offset": 0, "scale": 500},
+                ],
+                "V": [
+                    {"channel": "V_Volt", "offset": 0, "scale": 100},
+                    {"channel": "V_Bin", "offset": 0, "scale": 500},
+                ],
+                "W": [
+                    {"channel": "W_Volt", "offset": 0, "scale": 100},
+                    {"channel": "W_Bin", "offset": 0, "scale": 500},
+                ],
+            },
+        }
+
+        # assert defaults are set correctly
+        assert saved_metadata.priority == 1
+        assert saved_metadata.data_valid == True
+        assert saved_metadata.status == "updated"
+
+        # assert the metadata returned is the same as the saved metadata
+        assert compare_metadata_objects(a=saved_metadata, b=returned_metadata)
+
+
+@pytest.mark.asyncio
+async def test_create_metadata_with_times_as_datetime(
+    test_engine, metadata_with_datetimes
+):
+    returned_metadata = await MetadataDatabaseFactory(
+        engine=test_engine
+    ).create_metadata(new_metadata=metadata_with_datetimes)
+
+    # get the newly saved record to verify it was created correctly
+    with Session(test_engine) as test_db_session:
+        saved_metadata = test_db_session.get(MetadataTable, returned_metadata.id)
+
+        # assert the saved metadata fields are set correctly
+        assert saved_metadata.created_by == "test_metadata.py"
+        assert saved_metadata.created_time == datetime(
+            2021, 1, 3, 17, 24, 40, tzinfo=timezone.utc
+        )
+        assert saved_metadata.updated_time == datetime(
+            2021, 2, 3, 17, 24, 40, tzinfo=timezone.utc
+        )
+        assert saved_metadata.starttime == datetime(
+            2020, 12, 4, 22, 43, 27, tzinfo=timezone.utc
+        )
+        assert saved_metadata.endtime == datetime(
+            2020, 12, 4, 23, 43, 27, tzinfo=timezone.utc
+        )
+        assert saved_metadata.category == MetadataCategory.INSTRUMENT
+        assert saved_metadata.network == "NT"
+        assert saved_metadata.station == "BOU"
+        assert saved_metadata.metadata_ == {
+            "type": "Narod",
+            "channels": {
+                "U": [
+                    {"channel": "U_Volt", "offset": 0, "scale": 100},
+                    {"channel": "U_Bin", "offset": 0, "scale": 500},
+                ],
+                "V": [
+                    {"channel": "V_Volt", "offset": 0, "scale": 100},
+                    {"channel": "V_Bin", "offset": 0, "scale": 500},
+                ],
+                "W": [
+                    {"channel": "W_Volt", "offset": 0, "scale": 100},
+                    {"channel": "W_Bin", "offset": 0, "scale": 500},
+                ],
+            },
+        }
+
+        # assert defaults are set correctly
+        assert saved_metadata.priority == 1
+        assert saved_metadata.data_valid == True
+        assert saved_metadata.status == "updated"
+
+        # assert the metadata returned is the same as the saved metadata
+        assert compare_metadata_objects(a=saved_metadata, b=returned_metadata)
+
+
+@pytest.mark.asyncio
+async def test_create_metadata_with_times_as_naive_datetime(
+    test_engine, metadata_with_datetime_naive
+):
+    returned_metadata = await MetadataDatabaseFactory(
+        engine=test_engine
+    ).create_metadata(new_metadata=metadata_with_datetime_naive)
+
+    # get the newly saved record to verify it was created correctly
+    with Session(test_engine) as test_db_session:
+        saved_metadata = test_db_session.get(MetadataTable, returned_metadata.id)
+
+        # assert the saved metadata fields are set correctly
+        assert saved_metadata.created_by == "test_metadata.py"
+
+        # assert native datetimes are converted to aware datetimes by model_validate
+        assert saved_metadata.created_time == datetime(
+            2021, 1, 3, 17, 24, 40, tzinfo=timezone.utc
+        )
+        assert saved_metadata.updated_time == datetime(
+            2021, 2, 3, 17, 24, 40, tzinfo=timezone.utc
+        )
+        assert saved_metadata.starttime == datetime(
+            2020, 12, 5, 22, 43, 27, tzinfo=timezone.utc
+        )
+        assert saved_metadata.endtime == datetime(
+            2020, 12, 5, 23, 43, 27, tzinfo=timezone.utc
+        )
+        assert saved_metadata.category == MetadataCategory.INSTRUMENT
+        assert saved_metadata.network == "NT"
+        assert saved_metadata.station == "BOU"
+        assert saved_metadata.metadata_ == {
+            "type": "Narod",
+            "channels": {
+                "U": [
+                    {"channel": "U_Volt", "offset": 0, "scale": 100},
+                    {"channel": "U_Bin", "offset": 0, "scale": 500},
+                ],
+                "V": [
+                    {"channel": "V_Volt", "offset": 0, "scale": 100},
+                    {"channel": "V_Bin", "offset": 0, "scale": 500},
+                ],
+                "W": [
+                    {"channel": "W_Volt", "offset": 0, "scale": 100},
+                    {"channel": "W_Bin", "offset": 0, "scale": 500},
+                ],
+            },
+        }
+
+        # assert defaults are set correctly
+        assert saved_metadata.priority == 1
+        assert saved_metadata.data_valid == True
+        assert saved_metadata.status == "updated"
+
+        # assert the metadata returned is the same as the saved metadata
+        assert compare_metadata_objects(a=saved_metadata, b=returned_metadata)
+
+
+@pytest.mark.asyncio
+async def test_create_metadata_with_times_as_strings(
+    test_engine, metadata_with_time_strings
+):
+    returned_metadata = await MetadataDatabaseFactory(
+        engine=test_engine
+    ).create_metadata(new_metadata=metadata_with_time_strings)
+
+    # get the newly saved record to verify it was created correctly
+    with Session(test_engine) as test_db_session:
+        saved_metadata = test_db_session.get(MetadataTable, returned_metadata.id)
+
+        # assert the saved metadata fields are set correctly
+        assert saved_metadata.created_by == "test_metadata.py"
+
+        # assert native datetimes are converted to aware datetimes by model_validate
+        assert saved_metadata.created_time == datetime(
+            2021, 1, 3, 17, 24, 40, tzinfo=timezone.utc
+        )
+        assert saved_metadata.starttime == datetime(
+            2020, 12, 3, 22, 43, 27, tzinfo=timezone.utc
+        )
+        assert saved_metadata.endtime == datetime(
+            2020, 12, 3, 23, 43, 27, tzinfo=timezone.utc
+        )
+        assert saved_metadata.updated_time == None
+        assert saved_metadata.category == MetadataCategory.INSTRUMENT
+        assert saved_metadata.network == "NT"
+        assert saved_metadata.station == "BOU"
+        assert saved_metadata.metadata_ == {
+            "type": "Narod",
+            "channels": {
+                "U": [
+                    {"channel": "U_Volt", "offset": 0, "scale": 100},
+                    {"channel": "U_Bin", "offset": 0, "scale": 500},
+                ],
+                "V": [
+                    {"channel": "V_Volt", "offset": 0, "scale": 100},
+                    {"channel": "V_Bin", "offset": 0, "scale": 500},
+                ],
+                "W": [
+                    {"channel": "W_Volt", "offset": 0, "scale": 100},
+                    {"channel": "W_Bin", "offset": 0, "scale": 500},
+                ],
+            },
+        }
+
+        # assert defaults are set correctly
+        assert saved_metadata.priority == 1
+        assert saved_metadata.data_valid == True
+        assert saved_metadata.status == "new"
+
+        # assert the metadata returned is the same as the saved metadata
+        assert compare_metadata_objects(a=saved_metadata, b=returned_metadata)
+
+
+@pytest.mark.asyncio
+async def test_create_metadata_invalid_metadata(test_engine, instrument_metadata):
+    instrument_metadata.starttime = "invalid starttime"
+
+    try:
+        await MetadataDatabaseFactory(engine=test_engine).create_metadata(
+            new_metadata=instrument_metadata
+        )
+    except Exception as e:
+        assert e is not None
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_with_starttime(test_engine, get_all_metadata):
+    # clear in memory Metadata table
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    # save a bunch of metadata to the test database
+    await MetadataDatabaseFactory(engine=test_engine).batch_create_metadata(
+        get_all_metadata
+    )
+
+    query = MetadataQuery(
+        category=MetadataCategory.INSTRUMENT, starttime="2019-01-01T00:00:00"
+    )
+
+    results = await MetadataDatabaseFactory(engine=test_engine).get_metadata(
+        query=query
+    )
+
+    # expect 5 metadata rows to be returned. 4 with the endtime after the query starttime and 1 with no endtime
+    assert len(results) == 5
+
+    for result in results:
+        assert result.category == MetadataCategory.INSTRUMENT
+        # assert metadata is set correctly and not as metadata_
+        assert result.metadata["type"] != None
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_with_endtime(test_engine, get_all_metadata):
+    # clear in memory Metadata table
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    # save a bunch of metadata to the test database
+    await MetadataDatabaseFactory(engine=test_engine).batch_create_metadata(
+        get_all_metadata
+    )
+
+    query = MetadataQuery(
+        category=MetadataCategory.INSTRUMENT, endtime="2020-12-04T00:00:00"
+    )
+
+    results = await MetadataDatabaseFactory(engine=test_engine).get_metadata(
+        query=query
+    )
+
+    # expect 3 metadata rows to be returned. 2 with the starttime before the query endtime and 1 with no endtime
+    assert len(results) == 3
+
+    for result in results:
+        assert result.category == MetadataCategory.INSTRUMENT
+        # assert metadata is set correctly and not as metadata_
+        assert result.metadata["type"] != None
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_with_channel(test_engine, get_all_metadata):
+    # clear in memory Metadata table
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    # save a bunch of metadata to the test database
+    await MetadataDatabaseFactory(engine=test_engine).batch_create_metadata(
+        get_all_metadata
+    )
+
+    query = MetadataQuery(
+        category=MetadataCategory.INSTRUMENT, endtime="2020-12-04T00:00:00", channel="F"
+    )
+
+    results = await MetadataDatabaseFactory(engine=test_engine).get_metadata(
+        query=query
+    )
+
+    assert len(results) == 1
+    assert results[0].channel == "F"
+    assert results[0].metadata["type"] == "FGE"
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_with_location(test_engine, get_all_metadata):
+    # clear in memory Metadata table
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    # save a bunch of metadata to the test database
+    await MetadataDatabaseFactory(engine=test_engine).batch_create_metadata(
+        get_all_metadata
+    )
+
+    query = MetadataQuery(
+        category=MetadataCategory.INSTRUMENT,
+        endtime="2020-12-04T00:00:00",
+        location="R0",
+    )
+
+    results = await MetadataDatabaseFactory(engine=test_engine).get_metadata(
+        query=query
+    )
+
+    assert len(results) == 1
+    assert results[0].location == "R0"
+    assert results[0].metadata["type"] == "FGE"
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_with_single_status(test_engine, get_all_metadata):
+    # clear in memory Metadata table
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    # save a bunch of metadata to the test database
+    await MetadataDatabaseFactory(engine=test_engine).batch_create_metadata(
+        get_all_metadata
+    )
+
+    query = MetadataQuery(
+        category=MetadataCategory.INSTRUMENT,
+        starttime="2019-01-01T00:00:00",
+        status=["new"],
+    )
+
+    results = await MetadataDatabaseFactory(engine=test_engine).get_metadata(
+        query=query
+    )
+
+    # expect 2 metadata rows to be returned. the 1st object and the 5th where status is set to new by default
+    assert len(results) == 2
+
+    for result in results:
+        assert result.category == MetadataCategory.INSTRUMENT
+        assert result.status == "new"
+        assert result.metadata["type"] != None
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_with_multiple_statuses(test_engine, get_all_metadata):
+    # clear in memory Metadata table
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    # save a bunch of metadata to the test database
+    await MetadataDatabaseFactory(engine=test_engine).batch_create_metadata(
+        get_all_metadata
+    )
+
+    query = MetadataQuery(
+        category=MetadataCategory.INSTRUMENT,
+        starttime="2019-01-01T00:00:00",
+        status=["new", "updated"],
+    )
+
+    results = await MetadataDatabaseFactory(engine=test_engine).get_metadata(
+        query=query
+    )
+
+    # expect 5 metadata rows to be returned. all of the instrument metadata has a "new" or "updated" status and passes the starttime conditional
+    assert len(results) == 5
+
+    for result in results:
+        assert result.category == MetadataCategory.INSTRUMENT
+        assert result.status == "new" or result.status == "updated"
+        assert result.metadata["type"] != None
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_with_created_before(test_engine, get_all_metadata):
+    # clear in memory Metadata table
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    # save a bunch of metadata to the test database
+    await MetadataDatabaseFactory(engine=test_engine).batch_create_metadata(
+        get_all_metadata
+    )
+
+    query = MetadataQuery(
+        created_before=UTCDateTime(2025, 1, 1),
+        category=MetadataCategory.INSTRUMENT,
+        starttime="2019-01-01T00:00:00",
+        status=["new", "updated"],
+    )
+
+    results = await MetadataDatabaseFactory(engine=test_engine).get_metadata(
+        query=query
+    )
+
+    # expect 4 metadata rows to be returned. all of the instrument metadata except for the first one
+    assert len(results) == 4
+
+    for result in results:
+        assert result.category == MetadataCategory.INSTRUMENT
+        assert result.status == "new" or result.status == "updated"
+        assert result.metadata["type"] == "Narod"
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_with_created_after(test_engine, get_all_metadata):
+    # clear in memory Metadata table
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    # save a bunch of metadata to the test database
+    await MetadataDatabaseFactory(engine=test_engine).batch_create_metadata(
+        get_all_metadata
+    )
+
+    query = MetadataQuery(
+        created_after=UTCDateTime(2019, 1, 1),
+        category=MetadataCategory.INSTRUMENT,
+        starttime="2019-01-01T00:00:00",
+        status=["new", "updated"],
+    )
+
+    results = await MetadataDatabaseFactory(engine=test_engine).get_metadata(
+        query=query
+    )
+
+    # expect 5 all of the instrument metadata rows to be returned
+    assert len(results) == 5
+
+    for result in results:
+        assert result.category == MetadataCategory.INSTRUMENT
+        assert result.status == "new" or result.status == "updated"
+        assert result.metadata["type"] != None
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_returns_one_in_list(test_engine, get_all_metadata):
+    # clear in memory Metadata table
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    # save a bunch of metadata to the test database
+    await MetadataDatabaseFactory(engine=test_engine).batch_create_metadata(
+        get_all_metadata
+    )
+
+    query = MetadataQuery(
+        created_after=UTCDateTime(2019, 1, 1), category=MetadataCategory.OBSERVATORY
+    )
+
+    results = await MetadataDatabaseFactory(engine=test_engine).get_metadata(
+        query=query
+    )
+
+    # expect 1 metadata observatory object to be returned as a list
+    assert len(results) == 1
+    assert results[0].category == MetadataCategory.OBSERVATORY
+    assert results[0].metadata == {
+        "id": "BRW",
+        "name": "Barrow",
+        "marks": [{"name": "m", "azimuth": "1"}],
+        "piers": [{"name": "p", "correction": "12"}],
+        "agency": "USGS",
+        "latitude": 71.322,
+        "elevation": 10,
+        "longitude": 203.378,
+        "agency_name": "United States Geological Survey (USGS)",
+        "electronics": ["1"],
+        "theodolites": ["12"],
+        "declination_base": 10589,
+        "sensor_orientation": "HDZF",
+    }
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_returns_empty_list(test_engine, get_all_metadata):
+    # clear in memory Metadata table
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    # save a bunch of metadata to the test database
+    await MetadataDatabaseFactory(engine=test_engine).batch_create_metadata(
+        get_all_metadata
+    )
+
+    query = MetadataQuery(category=MetadataCategory.READING)
+
+    results = await MetadataDatabaseFactory(engine=test_engine).get_metadata(
+        query=query
+    )
+
+    # assert that it returns an empty list and does not error
+    assert len(results) == 0
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_by_id(test_engine, get_all_metadata):
+    # clear in memory Metadata table
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    # save a bunch of metadata to the test database
+    await MetadataDatabaseFactory(engine=test_engine).batch_create_metadata(
+        get_all_metadata
+    )
+
+    result = await MetadataDatabaseFactory(engine=test_engine).get_metadata_by_id(3)
+    assert result.id == 3
+    assert result.created_time == datetime(2021, 1, 3, 17, 24, 40, tzinfo=timezone.utc)
+    assert result.updated_time == datetime(2021, 2, 3, 17, 24, 40, tzinfo=timezone.utc)
+    assert result.starttime == datetime(2020, 12, 4, 22, 43, 27, tzinfo=timezone.utc)
+    assert result.endtime == datetime(2020, 12, 4, 23, 43, 27, tzinfo=timezone.utc)
+    assert result.category == MetadataCategory.INSTRUMENT
+    assert result.network == "NT"
+    assert result.station == "BOU"
+    assert result.location == "R0"
+    assert result.metadata == {
+        "type": "Narod",
+        "channels": {
+            "U": [
+                {"channel": "U_Volt", "offset": 0, "scale": 100},
+                {"channel": "U_Bin", "offset": 0, "scale": 500},
+            ],
+            "V": [
+                {"channel": "V_Volt", "offset": 0, "scale": 100},
+                {"channel": "V_Bin", "offset": 0, "scale": 500},
+            ],
+            "W": [
+                {"channel": "W_Volt", "offset": 0, "scale": 100},
+                {"channel": "W_Bin", "offset": 0, "scale": 500},
+            ],
+        },
+    }
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_by_id_no_metadatareturned(test_engine):
+    result = await MetadataDatabaseFactory(engine=test_engine).get_metadata_by_id(12345)
+
+    # assert the result is None and not an error
+    assert result == None
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_by_id_invalid(test_engine):
+    try:
+        await MetadataDatabaseFactory(engine=test_engine).get_metadata_by_id("3")
+    except Exception as e:
+        assert str(e) == "expected an int"
+
+
+def compare_metadata_and_metadatahistory_objects(
+    a: Metadata, b: MetadataHistoryTable
+) -> bool:
+    if a.id != b.metadata_id:
+        raise ValueError(
+            "ids do not match. a.id = "
+            + str(a.id)
+            + " and b.metadata_id = "
+            + str(b.metadata_id)
+        )
+    if a.created_by != b.created_by:
+        raise ValueError(
+            "created_by does not match. a.created_by = "
+            + a.created_by
+            + " and b.created_by = "
+            + b.created_by
+        )
+    if a.created_time != b.created_time:
+        raise ValueError(
+            "created_time does not match. a.created_time = "
+            + a.created_time
+            + " and b.created_time = "
+            + b.created_time
+        )
+    if a.updated_by != b.updated_by:
+        raise ValueError(
+            "updated_by does not match. a.updated_by = "
+            + a.updated_by
+            + " and b.updated_by = "
+            + b.updated_by
+        )
+    if a.updated_time != b.updated_time:
+        raise ValueError(
+            "updated_time does not match. a.updated_time = "
+            + a.updated_time
+            + " and b.updated_time = "
+            + b.updated_time
+        )
+    if a.starttime != b.starttime:
+        raise ValueError(
+            "starttime does not match. a.starttime = "
+            + a.starttime
+            + " and b.starttime = "
+            + b.starttime
+        )
+    if a.endtime != b.endtime:
+        raise ValueError(
+            "endtime does not match. a.endtime = "
+            + a.endtime
+            + " and b.endtime = "
+            + b.endtime
+        )
+    if a.network != b.network:
+        raise ValueError(
+            "network does not match. a.network = "
+            + a.network
+            + " and b.network = "
+            + b.network
+        )
+    if a.station != b.station:
+        raise ValueError(
+            "station does not match. a.station = "
+            + a.station
+            + " and b.station = "
+            + b.station
+        )
+    if a.channel != b.channel:
+        raise ValueError(
+            "channel does not match. a.channel = "
+            + a.channel
+            + " and b.channel = "
+            + b.channel
+        )
+    if a.location != b.location:
+        raise ValueError(
+            "location does not match. a.location = "
+            + a.location
+            + " and b.location = "
+            + b.location
+        )
+    if a.category != b.category:
+        raise ValueError(
+            "category does not match. a.category = "
+            + a.category
+            + " and b.category = "
+            + b.category
+        )
+    if a.priority != b.priority:
+        raise ValueError(
+            "priority does not match. a.priority = "
+            + a.priority
+            + " and b.priority = "
+            + b.priority
+        )
+    if a.data_valid != b.data_valid:
+        raise ValueError(
+            "data_valid does not match. a.data_valid = "
+            + a.data_valid
+            + " and b.data_valid = "
+            + b.data_valid
+        )
+    if a.status != b.status:
+        raise ValueError(
+            "status does not match. a.status = "
+            + a.status
+            + " and b.status = "
+            + b.status
+        )
+    if a.metadata != b.metadata_:
+        raise ValueError(
+            "metadata does not match. a.metadata = "
+            + a.metadata
+            + " and b.metadata_ = "
+            + b.metadata_
+        )
+    if a.comment != b.comment:
+        raise ValueError(
+            "comment does not match. a.comment = "
+            + a.comment
+            + " and b.comment = "
+            + b.comment
+        )
+    if a.review_comment != b.review_comment:
+        raise ValueError(
+            "review_comment does not match. a.review_comment = "
+            + a.review_comment
+            + " and b.review_comment = "
+            + b.review_comment
+        )
+    return True
+
+
+@pytest.mark.asyncio
+async def test_update_metadata(test_engine, observatory_metadata):
+    with Session(test_engine) as test_db_session:
+        # clear in memory MetadataHistoryTable table
+        statement = delete(MetadataHistoryTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+        # create a metadata row to update
+        created_metadata = await MetadataDatabaseFactory(
+            engine=test_engine
+        ).create_metadata(observatory_metadata)
+
+        updated_metadata = MetadataUpdate(
+            id=created_metadata.id, comment="Adding a comment", status="reviewed"
+        )
+
+        returned_metadata = await MetadataDatabaseFactory(
+            engine=test_engine
+        ).update_metadata(updated_metadata=updated_metadata, updated_by="tester")
+
+        # get metadatahistory row
+        statement = select(MetadataHistoryTable).where(
+            MetadataHistoryTable.metadata_id == created_metadata.id
+        )
+        results = test_db_session.exec(statement)
+        metadatahistory = results.one()
+
+        # assert the original created metadata saved to the metadatahistory table is identical to the original created metadata aside from id and metadataid
+        assert compare_metadata_and_metadatahistory_objects(
+            a=created_metadata, b=metadatahistory
+        )
+
+        # get metadata row to assert it was updated with the new values
+        newly_updated_metadata = test_db_session.get(MetadataTable, created_metadata.id)
+
+        # assert values not included in the MetadataUpdate object are not changed
+        assert newly_updated_metadata.id == created_metadata.id
+        assert newly_updated_metadata.created_by == created_metadata.created_by
+        assert newly_updated_metadata.created_time == created_metadata.created_time
+        assert newly_updated_metadata.starttime == created_metadata.starttime
+        assert newly_updated_metadata.endtime == created_metadata.endtime
+        assert newly_updated_metadata.network == created_metadata.network
+        assert newly_updated_metadata.station == created_metadata.station
+        assert newly_updated_metadata.channel == created_metadata.channel
+        assert newly_updated_metadata.location == created_metadata.location
+        assert newly_updated_metadata.category == created_metadata.category
+        assert newly_updated_metadata.priority == created_metadata.priority
+        assert newly_updated_metadata.data_valid == created_metadata.data_valid
+        assert newly_updated_metadata.metadata_ == created_metadata.metadata
+        assert newly_updated_metadata.review_comment == created_metadata.review_comment
+
+        # assert updated_by is set correctly
+        assert newly_updated_metadata.updated_by == "tester"
+        discard = timedelta(
+            minutes=newly_updated_metadata.created_time.minute,
+            seconds=newly_updated_metadata.created_time.second,
+            microseconds=newly_updated_metadata.created_time.microsecond,
+        )
+        saved_time = newly_updated_metadata.created_time - discard
+
+        now = UTCDateTime.now()
+        current_time = datetime(
+            year=now.year,
+            month=now.month,
+            day=now.day,
+            hour=now.hour,
+            tzinfo=timezone.utc,
+        )
+
+        # assert updated_time is set to within one minute of now
+        assert current_time == saved_time
+
+        # assert the values included in the MetadataUpdate object are changed to the new values
+        assert newly_updated_metadata.status == "reviewed"
+        assert newly_updated_metadata.comment == "Adding a comment"
+
+        # assert the returned updated metadata is the same as the metadata table row
+        assert compare_metadata_objects(a=newly_updated_metadata, b=returned_metadata)
+
+
+@pytest.mark.asyncio
+async def test_update_metadata_remove_field(test_engine, observatory_metadata):
+    with Session(test_engine) as test_db_session:
+        # clear in memory MetadataHistoryTable table
+        statement = delete(MetadataHistoryTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+        # create a metadata row to update
+        observatory_metadata.comment = "to be deleted"
+        created_metadata = await MetadataDatabaseFactory(
+            engine=test_engine
+        ).create_metadata(observatory_metadata)
+
+        updated_metadata = MetadataUpdate(
+            id=created_metadata.id, comment=None, status="reviewed"
+        )
+
+        returned_metadata = await MetadataDatabaseFactory(
+            engine=test_engine
+        ).update_metadata(updated_metadata=updated_metadata, updated_by="tester")
+
+        # get metadatahistory row
+        statement = select(MetadataHistoryTable).where(
+            MetadataHistoryTable.metadata_id == created_metadata.id
+        )
+        results = test_db_session.exec(statement)
+        metadatahistory = results.one()
+
+        # assert the original created metadata saved to the metadatahistory table is identical to the original created metadata aside from id and metadataid
+        assert compare_metadata_and_metadatahistory_objects(
+            a=created_metadata, b=metadatahistory
+        )
+
+        # get metadata row to assert it was updated with the new values
+        newly_updated_metadata = test_db_session.get(MetadataTable, created_metadata.id)
+
+        # assert values not included in the MetadataUpdate object are not changed
+        assert newly_updated_metadata.id == created_metadata.id
+        assert newly_updated_metadata.created_by == created_metadata.created_by
+        assert newly_updated_metadata.created_time == created_metadata.created_time
+        assert newly_updated_metadata.starttime == created_metadata.starttime
+        assert newly_updated_metadata.endtime == created_metadata.endtime
+        assert newly_updated_metadata.network == created_metadata.network
+        assert newly_updated_metadata.station == created_metadata.station
+        assert newly_updated_metadata.channel == created_metadata.channel
+        assert newly_updated_metadata.location == created_metadata.location
+        assert newly_updated_metadata.category == created_metadata.category
+        assert newly_updated_metadata.priority == created_metadata.priority
+        assert newly_updated_metadata.data_valid == created_metadata.data_valid
+        assert newly_updated_metadata.metadata_ == created_metadata.metadata
+        assert newly_updated_metadata.review_comment == created_metadata.review_comment
+
+        # assert updated_by is set correctly
+        assert newly_updated_metadata.updated_by == "tester"
+        discard = timedelta(
+            minutes=newly_updated_metadata.created_time.minute,
+            seconds=newly_updated_metadata.created_time.second,
+            microseconds=newly_updated_metadata.created_time.microsecond,
+        )
+        saved_time = newly_updated_metadata.created_time - discard
+
+        now = UTCDateTime.now()
+        current_time = datetime(
+            year=now.year,
+            month=now.month,
+            day=now.day,
+            hour=now.hour,
+            tzinfo=timezone.utc,
+        )
+
+        # assert updated_time is set to within one minute of now
+        assert current_time == saved_time
+
+        # assert the comment is removed
+        assert newly_updated_metadata.status == "reviewed"
+        assert newly_updated_metadata.comment == None
+
+        # assert the returned updated metadata is the same as the metadata table row
+        assert compare_metadata_objects(a=newly_updated_metadata, b=returned_metadata)
+
+
+@pytest.mark.asyncio
+async def test_update_metadata_multiple_updates(test_engine, observatory_metadata):
+    with Session(test_engine) as test_db_session:
+        # clear in memory MetadataHistoryTable table
+        statement = delete(MetadataHistoryTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+        # create a metadata row to update
+        created_metadata = await MetadataDatabaseFactory(
+            engine=test_engine
+        ).create_metadata(observatory_metadata)
+
+        updated_metadata = MetadataUpdate(
+            id=created_metadata.id,
+            metadata={
+                "id": "BRW",
+                "name": "Barrow",
+                "marks": [{"name": "m", "azimuth": "1"}],
+                "piers": [{"name": "p", "correction": "12"}],
+                "agency": "USGS",
+                "latitude": 100.00,
+                "elevation": 10,
+                "longitude": 203.378,
+                "agency_name": "United States Geological Survey (USGS)",
+                "electronics": ["1"],
+                "theodolites": ["12"],
+                "declination_base": 10589,
+                "sensor_orientation": "HDZF",
+            },
+            status="updated",
+        )
+
+        returned_metadata = await MetadataDatabaseFactory(
+            engine=test_engine
+        ).update_metadata(updated_metadata=updated_metadata, updated_by="tester")
+
+        # get metadatahistory row
+        statement = select(MetadataHistoryTable).where(
+            MetadataHistoryTable.metadata_id == created_metadata.id
+        )
+        results = test_db_session.exec(statement)
+        metadatahistory = results.one()
+
+        # assert the original created metadata saved to the metadatahistory table is identical to the original created metadata aside from id and metadataid
+        assert compare_metadata_and_metadatahistory_objects(
+            a=created_metadata, b=metadatahistory
+        )
+
+        # get metadata row to assert it was updated with the new values
+        newly_updated_metadata = test_db_session.get(MetadataTable, created_metadata.id)
+
+        # assert values not included in the MetadataUpdate object are not changed
+        assert newly_updated_metadata.id == created_metadata.id
+        assert newly_updated_metadata.created_by == created_metadata.created_by
+        assert newly_updated_metadata.created_time == created_metadata.created_time
+        assert newly_updated_metadata.starttime == created_metadata.starttime
+        assert newly_updated_metadata.endtime == created_metadata.endtime
+        assert newly_updated_metadata.network == created_metadata.network
+        assert newly_updated_metadata.station == created_metadata.station
+        assert newly_updated_metadata.channel == created_metadata.channel
+        assert newly_updated_metadata.location == created_metadata.location
+        assert newly_updated_metadata.category == created_metadata.category
+        assert newly_updated_metadata.priority == created_metadata.priority
+        assert newly_updated_metadata.data_valid == created_metadata.data_valid
+        assert newly_updated_metadata.comment == created_metadata.comment
+        assert newly_updated_metadata.review_comment == created_metadata.review_comment
+
+        # assert updated_by is set correctly
+        assert newly_updated_metadata.updated_by == "tester"
+        discard = timedelta(
+            minutes=newly_updated_metadata.created_time.minute,
+            seconds=newly_updated_metadata.created_time.second,
+            microseconds=newly_updated_metadata.created_time.microsecond,
+        )
+        saved_time = newly_updated_metadata.created_time - discard
+
+        now = UTCDateTime.now()
+        current_time = datetime(
+            year=now.year,
+            month=now.month,
+            day=now.day,
+            hour=now.hour,
+            tzinfo=timezone.utc,
+        )
+
+        # assert updated_time is set to within one minute of now
+        assert current_time == saved_time
+
+        # assert the values included in the MetadataUpdate object are changed to the new values
+        assert newly_updated_metadata.status == "updated"
+        assert newly_updated_metadata.metadata_ == {
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "12"}],
+            "agency": "USGS",
+            "latitude": 100.00,
+            "elevation": 10,
+            "longitude": 203.378,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        }
+
+        # assert the returned updated metadata is the same as the metadata table row
+        assert compare_metadata_objects(a=newly_updated_metadata, b=returned_metadata)
+
+        # update the same metadata again
+        second_update = MetadataUpdate(id=created_metadata.id, status="reviewed")
+
+        second_returned = await MetadataDatabaseFactory(
+            engine=test_engine
+        ).update_metadata(updated_metadata=second_update, updated_by="different tester")
+
+        # get the associated metadatahistory rows
+        second_statement = select(MetadataHistoryTable).where(
+            MetadataHistoryTable.metadata_id == created_metadata.id
+        )
+        second_results = test_db_session.exec(second_statement)
+        metadatahistory_rows = second_results.all()
+
+        assert len(metadatahistory_rows) == 2
+        for row in metadatahistory_rows:
+            assert row.metadata_id == created_metadata.id
+            # assert the values that weren't updated didn't change
+            assert row.created_by == created_metadata.created_by
+            assert row.created_time == created_metadata.created_time
+            assert row.starttime == created_metadata.starttime
+            assert row.endtime == created_metadata.endtime
+            assert row.network == created_metadata.network
+            assert row.station == created_metadata.station
+            assert row.channel == created_metadata.channel
+            assert row.location == created_metadata.location
+            assert row.category == created_metadata.category
+            assert row.priority == created_metadata.priority
+            assert row.data_valid == created_metadata.data_valid
+            assert row.review_comment == created_metadata.review_comment
+
+        assert metadatahistory_rows[0].status == "new"
+        assert metadatahistory_rows[1].status == "updated"
+
+        assert metadatahistory_rows[0].metadata_ == {
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "12"}],
+            "agency": "USGS",
+            "latitude": 71.322,
+            "elevation": 10,
+            "longitude": 203.378,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        }
+        assert metadatahistory_rows[1].metadata_ == {
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "12"}],
+            "agency": "USGS",
+            "latitude": 100.00,
+            "elevation": 10,
+            "longitude": 203.378,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        }
+
+        assert metadatahistory_rows[1].updated_by == "tester"
+
+        assert second_returned.updated_by == "different tester"
+        assert second_returned.status == "reviewed"
+
+
+@pytest.mark.asyncio
+async def test_update_metadata_invalid_id(test_engine):
+    updated_metadata = MetadataUpdate(
+        id=476487562, comment="Adding a comment", status="reviewed"
+    )
+
+    try:
+        await MetadataDatabaseFactory(engine=test_engine).update_metadata(
+            updated_metadata=updated_metadata, updated_by="tester"
+        )
+    except Exception as e:
+        assert str(e) == "metadata not found"
diff --git a/test/api_test/db/factories/MetadataHistoryDatabaseFactory_test.py b/test/api_test/db/factories/MetadataHistoryDatabaseFactory_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..542d04f997023b89d6b89f858f6cb57b663ec575
--- /dev/null
+++ b/test/api_test/db/factories/MetadataHistoryDatabaseFactory_test.py
@@ -0,0 +1,385 @@
+import pytest
+
+from datetime import datetime, timezone
+from sqlmodel import Session, delete
+
+from geomagio.api.db.models.metadata import (
+    MetadataTable,
+    MetadataQuery,
+    MetadataCategory,
+)
+from geomagio.api.db.models.metadata_history import MetadataHistoryTable
+from geomagio.api.db.factories.MetadataHistoryDatabaseFactory import (
+    MetadataHistoryDatabaseFactory,
+)
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_history(test_engine, get_all_related_metadata_history):
+    # clear in memory metadata_history table
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataHistoryTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    # save a bunch of metadata_history objects to the test database
+    await MetadataHistoryDatabaseFactory(
+        engine=test_engine
+    ).batch_create_metadata_history(get_all_related_metadata_history)
+
+    query = MetadataQuery(
+        category=MetadataCategory.OBSERVATORY, starttime="2019-01-01T00:00:00"
+    )
+
+    results = await MetadataHistoryDatabaseFactory(
+        engine=test_engine
+    ).get_metadata_history(query=query)
+    assert len(results) == 3
+
+    for result in results:
+        assert result.category == MetadataCategory.OBSERVATORY
+        assert result.metadata["name"] == "Barrow"
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_history_returns_one(test_engine, metadata_history_1):
+    # clear in memory metadata_history table
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataHistoryTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    # save a metadata_history object to the test database
+    await MetadataHistoryDatabaseFactory(
+        engine=test_engine
+    ).batch_create_metadata_history([metadata_history_1])
+
+    query = MetadataQuery(
+        category=MetadataCategory.OBSERVATORY, starttime="2019-01-01T00:00:00"
+    )
+
+    results = await MetadataHistoryDatabaseFactory(
+        engine=test_engine
+    ).get_metadata_history(query=query)
+    assert len(results) == 1
+    assert results[0].category == MetadataCategory.OBSERVATORY
+    assert results[0].metadata["name"] == "Barrow"
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_history_returns_none(test_engine):
+    # clear in memory metadata_history table
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataHistoryTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    query = MetadataQuery(
+        category=MetadataCategory.OBSERVATORY, starttime="2019-01-01T00:00:00"
+    )
+
+    results = await MetadataHistoryDatabaseFactory(
+        engine=test_engine
+    ).get_metadata_history(query=query)
+
+    # assert this returns an empty list and does not error
+    assert len(results) == 0
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_history_by_id(
+    test_engine, get_all_related_metadata_history
+):
+    # clear in memory metadata_history table
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataHistoryTable)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    # save a bunch of metadata to the test database
+    await MetadataHistoryDatabaseFactory(
+        engine=test_engine
+    ).batch_create_metadata_history(get_all_related_metadata_history)
+
+    result = await MetadataHistoryDatabaseFactory(
+        engine=test_engine
+    ).get_metadata_history_by_id(2)
+    assert result.id == 2
+    assert result.metadata_id == 38742
+    assert result.created_by == "test_metadata.py"
+    assert result.updated_by == "tester"
+    assert result.updated_time == datetime(2023, 1, 21, 3, 30, 43, tzinfo=timezone.utc)
+    assert result.starttime == datetime(2022, 2, 3, 22, 43, 27, tzinfo=timezone.utc)
+    assert result.endtime == datetime(2022, 2, 3, 23, 43, 27, tzinfo=timezone.utc)
+    assert result.category == MetadataCategory.OBSERVATORY
+    assert result.network == "NT"
+    assert result.station == "BRW"
+    assert result.metadata == {
+        "id": "BRW",
+        "name": "Barrow",
+        "marks": [{"name": "m", "azimuth": "1"}],
+        "piers": [{"name": "p", "correction": "22"}],
+        "agency": "USGS",
+        "latitude": 71.322,
+        "elevation": 10,
+        "longitude": 203.378,
+        "agency_name": "United States Geological Survey (USGS)",
+        "electronics": ["1"],
+        "theodolites": ["12"],
+        "declination_base": 10589,
+        "sensor_orientation": "HDZF",
+    }
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_by_id_no_metadata_returned(test_engine):
+    result = await MetadataHistoryDatabaseFactory(
+        engine=test_engine
+    ).get_metadata_history_by_id(12345)
+
+    # assert the result is None and not an error
+    assert result == None
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_by_id_invalid(test_engine):
+    try:
+        await MetadataHistoryDatabaseFactory(
+            engine=test_engine
+        ).get_metadata_history_by_id("3")
+    except Exception as e:
+        assert str(e) == "expected an int"
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_history_by_metadata_id(
+    test_engine,
+    current_metadata_for_related_metadata_history,
+    get_all_related_metadata_history,
+):
+    # clear metadata and metadata_history tables
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataTable)
+        history_statement = delete(MetadataHistoryTable)
+        test_db_session.exec(statement)
+        test_db_session.exec(history_statement)
+        test_db_session.commit()
+
+        # save related metadata directly to db to make sure we're setting the right id
+        test_db_session.add(current_metadata_for_related_metadata_history)
+        test_db_session.commit()
+
+        # save related metadata_history
+        await MetadataHistoryDatabaseFactory(
+            engine=test_engine
+        ).batch_create_metadata_history(get_all_related_metadata_history)
+
+        returned = await MetadataHistoryDatabaseFactory(
+            engine=test_engine
+        ).get_metadata_history_by_metadata_id(metadata_id=38742)
+        assert len(returned) == 4
+
+        # assert the first object is the metadata object
+        assert returned[0].id == 38742
+        assert returned[0].created_by == "test_metadata.py"
+        assert returned[0].created_time == datetime(
+            2025, 1, 22, 13, 45, 27, tzinfo=timezone.utc
+        )
+        assert returned[0].updated_by == "another_tester"
+        assert returned[0].updated_time == datetime(
+            2023, 6, 1, 13, 45, 27, tzinfo=timezone.utc
+        )
+        assert returned[0].starttime == datetime(
+            2022, 2, 3, 22, 43, 27, tzinfo=timezone.utc
+        )
+        assert returned[0].endtime == datetime(
+            2022, 2, 3, 23, 43, 27, tzinfo=timezone.utc
+        )
+        assert returned[0].category == MetadataCategory.OBSERVATORY
+        assert returned[0].network == "NT"
+        assert returned[0].station == "BRW"
+        assert returned[0].metadata == {
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "22"}],
+            "agency": "USGS",
+            "latitude": 87.32,
+            "elevation": 10,
+            "longitude": 202.234,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        }
+
+        # assert the metadata_history objects are in order of most recent updated_time
+        assert returned[1].id == 3
+        assert returned[1].metadata_id == 38742
+        assert returned[1].created_by == "test_metadata.py"
+        assert returned[1].updated_by == "another_tester"
+        assert returned[1].updated_time == datetime(
+            2023, 1, 22, 13, 30, 43, tzinfo=timezone.utc
+        )
+        assert returned[1].starttime == datetime(
+            2022, 2, 3, 22, 43, 27, tzinfo=timezone.utc
+        )
+        assert returned[1].endtime == datetime(
+            2022, 2, 3, 23, 43, 27, tzinfo=timezone.utc
+        )
+        assert returned[1].category == MetadataCategory.OBSERVATORY
+        assert returned[1].network == "NT"
+        assert returned[1].station == "BRW"
+        assert returned[1].metadata == {
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "22"}],
+            "agency": "USGS",
+            "latitude": 87.32,
+            "elevation": 10,
+            "longitude": 203.378,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        }
+
+        assert returned[2].id == 2
+        assert returned[2].metadata_id == 38742
+        assert returned[2].created_by == "test_metadata.py"
+        assert returned[2].updated_by == "tester"
+        assert returned[2].updated_time == datetime(
+            2023, 1, 21, 3, 30, 43, tzinfo=timezone.utc
+        )
+        assert returned[2].starttime == datetime(
+            2022, 2, 3, 22, 43, 27, tzinfo=timezone.utc
+        )
+        assert returned[2].endtime == datetime(
+            2022, 2, 3, 23, 43, 27, tzinfo=timezone.utc
+        )
+        assert returned[2].category == MetadataCategory.OBSERVATORY
+        assert returned[2].network == "NT"
+        assert returned[2].station == "BRW"
+        assert returned[2].metadata == {
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "22"}],
+            "agency": "USGS",
+            "latitude": 71.322,
+            "elevation": 10,
+            "longitude": 203.378,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        }
+
+        assert returned[3].id == 1
+        assert returned[3].metadata_id == 38742
+        assert returned[3].created_by == "test_metadata.py"
+        assert returned[3].updated_by == None
+        assert returned[3].updated_time == None
+        assert returned[3].starttime == datetime(
+            2022, 2, 3, 22, 43, 27, tzinfo=timezone.utc
+        )
+        assert returned[3].endtime == datetime(
+            2022, 2, 3, 23, 43, 27, tzinfo=timezone.utc
+        )
+        assert returned[3].category == MetadataCategory.OBSERVATORY
+        assert returned[3].network == "NT"
+        assert returned[3].station == "BRW"
+        assert returned[3].metadata == {
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "12"}],
+            "agency": "USGS",
+            "latitude": 71.322,
+            "elevation": 10,
+            "longitude": 203.378,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        }
+
+
+# @pytest.mark.asyncio
+async def test_get_metadata_history_by_metadata_id_no_history(
+    test_engine, current_metadata_for_related_metadata_history
+):
+    # clear metadata and MetadataHistoryTable tables
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataTable)
+        history_statement = delete(MetadataHistoryTable)
+        test_db_session.exec(statement)
+        test_db_session.exec(history_statement)
+        test_db_session.commit()
+
+        # save metadata directly to db to have control over the id
+        test_db_session.add(current_metadata_for_related_metadata_history)
+        test_db_session.commit()
+
+        returned = await MetadataHistoryDatabaseFactory(
+            engine=test_engine
+        ).get_metadata_history_by_metadata_id(metadata_id=38742)
+        assert len(returned) == 1
+
+        # assert the metadata object was returned
+        assert returned[0].id == 38742
+        assert returned[0].created_by == "test_metadata.py"
+        assert returned[0].created_time == datetime(
+            2025, 1, 22, 13, 45, 27, tzinfo=timezone.utc
+        )
+        assert returned[0].updated_by == "another_tester"
+        assert returned[0].updated_time == datetime(
+            2023, 6, 1, 13, 45, 27, tzinfo=timezone.utc
+        )
+        assert returned[0].starttime == datetime(
+            2022, 2, 3, 22, 43, 27, tzinfo=timezone.utc
+        )
+        assert returned[0].endtime == datetime(
+            2022, 2, 3, 23, 43, 27, tzinfo=timezone.utc
+        )
+        assert returned[0].category == MetadataCategory.OBSERVATORY
+        assert returned[0].network == "NT"
+        assert returned[0].station == "BRW"
+        assert returned[0].metadata == {
+            "id": "BRW",
+            "name": "Barrow",
+            "marks": [{"name": "m", "azimuth": "1"}],
+            "piers": [{"name": "p", "correction": "22"}],
+            "agency": "USGS",
+            "latitude": 87.32,
+            "elevation": 10,
+            "longitude": 202.234,
+            "agency_name": "United States Geological Survey (USGS)",
+            "electronics": ["1"],
+            "theodolites": ["12"],
+            "declination_base": 10589,
+            "sensor_orientation": "HDZF",
+        }
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_history_by_metadata_id_no_metadata(test_engine):
+    # clear metadata and metadata_history tables
+    with Session(test_engine) as test_db_session:
+        statement = delete(MetadataTable)
+        history_statement = delete(MetadataHistoryTable)
+        test_db_session.exec(statement)
+        test_db_session.exec(history_statement)
+        test_db_session.commit()
+
+        try:
+            await MetadataHistoryDatabaseFactory(
+                engine=test_engine
+            ).get_metadata_history_by_metadata_id(metadata_id=38742)
+        except Exception as e:
+            assert str(e) == "metadata not found"
diff --git a/test/api_test/db/factories/SessionDatabaseFactory_test.py b/test/api_test/db/factories/SessionDatabaseFactory_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..4d21c2f24a3fadb41686308b96197db031bdaf3c
--- /dev/null
+++ b/test/api_test/db/factories/SessionDatabaseFactory_test.py
@@ -0,0 +1,146 @@
+import os
+import uuid
+import json
+import pytest
+
+from sqlmodel import Session, delete, select
+
+from geomagio.api.db.models.session import session
+from geomagio.api.db.factories.SessionDatabaseFactory import SessionDatabaseFactory
+from geomagio.api.secure.encryption import get_fernet
+
+
+@pytest.mark.asyncio
+async def test_encryption(test_engine, valid_session):
+    with Session(test_engine) as test_db_session:
+        # clear test session table
+        statement = delete(session)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+        session_id = "randomstring"
+        await SessionDatabaseFactory(engine=test_engine).save_session(
+            session_id=session_id, data=valid_session
+        )
+
+        # get session directly from database to make sure its encrypted
+        statement = select(session).where(session.session_id == session_id)
+        results = test_db_session.exec(statement)
+        user_session = results.one()
+
+        # decrypt the data
+        encryption = get_fernet()
+        decrypted_data = encryption.decrypt(user_session.data.encode("utf8"))
+
+        # assert encryption is working as expected
+        returned_session = json.loads(decrypted_data)
+        assert returned_session == valid_session
+
+
+@pytest.mark.asyncio
+async def test_get_session_no_session(test_engine):
+    try:
+        await SessionDatabaseFactory(engine=test_engine).get_session(
+            session_id="idonotexist"
+        )
+    except Exception as e:
+        assert e is not None
+
+
+@pytest.mark.asyncio
+async def test_save_new_session(
+    test_engine, valid_session, valid_token, valid_userinfo
+):
+    with Session(test_engine) as test_db_session:
+        # clear test session table
+        statement = delete(session)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    session_id = "randomstring"
+    await SessionDatabaseFactory(engine=test_engine).save_session(
+        session_id=session_id, data=valid_session
+    )
+
+    # get the newly created session to make sure it's saved correctly and encryption is working as expected
+    user_session = await SessionDatabaseFactory(engine=test_engine).get_session(
+        session_id=session_id
+    )
+    assert user_session["token"] == valid_token
+    assert user_session["user"] == valid_userinfo
+
+
+@pytest.mark.asyncio
+async def test_update_current_session(
+    test_engine, valid_session, valid_token, valid_userinfo
+):
+    with Session(test_engine) as test_db_session:
+        # clear test session table
+        statement = delete(session)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+    # create an initial session to later update
+    session_id = "sessiontoupdate"
+    await SessionDatabaseFactory(engine=test_engine).save_session(
+        session_id=session_id, data=valid_session
+    )
+
+    # get the newly created session to make sure it's saved correctly and encryption is working as expected
+    user_session = await SessionDatabaseFactory(engine=test_engine).get_session(
+        session_id=session_id
+    )
+    assert user_session["token"] == valid_token
+    assert user_session["user"] == valid_userinfo
+    assert user_session["user"]["groups"] == ["group1", "group2"]
+
+    # change the userinfo and save it
+    updated_session_data = valid_session
+    updated_session_data["user"]["groups"] = []
+    await SessionDatabaseFactory(engine=test_engine).save_session(
+        session_id=session_id, data=updated_session_data
+    )
+
+    # get the updated session and assert the group info was changed as expected
+    updated_session = await SessionDatabaseFactory(engine=test_engine).get_session(
+        session_id=session_id
+    )
+    assert updated_session["token"] == valid_token
+    assert updated_session["user"]["email"] == "test_email"
+    assert updated_session["user"]["groups"] == []
+
+
+@pytest.mark.asyncio
+async def test_delete_session(test_engine, valid_session):
+    with Session(test_engine) as test_db_session:
+        # clear test session table
+        statement = delete(session)
+        test_db_session.exec(statement)
+        test_db_session.commit()
+
+        # create an initial session to later delete
+        session_id = "sessiontodelete"
+        await SessionDatabaseFactory(engine=test_engine).save_session(
+            session_id=session_id, data=valid_session
+        )
+
+        # delete session
+        await SessionDatabaseFactory(engine=test_engine).delete_session(
+            session_id=session_id
+        )
+
+        # try to get session to verify
+        second_statement = select(session).where(session.session_id == session_id)
+        results = test_db_session.exec(second_statement)
+        user_session = results.all()
+
+        assert len(user_session) == 0
+
+
+@pytest.mark.asyncio
+async def test_delete_non_existing(test_engine):
+    session_id = "idonotexist"
+    # this should not error
+    await SessionDatabaseFactory(engine=test_engine).delete_session(
+        session_id=session_id
+    )
diff --git a/test/api_test/secure_test/conftest.py b/test/api_test/secure_test/conftest.py
deleted file mode 100644
index 0df413243c229a44d02c4c9caf120064af32a7ee..0000000000000000000000000000000000000000
--- a/test/api_test/secure_test/conftest.py
+++ /dev/null
@@ -1,229 +0,0 @@
-import pytest
-
-
-@pytest.fixture()
-def valid_token():
-    return {
-        "access_token": "test_access_token",
-        "token_type": "Bearer",
-        "expires_in": 7200,
-        "refresh_token": "test_refresh_toekn",
-        "scope": "openid email profile",
-        "created_at": 1733244369,
-        "id_token": "test_id_token",
-        "expires_at": 1733251569,
-        "userinfo": {
-            "iss": "http://test_url",
-            "sub": "1234",
-            "aud": "test_aud",
-            "exp": 1733244489,
-            "iat": 1733244369,
-            "nonce": "test_nonce",
-            "auth_time": 1733244368,
-            "sub_legacy": "test_sub_legacy",
-            "name": "test_user",
-            "nickname": "test_user_nickname",
-            "preferred_username": "test_preferred_username",
-            "email": "test_email",
-            "email_verified": True,
-            "profile": "http://test_url/user",
-            "picture": "http://picture_url",
-            "groups_direct": ["group1", "group2"],
-        },
-    }
-
-
-@pytest.fixture()
-def valid_userinfo():
-    return {
-        "sub": "1234",
-        "sub_legacy": "test_sub_legacy",
-        "name": "test_user",
-        "nickname": "test_user_nickname",
-        "preferred_username": "test_preferred_username",
-        "email": "test_email",
-        "email_verified": True,
-        "profile": "http://test_url/user",
-        "picture": "http://picture_url",
-        "groups": ["group1", "group2"],
-    }
-
-
-@pytest.fixture()
-def valid_gitlab_user():
-    return {
-        "id": 1234,
-        "email": "test_email",
-        "name": "test_user",
-        "username": "test_user_nickname",
-        "avatar_url": "http://picture_url",
-    }
-
-
-@pytest.fixture()
-def valid_reading_metadata():
-    return {
-        "created_by": "test_user",
-        "starttime": "2024-11-07T12:12:12Z",
-        "endtime": "2024-11-07T20:20:20Z",
-        "network": "NT",
-        "station": "BOU",
-        "category": "reading",
-        "data_valid": True,
-        "metadata": {
-            "azimuth": 199.1383,
-            "metadata": {
-                "time": "2020-01-03T17:12:47Z",
-                "station": "BOU",
-                "observer": "Test Observer",
-                "reviewed": True,
-                "reviewer": "Test Reviewer",
-                "mark_name": "AZ",
-                "pier_name": "MainPCDCP",
-                "theodolite": "108449",
-                "electronics": "0110",
-                "mark_azimuth": 199.1383,
-                "pier_correction": -22,
-            },
-            "absolutes": [
-                {
-                    "shift": 0,
-                    "valid": True,
-                    "element": "D",
-                    "endtime": "2020-01-03T17:16:21.000000Z",
-                    "absolute": 8.3851056,
-                    "baseline": 8.58571,
-                    "starttime": "2020-01-03T17:12:47.000000Z",
-                },
-                {
-                    "shift": 0,
-                    "valid": True,
-                    "element": "H",
-                    "endtime": "2020-01-03T17:24:40.000000Z",
-                    "absolute": 20728.0650365,
-                    "baseline": -71.7177135,
-                    "starttime": "2020-01-03T17:20:48.000000Z",
-                },
-                {
-                    "shift": 0,
-                    "valid": True,
-                    "element": "Z",
-                    "endtime": "2020-01-03T17:24:40.000000Z",
-                    "absolute": 47450.1529433,
-                    "baseline": 578.2041933,
-                    "starttime": "2020-01-03T17:20:48.000000Z",
-                },
-            ],
-            "hemisphere": 1,
-            "diagnostics": None,
-            "scale_value": None,
-            "measurements": [
-                {
-                    "e": None,
-                    "f": None,
-                    "h": None,
-                    "z": None,
-                    "angle": 191.56666666666666,
-                    "measurement_type": "FirstMarkDown",
-                },
-                {
-                    "e": None,
-                    "f": None,
-                    "h": None,
-                    "z": None,
-                    "angle": 11.566666666666666,
-                    "measurement_type": "FirstMarkUp",
-                },
-                {
-                    "e": None,
-                    "f": None,
-                    "h": None,
-                    "z": None,
-                    "angle": 191.56666666666666,
-                    "measurement_type": "SecondMarkDown",
-                },
-                {
-                    "e": None,
-                    "f": None,
-                    "h": None,
-                    "z": None,
-                    "angle": 11.566666666666666,
-                    "measurement_type": "SecondMarkUp",
-                },
-                {
-                    "e": -72.242,
-                    "f": 51801.81,
-                    "h": 20800.329,
-                    "z": 46871.49,
-                    "angle": 270.71666666666664,
-                    "measurement_type": "WestDown",
-                    "time": "2024-11-07T12:12:12Z",
-                },
-                {
-                    "e": -72.636,
-                    "f": 51801.92,
-                    "h": 20800.259,
-                    "z": 46871.641,
-                    "angle": 90.66666666666667,
-                    "measurement_type": "EastDown",
-                    "time": "2024-11-07T12:13:14Z",
-                },
-                {
-                    "e": -72.657,
-                    "f": 51801.82,
-                    "h": 20800.259,
-                    "z": 46871.521,
-                    "angle": 90.93333333333334,
-                    "measurement_type": "WestUp",
-                    "time": "2024-11-07T13:13:11Z",
-                },
-                {
-                    "e": -72.758,
-                    "f": 51801.92,
-                    "h": 20800.086,
-                    "z": 46871.707,
-                    "angle": 270.96666666666664,
-                    "measurement_type": "EastUp",
-                    "time": "2024-11-07T13:15:12Z",
-                },
-                {
-                    "e": -72.898,
-                    "f": 51801.89,
-                    "h": 20799.796,
-                    "z": 46871.802,
-                    "angle": 246.38333333333333,
-                    "measurement_type": "SouthDown",
-                    "time": "2024-11-07T15:12:12Z",
-                },
-                {
-                    "e": -72.8,
-                    "f": 51802.01,
-                    "h": 20799.852,
-                    "z": 46871.919,
-                    "angle": 66.38333333333334,
-                    "measurement_type": "NorthUp",
-                    "time": "2024-11-07T18:12:12Z",
-                },
-                {
-                    "e": -72.775,
-                    "f": 51802.01,
-                    "h": 20799.668,
-                    "z": 46871.997,
-                    "angle": 113.58333333333333,
-                    "measurement_type": "SouthUp",
-                    "time": "2024-11-07T19:12:19Z",
-                },
-                {
-                    "e": -72.813,
-                    "f": 51802.14,
-                    "h": 20799.815,
-                    "z": 46872.077,
-                    "angle": 293.5833333333333,
-                    "measurement_type": "NorthDown",
-                    "time": "2024-11-07T20:20:20Z",
-                },
-                {"measurement_type": "Meridian", "angle": 21},
-            ],
-            "pier_correction": -22,
-        },
-    }
diff --git a/test/api_test/secure_test/login_test.py b/test/api_test/secure_test/login_routes_test.py
similarity index 64%
rename from test/api_test/secure_test/login_test.py
rename to test/api_test/secure_test/login_routes_test.py
index 389796565cebb401741ec9679719f1fc2224f90c..fcb3dc18551440a714891297996d568c086063dc 100644
--- a/test/api_test/secure_test/login_test.py
+++ b/test/api_test/secure_test/login_routes_test.py
@@ -1,40 +1,44 @@
 import pytest
 import httpx
+
 from unittest.mock import ANY
 from fastapi.testclient import TestClient
 from authlib.integrations.base_client.errors import MismatchingStateError
 
-from geomagio.api.secure import app
-from geomagio.api.secure.SessionMiddleware import SessionMiddleware
-from geomagio.api.secure.login import User
+from geomagio.api.secure.secure_app import app
+from geomagio.api.secure.login_routes import User
+from geomagio.api.db.factories.SessionDatabaseFactory import SessionDatabaseFactory
 
 client = TestClient(app)
 
 
 @pytest.mark.asyncio
-async def test_authorization_valid(valid_token, valid_userinfo, mocker):
-
+async def test_authorization_valid(valid_session, valid_token, valid_userinfo, mocker):
     mock_authorize = mocker.AsyncMock(return_value=valid_token)
     mocker.patch(
-        "geomagio.api.secure.login.oauth.openid.authorize_access_token", mock_authorize
+        "geomagio.api.secure.login_routes.oauth.openid.authorize_access_token",
+        mock_authorize,
     )
 
     mock_userinfo = mocker.AsyncMock(return_value=valid_userinfo)
-    mocker.patch("geomagio.api.secure.login.oauth.openid.userinfo", mock_userinfo)
+    mocker.patch(
+        "geomagio.api.secure.login_routes.oauth.openid.userinfo", mock_userinfo
+    )
 
     mock_save_session = mocker.AsyncMock()
-    mocker.patch.object(SessionMiddleware, "save_session", mock_save_session)
+    mocker.patch.object(SessionDatabaseFactory, "save_session", mock_save_session)
 
     response = client.get("/authorize")
     assert response.status_code == 200
 
-    valid_session = {
-        "token": valid_token,
-        "user": valid_userinfo,
-    }
+    mock_save_session.assert_called_once_with(session_id=ANY, data=valid_session)
 
-    # assert the session data is correct
-    mock_save_session.assert_called_once_with(ANY, valid_session)
+
+@pytest.mark.asyncio
+async def test_authorization_invalid_authorize_access_token():
+    # assert that requests directly to GET /authorize cause an exception
+    with pytest.raises(MismatchingStateError) as err:
+        client.get("/authorize")
 
 
 @pytest.mark.asyncio
@@ -52,11 +56,12 @@ async def test_authorization_invalid_authorize_access_token():
 @pytest.mark.asyncio
 async def test_login_redirects(mocker):
     mock_save_session = mocker.AsyncMock()
-    mocker.patch.object(SessionMiddleware, "save_session", mock_save_session)
+    mocker.patch.object(SessionDatabaseFactory, "save_session", mock_save_session)
 
     mock_redirect = mocker.AsyncMock(return_value={"status_code": 302})
     mocker.patch(
-        "geomagio.api.secure.login.oauth.openid.authorize_redirect", mock_redirect
+        "geomagio.api.secure.login_routes.oauth.openid.authorize_redirect",
+        mock_redirect,
     )
 
     header = "https://testserver/ws/secure/metadata/1"
@@ -68,37 +73,38 @@ async def test_login_redirects(mocker):
     )
 
     # assert session is saved with correct referrer header
-    mock_save_session.assert_called_once_with(ANY, {"after_authorize_redirect": header})
+    mock_save_session.assert_called_once_with(
+        session_id=ANY, data={"after_authorize_redirect": header}
+    )
 
 
 @pytest.mark.asyncio
-async def test_logout(valid_token, valid_userinfo, mocker):
-    valid_session = {"token": valid_token, "user": valid_userinfo}
+async def test_logout(valid_session, mocker):
     mock_get_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "get_session", mock_get_session)
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
 
     mock_delete_session = mocker.AsyncMock()
-    mocker.patch.object(SessionMiddleware, "delete_session", mock_delete_session)
+    mocker.patch.object(SessionDatabaseFactory, "delete_session", mock_delete_session)
 
     client.get(url="/logout", headers={"Cookie": "PHPSESSID=valid_session_id"})
 
-    mock_get_session.assert_called_once_with("valid_session_id")
-    mock_delete_session.assert_called_once_with("valid_session_id")
+    mock_get_session.assert_called_once_with(session_id="valid_session_id")
+    mock_delete_session.assert_called_once_with(session_id="valid_session_id")
 
 
 @pytest.mark.asyncio
 async def test_user_with_valid_session(valid_token, valid_userinfo, mocker):
     valid_session = {"token": valid_token, "user": valid_userinfo}
     mock_get_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "get_session", mock_get_session)
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
 
-    mock_save_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "save_session", mock_save_session)
+    mock_save_session = mocker.AsyncMock()
+    mocker.patch.object(SessionDatabaseFactory, "save_session", mock_save_session)
 
     response = client.get(url="/user", headers={"Cookie": "PHPSESSID=valid_session_id"})
 
     assert response.status_code == 200
-    mock_get_session.assert_called_once_with("valid_session_id")
+    mock_get_session.assert_called_once_with(session_id="valid_session_id")
 
     user = User(**response.json())
     assert user.email == "test_email"
@@ -133,8 +139,8 @@ async def test_user_with_no_session_and_valid_token(
 
     valid_session = {"token": valid_token, "user": valid_userinfo}
 
-    mock_save_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "save_session", mock_save_session)
+    mock_save_session = mocker.AsyncMock()
+    mocker.patch.object(SessionDatabaseFactory, "save_session", mock_save_session)
 
     response = client.get(url="/user", headers={"Authorization": "valid_gitlab_token"})
 
@@ -148,7 +154,9 @@ async def test_user_with_no_session_and_valid_token(
     }
 
     assert response.status_code == 200
-    mock_save_session.assert_called_once_with(ANY, {"user": session_user})
+    mock_save_session.assert_called_once_with(
+        session_id=ANY, data={"user": session_user}
+    )
 
     user = User(**response.json())
     assert user.email == "test_email"
@@ -159,7 +167,7 @@ async def test_user_with_no_session_and_valid_token(
 @pytest.mark.asyncio
 async def test_user_with_no_session(mocker):
     mock_get_session = mocker.AsyncMock()
-    mocker.patch.object(SessionMiddleware, "get_session", mock_get_session)
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
 
     # do not pass in cookie
     response = client.get(url="/user")
@@ -171,10 +179,12 @@ async def test_user_with_no_session(mocker):
 @pytest.mark.asyncio
 async def test_user_with_no_session_and_invalid_token(mocker):
     mock_get_session = mocker.AsyncMock()
-    mocker.patch.object(SessionMiddleware, "get_session", mock_get_session)
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
 
     mock_gitlab_request = mocker.AsyncMock(return_value=None)
-    mocker.patch("geomagio.api.secure.login.get_gitlab_user", mock_gitlab_request)
+    mocker.patch(
+        "geomagio.api.secure.login_routes.get_gitlab_user", mock_gitlab_request
+    )
 
     # do not pass in cookie
     response = client.get(
@@ -195,14 +205,14 @@ async def test_user_invalid_session(mocker):
         "url": "test_url",
     }
     mock_get_session = mocker.AsyncMock(return_value=invalid_session)
-    mocker.patch.object(SessionMiddleware, "get_session", mock_get_session)
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
 
     mock_save_session = mocker.AsyncMock()
-    mocker.patch.object(SessionMiddleware, "save_session", mock_save_session)
+    mocker.patch.object(SessionDatabaseFactory, "save_session", mock_save_session)
 
     response = client.get(
         url="/user", headers={"Cookie": "PHPSESSID=invalid_session_id"}
     )
 
     assert response.status_code == 401
-    mock_get_session.assert_called_once_with("invalid_session_id")
+    mock_get_session.assert_called_once_with(session_id="invalid_session_id")
diff --git a/test/api_test/secure_test/metadata_routes_test.py b/test/api_test/secure_test/metadata_routes_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..cc97e394d9663136932e1d30746aa23005a3ee1f
--- /dev/null
+++ b/test/api_test/secure_test/metadata_routes_test.py
@@ -0,0 +1,931 @@
+import pytest
+import json
+import httpx
+
+from unittest.mock import ANY
+from fastapi.testclient import TestClient
+from obspy import UTCDateTime
+
+from geomagio.api.secure.secure_app import app
+from geomagio.api.db.models.metadata import Metadata
+from geomagio.api.db.factories.MetadataDatabaseFactory import MetadataDatabaseFactory
+from geomagio.api.db.factories.MetadataHistoryDatabaseFactory import (
+    MetadataHistoryDatabaseFactory,
+)
+from geomagio.api.db.factories.SessionDatabaseFactory import SessionDatabaseFactory
+
+client = TestClient(app)
+
+
+@pytest.mark.asyncio
+async def test_create_metadata_valid_session(
+    valid_session, valid_reading_metadata, mocker
+):
+    mock_get_session = mocker.AsyncMock(return_value=valid_session)
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
+
+    mock_save_session = mocker.AsyncMock()
+    mocker.patch.object(SessionDatabaseFactory, "save_session", mock_save_session)
+
+    # create mock response for MetadataDatabaseFactory().create_metadata
+    valid_reading_metadata["id"] = 7204
+    created_metadata = Metadata(**valid_reading_metadata)
+    created_metadata = Metadata.model_validate(created_metadata)
+
+    mock_save_metadata = mocker.AsyncMock(return_value=created_metadata)
+    mocker.patch.object(MetadataDatabaseFactory, "create_metadata", mock_save_metadata)
+
+    body = json.dumps(valid_reading_metadata)
+
+    response = client.post(
+        url="/metadata", headers={"Cookie": "PHPSESSID=valid_session_id"}, content=body
+    )
+
+    assert response.status_code == 201
+    mock_get_session.assert_called_once_with(session_id="valid_session_id")
+
+    # assert the response matches the object returned by MetadataDatabaseFactory().create_metadata
+    response_body = response.json()
+    assert response_body["id"] == 7204
+    assert response_body["category"] == "reading"
+    assert response_body["starttime"] == UTCDateTime("2024-11-07T12:12:12Z")
+    assert response_body["endtime"] == UTCDateTime("2024-11-07T20:20:20Z")
+    assert response_body["network"] == "NT"
+    assert response_body["station"] == "BOU"
+    assert response_body["status"] == "new"
+    assert response_body["priority"] == 1
+    assert response_body["metadata"] == created_metadata.metadata
+
+
+@pytest.mark.asyncio
+async def test_create_metadata_valid_session_invalid_user_input(valid_session, mocker):
+    mock_get_session = mocker.AsyncMock(return_value=valid_session)
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
+
+    mock_save_session = mocker.AsyncMock()
+    mocker.patch.object(SessionDatabaseFactory, "save_session", mock_save_session)
+
+    invalid_user_input = {
+        "created_by": "test_user",
+        "starttime": "January 28th, 2025 08:00:00",
+        "endtime": "2024-11-07T20:20:20Z",
+        "network": "NT",
+        "station": "BOU",
+        "category": "reading",
+        "data_valid": True,
+    }
+
+    body = json.dumps(invalid_user_input)
+
+    response = client.post(
+        url="/metadata", headers={"Cookie": "PHPSESSID=valid_session_id"}, content=body
+    )
+
+    response_body = response.json()
+
+    assert response.status_code == 422
+    mock_get_session.assert_called_once_with(session_id="valid_session_id")
+
+    assert (
+        response_body["detail"][0]["msg"]
+        == "Input should be an instance of UTCDateTime"
+    )
+    assert (
+        response_body["detail"][1]["msg"]
+        == "Value error, Invalid time type. See obspy UTCDateTime for more information."
+    )
+
+
+@pytest.mark.asyncio
+async def test_create_metadata_valid_auth_token(
+    valid_token, valid_userinfo, valid_gitlab_user, valid_reading_metadata, mocker
+):
+    valid_user_response = httpx.Response(status_code=200, json=valid_gitlab_user)
+
+    valid_groups = [
+        {
+            "id": 1,
+            "full_path": "group1",
+        },
+        {"id": 2, "full_path": "group2"},
+    ]
+
+    valid_groups_response = httpx.Response(status_code=200, json=valid_groups)
+
+    mock_gitlab_request = mocker.AsyncMock(
+        side_effect=[
+            valid_user_response,
+            valid_groups_response,
+            httpx.Response(status_code=200, json={}),
+        ]
+    )
+    mocker.patch("httpx.AsyncClient.get", mock_gitlab_request)
+
+    valid_session = {"token": valid_token, "user": valid_userinfo}
+
+    mock_save_session = mocker.AsyncMock(return_value=valid_session)
+    mocker.patch.object(SessionDatabaseFactory, "save_session", mock_save_session)
+
+    # create mock response for MetadataDatabaseFactory().create_metadata
+    valid_reading_metadata["id"] = 7204
+    created_metadata = Metadata(**valid_reading_metadata)
+    created_metadata = Metadata.model_validate(created_metadata)
+    mock_save_metadata = mocker.AsyncMock(return_value=created_metadata)
+    mocker.patch.object(MetadataDatabaseFactory, "create_metadata", mock_save_metadata)
+
+    body = json.dumps(valid_reading_metadata)
+
+    response = client.post(
+        url="/metadata", headers={"Authorization": "auth_token"}, content=body
+    )
+
+    session_user = {
+        "email": "test_email",
+        "sub": 1234,
+        "groups": ["group1", "group2"],
+        "name": "test_user",
+        "nickname": "test_user_nickname",
+        "picture": "http://picture_url",
+    }
+
+    assert response.status_code == 201
+    mock_save_session.assert_called_once_with(
+        session_id=ANY, data={"user": session_user}
+    )
+
+    returned_metadata = Metadata(**response.json())
+    assert returned_metadata.id == 7204
+    assert returned_metadata.category == "reading"
+    assert returned_metadata.starttime == UTCDateTime("2024-11-07T12:12:12Z")
+    assert returned_metadata.endtime == UTCDateTime("2024-11-07T20:20:20Z")
+    assert returned_metadata.network == "NT"
+    assert returned_metadata.station == "BOU"
+    assert returned_metadata.status == "new"
+    assert returned_metadata.priority == 1
+
+
+@pytest.mark.asyncio
+async def test_create_metadata_no_session_or_token(valid_reading_metadata, mocker):
+    mock_get_session = mocker.AsyncMock()
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
+
+    body = json.dumps(valid_reading_metadata)
+
+    # do not pass in cookie or auth
+    response = client.post(url="/metadata", content=body)
+    assert response.status_code == 401
+
+    mock_get_session.assert_not_called()
+
+
+@pytest.mark.asyncio
+async def test_create_metadata_with_no_session_and_invalid_token(mocker):
+    mock_get_session = mocker.AsyncMock()
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
+
+    mock_gitlab_request = mocker.AsyncMock(return_value=None)
+    mocker.patch(
+        "geomagio.api.secure.login_routes.get_gitlab_user", mock_gitlab_request
+    )
+
+    response = client.post(
+        url="/metadata", headers={"Authorization": "invalid_gitlab_token"}
+    )
+    assert response.status_code == 401
+
+    mock_get_session.assert_not_called()
+    mock_gitlab_request.assert_called_once_with(token="invalid_gitlab_token")
+
+
+@pytest.mark.asyncio
+async def test_create_metadata_invalid_session(mocker):
+    # mock invalid session. this is created when users GET /metadata without logging in
+    invalid_session = {
+        "redirect_uri": "redirect_uri",
+        "nonce": "nonce_str",
+        "url": "test_url",
+    }
+    mock_get_session = mocker.AsyncMock(return_value=invalid_session)
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
+
+    mock_save_session = mocker.AsyncMock()
+    mocker.patch.object(SessionDatabaseFactory, "save_session", mock_save_session)
+
+    response = client.post(
+        url="/metadata", headers={"Cookie": "PHPSESSID=invalid_session_id"}
+    )
+
+    assert response.status_code == 401
+    mock_get_session.assert_called_once_with(session_id="invalid_session_id")
+
+
+@pytest.mark.asyncio
+async def test_update_metadata(
+    valid_token, valid_userinfo, instrument_metadata, mocker
+):
+    valid_userinfo["groups"] = ["ghsc/geomag/operations/roles/reviewer"]
+    valid_session = {"token": valid_token, "user": valid_userinfo}
+    mock_get_session = mocker.AsyncMock(return_value=valid_session)
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
+
+    mock_save_session = mocker.AsyncMock(return_value=valid_session)
+    mocker.patch.object(SessionDatabaseFactory, "save_session", mock_save_session)
+
+    # mock response for MetadataDatabaseFactory().update_metadata
+    instrument_metadata.id = 7204
+    instrument_metadata.updated_by = "test_user"
+    instrument_metadata.updated_time = UTCDateTime.now()
+    instrument_metadata.metadata = {
+        "type": "FGE",
+        "channels": {
+            "U": [{"channel": "U_Volt", "offset": 0, "scale": 313.2}],
+            "V": [{"channel": "V_Volt", "offset": 0, "scale": 312.3}],
+            "W": [{"channel": "W_Volt", "offset": 0, "scale": 312.0}],
+        },
+        "electronics": {
+            "serial": "E0542",
+            "x-scale": 313.2,
+            "y-scale": 312.3,
+            "z-scale": 312.0,
+            "temperature-scale": 0.01,
+        },
+        "sensor": {
+            "serial": "S0419",
+            "x-constant": 36958,
+            "y-constant": 36849,
+            "z-constant": 36810,
+        },
+    }
+    instrument_metadata = Metadata.model_validate(instrument_metadata)
+    mock_update = mocker.AsyncMock(return_value=instrument_metadata)
+    mocker.patch.object(MetadataDatabaseFactory, "update_metadata", mock_update)
+
+    valid_user_input = {
+        "id": 7204,
+        "metadata": {
+            "type": "FGE",
+            "channels": {
+                "U": [{"channel": "U_Volt", "offset": 0, "scale": 313.2}],
+                "V": [{"channel": "V_Volt", "offset": 0, "scale": 312.3}],
+                "W": [{"channel": "W_Volt", "offset": 0, "scale": 312.0}],
+            },
+            "electronics": {
+                "serial": "E0542",
+                "x-scale": 313.2,
+                "y-scale": 312.3,
+                "z-scale": 312.0,
+                "temperature-scale": 0.01,
+            },
+            "sensor": {
+                "serial": "S0419",
+                "x-constant": 36958,
+                "y-constant": 36849,
+                "z-constant": 36810,
+            },
+        },
+    }
+
+    body = json.dumps(valid_user_input)
+
+    response = client.put(
+        url="/metadata/1234",
+        headers={"Cookie": "PHPSESSID=valid_session_id"},
+        content=body,
+    )
+    assert response.status_code == 200
+    mock_get_session.assert_called_once_with(session_id="valid_session_id")
+
+    # assert the response matches the object returned by MetadataDatabaseFactory().update_metadata
+    response_body = response.json()
+    assert response_body["id"] == 7204
+    assert response_body["updated_by"] == "test_user"
+    assert response_body["category"] == "instrument"
+    assert response_body["network"] == "NT"
+    assert response_body["station"] == "BDT"
+    assert response_body["status"] == "new"
+    assert response_body["priority"] == 1
+    assert response_body["metadata"] == instrument_metadata.metadata
+
+
+@pytest.mark.asyncio
+async def test_update_metadata_invalid_input(valid_token, valid_userinfo, mocker):
+    valid_userinfo["groups"] = ["ghsc/geomag/operations/roles/reviewer"]
+    valid_session = {"token": valid_token, "user": valid_userinfo}
+    mock_get_session = mocker.AsyncMock(return_value=valid_session)
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
+
+    mock_save_session = mocker.AsyncMock(return_value=valid_session)
+    mocker.patch.object(SessionDatabaseFactory, "save_session", mock_save_session)
+
+    invalid_user_input = {
+        "id": 7204,
+        "starttime": "12:20:32",
+    }
+
+    body = json.dumps(invalid_user_input)
+
+    response = client.put(
+        url="/metadata/1234",
+        headers={"Cookie": "PHPSESSID=valid_session_id"},
+        content=body,
+    )
+    assert response.status_code == 422
+    mock_get_session.assert_called_once_with(session_id="valid_session_id")
+
+    response_body = response.json()
+
+    assert (
+        response_body["detail"][0]["msg"]
+        == "Input should be an instance of UTCDateTime"
+    )
+    assert (
+        response_body["detail"][1]["msg"]
+        == "Value error, Invalid time type. See obspy UTCDateTime for more information."
+    )
+
+
+@pytest.mark.asyncio
+async def test_update_metadata_valid_auth_token(
+    valid_token,
+    valid_userinfo,
+    valid_gitlab_user,
+    instrument_metadata,
+    mocker,
+):
+    valid_user_response = httpx.Response(status_code=200, json=valid_gitlab_user)
+
+    valid_groups = [
+        {
+            "id": 1,
+            "full_path": "group1",
+        },
+        {"id": 2, "full_path": "ghsc/geomag/operations/roles/reviewer"},
+    ]
+
+    valid_groups_response = httpx.Response(status_code=200, json=valid_groups)
+
+    mock_gitlab_request = mocker.AsyncMock(
+        side_effect=[
+            valid_user_response,
+            valid_groups_response,
+            httpx.Response(status_code=200, json={}),
+        ]
+    )
+    mocker.patch("httpx.AsyncClient.get", mock_gitlab_request)
+
+    valid_session = {"token": valid_token, "user": valid_userinfo}
+
+    mock_save_session = mocker.AsyncMock(return_value=valid_session)
+    mocker.patch.object(SessionDatabaseFactory, "save_session", mock_save_session)
+
+    # mock response for MetadataDatabaseFactory().update_metadata
+    instrument_metadata.id = 7204
+    instrument_metadata.updated_by = "test_user"
+    instrument_metadata.updated_time = UTCDateTime.now()
+    instrument_metadata.metadata = {
+        "type": "FGE",
+        "channels": {
+            "U": [{"channel": "U_Volt", "offset": 0, "scale": 313.2}],
+            "V": [{"channel": "V_Volt", "offset": 0, "scale": 312.3}],
+            "W": [{"channel": "W_Volt", "offset": 0, "scale": 312.0}],
+        },
+        "electronics": {
+            "serial": "E0542",
+            "x-scale": 313.2,
+            "y-scale": 312.3,
+            "z-scale": 312.0,
+            "temperature-scale": 0.01,
+        },
+        "sensor": {
+            "serial": "S0419",
+            "x-constant": 36958,
+            "y-constant": 36849,
+            "z-constant": 36810,
+        },
+    }
+    instrument_metadata = Metadata.model_validate(instrument_metadata)
+    mock_update = mocker.AsyncMock(return_value=instrument_metadata)
+    mocker.patch.object(MetadataDatabaseFactory, "update_metadata", mock_update)
+
+    valid_user_input = {
+        "id": 7204,
+        "metadata": {
+            "type": "FGE",
+            "channels": {
+                "U": [{"channel": "U_Volt", "offset": 0, "scale": 313.2}],
+                "V": [{"channel": "V_Volt", "offset": 0, "scale": 312.3}],
+                "W": [{"channel": "W_Volt", "offset": 0, "scale": 312.0}],
+            },
+            "electronics": {
+                "serial": "E0542",
+                "x-scale": 313.2,
+                "y-scale": 312.3,
+                "z-scale": 312.0,
+                "temperature-scale": 0.01,
+            },
+            "sensor": {
+                "serial": "S0419",
+                "x-constant": 36958,
+                "y-constant": 36849,
+                "z-constant": 36810,
+            },
+        },
+    }
+
+    body = json.dumps(valid_user_input)
+
+    response = client.put(
+        url="/metadata/1234", headers={"Authorization": "auth_token"}, content=body
+    )
+
+    session_user = {
+        "email": "test_email",
+        "sub": 1234,
+        "groups": ["group1", "ghsc/geomag/operations/roles/reviewer"],
+        "name": "test_user",
+        "nickname": "test_user_nickname",
+        "picture": "http://picture_url",
+    }
+
+    assert response.status_code == 200
+    mock_save_session.assert_called_once_with(
+        session_id=ANY, data={"user": session_user}
+    )
+
+    # assert the response matches the object returned by MetadataDatabaseFactory().update_metadata
+    response_body = response.json()
+    assert response_body["id"] == 7204
+    assert response_body["updated_by"] == "test_user"
+    assert response_body["category"] == "instrument"
+    assert response_body["network"] == "NT"
+    assert response_body["station"] == "BDT"
+    assert response_body["status"] == "new"
+    assert response_body["priority"] == 1
+    assert response_body["metadata"] == instrument_metadata.metadata
+
+
+@pytest.mark.asyncio
+async def test_update_metadata_no_valid_group(
+    valid_session, valid_reading_metadata, mocker
+):
+    mock_get_session = mocker.AsyncMock(return_value=valid_session)
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
+
+    mock_save_session = mocker.AsyncMock(return_value=valid_session)
+    mocker.patch.object(SessionDatabaseFactory, "save_session", mock_save_session)
+
+    body = json.dumps(valid_reading_metadata)
+
+    response = client.put(
+        url="/metadata/1234",
+        headers={"Cookie": "PHPSESSID=valid_session_id"},
+        content=body,
+    )
+    assert response.status_code == 403
+    mock_get_session.assert_called_once_with(session_id="valid_session_id")
+
+
+@pytest.mark.asyncio
+async def test_update_metadata_no_session(
+    valid_session, valid_reading_metadata, mocker
+):
+    mock_get_session = mocker.AsyncMock(return_value=valid_session)
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
+
+    mock_save_session = mocker.AsyncMock(return_value=valid_session)
+    mocker.patch.object(SessionDatabaseFactory, "save_session", mock_save_session)
+
+    body = json.dumps(valid_reading_metadata)
+
+    response = client.put(url="/metadata/1234", content=body)
+    assert response.status_code == 401
+
+    mock_get_session.assert_not_called()
+
+
+@pytest.mark.asyncio
+async def test_update_metadata_invalid_session(mocker, valid_reading_metadata):
+    # mock invalid session. this is created when users GET /metadata without logging in
+    invalid_session = {
+        "redirect_uri": "redirect_uri",
+        "nonce": "nonce_str",
+        "url": "test_url",
+    }
+    mock_get_session = mocker.AsyncMock(return_value=invalid_session)
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
+
+    mock_save_session = mocker.AsyncMock()
+    mocker.patch.object(SessionDatabaseFactory, "save_session", mock_save_session)
+
+    body = json.dumps(valid_reading_metadata)
+
+    response = client.put(
+        url="/metadata/1234",
+        headers={"Cookie": "PHPSESSID=invalid_session_id"},
+        content=body,
+    )
+
+    assert response.status_code == 401
+    mock_get_session.assert_called_once_with(session_id="invalid_session_id")
+
+
+@pytest.mark.asyncio
+async def test_update_metadata_with_no_session_and_invalid_token(
+    mocker, valid_reading_metadata
+):
+    mock_get_session = mocker.AsyncMock()
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
+
+    mock_gitlab_request = mocker.AsyncMock(return_value=None)
+    mocker.patch(
+        "geomagio.api.secure.login_routes.get_gitlab_user", mock_gitlab_request
+    )
+
+    body = json.dumps(valid_reading_metadata)
+
+    response = client.put(
+        url="/metadata/1234",
+        headers={"Authorization": "invalid_gitlab_token"},
+        content=body,
+    )
+    assert response.status_code == 401
+
+    mock_get_session.assert_not_called()
+    mock_gitlab_request.assert_called_once_with(token="invalid_gitlab_token")
+
+
+@pytest.mark.asyncio
+async def test_update_metadata_no_existing_metadata(
+    valid_userinfo, valid_token, mocker
+):
+    valid_userinfo["groups"] = ["ghsc/geomag/operations/roles/reviewer"]
+    valid_session = {"token": valid_token, "user": valid_userinfo}
+    mock_get_session = mocker.AsyncMock(return_value=valid_session)
+    mocker.patch.object(SessionDatabaseFactory, "get_session", mock_get_session)
+
+    mock_save_session = mocker.AsyncMock(return_value=valid_session)
+    mocker.patch.object(SessionDatabaseFactory, "save_session", mock_save_session)
+
+    mock_update = mocker.AsyncMock(side_effect=ValueError("metadata not found"))
+    mocker.patch.object(MetadataDatabaseFactory, "update_metadata", mock_update)
+
+    body = json.dumps({"id": "1234", "category": "instrument"})
+
+    response = client.put(
+        url="/metadata/1234",
+        headers={"Cookie": "PHPSESSID=valid_session_id"},
+        content=body,
+    )
+    assert response.status_code == 404
+    mock_get_session.assert_called_once_with(session_id="valid_session_id")
+
+
+@pytest.mark.asyncio
+async def test_get_metadata(instrument_metadata, metadata_with_datetimes, mocker):
+    # mock return multiple metadata
+    instrument_metadata.id = 1242
+    instrument_metadata.station = "BOU"
+    metadata_with_datetimes.id = 821
+    all_instrument_metadata = [instrument_metadata, metadata_with_datetimes]
+    mock_get = mocker.AsyncMock(return_value=all_instrument_metadata)
+    mocker.patch.object(MetadataDatabaseFactory, "get_metadata", mock_get)
+
+    response = client.get(url="/metadata?station=BOU&category=instrument&network=NT")
+    assert response.status_code == 200
+
+    response_body = response.json()
+    assert len(response_body) == 2
+
+    for metadata in response_body:
+        assert metadata["station"] == "BOU"
+        assert metadata["category"] == "instrument"
+        assert metadata["network"] == "NT"
+
+        # assert nested metadata is returned as "metadata" and not "metadata_" to the user
+        assert metadata["metadata"] != None
+        assert metadata.get("metadata_") == None
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_return_single(instrument_metadata, mocker):
+    # mock return single metadata
+    instrument_metadata.id = 1242
+    all_instrument_metadata = [instrument_metadata]
+    mock_get = mocker.AsyncMock(return_value=all_instrument_metadata)
+    mocker.patch.object(MetadataDatabaseFactory, "get_metadata", mock_get)
+
+    response = client.get(url="/metadata?station=BDT&category=instrument&network=NT")
+    assert response.status_code == 200
+
+    response_body = response.json()
+    assert len(response_body) == 1
+    assert response_body[0]["station"] == "BDT"
+    assert response_body[0]["category"] == "instrument"
+    assert response_body[0]["network"] == "NT"
+
+    # assert nested metadata is returned as "metadata" and not "metadata_" to the user
+    assert response_body[0]["metadata"] != None
+    assert response_body[0].get("metadata_") == None
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_return_empty(mocker):
+    # mock return empty list
+
+    mock_get = mocker.AsyncMock(return_value=[])
+    mocker.patch.object(MetadataDatabaseFactory, "get_metadata", mock_get)
+
+    response = client.get(url="/metadata?station=BDT&category=instrument&network=NT")
+    assert response.status_code == 200
+
+    response_body = response.json()
+    assert len(response_body) == 0
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_invalid_input():
+    response = client.get(
+        url="/metadata?station=BDT&category=instrument&starttime=1:12:24T8:8:43"
+    )
+    assert response.status_code == 422
+
+    response_body = response.json()
+    assert (
+        response_body["detail"][0]["msg"]
+        == "Input should be an instance of UTCDateTime"
+    )
+    assert (
+        response_body["detail"][1]["msg"]
+        == "Value error, Invalid time type. See obspy UTCDateTime for more information."
+    )
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_by_id(instrument_metadata, mocker):
+    instrument_metadata.id = 1242
+    mock_get = mocker.AsyncMock(return_value=instrument_metadata)
+    mocker.patch.object(MetadataDatabaseFactory, "get_metadata_by_id", mock_get)
+
+    response = client.get(url="/metadata/1242")
+    assert response.status_code == 200
+
+    response_body = response.json()
+    assert response_body["station"] == "BDT"
+    assert response_body["category"] == "instrument"
+    assert response_body["network"] == "NT"
+
+    # assert nested metadata is returned as "metadata" and not "metadata_" to the user
+    assert response_body["metadata"] == {
+        "type": "FGE",
+        "channels": {
+            "U": [{"channel": "U_Volt", "offset": 0, "scale": 313.2}],
+            "V": [{"channel": "V_Volt", "offset": 0, "scale": 312.3}],
+            "W": [{"channel": "W_Volt", "offset": 0, "scale": 312.0}],
+        },
+        "electronics": {
+            "serial": "E0542",
+            "x-scale": 313.2,
+            "y-scale": 312.3,
+            "z-scale": 312.0,
+            "temperature-scale": 0.01,
+        },
+        "sensor": {
+            "serial": "S0419",
+            "x-constant": 36958,
+            "y-constant": 36849,
+            "z-constant": 36811,
+        },
+    }
+    assert response_body.get("metadata_") == None
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_by_id_no_metadata_found(mocker):
+    mock_get = mocker.AsyncMock(return_value=None)
+    mocker.patch.object(MetadataDatabaseFactory, "get_metadata_by_id", mock_get)
+
+    response = client.get(url="/metadata/1242")
+    assert response.status_code == 404
+    response_body = response.json()
+    assert response_body["detail"] == "metadata not found"
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_by_id_invalid_id():
+    response = client.get(url="/metadata/828cskje9w8e")
+    assert response.status_code == 422
+
+    response_body = response.json()
+    assert (
+        response_body["detail"][0]["msg"]
+        == "Input should be a valid integer, unable to parse string as an integer"
+    )
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_history(
+    formatted_metadata_history_1, formatted_metadata_history_2, mocker
+):
+    # mock return multiple metadata_history
+    mock_get = mocker.AsyncMock(
+        return_value=[formatted_metadata_history_1, formatted_metadata_history_2]
+    )
+    mocker.patch.object(
+        MetadataHistoryDatabaseFactory, "get_metadata_history", mock_get
+    )
+
+    response = client.get(
+        url="/metadata/history?station=BRW&category=observatory&network=NT"
+    )
+    assert response.status_code == 200
+
+    response_body = response.json()
+    assert len(response_body) == 2
+
+    for metadata_history in response_body:
+        assert metadata_history["metadata_id"] == 38746
+        assert metadata_history["station"] == "BRW"
+        assert metadata_history["category"] == "observatory"
+        assert metadata_history["network"] == "NT"
+
+        # assert nested metadata is returned as "metadata" and not "metadata_" to the user
+        assert metadata_history["metadata"] != None
+        assert metadata_history.get("metadata_") == None
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_history_return_single(formatted_metadata_history_1, mocker):
+    # mock return single MetadataHistoryTable
+    mock_get = mocker.AsyncMock(return_value=[formatted_metadata_history_1])
+    mocker.patch.object(
+        MetadataHistoryDatabaseFactory, "get_metadata_history", mock_get
+    )
+
+    response = client.get(
+        url="/metadata/history?station=BRW&category=observatory&network=NT"
+    )
+    assert response.status_code == 200
+
+    response_body = response.json()
+    assert len(response_body) == 1
+
+    assert response_body[0]["metadata_id"] == 38746
+    assert response_body[0]["station"] == "BRW"
+    assert response_body[0]["category"] == "observatory"
+    assert response_body[0]["network"] == "NT"
+
+    # assert nested metadata is returned as "metadata" and not "metadata_" to the user
+    assert response_body[0]["metadata"] != None
+    assert response_body[0].get("metadata_") == None
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_history_return_empty(mocker):
+    # mock return empty
+
+    mock_get = mocker.AsyncMock(return_value=[])
+    mocker.patch.object(
+        MetadataHistoryDatabaseFactory, "get_metadata_history", mock_get
+    )
+
+    response = client.get(
+        url="/metadata/history?station=BRW&category=observatory&network=NT"
+    )
+    assert response.status_code == 200
+
+    response_body = response.json()
+    assert len(response_body) == 0
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_history_invalid_input():
+    response = client.get(
+        url="/metadata/history?station=BRW&category=observatory&network=NT&starttime=today"
+    )
+    assert response.status_code == 422
+
+    response_body = response.json()
+    assert (
+        response_body["detail"][0]["msg"]
+        == "Input should be an instance of UTCDateTime"
+    )
+    assert (
+        response_body["detail"][1]["msg"]
+        == "Value error, Invalid time type. See obspy UTCDateTime for more information."
+    )
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_history_by_id(formatted_metadata_history_1, mocker):
+    mock_get = mocker.AsyncMock(return_value=formatted_metadata_history_1)
+    mocker.patch.object(
+        MetadataHistoryDatabaseFactory, "get_metadata_history_by_id", mock_get
+    )
+
+    response = client.get(url="/metadata/history/124221")
+    assert response.status_code == 200
+
+    response_body = response.json()
+    assert response_body["metadata_id"] == 38746
+    assert response_body["station"] == "BRW"
+    assert response_body["category"] == "observatory"
+    assert response_body["network"] == "NT"
+
+    # assert nested metadata is returned as "metadata" and not "metadata_" to the user
+    assert response_body["metadata"] == {
+        "id": "BRW",
+        "name": "Barrow",
+        "marks": [{"name": "m", "azimuth": "1"}],
+        "piers": [{"name": "p", "correction": "12"}],
+        "agency": "USGS",
+        "latitude": 71.322,
+        "elevation": 10,
+        "longitude": 203.378,
+        "agency_name": "United States Geological Survey (USGS)",
+        "electronics": ["1"],
+        "theodolites": ["12"],
+        "declination_base": 10589,
+        "sensor_orientation": "HDZF",
+    }
+    assert response_body.get("metadata_") == None
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_history_by_id_no_metadata_history_found(mocker):
+    mock_get = mocker.AsyncMock(return_value=None)
+    mocker.patch.object(
+        MetadataHistoryDatabaseFactory, "get_metadata_history_by_id", mock_get
+    )
+
+    response = client.get(url="/metadata/history/34253")
+    assert response.status_code == 404
+    response_body = response.json()
+    assert response_body["detail"] == "metadata_history not found"
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_history_by_id_invalid_id():
+    response = client.get(url="/metadata/history/8239453knsoi23yr")
+    assert response.status_code == 422
+
+    response_body = response.json()
+    assert (
+        response_body["detail"][0]["msg"]
+        == "Input should be a valid integer, unable to parse string as an integer"
+    )
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_history_by_metadata_id(
+    get_all_formatted_metadata_history, mocker
+):
+    mock_get = mocker.AsyncMock(return_value=get_all_formatted_metadata_history)
+    mocker.patch.object(
+        MetadataHistoryDatabaseFactory, "get_metadata_history_by_metadata_id", mock_get
+    )
+
+    response = client.get(url="/metadata/12532/history")
+
+    assert response.status_code == 200
+
+    response_body = response.json()
+    assert len(response_body) == 4
+    assert response_body[0]["id"] == 38746
+    assert response_body[1]["metadata_id"] == 38746
+    assert response_body[2]["metadata_id"] == 38746
+    assert response_body[3]["metadata_id"] == 38746
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_history_by_metadata_id_only_metadata(
+    formatted_metadata_for_formatted_metadata_history, mocker
+):
+    mock_get = mocker.AsyncMock(
+        return_value=[formatted_metadata_for_formatted_metadata_history]
+    )
+    mocker.patch.object(
+        MetadataHistoryDatabaseFactory, "get_metadata_history_by_metadata_id", mock_get
+    )
+
+    response = client.get(url="/metadata/12532/history")
+
+    assert response.status_code == 200
+
+    response_body = response.json()
+    assert len(response_body) == 1
+    assert response_body[0]["id"] == 38746
+
+
+@pytest.mark.asyncio
+async def test_get_metadata_history_by_metadata_id_not_found(mocker):
+    mock_get = mocker.AsyncMock(return_value=None)
+    mocker.patch.object(
+        MetadataHistoryDatabaseFactory, "get_metadata_history_by_metadata_id", mock_get
+    )
+
+    response = client.get(url="/metadata/3824598235973/history")
+
+    assert response.status_code == 404
+
+    response_body = response.json()
+    assert response_body["detail"] == "metadata not found"
diff --git a/test/api_test/secure_test/metadata_test.py b/test/api_test/secure_test/metadata_test.py
deleted file mode 100644
index b2c50090e8e682f2f36ddb3fb2dbf043c6be391a..0000000000000000000000000000000000000000
--- a/test/api_test/secure_test/metadata_test.py
+++ /dev/null
@@ -1,867 +0,0 @@
-import pytest
-import json
-import httpx
-import unittest
-import datetime
-from unittest.mock import ANY, AsyncMock, patch
-from fastapi.testclient import TestClient
-from obspy import UTCDateTime
-
-from geomagio.api.secure import app
-from geomagio.api.secure.SessionMiddleware import SessionMiddleware
-from geomagio.metadata.Metadata import Metadata
-from geomagio.api.db.MetadataDatabaseFactory import MetadataDatabaseFactory
-
-client = TestClient(app)
-
-
-@pytest.mark.asyncio
-@patch("databases.Database.execute", new_callable=AsyncMock)
-async def test_create_metadata_valid_session(
-    valid_token, valid_userinfo, valid_reading_metadata, mocker
-):
-    valid_session = {"token": valid_token, "user": valid_userinfo}
-    mock_get_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "get_session", mock_get_session)
-
-    mock_save_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "save_session", mock_save_session)
-
-    body = json.dumps(valid_reading_metadata)
-
-    response = client.post(
-        url="/metadata", headers={"Cookie": "PHPSESSID=valid_session_id"}, content=body
-    )
-
-    assert response.status_code == 201
-    mock_get_session.assert_called_once_with("valid_session_id")
-
-    returned_metadata = Metadata(**response.json())
-    assert returned_metadata.id == 1
-    assert returned_metadata.category == "reading"
-    assert returned_metadata.starttime == UTCDateTime("2024-11-07T12:12:12Z")
-    assert returned_metadata.endtime == UTCDateTime("2024-11-07T20:20:20Z")
-    assert returned_metadata.network == "NT"
-    assert returned_metadata.station == "BOU"
-    assert returned_metadata.status == "new"
-    assert returned_metadata.priority == 1
-
-
-@pytest.mark.asyncio
-@patch("databases.Database.execute", new_callable=AsyncMock)
-async def test_create_metadata_with_id(
-    valid_token, valid_userinfo, valid_reading_metadata, mocker
-):
-    # input id is removed and replaced with new db id
-    valid_session = {"token": valid_token, "user": valid_userinfo}
-    mock_get_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "get_session", mock_get_session)
-
-    mock_save_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "save_session", mock_save_session)
-
-    valid_reading_metadata["id"] = 1234
-    body = json.dumps(valid_reading_metadata)
-
-    response = client.post(
-        url="/metadata", headers={"Cookie": "PHPSESSID=valid_session_id"}, content=body
-    )
-
-    assert response.status_code == 201
-    mock_get_session.assert_called_once_with("valid_session_id")
-
-    returned_metadata = Metadata(**response.json())
-    assert returned_metadata.id == 1
-    assert returned_metadata.category == "reading"
-    assert returned_metadata.starttime == UTCDateTime("2024-11-07T12:12:12Z")
-    assert returned_metadata.endtime == UTCDateTime("2024-11-07T20:20:20Z")
-    assert returned_metadata.network == "NT"
-    assert returned_metadata.station == "BOU"
-    assert returned_metadata.status == "new"
-    assert returned_metadata.priority == 1
-
-
-@pytest.mark.asyncio
-@patch("databases.Database.execute", new_callable=AsyncMock)
-async def test_create_metadata_valid_auth_token(
-    valid_token, valid_userinfo, valid_gitlab_user, valid_reading_metadata, mocker
-):
-    valid_user_response = httpx.Response(status_code=200, json=valid_gitlab_user)
-
-    valid_groups = [
-        {
-            "id": 1,
-            "full_path": "group1",
-        },
-        {"id": 2, "full_path": "group2"},
-    ]
-
-    valid_groups_response = httpx.Response(status_code=200, json=valid_groups)
-
-    mock_gitlab_request = mocker.AsyncMock(
-        side_effect=[
-            valid_user_response,
-            valid_groups_response,
-            httpx.Response(status_code=200, json={}),
-        ]
-    )
-    mocker.patch("httpx.AsyncClient.get", mock_gitlab_request)
-
-    valid_session = {"token": valid_token, "user": valid_userinfo}
-
-    mock_save_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "save_session", mock_save_session)
-
-    body = json.dumps(valid_reading_metadata)
-
-    response = client.post(
-        url="/metadata", headers={"Authorization": "auth_token"}, content=body
-    )
-
-    session_user = {
-        "email": "test_email",
-        "sub": 1234,
-        "groups": ["group1", "group2"],
-        "name": "test_user",
-        "nickname": "test_user_nickname",
-        "picture": "http://picture_url",
-    }
-
-    assert response.status_code == 201
-    mock_save_session.assert_called_once_with(ANY, {"user": session_user})
-
-    returned_metadata = Metadata(**response.json())
-    assert returned_metadata.id == 1
-    assert returned_metadata.category == "reading"
-    assert returned_metadata.starttime == UTCDateTime("2024-11-07T12:12:12Z")
-    assert returned_metadata.endtime == UTCDateTime("2024-11-07T20:20:20Z")
-    assert returned_metadata.network == "NT"
-    assert returned_metadata.station == "BOU"
-    assert returned_metadata.status == "new"
-    assert returned_metadata.priority == 1
-
-
-@pytest.mark.asyncio
-async def test_create_metadata_no_session_or_token(valid_reading_metadata, mocker):
-    mock_get_session = mocker.AsyncMock()
-    mocker.patch.object(SessionMiddleware, "get_session", mock_get_session)
-
-    body = json.dumps(valid_reading_metadata)
-
-    # do not pass in cookie or auth
-    response = client.post(url="/metadata", content=body)
-    assert response.status_code == 401
-
-    mock_get_session.assert_not_called()
-
-
-@pytest.mark.asyncio
-async def test_create_metadata_with_no_session_and_invalid_token(mocker):
-    mock_get_session = mocker.AsyncMock()
-    mocker.patch.object(SessionMiddleware, "get_session", mock_get_session)
-
-    mock_gitlab_request = mocker.AsyncMock(return_value=None)
-    mocker.patch("geomagio.api.secure.login.get_gitlab_user", mock_gitlab_request)
-
-    response = client.post(
-        url="/metadata", headers={"Authorization": "invalid_gitlab_token"}
-    )
-    assert response.status_code == 401
-
-    mock_get_session.assert_not_called()
-    mock_gitlab_request.assert_called_once_with(token="invalid_gitlab_token")
-
-
-@pytest.mark.asyncio
-async def test_create_metadata_invalid_session(mocker):
-    # mock invalid session. this is created when users GET /metadata without logging in
-    invalid_session = {
-        "redirect_uri": "redirect_uri",
-        "nonce": "nonce_str",
-        "url": "test_url",
-    }
-    mock_get_session = mocker.AsyncMock(return_value=invalid_session)
-    mocker.patch.object(SessionMiddleware, "get_session", mock_get_session)
-
-    mock_save_session = mocker.AsyncMock()
-    mocker.patch.object(SessionMiddleware, "save_session", mock_save_session)
-
-    response = client.post(
-        url="/metadata", headers={"Cookie": "PHPSESSID=invalid_session_id"}
-    )
-
-    assert response.status_code == 401
-    mock_get_session.assert_called_once_with("invalid_session_id")
-
-
-@pytest.mark.asyncio
-@patch("databases.Database.transaction")
-@patch("databases.Database.fetch_all", new_callable=AsyncMock)
-@patch("databases.Database.execute", new_callable=AsyncMock)
-async def test_update_metadata(
-    mock_execute,
-    mock_fetch,
-    mock_transaction,
-    valid_token,
-    valid_userinfo,
-    valid_reading_metadata,
-    mocker,
-):
-    valid_userinfo["groups"] = ["ghsc/geomag/operations/roles/reviewer"]
-    valid_session = {"token": valid_token, "user": valid_userinfo}
-    mock_get_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "get_session", mock_get_session)
-
-    mock_save_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "save_session", mock_save_session)
-
-    valid_reading_metadata["id"] = 1234
-    mock_fetch.side_effect = [
-        [
-            {
-                "id": 1234,
-                "category": "instrument",
-                "data_valid": True,
-                "priority": 1,
-            }
-        ],
-        [valid_reading_metadata],
-    ]
-    mock_execute.side_effect = [1, None]
-
-    body = json.dumps(valid_reading_metadata)
-
-    response = client.put(
-        url="/metadata/1234",
-        headers={"Cookie": "PHPSESSID=valid_session_id"},
-        content=body,
-    )
-    assert response.status_code == 200
-
-    returned_metadata = Metadata(**response.json())
-    returned_metadata.id = 1234
-    returned_metadata.category = "reading"
-
-    assert mock_fetch.call_count == 2
-    assert mock_execute.call_count == 2
-
-    # the query for both fetch_all requests should be the same
-    fetch_called_pararms = mock_fetch.call_args.args[0].compile().params
-    assert fetch_called_pararms["id_1"] == 1234
-
-    # assert save to metadata history is correct
-    insert_called_params = mock_execute.call_args_list[0].args[0].compile().params
-    assert insert_called_params["metadata_id"] == 1234
-    assert insert_called_params["category"] == "instrument"
-    assert insert_called_params["data_valid"] == True
-    assert insert_called_params["priority"] == 1
-
-    # assert update to metadata table is correct
-    update_called_params = mock_execute.call_args_list[1].args[0].compile().params
-    assert update_called_params["id_1"] == 1234
-    assert update_called_params["category"] == "reading"
-
-
-@pytest.mark.asyncio
-@patch("databases.Database.transaction")
-@patch("databases.Database.fetch_all", new_callable=AsyncMock)
-@patch("databases.Database.execute", new_callable=AsyncMock)
-async def test_update_metadata_valid_auth_token(
-    mock_execute,
-    mock_fetch,
-    mock_transaction,
-    valid_token,
-    valid_userinfo,
-    valid_gitlab_user,
-    valid_reading_metadata,
-    mocker,
-):
-    valid_user_response = httpx.Response(status_code=200, json=valid_gitlab_user)
-
-    valid_groups = [
-        {
-            "id": 1,
-            "full_path": "group1",
-        },
-        {"id": 2, "full_path": "ghsc/geomag/operations/roles/reviewer"},
-    ]
-
-    valid_groups_response = httpx.Response(status_code=200, json=valid_groups)
-
-    mock_gitlab_request = mocker.AsyncMock(
-        side_effect=[
-            valid_user_response,
-            valid_groups_response,
-            httpx.Response(status_code=200, json={}),
-        ]
-    )
-    mocker.patch("httpx.AsyncClient.get", mock_gitlab_request)
-
-    valid_session = {"token": valid_token, "user": valid_userinfo}
-
-    mock_save_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "save_session", mock_save_session)
-
-    valid_reading_metadata["id"] = 1234
-    mock_fetch.side_effect = [
-        [
-            {
-                "id": 1234,
-                "category": "instrument",
-                "data_valid": True,
-                "priority": 1,
-            }
-        ],
-        [valid_reading_metadata],
-    ]
-    mock_execute.side_effect = [1, None]
-
-    body = json.dumps(valid_reading_metadata)
-
-    response = client.put(
-        url="/metadata/1234", headers={"Authorization": "auth_token"}, content=body
-    )
-
-    session_user = {
-        "email": "test_email",
-        "sub": 1234,
-        "groups": ["group1", "ghsc/geomag/operations/roles/reviewer"],
-        "name": "test_user",
-        "nickname": "test_user_nickname",
-        "picture": "http://picture_url",
-    }
-
-    assert response.status_code == 200
-    mock_save_session.assert_called_once_with(ANY, {"user": session_user})
-
-    returned_metadata = Metadata(**response.json())
-    returned_metadata.id = 1234
-    returned_metadata.category = "reading"
-
-    assert mock_fetch.call_count == 2
-    assert mock_execute.call_count == 2
-
-    # the query for both fetch_all requests should be the same
-    fetch_called_pararms = mock_fetch.call_args.args[0].compile().params
-    assert fetch_called_pararms["id_1"] == 1234
-
-    # assert save to metadata history is correct
-    insert_called_params = mock_execute.call_args_list[0].args[0].compile().params
-    assert insert_called_params["metadata_id"] == 1234
-    assert insert_called_params["category"] == "instrument"
-    assert insert_called_params["data_valid"] == True
-    assert insert_called_params["priority"] == 1
-
-    # assert update to metadata table is correct
-    update_called_params = mock_execute.call_args_list[1].args[0].compile().params
-    assert update_called_params["id_1"] == 1234
-    assert update_called_params["category"] == "reading"
-
-
-@pytest.mark.asyncio
-@patch("databases.Database.transaction")
-@patch("databases.Database.fetch_all", new_callable=AsyncMock)
-@patch("databases.Database.execute", new_callable=AsyncMock)
-async def test_update_metadata_no_existing_metadata(
-    mock_execute,
-    mock_fetch,
-    mock_transaction,
-    valid_token,
-    valid_userinfo,
-    valid_reading_metadata,
-    mocker,
-):
-    valid_userinfo["groups"] = ["ghsc/geomag/operations/roles/reviewer"]
-    valid_session = {"token": valid_token, "user": valid_userinfo}
-    mock_get_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "get_session", mock_get_session)
-
-    mock_save_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "save_session", mock_save_session)
-
-    valid_reading_metadata["id"] = 1234
-    mock_fetch.return_value = []
-
-    body = json.dumps(valid_reading_metadata)
-
-    try:
-        response = client.put(
-            url="/metadata/1234",
-            headers={"Cookie": "PHPSESSID=valid_session_id"},
-            content=body,
-        )
-    except Exception as e:
-        assert type(e) == ValueError
-
-
-@pytest.mark.asyncio
-async def test_update_metadata_no_valid_group(
-    valid_token, valid_userinfo, valid_reading_metadata, mocker
-):
-    valid_session = {"token": valid_token, "user": valid_userinfo}
-    mock_get_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "get_session", mock_get_session)
-
-    mock_save_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "save_session", mock_save_session)
-
-    body = json.dumps(valid_reading_metadata)
-
-    response = client.put(
-        url="/metadata/1234",
-        headers={"Cookie": "PHPSESSID=valid_session_id"},
-        content=body,
-    )
-    assert response.status_code == 403
-
-
-@pytest.mark.asyncio
-async def test_update_metadata_no_session(
-    valid_token, valid_userinfo, valid_reading_metadata, mocker
-):
-    valid_session = {"token": valid_token, "user": valid_userinfo}
-    mock_get_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "get_session", mock_get_session)
-
-    mock_save_session = mocker.AsyncMock(return_value=valid_session)
-    mocker.patch.object(SessionMiddleware, "save_session", mock_save_session)
-
-    body = json.dumps(valid_reading_metadata)
-
-    response = client.put(url="/metadata/1234", content=body)
-    assert response.status_code == 401
-
-    mock_get_session.assert_not_called()
-
-
-@pytest.mark.asyncio
-async def test_update_metadata_invalid_session(mocker, valid_reading_metadata):
-    # mock invalid session. this is created when users GET /metadata without logging in
-    invalid_session = {
-        "redirect_uri": "redirect_uri",
-        "nonce": "nonce_str",
-        "url": "test_url",
-    }
-    mock_get_session = mocker.AsyncMock(return_value=invalid_session)
-    mocker.patch.object(SessionMiddleware, "get_session", mock_get_session)
-
-    mock_save_session = mocker.AsyncMock()
-    mocker.patch.object(SessionMiddleware, "save_session", mock_save_session)
-
-    body = json.dumps(valid_reading_metadata)
-
-    response = client.put(
-        url="/metadata/1234",
-        headers={"Cookie": "PHPSESSID=invalid_session_id"},
-        content=body,
-    )
-
-    assert response.status_code == 401
-    mock_get_session.assert_called_once_with("invalid_session_id")
-
-
-@pytest.mark.asyncio
-async def test_update_metadata_with_no_session_and_invalid_token(
-    mocker, valid_reading_metadata
-):
-    mock_get_session = mocker.AsyncMock()
-    mocker.patch.object(SessionMiddleware, "get_session", mock_get_session)
-
-    mock_gitlab_request = mocker.AsyncMock(return_value=None)
-    mocker.patch("geomagio.api.secure.login.get_gitlab_user", mock_gitlab_request)
-
-    body = json.dumps(valid_reading_metadata)
-
-    response = client.put(
-        url="/metadata/1234",
-        headers={"Authorization": "invalid_gitlab_token"},
-        content=body,
-    )
-    assert response.status_code == 401
-
-    mock_get_session.assert_not_called()
-    mock_gitlab_request.assert_called_once_with(token="invalid_gitlab_token")
-
-
-# # # the way the database class is set up prevents us from using pytest-mocks here. unittest is a bit more
-# # # powerful but less intuitive and allows us to inspect the parameters to mocked database calls
-class TestMetadata(unittest.IsolatedAsyncioTestCase):
-    @patch("databases.Database.fetch_all", new_callable=AsyncMock)
-    async def test_get_metadata(self, mock_fetch):
-        mock_fetch.return_value = [
-            {
-                "id": 8372,
-                "station": "BOU",
-                "category": "instrument",
-                "network": "NT",
-                "channel": "F",
-                "location": "R0",
-                "data_valid": True,
-                "status": "new",
-                "metadata": {"metadata1": "metadata1"},
-            },
-            {
-                "id": 8376,
-                "station": "BOU",
-                "category": "instrument",
-                "network": "NT",
-                "channel": "F",
-                "location": "R0",
-                "data_valid": True,
-                "status": "new",
-                "metadata": {"metadata2": "metadata2"},
-            },
-        ]
-
-        response = client.get(
-            url="/metadata?station=BOU&category=instrument&network=NT&channel=F&location=R0&data_valid=true&status=new"
-        )
-        assert response.status_code == 200
-
-        mock_fetch.assert_called_once()
-        called_pararms = mock_fetch.call_args.args[0].compile().params
-
-        assert called_pararms["station_1"] == "BOU"
-        assert called_pararms["category_1"] == "instrument"
-        assert called_pararms["network_1"] == "NT"
-        assert called_pararms["channel_1"] == "F"
-        assert called_pararms["location_1"] == "R0"
-        assert called_pararms["status_1"] == ["new"]
-
-        returned_metadata = []
-
-        rows = response.json()
-        for metadata in rows:
-            returned_metadata.append(Metadata(**metadata))
-
-        assert returned_metadata[0].id == 8372
-        assert returned_metadata[1].id == 8376
-
-    @patch("databases.Database.fetch_all", new_callable=AsyncMock)
-    async def test_get_metadata_with_starttime(self, mock_fetch):
-        mock_fetch.return_value = [
-            {
-                "id": 8372,
-                "station": "BOU",
-                "category": "instrument",
-                "network": "NT",
-                "channel": "F",
-                "location": "R0",
-                "data_valid": True,
-                "status": "new",
-                "metadata": {"metadata1": "metadata1"},
-            },
-            {
-                "id": 8376,
-                "station": "BOU",
-                "category": "instrument",
-                "network": "NT",
-                "channel": "F",
-                "location": "R0",
-                "data_valid": True,
-                "status": "new",
-                "metadata": {"metadata2": "metadata2"},
-            },
-        ]
-        response = client.get(
-            url="/metadata?station=BOU&category=instrument&starttime=2024-11-06T04:27:40Z"
-        )
-        assert response.status_code == 200
-
-        mock_fetch.assert_called_once()
-        called_pararms = mock_fetch.call_args.args[0].compile().params
-
-        assert called_pararms["station_1"] == "BOU"
-        assert called_pararms["category_1"] == "instrument"
-        # assert endtime is set to starttime because the query is where endtime is None or greater than starttime
-        assert called_pararms["endtime_1"] == datetime.datetime(
-            2024, 11, 6, 4, 27, 40, tzinfo=datetime.timezone.utc
-        )
-
-        returned_metadata = []
-
-        rows = response.json()
-        for metadata in rows:
-            returned_metadata.append(Metadata(**metadata))
-
-        assert returned_metadata[0].id == 8372
-        assert returned_metadata[1].id == 8376
-
-    @patch("databases.Database.fetch_all", new_callable=AsyncMock)
-    async def test_get_metadata_with_endtime(self, mock_fetch):
-        mock_fetch.return_value = [
-            {
-                "id": 8372,
-                "station": "BOU",
-                "category": "instrument",
-                "network": "NT",
-                "channel": "F",
-                "location": "R0",
-                "data_valid": True,
-                "status": "new",
-                "metadata": {"metadata1": "metadata1"},
-            },
-            {
-                "id": 8376,
-                "station": "BOU",
-                "category": "instrument",
-                "network": "NT",
-                "channel": "F",
-                "location": "R0",
-                "data_valid": True,
-                "status": "new",
-                "metadata": {"metadata2": "metadata2"},
-            },
-        ]
-        response = client.get(
-            url="/metadata?station=BOU&category=instrument&endtime=2024-12-06T04:27:40Z"
-        )
-        assert response.status_code == 200
-
-        mock_fetch.assert_called_once()
-        called_pararms = mock_fetch.call_args.args[0].compile().params
-
-        assert called_pararms["station_1"] == "BOU"
-        assert called_pararms["category_1"] == "instrument"
-        # assert starttime is set to endtime because the query is where starttime is None or greater than endtime
-        assert called_pararms["starttime_1"] == datetime.datetime(
-            2024, 12, 6, 4, 27, 40, tzinfo=datetime.timezone.utc
-        )
-
-        returned_metadata = []
-
-        rows = response.json()
-        for metadata in rows:
-            returned_metadata.append(Metadata(**metadata))
-
-        assert returned_metadata[0].id == 8372
-        assert returned_metadata[1].id == 8376
-
-    @patch("databases.Database.fetch_all", new_callable=AsyncMock)
-    async def test_get_metadata_with_created_after(self, mock_fetch):
-        mock_fetch.return_value = [
-            {
-                "id": 8372,
-                "station": "BOU",
-                "category": "instrument",
-                "network": "NT",
-                "channel": "F",
-                "location": "R0",
-                "data_valid": True,
-                "status": "new",
-                "metadata": {"metadata1": "metadata1"},
-            },
-            {
-                "id": 8376,
-                "station": "BOU",
-                "category": "instrument",
-                "network": "NT",
-                "channel": "F",
-                "location": "R0",
-                "data_valid": True,
-                "status": "new",
-                "metadata": {"metadata2": "metadata2"},
-            },
-        ]
-        response = client.get(
-            url="/metadata?station=BOU&category=instrument&created_after=2024-10-06T04:27:40Z"
-        )
-        assert response.status_code == 200
-
-        mock_fetch.assert_called_once()
-        called_pararms = mock_fetch.call_args.args[0].compile().params
-
-        assert called_pararms["station_1"] == "BOU"
-        assert called_pararms["category_1"] == "instrument"
-        # assert created_time is set to created_after because the query is where created_time is greater than created_after
-        assert called_pararms["created_time_1"] == datetime.datetime(
-            2024, 10, 6, 4, 27, 40, tzinfo=datetime.timezone.utc
-        )
-
-        returned_metadata = []
-
-        rows = response.json()
-        for metadata in rows:
-            returned_metadata.append(Metadata(**metadata))
-
-        assert returned_metadata[0].id == 8372
-        assert returned_metadata[1].id == 8376
-
-    @patch("databases.Database.fetch_all", new_callable=AsyncMock)
-    async def test_get_metadata_with_created_before(self, mock_fetch):
-        mock_fetch.return_value = [
-            {
-                "id": 8372,
-                "station": "BOU",
-                "category": "instrument",
-                "network": "NT",
-                "channel": "F",
-                "location": "R0",
-                "data_valid": True,
-                "status": "new",
-                "metadata": {"metadata1": "metadata1"},
-            },
-            {
-                "id": 8376,
-                "station": "BOU",
-                "category": "instrument",
-                "network": "NT",
-                "channel": "F",
-                "location": "R0",
-                "data_valid": True,
-                "status": "new",
-                "metadata": {"metadata2": "metadata2"},
-            },
-        ]
-        response = client.get(
-            url="/metadata?station=BOU&category=instrument&created_before=2024-09-06T04:27:40Z"
-        )
-        assert response.status_code == 200
-
-        mock_fetch.assert_called_once()
-        called_pararms = mock_fetch.call_args.args[0].compile().params
-
-        assert called_pararms["station_1"] == "BOU"
-        assert called_pararms["category_1"] == "instrument"
-        # assert created_time is set to created_before because the query is where created_time is less than created_before
-        assert called_pararms["created_time_1"] == datetime.datetime(
-            2024, 9, 6, 4, 27, 40, tzinfo=datetime.timezone.utc
-        )
-
-        returned_metadata = []
-
-        rows = response.json()
-        for metadata in rows:
-            returned_metadata.append(Metadata(**metadata))
-
-        assert returned_metadata[0].id == 8372
-        assert returned_metadata[1].id == 8376
-
-    @patch("databases.Database.fetch_all", new_callable=AsyncMock)
-    async def test_get_metadata_returns_empty(self, mock_fetch):
-        mock_fetch.return_value = []
-
-        response = client.get(
-            url="/metadata?station=BOU&category=instrument&created_before=2024-09-06T04:27:40Z"
-        )
-        assert response.status_code == 200
-
-        mock_fetch.assert_called_once()
-
-        # assert response is an empty list
-        assert response.json() == []
-
-    @patch("databases.Database.fetch_all", new_callable=AsyncMock)
-    async def test_get_metadata_by_id(self, mock_fetch):
-        mock_fetch.return_value = [{"id": 1234, "category": "instrument"}]
-        response = client.get(url="/metadata/1234")
-        assert response.status_code == 200
-
-        mock_fetch.assert_called_once()
-        called_pararms = mock_fetch.call_args.args[0].compile().params
-
-        assert called_pararms["id_1"] == 1234
-
-        returned_metadata = Metadata(**response.json())
-        assert returned_metadata.id == 1234
-        assert returned_metadata.category == "instrument"
-
-    @patch("databases.Database.fetch_all", new_callable=AsyncMock)
-    async def test_get_metadata_by_id_returns_empty(self, mock_fetch):
-        mock_fetch.return_value = []
-        try:
-            client.get(url="/metadata/1234")
-        except Exception as e:
-            assert type(e) == ValueError
-
-        mock_fetch.assert_called_once()
-
-    @patch("databases.Database.fetch_one", new_callable=AsyncMock)
-    async def test_get_metadata_history_by_id(self, mock_fetch):
-        mock_fetch.return_value = {"id": 1234, "category": "instrument"}
-        response = client.get(url="/metadata/history/1234")
-        assert response.status_code == 200
-
-        mock_fetch.assert_called_once()
-        called_pararms = mock_fetch.call_args.args[0].compile().params
-
-        assert called_pararms["id_1"] == 1234
-
-        returned_metadata = Metadata(**response.json())
-        assert returned_metadata.id == 1234
-        assert returned_metadata.category == "instrument"
-
-    @patch("databases.Database.fetch_one", new_callable=AsyncMock)
-    async def test_get_metadata_history_by_id_returns_empty(self, mock_fetch):
-        mock_fetch.return_value = None
-
-        response = client.get(url="/metadata/history/1234")
-        assert response.status_code == 404
-
-        mock_fetch.assert_called_once()
-        called_pararms = mock_fetch.call_args.args[0].compile().params
-
-        assert called_pararms["id_1"] == 1234
-
-    @patch("databases.Database.transaction")
-    @patch("databases.Database.fetch_all", new_callable=AsyncMock)
-    async def test_get_metadata_history_by_metadata_id(
-        self, mock_fetch, mock_transaction
-    ):
-        mock_fetch.side_effect = [
-            [{"id": 242, "metadata_id": 1234, "category": "instrument"}],
-            [{"id": 1234, "category": "reading"}],
-        ]
-        response = client.get(url="/metadata/1234/history")
-        assert response.status_code == 200
-
-        assert mock_fetch.call_count == 2
-        called_pararms = mock_fetch.call_args.args[0].compile().params
-
-        assert called_pararms["id_1"] == 1234
-
-        returned_metadata = []
-
-        rows = response.json()
-        for metadata in rows:
-            returned_metadata.append(Metadata(**metadata))
-
-        # expect the metadata from the metadata table to be first
-        assert returned_metadata[0].id == 1234
-        assert returned_metadata[1].id == 242
-
-    @patch("databases.Database.transaction")
-    @patch("databases.Database.fetch_all", new_callable=AsyncMock)
-    async def test_get_metadata_history_by_metadata_id_no_history(
-        self, mock_fetch, mock_transaction
-    ):
-        mock_fetch.side_effect = [[], [{"id": 1234, "category": "reading"}]]
-        response = client.get(url="/metadata/1234/history")
-        assert response.status_code == 200
-
-        assert mock_fetch.call_count == 2
-        called_pararms = mock_fetch.call_args.args[0].compile().params
-
-        assert called_pararms["id_1"] == 1234
-
-        returned_metadata = []
-
-        rows = response.json()
-        for metadata in rows:
-            returned_metadata.append(Metadata(**metadata))
-
-        assert returned_metadata[0].id == 1234
-
-    @patch("databases.Database.transaction")
-    @patch("databases.Database.fetch_all", new_callable=AsyncMock)
-    async def test_get_metadata_history_by_metadata_id_no_history_or_metadata(
-        self, mock_fetch, mock_transaction
-    ):
-        mock_fetch.side_effect = [[], []]
-
-        try:
-            client.get(url="/metadata/1234/history")
-        except Exception as e:
-            assert type(e) == ValueError
-
-        assert mock_fetch.call_count == 2
diff --git a/test/api_test/ws_test/data_test.py b/test/api_test/ws_test/data_test.py
index 765fa407b013db3f9e87b87d04c56a3b576d6d99..dd7a24bde261748ba452c681e60fd6a66eb8a2f4 100644
--- a/test/api_test/ws_test/data_test.py
+++ b/test/api_test/ws_test/data_test.py
@@ -5,7 +5,7 @@ from numpy.testing import assert_equal
 from obspy import UTCDateTime
 import pytest
 
-from geomagio.api.ws import app
+from geomagio.api.ws.ws_app import app
 from geomagio.api.ws.data import get_data_query
 from geomagio.api.ws.DataApiQuery import (
     DataApiQuery,
diff --git a/test/api_test/ws_test/elements_test.py b/test/api_test/ws_test/elements_test.py
index 5549fb76f7c0db2155369e57d9c0581e1da9ae04..6f0ba79f3c8170dea51927ab7ce81141e336bcfb 100644
--- a/test/api_test/ws_test/elements_test.py
+++ b/test/api_test/ws_test/elements_test.py
@@ -1,6 +1,6 @@
 from fastapi.testclient import TestClient
 
-from geomagio.api.ws import app
+from geomagio.api.ws.ws_app import app
 
 client = TestClient(app)
 
diff --git a/test/api_test/ws_test/filter_test.py b/test/api_test/ws_test/filter_test.py
index 67a09a4217317ddba87fce88a3e63e4df30fc5df..48cf0da56ce0798d691dd84970f02fd9f4862008 100644
--- a/test/api_test/ws_test/filter_test.py
+++ b/test/api_test/ws_test/filter_test.py
@@ -5,7 +5,7 @@ from numpy.testing import assert_equal
 from obspy import UTCDateTime
 import pytest
 
-from geomagio.api.ws import app
+from geomagio.api.ws.ws_app import app
 from geomagio.api.ws.filter import get_filter_data_query
 from geomagio.api.ws.FilterApiQuery import FilterApiQuery
 from geomagio.api.ws.DataApiQuery import SamplingPeriod, OutputFormat, DataType
diff --git a/test/api_test/ws_test/observatories_test.py b/test/api_test/ws_test/observatories_test.py
index dbe95cda17342936a88b3c6080f6c156907dd2b3..cdc55629404f72782bd017c33f79f8d1042eb545 100644
--- a/test/api_test/ws_test/observatories_test.py
+++ b/test/api_test/ws_test/observatories_test.py
@@ -1,6 +1,6 @@
 from fastapi.testclient import TestClient
 
-from geomagio.api.ws import app
+from geomagio.api.ws.ws_app import app
 
 client = TestClient(app)
 
diff --git a/test/api_test/ws_test/variometers_test.py b/test/api_test/ws_test/variometers_test.py
index 163411efaf22aa75f098c17e839d3d4ff0d7bcdc..5d7ee4744fed5107d7fa26867accb3da12c71c95 100644
--- a/test/api_test/ws_test/variometers_test.py
+++ b/test/api_test/ws_test/variometers_test.py
@@ -1,6 +1,6 @@
 from fastapi.testclient import TestClient
 
-from geomagio.api.ws import app
+from geomagio.api.ws.ws_app import app
 
 client = TestClient(app)
 
diff --git a/test/conftest.py b/test/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..b7a28d1139c1bd3bc83da8838d7c89ddeb9465f4
--- /dev/null
+++ b/test/conftest.py
@@ -0,0 +1,16 @@
+import pytest
+
+from sqlmodel import SQLModel, create_engine
+
+from geomagio.api.db.models.metadata import MetadataTable
+from geomagio.api.db.models.metadata_history import MetadataHistoryTable
+from geomagio.api.db.models.session import session
+
+# create a test engine that connects to an in memory test database
+engine = create_engine("sqlite://")
+SQLModel.metadata.create_all(engine)
+
+
+@pytest.fixture()
+def test_engine():
+    return engine
diff --git a/test/db/MetadataDatabaseFactory_test.py b/test/db/MetadataDatabaseFactory_test.py
deleted file mode 100644
index 61e60299d56e345432327275b20cf047c721efff..0000000000000000000000000000000000000000
--- a/test/db/MetadataDatabaseFactory_test.py
+++ /dev/null
@@ -1,443 +0,0 @@
-import datetime
-import unittest
-from dateutil import tz
-from unittest.mock import AsyncMock, patch
-from databases import Database
-
-from obspy import UTCDateTime
-
-from geomagio.api.db import MetadataDatabaseFactory
-from geomagio.metadata import Metadata, MetadataCategory, MetadataQuery
-
-
-class TestMetadataDatabaseFactory(unittest.IsolatedAsyncioTestCase):
-
-    @patch("databases.Database.execute", new_callable=AsyncMock)
-    async def test_create_metadata_defaults(self, mock_execute):
-        test_data = Metadata(
-            category=MetadataCategory.INSTRUMENT,
-            created_by="test_metadata.py",
-            network="NT",
-            station="BDT",
-            metadata={
-                "type": "FGE",
-                "channels": {
-                    "U": [{"channel": "U_Volt", "offset": 0, "scale": 313.2}],
-                    "V": [{"channel": "V_Volt", "offset": 0, "scale": 312.3}],
-                    "W": [{"channel": "W_Volt", "offset": 0, "scale": 312.0}],
-                },
-                "electronics": {
-                    "serial": "E0542",
-                    "x-scale": 313.2,
-                    "y-scale": 312.3,
-                    "z-scale": 312.0,
-                    "temperature-scale": 0.01,
-                },
-                "sensor": {
-                    "serial": "S0419",
-                    "x-constant": 36958,
-                    "y-constant": 36849,
-                    "z-constant": 36811,
-                },
-            },
-        )
-
-        db = Database("sqlite:///:memory:")
-
-        await MetadataDatabaseFactory(database=db).create_metadata(test_data)
-
-        # assert data_valid, priority, and status are set to the correct defaults
-        expected_values = {
-            "category": "instrument",
-            "created_by": "test_metadata.py",
-            "network": "NT",
-            "station": "BDT",
-            "metadata": {
-                "type": "FGE",
-                "channels": {
-                    "U": [{"channel": "U_Volt", "offset": 0, "scale": 313.2}],
-                    "V": [{"channel": "V_Volt", "offset": 0, "scale": 312.3}],
-                    "W": [{"channel": "W_Volt", "offset": 0, "scale": 312.0}],
-                },
-                "electronics": {
-                    "serial": "E0542",
-                    "x-scale": 313.2,
-                    "y-scale": 312.3,
-                    "z-scale": 312.0,
-                    "temperature-scale": 0.01,
-                },
-                "sensor": {
-                    "serial": "S0419",
-                    "x-constant": 36958,
-                    "y-constant": 36849,
-                    "z-constant": 36811,
-                },
-            },
-            "data_valid": True,
-            "priority": 1,
-            "status": "new",
-        }
-
-        mock_execute.assert_called_once()
-        called_params = mock_execute.call_args.args[0].compile().params
-
-        assert called_params == expected_values
-
-    @patch("databases.Database.execute", new_callable=AsyncMock)
-    async def test_create_metadata_created_time(self, mock_execute):
-        now = UTCDateTime()
-        test_data = Metadata(
-            created_time=now,
-            category=MetadataCategory.INSTRUMENT,
-            created_by="test_metadata.py",
-            network="NT",
-            station="BDT",
-            metadata={
-                "type": "FGE",
-                "channels": {
-                    "U": [{"channel": "U_Volt", "offset": 0, "scale": 313.2}],
-                    "V": [{"channel": "V_Volt", "offset": 0, "scale": 312.3}],
-                    "W": [{"channel": "W_Volt", "offset": 0, "scale": 312.0}],
-                },
-                "electronics": {
-                    "serial": "E0542",
-                    "x-scale": 313.2,
-                    "y-scale": 312.3,
-                    "z-scale": 312.0,
-                    "temperature-scale": 0.01,
-                },
-                "sensor": {
-                    "serial": "S0419",
-                    "x-constant": 36958,
-                    "y-constant": 36849,
-                    "z-constant": 36811,
-                },
-            },
-        )
-
-        db = Database("sqlite:///:memory:")
-
-        await MetadataDatabaseFactory(database=db).create_metadata(test_data)
-
-        # assert data_valid, priority, and status are set to the correct defaults
-        expected_values = {
-            "created_time": datetime.datetime(
-                year=now.year,
-                month=now.month,
-                day=now.day,
-                hour=now.hour,
-                minute=now.minute,
-                second=now.second,
-                microsecond=now.microsecond,
-                tzinfo=tz.tzutc(),
-            ),
-            "category": "instrument",
-            "created_by": "test_metadata.py",
-            "network": "NT",
-            "station": "BDT",
-            "metadata": {
-                "type": "FGE",
-                "channels": {
-                    "U": [{"channel": "U_Volt", "offset": 0, "scale": 313.2}],
-                    "V": [{"channel": "V_Volt", "offset": 0, "scale": 312.3}],
-                    "W": [{"channel": "W_Volt", "offset": 0, "scale": 312.0}],
-                },
-                "electronics": {
-                    "serial": "E0542",
-                    "x-scale": 313.2,
-                    "y-scale": 312.3,
-                    "z-scale": 312.0,
-                    "temperature-scale": 0.01,
-                },
-                "sensor": {
-                    "serial": "S0419",
-                    "x-constant": 36958,
-                    "y-constant": 36849,
-                    "z-constant": 36811,
-                },
-            },
-            "data_valid": True,
-            "priority": 1,
-            "status": "new",
-        }
-
-        mock_execute.assert_called_once()
-        called_params = mock_execute.call_args.args[0].compile().params
-
-        assert called_params == expected_values
-
-    @patch("databases.Database.execute", new_callable=AsyncMock)
-    async def test_create_metadata_with_ids(self, mock_execute):
-        test_data = Metadata(
-            id=1234,
-            metadata_id=5678,
-            category=MetadataCategory.INSTRUMENT,
-            created_by="test_metadata.py",
-            network="NT",
-            station="BDT",
-            metadata={
-                "type": "FGE",
-                "channels": {
-                    "U": [{"channel": "U_Volt", "offset": 0, "scale": 313.2}],
-                    "V": [{"channel": "V_Volt", "offset": 0, "scale": 312.3}],
-                    "W": [{"channel": "W_Volt", "offset": 0, "scale": 312.0}],
-                },
-                "electronics": {
-                    "serial": "E0542",
-                    "x-scale": 313.2,
-                    "y-scale": 312.3,
-                    "z-scale": 312.0,
-                    "temperature-scale": 0.01,
-                },
-                "sensor": {
-                    "serial": "S0419",
-                    "x-constant": 36958,
-                    "y-constant": 36849,
-                    "z-constant": 36811,
-                },
-            },
-        )
-
-        db = Database("sqlite:///:memory:")
-
-        await MetadataDatabaseFactory(database=db).create_metadata(test_data)
-
-        # assert id and metadata_id are removed
-        expected_values = {
-            "category": "instrument",
-            "created_by": "test_metadata.py",
-            "network": "NT",
-            "station": "BDT",
-            "metadata": {
-                "type": "FGE",
-                "channels": {
-                    "U": [{"channel": "U_Volt", "offset": 0, "scale": 313.2}],
-                    "V": [{"channel": "V_Volt", "offset": 0, "scale": 312.3}],
-                    "W": [{"channel": "W_Volt", "offset": 0, "scale": 312.0}],
-                },
-                "electronics": {
-                    "serial": "E0542",
-                    "x-scale": 313.2,
-                    "y-scale": 312.3,
-                    "z-scale": 312.0,
-                    "temperature-scale": 0.01,
-                },
-                "sensor": {
-                    "serial": "S0419",
-                    "x-constant": 36958,
-                    "y-constant": 36849,
-                    "z-constant": 36811,
-                },
-            },
-            "data_valid": True,
-            "priority": 1,
-            "status": "new",
-        }
-
-        mock_execute.assert_called_once()
-        called_params = mock_execute.call_args.args[0].compile().params
-
-        assert called_params == expected_values
-
-    @patch("databases.Database.execute", new_callable=AsyncMock)
-    async def test_create_metadata_with_starttime_and_endtime(self, mock_execute):
-        now = UTCDateTime()
-        t = UTCDateTime(2020, 1, 3, 17, 24, 40)
-        test_data = Metadata(
-            created_by="test_metadata.py",
-            created_time=now,
-            starttime=t,
-            endtime=t,
-            network="NT",
-            station="BOU",
-            channel=None,
-            location=None,
-            category=MetadataCategory.READING,
-            priority=1,
-            data_valid=True,
-            metadata={},
-        )
-
-        db = Database("sqlite:///:memory:")
-
-        await MetadataDatabaseFactory(database=db).create_metadata(test_data)
-
-        # assert starttime and endtime are strings of expected UTCDateTime
-        expected_values = {
-            "category": "reading",
-            "created_time": datetime.datetime(
-                year=now.year,
-                month=now.month,
-                day=now.day,
-                hour=now.hour,
-                minute=now.minute,
-                second=now.second,
-                microsecond=now.microsecond,
-                tzinfo=tz.tzutc(),
-            ),
-            "created_by": "test_metadata.py",
-            "starttime": datetime.datetime(
-                year=t.year,
-                month=t.month,
-                day=t.day,
-                hour=t.hour,
-                minute=t.minute,
-                second=t.second,
-                microsecond=t.microsecond,
-                tzinfo=tz.tzutc(),
-            ),
-            "endtime": datetime.datetime(
-                year=t.year,
-                month=t.month,
-                day=t.day,
-                hour=t.hour,
-                minute=t.minute,
-                second=t.second,
-                microsecond=t.microsecond,
-                tzinfo=tz.tzutc(),
-            ),
-            "network": "NT",
-            "station": "BOU",
-            "metadata": {},
-            "data_valid": True,
-            "priority": 1,
-            "status": "new",
-        }
-
-        mock_execute.assert_called_once()
-        called_params = mock_execute.call_args.args[0].compile().params
-
-        assert called_params == expected_values
-
-    @patch("databases.Database.execute", new_callable=AsyncMock)
-    async def test_create_metadata_with_times_as_datetime(self, mock_execute):
-        # assert datetime is aware if not explicitly set by the user
-        s = datetime.datetime(2020, 1, 3, 17, 24, 40)
-        e = datetime.datetime(2020, 1, 3, 17, 24, 40, tzinfo=tz.tzutc())
-        test_data = Metadata(
-            created_by="test_metadata.py",
-            starttime=s,
-            endtime=e,
-            network="NT",
-            station="BOU",
-            channel=None,
-            location=None,
-            category=MetadataCategory.READING,
-            priority=1,
-            data_valid=True,
-            metadata={},
-        )
-
-        db = Database("sqlite:///:memory:")
-
-        await MetadataDatabaseFactory(database=db).create_metadata(test_data)
-
-        mock_execute.assert_called_once()
-        called_params = mock_execute.call_args.args[0].compile().params
-
-        assert called_params["starttime"] == datetime.datetime(
-            year=s.year,
-            month=s.month,
-            day=s.day,
-            hour=s.hour,
-            minute=s.minute,
-            second=s.second,
-            microsecond=s.microsecond,
-            tzinfo=tz.tzutc(),
-        )
-        assert called_params["endtime"] == datetime.datetime(
-            year=e.year,
-            month=e.month,
-            day=e.day,
-            hour=e.hour,
-            minute=e.minute,
-            second=e.second,
-            microsecond=e.microsecond,
-            tzinfo=tz.tzutc(),
-        )
-
-    @patch("databases.Database.execute", new_callable=AsyncMock)
-    @patch("databases.Database.fetch_all", new_callable=AsyncMock)
-    async def test_update_metadata_defaults(self, mock_fetch_all, mock_execute):
-        test_data = Metadata(
-            category=MetadataCategory.INSTRUMENT,
-            network="NT",
-            station="BDT",
-            metadata={
-                "type": "FGE",
-                "channels": {
-                    "U": [{"channel": "U_Volt", "offset": 0, "scale": 313.2}],
-                    "V": [{"channel": "V_Volt", "offset": 0, "scale": 312.3}],
-                    "W": [{"channel": "W_Volt", "offset": 0, "scale": 312.0}],
-                },
-                "electronics": {
-                    "serial": "E0542",
-                    "x-scale": 313.2,
-                    "y-scale": 312.3,
-                    "z-scale": 312.0,
-                    "temperature-scale": 0.01,
-                },
-                "sensor": {
-                    "serial": "S0419",
-                    "x-constant": 36958,
-                    "y-constant": 36849,
-                    "z-constant": 36811,
-                },
-            },
-        )
-
-        db = Database("sqlite:///:memory:")
-        yesterday = datetime.datetime(2024, 11, 1, 8, 15, tzinfo=tz.tzutc())
-
-        mock_fetch_all.return_value = (
-            {
-                "id": 1234,
-                "created_time": yesterday,
-                "category": "instrument",
-                "network": "NT",
-                "station": "BDT",
-                "metadata": {
-                    "foo": "bar",
-                },
-            },
-        )
-
-        await MetadataDatabaseFactory(database=db).update_metadata(
-            meta=test_data, updated_by="test_user"
-        )
-
-        assert mock_fetch_all.call_count == 2
-        assert mock_execute.call_count == 2
-
-        first_called_params = mock_execute.call_args_list[0].args[0].compile().params
-        second_called_params = mock_execute.call_args_list[1].args[0].compile().params
-
-        assert first_called_params["metadata_id"] == 1234
-        assert first_called_params["created_time"] == yesterday
-        assert first_called_params["metadata"] == {"foo": "bar"}
-
-        assert second_called_params["updated_by"] == "test_user"
-        assert second_called_params["updated_time"] is not None
-        assert second_called_params["metadata"] == test_data.metadata
-
-    @patch("databases.Database.fetch_all", new_callable=AsyncMock)
-    async def test_get_metadata(self, mock_fetch_all):
-        test_query = MetadataQuery(
-            category=MetadataCategory.INSTRUMENT,
-            station="BSL",
-            starttime=UTCDateTime(2020, 1, 20),
-        )
-
-        db = Database("sqlite:///:memory:")
-
-        await MetadataDatabaseFactory(database=db).get_metadata(params=test_query)
-
-        mock_fetch_all.assert_called_once()
-
-        called_params = mock_fetch_all.call_args.args[0].compile().params
-
-        assert called_params["category_1"] == "instrument"
-        assert called_params["station_1"] == "BSL"
-        assert called_params["endtime_1"] == datetime.datetime(
-            2020, 1, 20, tzinfo=tz.tzutc()
-        )
diff --git a/test/edge_test/EdgeFactory_test.py b/test/edge_test/EdgeFactory_test.py
index 52a4f662f5fbb21aa483ffe34b8819e2d55cb6ce..c836c1429206c4857bafa6dc2c5088576e4ec8ae 100644
--- a/test/edge_test/EdgeFactory_test.py
+++ b/test/edge_test/EdgeFactory_test.py
@@ -1,9 +1,10 @@
 """Tests for EdgeFactory.py"""
 
 from obspy.core import Stream, Trace, UTCDateTime
-from geomagio.edge import EdgeFactory
 from numpy.testing import assert_equal
 
+from geomagio.edge.EdgeFactory import EdgeFactory
+
 
 def test_get_timeseries():
     """edge_test.EdgeFactory_test.test_get_timeseries()"""
diff --git a/test/edge_test/FDSNFactory_test.py b/test/edge_test/FDSNFactory_test.py
index f409674a5a0be0a5540e3b52b075e9708fa782fa..0eb79c28d5fd46863ac6aaaf7c2751b0fdd5f47f 100644
--- a/test/edge_test/FDSNFactory_test.py
+++ b/test/edge_test/FDSNFactory_test.py
@@ -8,7 +8,7 @@ from obspy.core import Stream, Trace, UTCDateTime
 from obspy.core.inventory import Inventory, Network, Station, Channel, Site
 import pytest
 
-from geomagio.edge import FDSNFactory
+from geomagio.edge.FDSNFactory import FDSNFactory
 from geomagio.metadata.instrument.InstrumentCalibrations import (
     get_instrument_calibrations,
 )
diff --git a/test/edge_test/FDSNSNCL_test.py b/test/edge_test/FDSNSNCL_test.py
index 4d66363ab58ff0e90689b674ca671731f66fa019..22e13a8b44a874f0c245316972b0e18eac00addf 100644
--- a/test/edge_test/FDSNSNCL_test.py
+++ b/test/edge_test/FDSNSNCL_test.py
@@ -1,7 +1,6 @@
 import pytest
 
-from geomagio.edge import FDSNSNCL
-from geomagio.edge.FDSNSNCL import get_FDSN_channel, get_location
+from geomagio.edge.FDSNSNCL import FDSNSNCL, get_FDSN_channel, get_location
 
 
 def test_data_type():
diff --git a/test/edge_test/IRISSNCL_test.py b/test/edge_test/IRISSNCL_test.py
index 06021877a04cee4724d5a0b017be8cbbe14773db..60f9949ee1f868b46db65653fe1806c07287dc86 100644
--- a/test/edge_test/IRISSNCL_test.py
+++ b/test/edge_test/IRISSNCL_test.py
@@ -1,7 +1,6 @@
 import pytest
 
-from geomagio.edge import IRISSNCL
-from geomagio.edge.IRISSNCL import get_iris_channel, get_location
+from geomagio.edge.IRISSNCL import IRISSNCL, get_iris_channel, get_location
 
 
 def test_data_type():
diff --git a/test/edge_test/MiniSeedFactory_test.py b/test/edge_test/MiniSeedFactory_test.py
index 75f895e5c8a7a2bb83d0bdbe9215076a52095635..d0e1873486634f6593fd39f52dfebb5044e453e4 100644
--- a/test/edge_test/MiniSeedFactory_test.py
+++ b/test/edge_test/MiniSeedFactory_test.py
@@ -8,8 +8,9 @@ from numpy.testing import assert_equal, assert_array_equal
 from obspy.core import read, Stats, Stream, Trace, UTCDateTime
 import pytest
 
-from geomagio import TimeseriesUtility
-from geomagio.edge import MiniSeedFactory, MiniSeedInputClient
+from geomagio.TimeseriesFactory import TimeseriesUtility
+from geomagio.edge.MiniSeedFactory import MiniSeedFactory
+from geomagio.edge.MiniSeedInputClient import MiniSeedInputClient
 from geomagio.metadata.instrument.InstrumentCalibrations import (
     get_instrument_calibrations,
 )
diff --git a/test/edge_test/RawInputClient_test.py b/test/edge_test/RawInputClient_test.py
index 876bc5694d781cff4029853c05082dceb00997bd..d2e3c67e9301dc0c3b5aff5f40bae4064a78dbfa 100644
--- a/test/edge_test/RawInputClient_test.py
+++ b/test/edge_test/RawInputClient_test.py
@@ -1,10 +1,8 @@
 """Tests for RawInputClient.py"""
 
 import numpy
-from datetime import datetime
-import logging
 from obspy.core import Stats, Trace, UTCDateTime
-from geomagio.edge import EdgeFactory, RawInputClient
+from geomagio.edge.RawInputClient import RawInputClient
 from numpy.testing import assert_equal
 
 
diff --git a/test/edge_test/mseed_test_clients.py b/test/edge_test/mseed_test_clients.py
index d706113d2968582962b71e927a53dfe456f8b286..5d42627ad166375a132c1584cb65332decbe8bdf 100644
--- a/test/edge_test/mseed_test_clients.py
+++ b/test/edge_test/mseed_test_clients.py
@@ -3,7 +3,7 @@ from obspy import Stream, UTCDateTime
 from obspy.clients.neic.client import Client
 
 from geomagio import TimeseriesUtility
-from geomagio.edge import SNCL
+from geomagio.edge.SNCL import SNCL
 
 
 class MockMiniSeedClient(Client):
diff --git a/test/iaga2002_test/IAGA2002Factory_test.py b/test/iaga2002_test/IAGA2002Factory_test.py
index 7c1531dbfea3220f96fd959c78268837041f66dc..189dd6399d39b6ca47bd469f7bc4cdfb40fa76bf 100644
--- a/test/iaga2002_test/IAGA2002Factory_test.py
+++ b/test/iaga2002_test/IAGA2002Factory_test.py
@@ -1,7 +1,7 @@
 """Tests for IAGA2002Factory class"""
 
 from numpy.testing import assert_equal
-from geomagio.iaga2002 import IAGA2002Factory
+from geomagio.iaga2002.IAGA2002Factory import IAGA2002Factory
 
 
 def test_parse_empty():
diff --git a/test/iaga2002_test/IAGA2002Parser_test.py b/test/iaga2002_test/IAGA2002Parser_test.py
index 04ccc9c3a5818059dd72f2e218eea0d5284a75b3..17e9b40e3ae57bb9376a4d93fe94e1fd5cb53beb 100644
--- a/test/iaga2002_test/IAGA2002Parser_test.py
+++ b/test/iaga2002_test/IAGA2002Parser_test.py
@@ -1,7 +1,7 @@
 """Tests for the IAGA2002 Parser class."""
 
 from numpy.testing import assert_equal
-from geomagio.iaga2002 import IAGA2002Parser
+from geomagio.iaga2002.IAGA2002Parser import IAGA2002Parser
 
 
 IAGA2002_EXAMPLE = """ Format                 IAGA-2002                                    |
diff --git a/test/imfjson_test/IMFJSONWriter_test.py b/test/imfjson_test/IMFJSONWriter_test.py
index 7792ded4542886badb21f24529776e48ebd412a7..c412bddbaf5503fffba765a58e9100960718f967 100644
--- a/test/imfjson_test/IMFJSONWriter_test.py
+++ b/test/imfjson_test/IMFJSONWriter_test.py
@@ -1,8 +1,8 @@
 """Tests for the IMFJSON Writer class."""
 
 from numpy.testing import assert_equal
-from geomagio.iaga2002 import IAGA2002Factory
-from geomagio.imfjson import IMFJSONWriter
+from geomagio.iaga2002.IAGA2002Factory import IAGA2002Factory
+from geomagio.imfjson.IMFJSONWriter import IMFJSONWriter
 import numpy as np
 
 
diff --git a/test/imfv122_test/IMFV122Parser_test.py b/test/imfv122_test/IMFV122Parser_test.py
index 2a3d87a1a9d2f421e647276f53a74b97bf335506..d92848cd2ef73e5dd8684563d8d81e02b660fc4f 100644
--- a/test/imfv122_test/IMFV122Parser_test.py
+++ b/test/imfv122_test/IMFV122Parser_test.py
@@ -1,7 +1,7 @@
 """Tests for the IMFV122 Parser class."""
 
 from numpy.testing import assert_equal
-from geomagio.imfv122 import IMFV122Parser
+from geomagio.imfv122.IMFV122Parser import IMFV122Parser
 from obspy.core import UTCDateTime
 
 
diff --git a/test/imfv283_test/IMFV283Parser_test.py b/test/imfv283_test/IMFV283Parser_test.py
index fb7e5395ab92b0d0648c1033232d6d1c8cde0e94..f6cd01e832c24aee366c48979bb86a5467eac793 100644
--- a/test/imfv283_test/IMFV283Parser_test.py
+++ b/test/imfv283_test/IMFV283Parser_test.py
@@ -5,7 +5,8 @@ from __future__ import unicode_literals
 from numpy.testing import assert_equal
 from obspy import UTCDateTime
 
-from geomagio.imfv283 import IMFV283Parser, imfv283_codes
+from geomagio.imfv283.IMFV283Parser import IMFV283Parser
+from geomagio.imfv283.imfv283_codes import OBSERVATORIES
 
 
 IMFV283_EXAMPLE_VIC = (
@@ -45,7 +46,7 @@ def test_parse_msg_header():
 def test_parse_goes_header_VIC():
     """imfv283_test.IMFV283Parser_test.test_parse_goes_header_VIC()"""
     goes_data = IMFV283Parser()._process_ness_block(
-        IMFV283_EXAMPLE_VIC, imfv283_codes.OBSERVATORIES["VIC"], 191
+        IMFV283_EXAMPLE_VIC, OBSERVATORIES["VIC"], 191
     )
     actual_goes_header = IMFV283Parser()._parse_goes_header(goes_data)
 
@@ -64,7 +65,7 @@ def test_parse_goes_header_VIC():
 def test_parse_goes_header_STJ():
     """imfv283_test.IMFV283Parser_test.test_parse_goes_header_STJ()"""
     goes_data = IMFV283Parser()._process_ness_block(
-        IMFV283_EXAMPLE_STJ, imfv283_codes.OBSERVATORIES["STJ"], 191
+        IMFV283_EXAMPLE_STJ, OBSERVATORIES["STJ"], 191
     )
     actual_goes_header = IMFV283Parser()._parse_goes_header(goes_data)
 
diff --git a/test/metadata/MetadataFactory_test.py b/test/metadata/MetadataFactory_test.py
index cde42da539556cdabe34dff1d6e8d2a1ff57509f..268d98b93c01acaf4f8ddf5fcac7a1c176612d62 100644
--- a/test/metadata/MetadataFactory_test.py
+++ b/test/metadata/MetadataFactory_test.py
@@ -1,7 +1,7 @@
 import datetime
 from obspy import UTCDateTime
 
-from geomagio.metadata import MetadataQuery
+from geomagio.api.db.models.metadata import MetadataQuery
 from geomagio.metadata.MetadataFactory import parse_params
 
 
@@ -14,8 +14,8 @@ def test_parse_params_with_UTCDateTime():
 
     params = parse_params(query)
 
-    assert params["starttime"] == "2024-11-07T00:00:00+00:00"
-    assert params["endtime"] == "2024-11-08T00:00:00+00:00"
+    assert params["starttime"] == "2024-11-07T00:00:00.000000Z"
+    assert params["endtime"] == "2024-11-08T00:00:00.000000Z"
 
 
 def test_parse_params_with_datetime():
@@ -27,5 +27,5 @@ def test_parse_params_with_datetime():
 
     params = parse_params(query)
 
-    assert params["starttime"] == "2024-11-07T00:00:00+00:00"
-    assert params["endtime"] == "2024-11-08T00:00:00+00:00"
+    assert params["starttime"] == "2024-11-07T00:00:00.000000Z"
+    assert params["endtime"] == "2024-11-08T00:00:00.000000Z"
diff --git a/test/pcdcp_test/PCDCPFactory_test.py b/test/pcdcp_test/PCDCPFactory_test.py
index 08a92e85ee18118eec9a42780ffb3eba056a710e..cdb9ff79e31833216d058d261c866d121144253f 100644
--- a/test/pcdcp_test/PCDCPFactory_test.py
+++ b/test/pcdcp_test/PCDCPFactory_test.py
@@ -1,6 +1,6 @@
 """Tests for PCDCPFactory."""
 
-from geomagio.pcdcp import PCDCPFactory
+from geomagio.pcdcp.PCDCPFactory import PCDCPFactory
 from obspy.core.utcdatetime import UTCDateTime
 from obspy.core.stream import Stream
 from numpy.testing import assert_equal
diff --git a/test/pcdcp_test/PCDCPParser_test.py b/test/pcdcp_test/PCDCPParser_test.py
index 62ef09918a71ef82c9aa1c534062a67a20b1108d..c1aaaaa572d9f4d14eee65a824c170f76a24fd77 100644
--- a/test/pcdcp_test/PCDCPParser_test.py
+++ b/test/pcdcp_test/PCDCPParser_test.py
@@ -1,7 +1,7 @@
 """Tests for the PCDCP Parser class."""
 
 from numpy.testing import assert_equal
-from geomagio.pcdcp import PCDCPParser
+from geomagio.pcdcp.PCDCPParser import PCDCPParser
 
 
 PCDCP_EXAMPLE = """
diff --git a/test/residual_test/residual_test.py b/test/residual_test/residual_test.py
index 2b03f22272a1884973e4cf34987b2dc6d82fa1aa..6aef906d3a1e40e6d87172213bb6839b1cdca6b8 100644
--- a/test/residual_test/residual_test.py
+++ b/test/residual_test/residual_test.py
@@ -2,17 +2,13 @@ import json
 
 from numpy.testing import assert_almost_equal, assert_equal
 from pydantic import TypeAdapter
-import pytest
 from typing import List
 
 from obspy.core import UTCDateTime
-from geomagio.residual import (
-    calculate,
-    Reading,
-    SpreadsheetAbsolutesFactory,
-    SpreadsheetSummaryFactory,
-    WebAbsolutesFactory,
-)
+from geomagio.residual.Calculation import calculate
+from geomagio.residual.Reading import Reading
+from geomagio.residual.SpreadsheetAbsolutesFactory import SpreadsheetAbsolutesFactory
+from geomagio.residual.SpreadsheetSummaryFactory import SpreadsheetSummaryFactory
 
 
 def assert_readings_equal(expected: Reading, actual: Reading, decimal: int):
diff --git a/test_metadata.py b/test_metadata.py
index 4436aa0a68c2830ae8df80a22244f6c70173b83a..477ca2d0e4cefaab445705446a5aa304b5e000b1 100644
--- a/test_metadata.py
+++ b/test_metadata.py
@@ -3,9 +3,10 @@ import json
 from obspy import UTCDateTime
 
 from geomagio.adjusted import AdjustedMatrix, Metric
-from geomagio.api.db import database, MetadataDatabaseFactory
+from geomagio.api.db.models.metadata import Metadata, MetadataCategory
+from geomagio.api.db.factories.MetadataDatabaseFactory import MetadataDatabaseFactory
+from geomagio.api.db.database import engine
 from geomagio.api.ws.Observatory import OBSERVATORIES
-from geomagio.metadata import Metadata, MetadataCategory
 from geomagio.residual import SpreadsheetAbsolutesFactory, WebAbsolutesFactory
 
 
@@ -109,7 +110,7 @@ for observatory in OBSERVATORIES:
             created_by="test_metadata.py",
             network=network,
             station=observatory.id,
-            metadata=observatory.dict(),
+            metadata=observatory.model_dump(),
         )
     )
 
@@ -126,7 +127,7 @@ reading = SpreadsheetAbsolutesFactory().parse_spreadsheet(
 readings.append(reading)
 
 for reading in readings:
-    json_string = reading.json()
+    json_string = reading.model_dump_json()
     reading_dict = json.loads(json_string)
     try:
         reviewer = reading.metadata["reviewer"]
@@ -167,16 +168,17 @@ test_metadata.append(
         category="adjusted-matrix",
         station="FRD",
         network="NT",
-        metadata=adjusted_matrix.dict(),
+        metadata=adjusted_matrix.model_dump(),
     )
 )
 
 
 async def load_test_metadata():
-    await database.connect()
-    for meta in test_metadata:
-        await MetadataDatabaseFactory(database=database).create_metadata(meta)
-    await database.disconnect()
+    # validate before saving
+    for m in test_metadata:
+        m = Metadata.model_validate(m)
+
+    await MetadataDatabaseFactory(engine=engine).batch_create_metadata(test_metadata)
 
 
 if __name__ == "__main__":