Repository: dscc-admin-ch/lomas (language: python)

Issue:
add tests for opendp and fix divergence bugs



Discussion / hints:

Fix patch:
diff --git a/client/notebooks/Demo_Client_Notebook.ipynb b/client/notebooks/Demo_Client_Notebook.ipynb
index 2ace8fd4..eebd10ee 100644
--- a/client/notebooks/Demo_Client_Notebook.ipynb
+++ b/client/notebooks/Demo_Client_Notebook.ipynb
@@ -84,7 +84,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 1,
+   "execution_count": 3,
    "id": "6fb569fc",
    "metadata": {},
    "outputs": [],
@@ -112,12 +112,12 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 2,
+   "execution_count": 8,
    "id": "941991f7",
    "metadata": {},
    "outputs": [],
    "source": [
-    "APP_URL = \"http://localhost:80\"\n",
+    "APP_URL = \"http://lomas_server\"\n",
     "USER_NAME = \"Dr. Antartica\"\n",
     "DATASET_NAME = \"PENGUIN\"\n",
     "client = Client(url=APP_URL, user_name = USER_NAME, dataset_name = DATASET_NAME)"
@@ -153,7 +153,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 3,
+   "execution_count": 9,
    "id": "0fdebac9-57fc-4410-878b-5a77425af634",
    "metadata": {},
    "outputs": [
@@ -178,7 +178,7 @@
        "   'categories': ['MALE', 'FEMALE']}}}"
       ]
      },
-     "execution_count": 3,
+     "execution_count": 9,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -213,7 +213,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 10,
    "id": "01f4365a",
    "metadata": {},
    "outputs": [],
@@ -224,7 +224,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 11,
    "id": "3f553b29",
    "metadata": {},
    "outputs": [
@@ -336,7 +336,7 @@
        "4  5036.246870  FEMALE  "
       ]
      },
-     "execution_count": 5,
+     "execution_count": 11,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -391,7 +391,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 12,
    "id": "3946425d",
    "metadata": {},
    "outputs": [],
@@ -404,7 +404,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 13,
    "id": "90cf2a6d",
    "metadata": {},
    "outputs": [],
@@ -422,7 +422,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": 14,
    "id": "a30f277e",
    "metadata": {},
    "outputs": [
@@ -430,7 +430,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Average bill length in remote dummy: 47.52mm.\n",
+      "Average bill length in remote dummy: 47.51mm.\n",
       "Number of rows in remote dummy: 199.\n"
      ]
     }
@@ -461,7 +461,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 15,
    "id": "61a467f3",
    "metadata": {},
    "outputs": [
@@ -471,7 +471,7 @@
        "{'initial_epsilon': 10.0, 'initial_delta': 0.005}"
       ]
      },
-     "execution_count": 9,
+     "execution_count": 15,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -492,7 +492,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 16,
    "id": "afd22f84",
    "metadata": {},
    "outputs": [
@@ -502,7 +502,7 @@
        "{'total_spent_epsilon': 0.0, 'total_spent_delta': 0.0}"
       ]
      },
-     "execution_count": 10,
+     "execution_count": 16,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -521,7 +521,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": 17,
    "id": "6260cf54",
    "metadata": {},
    "outputs": [
@@ -531,7 +531,7 @@
        "{'remaining_epsilon': 10.0, 'remaining_delta': 0.005}"
       ]
      },
-     "execution_count": 11,
+     "execution_count": 17,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -563,7 +563,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": 18,
    "id": "fd5ed08a",
    "metadata": {},
    "outputs": [],
@@ -574,7 +574,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 13,
+   "execution_count": 19,
    "id": "133020c6",
    "metadata": {},
    "outputs": [
@@ -584,7 +584,7 @@
        "{'epsilon_cost': 1.5, 'delta_cost': 0.00014999500000001387}"
       ]
      },
-     "execution_count": 13,
+     "execution_count": 19,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -618,7 +618,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 14,
+   "execution_count": 20,
    "id": "19e60263",
    "metadata": {},
    "outputs": [
@@ -628,7 +628,7 @@
        "{'remaining_epsilon': 10.0, 'remaining_delta': 0.005}"
       ]
      },
-     "execution_count": 14,
+     "execution_count": 20,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -639,7 +639,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 15,
+   "execution_count": 21,
    "id": "69767fac",
    "metadata": {},
    "outputs": [],
@@ -654,7 +654,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 16,
+   "execution_count": 22,
    "id": "6dbbdf93",
    "metadata": {},
    "outputs": [
@@ -662,8 +662,8 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Number of penguins in real data: 342.\n",
-      "Average bill length of penguins in real data: 42.93mm.\n"
+      "Number of penguins in real data: 344.\n",
+      "Average bill length of penguins in real data: 45.04mm.\n"
      ]
     }
    ],
@@ -685,7 +685,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 17,
+   "execution_count": 23,
    "id": "4ced8a56-e2c3-4bd1-b94b-95cbbcd58a15",
    "metadata": {},
    "outputs": [
@@ -694,12 +694,12 @@
       "text/plain": [
        "{'requested_by': 'Dr. Antartica',\n",
        " 'query_response':    nb_penguins  avg_bill_length_mm\n",
-       " 0          342           42.930651,\n",
+       " 0          344           45.043399,\n",
        " 'spent_epsilon': 1.5,\n",
        " 'spent_delta': 0.00014999500000001387}"
       ]
      },
-     "execution_count": 17,
+     "execution_count": 23,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -710,7 +710,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 18,
+   "execution_count": 24,
    "id": "39701fe5",
    "metadata": {},
    "outputs": [
@@ -720,7 +720,7 @@
        "{'remaining_epsilon': 8.5, 'remaining_delta': 0.004850004999999986}"
       ]
      },
-     "execution_count": 18,
+     "execution_count": 24,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -739,7 +739,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 19,
+   "execution_count": 25,
    "id": "487f835f",
    "metadata": {},
    "outputs": [
@@ -749,7 +749,7 @@
        "{'total_spent_epsilon': 1.5, 'total_spent_delta': 0.00014999500000001387}"
       ]
      },
-     "execution_count": 19,
+     "execution_count": 25,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -776,7 +776,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 20,
+   "execution_count": 26,
    "id": "b9685226",
    "metadata": {},
    "outputs": [],
@@ -806,7 +806,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 21,
+   "execution_count": 27,
    "id": "4331d86f",
    "metadata": {},
    "outputs": [
@@ -831,7 +831,7 @@
        "   'categories': ['MALE', 'FEMALE']}}}"
       ]
      },
-     "execution_count": 21,
+     "execution_count": 27,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -850,7 +850,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 22,
+   "execution_count": 28,
    "id": "ff8cb7b6",
    "metadata": {},
    "outputs": [],
@@ -860,7 +860,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 23,
+   "execution_count": 29,
    "id": "70b2bdb1",
    "metadata": {},
    "outputs": [
@@ -870,7 +870,7 @@
        "(30.0, 65.0)"
       ]
      },
-     "execution_count": 23,
+     "execution_count": 29,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -891,7 +891,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 24,
+   "execution_count": 30,
    "id": "75e4933b",
    "metadata": {},
    "outputs": [],
@@ -916,7 +916,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 25,
+   "execution_count": 31,
    "id": "8041a647",
    "metadata": {},
    "outputs": [
@@ -946,7 +946,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 26,
+   "execution_count": 32,
    "id": "b8162859",
    "metadata": {},
    "outputs": [],
@@ -967,7 +967,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 27,
+   "execution_count": 33,
    "id": "df61bce0",
    "metadata": {},
    "outputs": [
@@ -975,7 +975,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Dummy result for variance: 27.36\n"
+      "Dummy result for variance: 34.63\n"
      ]
     }
    ],
@@ -1029,7 +1029,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 29,
+   "execution_count": 44,
    "id": "085555a5",
    "metadata": {},
    "outputs": [],
@@ -1041,7 +1041,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 30,
+   "execution_count": 35,
    "id": "674332e7",
    "metadata": {},
    "outputs": [
@@ -1049,9 +1049,9 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Number of penguins: 342 (from previous smartnoise-sql query).\n",
-      "Average bill length: 42.93 (from previous smartnoise-sql query).\n",
-      "Variance of bill length: 27.686 (from opendp query).\n"
+      "Number of penguins: 344 (from previous smartnoise-sql query).\n",
+      "Average bill length: 45.04 (from previous smartnoise-sql query).\n",
+      "Variance of bill length: 33.325 (from opendp query).\n"
      ]
     }
    ],
@@ -1074,7 +1074,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 31,
+   "execution_count": 37,
    "id": "f72b19d0",
    "metadata": {},
    "outputs": [
@@ -1082,7 +1082,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Standard error of bill length: 0.28.\n"
+      "Standard error of bill length: 0.31.\n"
      ]
     }
    ],
@@ -1094,7 +1094,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 32,
+   "execution_count": 38,
    "id": "62630a03",
    "metadata": {},
    "outputs": [
@@ -1102,7 +1102,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "The 95% confidence interval of the bill length of all penguins is [42.37, 43.49].\n"
+      "The 95% confidence interval of the bill length of all penguins is [44.43, 45.65].\n"
      ]
     }
    ],
@@ -1134,7 +1134,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 33,
+   "execution_count": 39,
    "id": "558916a6-78a9-4589-abe8-41472f0c66d7",
    "metadata": {},
    "outputs": [
@@ -1144,7 +1144,7 @@
        "['Adelie', 'Chinstrap', 'Gentoo']"
       ]
      },
-     "execution_count": 33,
+     "execution_count": 39,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1164,7 +1164,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 34,
+   "execution_count": 40,
    "id": "505a2793-6750-4f2a-9a9d-09f3a4ae4099",
    "metadata": {},
    "outputs": [],
@@ -1187,7 +1187,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 35,
+   "execution_count": 41,
    "id": "712b1c74-cac6-4b3f-9421-8d8ffa9debc7",
    "metadata": {},
    "outputs": [
@@ -1195,7 +1195,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Dummy result for histogram: [38, 33, 28, 0]\n"
+      "Dummy result for histogram: [36, 33, 29, 0]\n"
      ]
     }
    ],
@@ -1217,7 +1217,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 36,
+   "execution_count": 42,
    "id": "3f942b1b-94bc-468e-8374-3034f2e6b8ce",
    "metadata": {},
    "outputs": [
@@ -1227,7 +1227,7 @@
        "{'epsilon_cost': 2.0, 'delta_cost': 0}"
       ]
      },
-     "execution_count": 36,
+     "execution_count": 42,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1249,7 +1249,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 37,
+   "execution_count": 43,
    "id": "c6fe47b7-048e-404a-bedb-720e734b0c70",
    "metadata": {},
    "outputs": [],
@@ -1404,7 +1404,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.11.0rc1"
+   "version": "3.11.9"
   }
  },
  "nbformat": 4,
diff --git a/client/notebooks/s3_example_notebook.ipynb b/client/notebooks/s3_example_notebook.ipynb
index 4dcba100..90e41700 100644
--- a/client/notebooks/s3_example_notebook.ipynb
+++ b/client/notebooks/s3_example_notebook.ipynb
@@ -57,12 +57,12 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 2,
+   "execution_count": 3,
    "id": "941991f7",
    "metadata": {},
    "outputs": [],
    "source": [
-    "APP_URL = \"http://localhost:80\"\n",
+    "APP_URL = \"http://lomas_server\"\n",
     "USER_NAME = \"Jack\"\n",
     "DATASET_NAME = \"TITANIC\"\n",
     "client = Client(url=APP_URL, user_name = USER_NAME, dataset_name = DATASET_NAME)"
@@ -86,7 +86,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 3,
+   "execution_count": 4,
    "id": "d15cbe39",
    "metadata": {},
    "outputs": [
@@ -113,7 +113,7 @@
        "  'Survived': {'type': 'boolean'}}}"
       ]
      },
-     "execution_count": 3,
+     "execution_count": 4,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -133,7 +133,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 5,
    "id": "01f4365a",
    "metadata": {},
    "outputs": [],
@@ -144,7 +144,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 6,
    "id": "3f553b29",
    "metadata": {},
    "outputs": [
@@ -286,7 +286,7 @@
        "4  6397.051061     G        C      True  "
       ]
      },
-     "execution_count": 5,
+     "execution_count": 6,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -319,7 +319,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 7,
    "id": "3946425d",
    "metadata": {},
    "outputs": [],
@@ -332,7 +332,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 8,
    "id": "90cf2a6d",
    "metadata": {},
    "outputs": [],
@@ -350,7 +350,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": 9,
    "id": "a30f277e",
    "metadata": {},
    "outputs": [
@@ -359,7 +359,7 @@
      "output_type": "stream",
      "text": [
       "Average age in remote dummy: 49.01 years old.\n",
-      "Number of rows in remote dummy: 200.\n"
+      "Number of rows in remote dummy: 199.\n"
      ]
     }
    ],
@@ -378,7 +378,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 10,
    "id": "61a467f3",
    "metadata": {},
    "outputs": [
@@ -388,7 +388,7 @@
        "{'initial_epsilon': 45.0, 'initial_delta': 0.2}"
       ]
      },
-     "execution_count": 9,
+     "execution_count": 10,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -399,7 +399,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 11,
    "id": "afd22f84",
    "metadata": {},
    "outputs": [
@@ -409,7 +409,7 @@
        "{'total_spent_epsilon': 0.0, 'total_spent_delta': 0.0}"
       ]
      },
-     "execution_count": 10,
+     "execution_count": 11,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -428,7 +428,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": 12,
    "id": "6260cf54",
    "metadata": {},
    "outputs": [
@@ -438,7 +438,7 @@
        "{'remaining_epsilon': 45.0, 'remaining_delta': 0.2}"
       ]
      },
-     "execution_count": 11,
+     "execution_count": 12,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -468,7 +468,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": 13,
    "id": "fd5ed08a",
    "metadata": {},
    "outputs": [],
@@ -479,7 +479,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 13,
+   "execution_count": 14,
    "id": "133020c6",
    "metadata": {},
    "outputs": [
@@ -489,7 +489,7 @@
        "{'epsilon_cost': 1.5, 'delta_cost': 0.00014999500000001387}"
       ]
      },
-     "execution_count": 13,
+     "execution_count": 14,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -512,7 +512,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 14,
+   "execution_count": 15,
    "id": "19e60263",
    "metadata": {},
    "outputs": [
@@ -522,7 +522,7 @@
        "{'remaining_epsilon': 45.0, 'remaining_delta': 0.2}"
       ]
      },
-     "execution_count": 14,
+     "execution_count": 15,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -533,7 +533,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 15,
+   "execution_count": 16,
    "id": "69767fac",
    "metadata": {},
    "outputs": [],
@@ -556,7 +556,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 16,
+   "execution_count": 17,
    "id": "6dbbdf93",
    "metadata": {},
    "outputs": [
@@ -565,7 +565,7 @@
      "output_type": "stream",
      "text": [
       "Number of passengers in real data: 891.\n",
-      "Average age in real data: 29.74.\n"
+      "Average age in real data: 30.27.\n"
      ]
     }
    ],
@@ -587,7 +587,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 17,
+   "execution_count": 18,
    "id": "39701fe5",
    "metadata": {},
    "outputs": [
@@ -597,7 +597,7 @@
        "{'remaining_epsilon': 43.5, 'remaining_delta': 0.199850005}"
       ]
      },
-     "execution_count": 17,
+     "execution_count": 18,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -616,7 +616,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 18,
+   "execution_count": 19,
    "id": "487f835f",
    "metadata": {},
    "outputs": [
@@ -626,7 +626,7 @@
        "{'total_spent_epsilon': 1.5, 'total_spent_delta': 0.00014999500000001387}"
       ]
      },
-     "execution_count": 18,
+     "execution_count": 19,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -645,7 +645,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 19,
+   "execution_count": 20,
    "id": "b9685226",
    "metadata": {},
    "outputs": [],
@@ -665,7 +665,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 20,
+   "execution_count": 21,
    "id": "4331d86f",
    "metadata": {},
    "outputs": [
@@ -692,7 +692,7 @@
        "  'Survived': {'type': 'boolean'}}}"
       ]
      },
-     "execution_count": 20,
+     "execution_count": 21,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -703,7 +703,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 21,
+   "execution_count": 22,
    "id": "ff8cb7b6",
    "metadata": {},
    "outputs": [],
@@ -713,7 +713,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 22,
+   "execution_count": 23,
    "id": "70b2bdb1",
    "metadata": {},
    "outputs": [
@@ -723,7 +723,7 @@
        "(0.1, 100.0)"
       ]
      },
-     "execution_count": 22,
+     "execution_count": 23,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -736,7 +736,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 23,
+   "execution_count": 24,
    "id": "75e4933b",
    "metadata": {},
    "outputs": [],
@@ -753,7 +753,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 24,
+   "execution_count": 25,
    "id": "8041a647",
    "metadata": {},
    "outputs": [
@@ -783,7 +783,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 25,
+   "execution_count": 26,
    "id": "b8162859",
    "metadata": {},
    "outputs": [],
@@ -804,7 +804,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 26,
+   "execution_count": 27,
    "id": "df61bce0",
    "metadata": {},
    "outputs": [
@@ -812,7 +812,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Dummy result for variance: 142.32\n"
+      "Dummy result for variance: 133.38\n"
      ]
     }
    ],
@@ -834,7 +834,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 27,
+   "execution_count": 28,
    "id": "7ae7f735",
    "metadata": {},
    "outputs": [
@@ -844,7 +844,7 @@
        "{'epsilon_cost': 2.240181818190626, 'delta_cost': 0}"
       ]
      },
-     "execution_count": 27,
+     "execution_count": 28,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -866,7 +866,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 28,
+   "execution_count": 29,
    "id": "085555a5",
    "metadata": {},
    "outputs": [],
@@ -878,7 +878,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 29,
+   "execution_count": 30,
    "id": "674332e7",
    "metadata": {},
    "outputs": [
@@ -887,8 +887,8 @@
      "output_type": "stream",
      "text": [
       "Number of passengers: 891 (from previous smartnoise-sql query).\n",
-      "Average age: 29.74 (from previous smartnoise-sql query).\n",
-      "Variance of age: 0.393 (from opendp query).\n"
+      "Average age: 30.27 (from previous smartnoise-sql query).\n",
+      "Variance of age: -4.3 (from opendp query).\n"
      ]
     }
    ],
@@ -903,7 +903,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 30,
+   "execution_count": 31,
    "id": "f72b19d0",
    "metadata": {},
    "outputs": [
@@ -911,7 +911,15 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Standard error of age: 0.02.\n"
+      "Standard error of age: nan.\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "/tmp/ipykernel_1062/2464255812.py:2: RuntimeWarning: invalid value encountered in sqrt\n",
+      "  standard_error = np.sqrt(var_age/nb_passengers)\n"
      ]
     }
    ],
@@ -923,7 +931,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 31,
+   "execution_count": 32,
    "id": "62630a03",
    "metadata": {},
    "outputs": [
@@ -931,7 +939,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "The 95% confidence interval of the age of all passengers is [29.7, 29.78].\n"
+      "The 95% confidence interval of the age of all passengers is [nan, nan].\n"
      ]
     }
    ],
@@ -942,6 +950,14 @@
     "upper_bound = np.round(avg_age + ZSCORE*standard_error, 2)\n",
     "print(f\"The 95% confidence interval of the age of all passengers is [{lower_bound}, {upper_bound}].\")"
    ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "3bcb4225-59c3-4e58-9be5-b9fae115b99d",
+   "metadata": {},
+   "outputs": [],
+   "source": []
   }
  ],
  "metadata": {
@@ -960,7 +976,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.11.0rc1"
+   "version": "3.11.9"
   }
  },
  "nbformat": 4,
diff --git a/client/notebooks/smartnoise_client_notebook.ipynb b/client/notebooks/smartnoise_client_notebook.ipynb
index e317a8a0..479b0ad7 100644
--- a/client/notebooks/smartnoise_client_notebook.ipynb
+++ b/client/notebooks/smartnoise_client_notebook.ipynb
@@ -80,7 +80,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "APP_URL = \"http://lomas_server_dev:80\"\n",
+    "APP_URL = \"http://lomas_server\"\n",
     "USER_NAME = \"Dr. Antartica\"\n",
     "DATASET_NAME = \"PENGUIN\"\n",
     "client = Client(url=APP_URL, user_name = USER_NAME, dataset_name = DATASET_NAME)"
@@ -514,7 +514,7 @@
     {
      "data": {
       "text/plain": [
-       "47.50783673123655"
+       "47.513689947669306"
       ]
      },
      "execution_count": 9,
@@ -587,7 +587,7 @@
     {
      "data": {
       "text/plain": [
-       "{'initial_epsilon': 10, 'initial_delta': 0.005}"
+       "{'initial_epsilon': 10.0, 'initial_delta': 0.005}"
       ]
      },
      "execution_count": 11,
@@ -618,7 +618,7 @@
     {
      "data": {
       "text/plain": [
-       "{'total_spent_epsilon': 0, 'total_spent_delta': 0}"
+       "{'total_spent_epsilon': 0.0, 'total_spent_delta': 0.0}"
       ]
      },
      "execution_count": 12,
@@ -647,7 +647,7 @@
     {
      "data": {
       "text/plain": [
-       "{'remaining_epsilon': 10, 'remaining_delta': 0.005}"
+       "{'remaining_epsilon': 10.0, 'remaining_delta': 0.005}"
       ]
      },
      "execution_count": 13,
@@ -777,7 +777,7 @@
     {
      "data": {
       "text/plain": [
-       "{'remaining_epsilon': 10, 'remaining_delta': 0.005}"
+       "{'remaining_epsilon': 10.0, 'remaining_delta': 0.005}"
       ]
      },
      "execution_count": 17,
@@ -814,7 +814,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Average bill length: 44.34mm.\n"
+      "Average bill length: 42.71mm.\n"
      ]
     }
    ],
@@ -965,8 +965,8 @@
        "  <tbody>\n",
        "    <tr>\n",
        "      <th>0</th>\n",
-       "      <td>99</td>\n",
-       "      <td>10.291426</td>\n",
+       "      <td>100</td>\n",
+       "      <td>10.291241</td>\n",
        "    </tr>\n",
        "  </tbody>\n",
        "</table>\n",
@@ -974,7 +974,7 @@
       ],
       "text/plain": [
        "   nb_penguin  std_bill_length_mm\n",
-       "0          99           10.291426"
+       "0         100           10.291241"
       ]
      },
      "execution_count": 23,
@@ -1100,8 +1100,8 @@
        "  <tbody>\n",
        "    <tr>\n",
        "      <th>0</th>\n",
-       "      <td>337</td>\n",
-       "      <td>9.193759</td>\n",
+       "      <td>342</td>\n",
+       "      <td>9.618499</td>\n",
        "    </tr>\n",
        "  </tbody>\n",
        "</table>\n",
@@ -1109,7 +1109,7 @@
       ],
       "text/plain": [
        "   nb_penguin  std_bill_length_mm\n",
-       "0         337            9.193759"
+       "0         342            9.618499"
       ]
      },
      "execution_count": 26,
@@ -1133,8 +1133,8 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Number of penguins: 337.\n",
-      "Standard deviation of bill length: 9.19.\n"
+      "Number of penguins: 342.\n",
+      "Standard deviation of bill length: 9.62.\n"
      ]
     }
    ],
@@ -1164,7 +1164,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Standard error of bill length: 0.5.\n"
+      "Standard error of bill length: 0.52.\n"
      ]
     }
    ],
@@ -1184,7 +1184,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "The 95% confidence interval of the bill length of all penguins is [43.36, 45.32].\n"
+      "The 95% confidence interval of the bill length of all penguins is [41.69, 43.73].\n"
      ]
     }
    ],
@@ -1290,9 +1290,9 @@
      "data": {
       "text/plain": [
        "{'query_response':      species  nb_penguin  avg_bill_length_mm  std_bill_length_mm\n",
-       " 0     Adelie          36           48.807157            8.785415\n",
-       " 1  Chinstrap          32           46.193166           18.700943\n",
-       " 2     Gentoo          30           41.849626            6.128719}"
+       " 0     Adelie          38           46.327798           13.066917\n",
+       " 1  Chinstrap          32           44.851940           26.347221\n",
+       " 2     Gentoo          27           46.466931           16.967135}"
       ]
      },
      "execution_count": 33,
diff --git a/docs/build_docs.py b/docs/build_docs.py
index a0f6de66..e0e74680 100644
--- a/docs/build_docs.py
+++ b/docs/build_docs.py
@@ -20,9 +20,10 @@ def build_doc(version,
     subprocess.run("sphinx-apidoc -o ./source ../client/lomas_client/ --tocfile client_modules", shell=True)
     subprocess.run("sphinx-apidoc -o ./source ../server/lomas_server/ --tocfile server_modules", shell=True)
     subprocess.run("mkdir ./source/notebooks", shell=True)
-    subprocess.run("cp -r ../client/notebooks/* ./source/notebooks", shell=True)
-    subprocess.run("cp -r ../server/notebooks/* ./source/notebooks", shell=True)
-    os.environ['SPHINXOPTS'] = "-D language='{}'".format(language)
+    subprocess.run("cp -r ../client/notebooks/Demo_Client_Notebook.ipynb ./source/notebooks/Demo_Client_Notebook.ipynb", shell=True)
+    subprocess.run("cp -r ../client/notebooks/s3_example_notebook.ipynb ./source/notebooks/s3_example_notebook.ipynb", shell=True)
+    subprocess.run("cp -r ../server/notebooks/local_admin_notebook.ipynb ./source/notebooks/local_admin_notebook.ipynb", shell=True)
+    # os.environ['SPHINXOPTS'] = "-D language='{}'".format(language)
     subprocess.run("make html", shell=True)    
 
 # a move dir method because we run multiple builds and bring the html folders to a 
diff --git a/docs/source/_templates/layout.html b/docs/source/_templates/layout.html
new file mode 100644
index 00000000..ca57e8ac
--- /dev/null
+++ b/docs/source/_templates/layout.html
@@ -0,0 +1,217 @@
+{# TEMPLATE VAR SETTINGS #}
+{%- set url_root = pathto('', 1) %}
+{%- if url_root == '#' %}{% set url_root = '' %}{% endif %}
+{%- if not embedded and docstitle %}
+  {%- set titlesuffix = " &mdash; "|safe + docstitle|e %}
+{%- else %}
+  {%- set titlesuffix = "" %}
+{%- endif %}
+{%- set lang_attr = 'en' if language == None else (language | replace('_', '-')) %}
+
+{# Build sphinx_version_info tuple from sphinx_version string in pure Jinja #}
+{%- set (_ver_major, _ver_minor) = (sphinx_version.split('.') | list)[:2] | map('int') -%}
+{%- set sphinx_version_info = (_ver_major, _ver_minor, -1) -%}
+
+<!DOCTYPE html>
+<html class="writer-html5" lang="{{ lang_attr }}"{% if sphinx_version_info >= (7, 2) %} data-content_root="{{ content_root }}"{% endif %}>
+<head>
+  <meta charset="utf-8" />
+  {{- metatags }}
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  {%- block htmltitle %}
+  <title>{{ title|striptags|e }}{{ titlesuffix }}</title>
+  {%- endblock -%}
+
+  {#- CSS #}
+  {%- for css_file in css_files %}
+    {%- if css_file|attr("filename") %}
+      {{ css_tag(css_file) }}
+    {%- else %}
+      <link rel="stylesheet" href="{{ pathto(css_file, 1)|escape }}" type="text/css" />
+    {%- endif %}
+  {%- endfor %}
+
+  {#
+      "extra_css_files" is an undocumented Read the Docs theme specific option.
+      There is no need to check for ``|attr("filename")`` here because it's always a string.
+      Note that this option should be removed in favor of regular ``html_css_files``:
+      https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_css_files
+  #}
+  {%- for css_file in extra_css_files %}
+    <link rel="stylesheet" href="{{ pathto(css_file, 1)|escape }}" type="text/css" />
+  {%- endfor -%}
+
+  {#- FAVICON #}
+  {%- if favicon_url %}
+    <link rel="shortcut icon" href="{{ favicon_url }}"/>
+  {%- endif %}
+
+  {#- CANONICAL URL (deprecated) #}
+  {%- if theme_canonical_url and not pageurl %}
+    <link rel="canonical" href="{{ theme_canonical_url }}{{ pagename }}.html"/>
+  {%- endif -%}
+
+  {#- CANONICAL URL #}
+  {%- if pageurl %}
+    <link rel="canonical" href="{{ pageurl|e }}" />
+  {%- endif -%}
+
+  {#- JAVASCRIPTS #}
+  {%- block scripts %}
+  {%- if not embedded %}
+    {%- for scriptfile in script_files %}
+      {{ js_tag(scriptfile) }}
+    {%- endfor %}
+    <script src="{{ pathto('_static/js/theme.js', 1) }}"></script>
+
+    {#- OPENSEARCH #}
+    {%- if use_opensearch %}
+    <link rel="search" type="application/opensearchdescription+xml"
+          title="{% trans docstitle=docstitle|e %}Search within {{ docstitle }}{% endtrans %}"
+          href="{{ pathto('_static/opensearch.xml', 1) }}"/>
+    {%- endif %}
+  {%- endif %}
+  {%- endblock %}
+
+  {%- block linktags %}
+    {%- if hasdoc('about') %}
+    <link rel="author" title="{{ _('About these documents') }}" href="{{ pathto('about') }}" />
+    {%- endif %}
+    {%- if hasdoc('genindex') %}
+    <link rel="index" title="{{ _('Index') }}" href="{{ pathto('genindex') }}" />
+    {%- endif %}
+    {%- if hasdoc('search') %}
+    <link rel="search" title="{{ _('Search') }}" href="{{ pathto('search') }}" />
+    {%- endif %}
+    {%- if hasdoc('copyright') %}
+    <link rel="copyright" title="{{ _('Copyright') }}" href="{{ pathto('copyright') }}" />
+    {%- endif %}
+    {%- if next %}
+    <link rel="next" title="{{ next.title|striptags|e }}" href="{{ next.link|e }}" />
+    {%- endif %}
+    {%- if prev %}
+    <link rel="prev" title="{{ prev.title|striptags|e }}" href="{{ prev.link|e }}" />
+    {%- endif %}
+  {%- endblock %}
+  {%- block extrahead %} {% endblock %}
+</head>
+
+<body class="wy-body-for-nav">
+
+  {%- block extrabody %} {% endblock %}
+  <div class="wy-grid-for-nav">
+    {#- SIDE NAV, TOGGLES ON MOBILE #}
+    <nav data-toggle="wy-nav-shift" class="wy-nav-side">
+      <div class="wy-side-scroll">
+        <div class="wy-side-nav-search" {% if theme_style_nav_header_background %} style="background: {{theme_style_nav_header_background}}" {% endif %}>
+          {%- block sidebartitle %}
+
+          {# the logo helper function was removed in Sphinx 6 and deprecated since Sphinx 4 #}
+          {# the master_doc variable was renamed to root_doc in Sphinx 4 (master_doc still exists in later Sphinx versions) #}
+          {%- set _logo_url = logo_url|default(pathto('_static/' + (logo or ""), 1)) %}
+          {%- set _root_doc = root_doc|default(master_doc) %}
+          <a href="{{ pathto(_root_doc) }}"{% if not theme_logo_only %} class="icon icon-home"{% endif %}>
+            {% if not theme_logo_only %}{{ project }}{% endif %}
+            {%- if logo or logo_url %}
+              <img src="{{ _logo_url }}" class="logo" alt="{{ _('Logo') }}"/>
+            {%- endif %}
+          </a>
+
+          {%- if theme_display_version %}
+              <div class="version">
+                {{ current_version }}
+              </div>
+          {%- endif %}
+
+          {%- include "searchbox.html" %}
+
+          {%- endblock %}
+        </div>
+
+        {%- block navigation %}
+        {#- Translators: This is an ARIA section label for the main navigation menu -#}
+        <div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="{{ _('Navigation menu') }}">
+          {%- block menu %}
+            {%- set toctree = toctree(maxdepth=theme_navigation_depth|int,
+                                      collapse=theme_collapse_navigation|tobool,
+                                      includehidden=theme_includehidden|tobool,
+                                      titles_only=theme_titles_only|tobool) %}
+            {%- if toctree %}
+              {{ toctree }}
+            {%- else %}
+              <!-- Local TOC -->
+              <div class="local-toc">{{ toc }}</div>
+            {%- endif %}
+          {%- endblock %}
+        </div>
+        {%- endblock %}
+      </div>
+    </nav>
+
+    <section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
+
+      {#- MOBILE NAV, TRIGGLES SIDE NAV ON TOGGLE #}
+      {#- Translators: This is an ARIA section label for the navigation menu that is visible when viewing the page on mobile devices -#}
+      <nav class="wy-nav-top" aria-label="{{ _('Mobile navigation menu') }}" {% if theme_style_nav_header_background %} style="background: {{theme_style_nav_header_background}}" {% endif %}>
+        {%- block mobile_nav %}
+          <i data-toggle="wy-nav-top" class="fa fa-bars"></i>
+          <a href="{{ pathto(master_doc) }}">{{ project }}</a>
+        {%- endblock %}
+      </nav>
+
+      <div class="wy-nav-content">
+      {%- block content %}
+        {%- if theme_style_external_links|tobool %}
+        <div class="rst-content style-external-links">
+        {%- else %}
+        <div class="rst-content">
+        {%- endif %}
+          {% include "breadcrumbs.html" %}
+          <div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
+          {%- block document %}
+           <div itemprop="articleBody">
+             {% block body %}{% endblock %}
+           </div>
+           {%- if self.comments()|trim %}
+             <div class="articleComments">
+               {%- block comments %}{% endblock %}
+             </div>
+           {%- endif%}
+          </div>
+          {%- endblock %}
+          {% include "footer.html" %}
+        </div>
+      {%- endblock %}
+      </div>
+    </section>
+  </div>
+  {% include "versions.html" -%}
+
+  <script>
+      jQuery(function () {
+          SphinxRtdTheme.Navigation.enable({{ 'true' if theme_sticky_navigation|tobool else 'false' }});
+      });
+  </script>
+
+  {#- Do not conflict with RTD insertion of analytics script #}
+  {%- if not READTHEDOCS %}
+    {%- if theme_analytics_id %}
+    <!-- Theme Analytics -->
+    <script async src="https://www.googletagmanager.com/gtag/js?id={{ theme_analytics_id }}"></script>
+    <script>
+      window.dataLayer = window.dataLayer || [];
+      function gtag(){dataLayer.push(arguments);}
+      gtag('js', new Date());
+
+      gtag('config', '{{ theme_analytics_id }}', {
+          'anonymize_ip': {{ 'true' if theme_analytics_anonymize_ip|tobool else 'false' }},
+      });
+    </script>
+
+    {%- endif %}
+  {%- endif %}
+
+  {%- block footer %} {% endblock %}
+
+</body>
+</html>
\ No newline at end of file
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 04dc3f3a..87a94751 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -73,13 +73,6 @@
 # to place at the top of the sidebar.
 html_logo = "_static/logo.png"
 
-html_context = {
-  'current_version' : "master",
-  'versions' : [["master", "link to master"], ["develop", "link to develop"]],
-  'current_language': 'en',
-  'languages': [["en", "link to en"]]
-}
-
 # get the environment variable build_all_docs and pages_root
 build_all_docs = os.environ.get("build_all_docs")
 pages_root = os.environ.get("pages_root", "")
diff --git a/server/lomas_server/.coveragerc b/server/lomas_server/.coveragerc
index afd978ca..695db014 100644
--- a/server/lomas_server/.coveragerc
+++ b/server/lomas_server/.coveragerc
@@ -2,6 +2,7 @@
 relative_files = true
 omit =
     mongodb_admin_cli.py
+    uvicorn_serve.py
 
 [report]
 exclude_lines =
@@ -9,4 +10,5 @@ exclude_lines =
     ^\s*raise\s+InternalServerException
     ^\s*except\s+CUSTOM_EXCEPTIONS
     ^\s*except\s+Exception
+    ^\s*except\s+yaml.YAMLError
     ^\s*case _:
\ No newline at end of file
diff --git a/server/lomas_server/app.py b/server/lomas_server/app.py
index e063589e..d24846f1 100644
--- a/server/lomas_server/app.py
+++ b/server/lomas_server/app.py
@@ -3,7 +3,7 @@
 from typing import Callable
 
 from fastapi import Body, Depends, FastAPI, Header, Request, Response
-from fastapi.responses import JSONResponse, StreamingResponse
+from fastapi.responses import JSONResponse, StreamingResponse, RedirectResponse
 
 from admin_database.utils import database_factory
 from constants import (
@@ -186,6 +186,15 @@ async def middleware(
 # -----------------------------------------------------------------------------
 
 
+@app.get("/")
+async def root():
+    """Redirect root endpoint to the state endpoint
+    Returns:
+        JSONResponse: The state of the server instance.
+    """
+    return RedirectResponse(url="/state")
+
+
 # Get server state
 @app.get("/state", tags=["ADMIN_USER"])
 async def get_state(
diff --git a/server/lomas_server/dataset_store/lru_dataset_store.py b/server/lomas_server/dataset_store/lru_dataset_store.py
index 6b1f09c5..b7d54eaf 100644
--- a/server/lomas_server/dataset_store/lru_dataset_store.py
+++ b/server/lomas_server/dataset_store/lru_dataset_store.py
@@ -68,20 +68,11 @@ def _add_dataset(self, dataset_name: str) -> None:
                 "to fit in dataset manager memory."
             )
 
-        while (
-            self.memory_usage + private_dataset_mem_usage
-            > self.max_memory_usage
-        ):
-            evicted_ds_name, evicted_ds = self.dataset_cache.popitem(
-                last=False
-            )
-            self.memory_usage -= evicted_ds.get_memory_usage()
-            LOG.info(f"Dataset {evicted_ds_name} was evicted from cache.")
-
         self.dataset_cache[dataset_name] = private_dataset
         self.memory_usage += private_dataset_mem_usage
 
         LOG.info(f"New dataset cache size: {self.memory_usage} MiB")
+        self.update_memory_usage()
 
     def update_memory_usage(self) -> None:
         """Remove least recently used datasets until the cache
diff --git a/server/lomas_server/dp_queries/dp_libraries/opendp.py b/server/lomas_server/dp_queries/dp_libraries/opendp.py
index b75a6c60..9be963d0 100644
--- a/server/lomas_server/dp_queries/dp_libraries/opendp.py
+++ b/server/lomas_server/dp_queries/dp_libraries/opendp.py
@@ -1,3 +1,5 @@
+from typing import List, Union
+
 import opendp as dp
 from opendp.mod import enable_features
 from opendp_logger import make_load_json
@@ -78,7 +80,8 @@ def cost(self, query_json: OpenDPInp) -> tuple[float, float]:
             case OpenDPMeasurement.SMOOTHED_MAX_DIVERGENCE:  # Approximate DP
                 if query_json.fixed_delta is None:
                     raise InvalidQueryException(
-                        "fixed_delta must be set for smooth max divergence."
+                        "fixed_delta must be set for smooth max divergence"
+                        + " and zero concentrated divergence."
                     )
                 epsilon = cost.epsilon(delta=query_json.fixed_delta)
                 delta = query_json.fixed_delta
@@ -89,7 +92,7 @@ def cost(self, query_json: OpenDPInp) -> tuple[float, float]:
 
         return epsilon, delta
 
-    def query(self, query_json: OpenDPInp) -> str:
+    def query(self, query_json: OpenDPInp) -> Union[List, int, float]:
         """Perform the query and return the response.
 
         Args:
@@ -100,8 +103,7 @@ def query(self, query_json: OpenDPInp) -> str:
                 external to this package.
 
         Returns:
-            TODO Check this, probably float or int.
-            str: The JSON encoded string representation of the query result.
+            (Union[List, int, float]) query result
         """
         opendp_pipe = reconstruct_measurement_pipeline(query_json.opendp_json)
 
@@ -118,10 +120,6 @@ def query(self, query_json: OpenDPInp) -> str:
                 "Error executing query:" + str(e),
             ) from e
 
-        # Note: leaving this here, support for opendp_polars
-        # if isinstance(release_data, polars.dataframe.frame.DataFrame):
-        #     release_data = release_data.write_json(file=None)
-
         return release_data
 
 
@@ -177,6 +175,15 @@ def get_output_measure(opendp_pipe: dp.Measurement) -> str:
     output_type = opendp_pipe.output_distance_type
     output_measure = opendp_pipe.output_measure
 
+    if not isinstance(output_type, str):
+        if output_type.origin in ["SMDCurve", "Tuple"]:
+            output_type = output_type.args[0]
+        else:
+            raise InternalServerException(
+                f"Cannot process output measure: {output_measure}"
+                + f"with output type {output_type}."
+            )
+
     if output_measure == dp.measures.fixed_smoothed_max_divergence(
         T=output_type
     ):
diff --git a/server/lomas_server/mongodb_admin.py b/server/lomas_server/mongodb_admin.py
index 47255969..b8efc36c 100644
--- a/server/lomas_server/mongodb_admin.py
+++ b/server/lomas_server/mongodb_admin.py
@@ -568,8 +568,6 @@ def add_dataset(  # pylint: disable=too-many-arguments, too-many-locals
     Returns:
         None
     """
-    if db.datasets.count_documents({"dataset_name": dataset_name}) > 0:
-        raise ValueError("Cannot add database because already set. ")
 
     # Step 1: Build dataset
     dataset: Dict = {
diff --git a/server/lomas_server/utils/utils.py b/server/lomas_server/utils/utils.py
index 12c9eb0d..c05b5b39 100644
--- a/server/lomas_server/utils/utils.py
+++ b/server/lomas_server/utils/utils.py
@@ -61,12 +61,17 @@ def stream_dataframe(df: pd.DataFrame) -> StreamingResponse:
     return response
 
 
-def add_demo_data_to_admindb() -> None:
+def add_demo_data_to_admindb(
+    user_yaml: str = "/data/collections/user_collection.yaml",
+    dataset_yaml: str = "/data/collections/dataset_collection.yaml",
+) -> None:
     """
     Adds the demo data to the mongodb admindb.
-
-    Uses hardcoded paths to user and dataset collections.
     Meant to be used in the develop mode of the service.
+
+    Args:
+        user_yaml (str): path to user collection yaml file
+        dataset_yaml (str): path to dataset collection yaml file
     """
     LOG.info("Creating example user collection")
     mongo_db: Database = get_mongodb()
@@ -76,7 +81,7 @@ def add_demo_data_to_admindb() -> None:
         mongo_db,
         clean=True,
         overwrite=True,
-        yaml_file="/data/collections/user_collection.yaml",
+        yaml_file=user_yaml,
     )
 
     LOG.info("Creating datasets and metadata collection")
@@ -85,7 +90,7 @@ def add_demo_data_to_admindb() -> None:
         clean=True,
         overwrite_datasets=True,
         overwrite_metadata=True,
-        yaml_file="/data/collections/dataset_collection.yaml",
+        yaml_file=dataset_yaml,
     )
 
     LOG.info("Empty archives")


Test patch:
diff --git a/server/docker-compose-test.yml b/server/docker-compose-test.yml
index c8d1aaa7..ff4a85f1 100644
--- a/server/docker-compose-test.yml
+++ b/server/docker-compose-test.yml
@@ -6,7 +6,6 @@ services:
     # The reason has to do with permissions (see https://stackoverflow.com/questions/29245216/write-in-shared-volumes-docker/29251160#29251160)
     # Still unclear why just setting chmod 777 ./data does not solve the issue.
     #user: 1000:1000
-    network_mode: "host"
     ports:
       - 127.0.0.1:27017:27017
     volumes:
@@ -19,5 +18,40 @@ services:
       - MONGODB_INITDB_ROOT_PASSWORD=root_pwd
       - MONGODB_INITDB_DATABASE=defaultdb
       # add user https://stackoverflow.com/questions/42912755/how-to-create-a-db-for-mongodb-container-on-start-up/54064268#54064268
-
-
+    networks:
+      - test-network
+  minio:
+    container_name: minio
+    image: minio/minio
+    command: server /data
+    environment:
+      - MINIO_ROOT_USER=admin
+      - MINIO_ROOT_PASSWORD=admin123
+      - ENDPOINT_URL=http://minio:9000
+    ports:
+      - "9000:9000"
+    volumes:
+      - minio-data:/data
+    networks:
+      - test-network
+  minio-client:
+    container_name: minio-client
+    image: minio/mc
+    depends_on:
+      - minio
+    entrypoint: /bin/sh -c "
+      sleep 15 && 
+      mc alias set myminio http://minio:9000 admin admin123 && 
+      mc mb myminio/example || true && 
+      mc cp /data/test_penguin.csv myminio/example/data/test_penguin.csv &&
+      mc cp /data/metadata/penguin_metadata.yaml myminio/example/metadata/penguin_metadata.yaml &&
+      mc ls --recursive --versions myminio/example"
+    volumes:
+      - ./lomas_server/tests/test_data/:/data
+    networks:
+      - test-network
+networks:
+  test-network:
+    driver: bridge
+volumes:
+  minio-data:
\ No newline at end of file
diff --git a/server/lomas_server/tests/constants.py b/server/lomas_server/tests/constants.py
index 248af8f3..ba343b7a 100644
--- a/server/lomas_server/tests/constants.py
+++ b/server/lomas_server/tests/constants.py
@@ -1,1 +1,4 @@
 ENV_MONGO_INTEGRATION = "LOMAS_TEST_MONGO_INTEGRATION"
+ENV_S3_INTEGRATION = "LOMAS_TEST_S3_INTEGRATION"
+TRUE_VALUES = ("true", "1", "t")
+FALSE_VALUES = ("false", "0", "f")
diff --git a/server/lomas_server/tests/test_api.py b/server/lomas_server/tests/test_api.py
index a649dd89..4c0a411d 100644
--- a/server/lomas_server/tests/test_api.py
+++ b/server/lomas_server/tests/test_api.py
@@ -3,21 +3,32 @@
 import unittest
 from io import StringIO
 
-import pandas as pd
 from fastapi import status
 from fastapi.testclient import TestClient
+import opendp.combinators as comb
+import opendp.measurements as meas
+from opendp.mod import enable_features
+import opendp.prelude as dp_p
+import opendp.transformations as trans
+from opendp_logger import enable_logging
+import pandas as pd
 from pymongo.database import Database
 
-from admin_database.utils import get_mongodb
+from admin_database.utils import get_mongodb, database_factory
 from mongodb_admin import (
     add_datasets_via_yaml,
     add_users_via_yaml,
     drop_collection,
 )
 from app import app
-from constants import EPSILON_LIMIT, DPLibraries
-from tests.constants import ENV_MONGO_INTEGRATION
+from constants import DatasetStoreType, EPSILON_LIMIT, DPLibraries
+from tests.constants import (
+    ENV_MONGO_INTEGRATION,
+    ENV_S3_INTEGRATION,
+    TRUE_VALUES,
+)
 from utils.config import CONFIG_LOADER
+from utils.error_handler import InternalServerException
 from utils.example_inputs import (
     DUMMY_NB_ROWS,
     PENGUIN_DATASET,
@@ -35,8 +46,11 @@
 INITAL_EPSILON = 10
 INITIAL_DELTA = 0.005
 
+enable_features("floating-point")
+enable_logging()
 
-class TestRootAPIEndpoint(unittest.TestCase):
+
+class TestRootAPIEndpoint(unittest.TestCase):  # pylint: disable=R0904
     """
     End-to-end tests of the api endpoints.
 
@@ -49,7 +63,7 @@ class TestRootAPIEndpoint(unittest.TestCase):
     @classmethod
     def setUpClass(cls) -> None:
         # Read correct config depending on the database we test against
-        if os.getenv(ENV_MONGO_INTEGRATION, "0").lower() in ("true", "1", "t"):
+        if os.getenv(ENV_MONGO_INTEGRATION, "0").lower() in TRUE_VALUES:
             CONFIG_LOADER.load_config(
                 config_path="tests/test_configs/test_config_mongo.yaml",
                 secrets_path="tests/test_configs/test_secrets.yaml",
@@ -75,7 +89,7 @@ def setUp(self) -> None:
         self.headers["user-name"] = self.user_name
 
         # Fill up database if needed
-        if os.getenv(ENV_MONGO_INTEGRATION, "0").lower() in ("true", "1", "t"):
+        if os.getenv(ENV_MONGO_INTEGRATION, "0").lower() in TRUE_VALUES:
             self.db: Database = get_mongodb()
 
             add_users_via_yaml(
@@ -84,9 +98,15 @@ def setUp(self) -> None:
                 clean=True,
                 overwrite=True,
             )
+
+            if os.getenv(ENV_S3_INTEGRATION, "0").lower() in TRUE_VALUES:
+                yaml_file = "tests/test_data/test_datasets_with_s3.yaml"
+            else:
+                yaml_file = "tests/test_data/test_datasets.yaml"
+
             add_datasets_via_yaml(
                 self.db,
-                yaml_file="tests/test_data/test_datasets.yaml",
+                yaml_file=yaml_file,
                 clean=True,
                 overwrite_datasets=True,
                 overwrite_metadata=True,
@@ -94,14 +114,39 @@ def setUp(self) -> None:
 
     def tearDown(self) -> None:
         # Clean up database if needed
-        if os.getenv(ENV_MONGO_INTEGRATION, "0").lower() in ("true", "1", "t"):
+        if os.getenv(ENV_MONGO_INTEGRATION, "0").lower() in TRUE_VALUES:
             drop_collection(self.db, "metadata")
             drop_collection(self.db, "datasets")
             drop_collection(self.db, "users")
             drop_collection(self.db, "queries_archives")
 
+    def test_config_and_internal_server_exception(self) -> None:
+        """Test set wrong configuration"""
+        config = CONFIG_LOADER.get_config()
+
+        # Put unknown admin database
+        previous_admin_db = config.admin_database.db_type
+        config.admin_database.db_type = "wrong_db"
+        with self.assertRaises(InternalServerException) as context:
+            database_factory(config.admin_database)
+        self.assertEqual(
+            str(context.exception), "Database type wrong_db not supported."
+        )
+        # Put original state back
+        config.admin_database.db_type = previous_admin_db
+
+    def test_root(self) -> None:
+        """Test root endpoint redirection to state endpoint"""
+        with TestClient(app, headers=self.headers) as client:
+            response_root = client.get("/", headers=self.headers)
+            response_state = client.get("/state", headers=self.headers)
+            assert response_root.status_code == response_state.status_code
+            assert json.loads(
+                response_root.content.decode("utf8")
+            ) == json.loads(response_state.content.decode("utf8"))
+
     def test_state(self) -> None:
-        """_summary_"""
+        """Test state endpoint"""
         with TestClient(app, headers=self.headers) as client:
             response = client.get("/state", headers=self.headers)
             assert response.status_code == status.HTTP_200_OK
@@ -110,8 +155,38 @@ def test_state(self) -> None:
             assert response_dict["requested_by"] == self.user_name
             assert response_dict["state"]["LIVE"]
 
+    def test_memory_usage(self) -> None:
+        """Test memory usage endpoint"""
+        with TestClient(app, headers=self.headers) as client:
+            config = CONFIG_LOADER.get_config()
+            if config.dataset_store.ds_store_type == DatasetStoreType.LRU:
+                # Test before adding data
+                response = client.get(
+                    "/get_memory_usage", headers=self.headers
+                )
+                assert response.status_code == status.HTTP_200_OK
+
+                response_dict = json.loads(response.content.decode("utf8"))
+                assert response_dict["memory_usage"] == 0
+
+                # Test after adding data
+                response = client.post(
+                    "/smartnoise_query",
+                    json=example_smartnoise_sql,
+                    headers=self.headers,
+                )
+                assert response.status_code == status.HTTP_200_OK
+
+                response = client.get(
+                    "/get_memory_usage", headers=self.headers
+                )
+                assert response.status_code == status.HTTP_200_OK
+
+                response_dict = json.loads(response.content.decode("utf8"))
+                assert response_dict["memory_usage"] > 0
+
     def test_get_dataset_metadata(self) -> None:
-        """_summary_"""
+        """test_get_dataset_metadata"""
         with TestClient(app) as client:
             # Expect to work
             response = client.post(
@@ -224,6 +299,24 @@ def test_smartnoise_query(self) -> None:
             assert response_dict[1]["type"] == "missing"
             assert response_dict[1]["loc"] == ["body", "mechanisms"]
 
+            # Expect to fail: not enough budget
+            input_smartnoise = dict(example_smartnoise_sql)
+            input_smartnoise["epsilon"] = 0.000000001
+            response = client.post(
+                "/smartnoise_query",
+                json=input_smartnoise,
+                headers=self.headers,
+            )
+            assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
+            assert response.json() == {
+                "ExternalLibraryException": "Error obtaining cost: "
+                + "Noise scale is too large using epsilon=1e-09 "
+                + "and bounds (0, 1) with Mechanism.gaussian.  "
+                + "Try preprocessing to reduce senstivity, "
+                + "or try different privacy parameters.",
+                "library": "smartnoise_sql",
+            }
+
             # Expect to fail: query does not make sense
             input_smartnoise = dict(example_smartnoise_sql)
             input_smartnoise[
@@ -285,8 +378,30 @@ def test_smartnoise_query(self) -> None:
                 + "Please, verify the client object initialisation."
             }
 
+    def test_smartnoise_query_on_s3_dataset(self) -> None:
+        """Test smartnoise-sql on s3 dataset"""
+        if os.getenv(ENV_S3_INTEGRATION, "0").lower() in TRUE_VALUES:
+            with TestClient(app, headers=self.headers) as client:
+                # Expect to work
+                input_smartnoise = dict(example_smartnoise_sql)
+                input_smartnoise["dataset_name"] = "TINTIN_S3_TEST"
+                response = client.post(
+                    "/smartnoise_query",
+                    json=input_smartnoise,
+                    headers=self.headers,
+                )
+                assert response.status_code == status.HTTP_200_OK
+
+                response_dict = json.loads(response.content.decode("utf8"))
+                assert response_dict["requested_by"] == self.user_name
+                assert response_dict["query_response"]["columns"] == ["NB_ROW"]
+                assert (
+                    response_dict["spent_epsilon"] == SMARTNOISE_QUERY_EPSILON
+                )
+                assert response_dict["spent_delta"] >= SMARTNOISE_QUERY_DELTA
+
     def test_dummy_smartnoise_query(self) -> None:
-        """_summary_"""
+        """test_dummy_smartnoise_query"""
         with TestClient(app) as client:
             # Expect to work
             response = client.post(
@@ -300,7 +415,7 @@ def test_dummy_smartnoise_query(self) -> None:
             assert response_dict["query_response"]["data"][0][0] < 200
 
     def test_smartnoise_cost(self) -> None:
-        """_summary_"""
+        """test_smartnoise_cost"""
         with TestClient(app) as client:
             # Expect to work
             response = client.post(
@@ -312,10 +427,10 @@ def test_smartnoise_cost(self) -> None:
             assert response_dict["epsilon_cost"] == SMARTNOISE_QUERY_EPSILON
             assert response_dict["delta_cost"] > SMARTNOISE_QUERY_DELTA
 
-    def test_opendp_query(self) -> None:
-        """_summary_"""
+    def test_opendp_query(self) -> None:  # pylint: disable=R0915
+        """test_opendp_query"""
         with TestClient(app, headers=self.headers) as client:
-            # Expect to work
+            # Basic test based on example with max divergence (Pure DP)
             response = client.post(
                 "/opendp_query",
                 json=example_opendp,
@@ -328,13 +443,31 @@ def test_opendp_query(self) -> None:
             assert response_dict["spent_epsilon"] > 0.1
             assert response_dict["spent_delta"] == 0
 
+            # Tests on different pipeline
+            colnames = [
+                "species",
+                "island",
+                "bill_length_mm",
+                "bill_depth_mm",
+                "flipper_length_mm",
+                "body_mass_g",
+                "sex",
+            ]
+            transformation_pipeline = (
+                trans.make_split_dataframe(separator=",", col_names=colnames)
+                >> trans.make_select_column(key="bill_length_mm", TOA=str)
+                >> trans.then_cast_default(TOA=float)
+                >> trans.then_clamp(bounds=(30.0, 65.0))
+                >> trans.then_resize(size=346, constant=43.61)
+                >> trans.then_variance()
+            )
+
             # Expect to fail: transormation instead of measurement
-            trans = '{"version": "0.8.0", "ast": {"_type": "partial_chain", "lhs": {"_type": "partial_chain", "lhs": {"_type": "partial_chain", "lhs": {"_type": "partial_chain", "lhs": {"_type": "constructor", "func": "make_chain_tt", "module": "combinators", "args": [{"_type": "constructor", "func": "make_select_column", "module": "transformations", "kwargs": {"key": "bill_length_mm", "TOA": "String"}}, {"_type": "constructor", "func": "make_split_dataframe", "module": "transformations", "kwargs": {"separator": ",", "col_names": {"_type": "list", "_items": ["species", "island", "bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g", "sex"]}}}]}, "rhs": {"_type": "constructor", "func": "then_cast_default", "module": "transformations", "kwargs": {"TOA": "f64"}}}, "rhs": {"_type": "constructor", "func": "then_clamp", "module": "transformations", "kwargs": {"bounds": [30.0, 65.0]}}}, "rhs": {"_type": "constructor", "func": "then_resize", "module": "transformations", "kwargs": {"size": 346, "constant": 43.61}}}, "rhs": {"_type": "constructor", "func": "then_variance", "module": "transformations"}}}'  # noqa: E501 # pylint: disable=C0301
             response = client.post(
                 "/opendp_query",
                 json={
                     "dataset_name": PENGUIN_DATASET,
-                    "opendp_json": trans,
+                    "opendp_json": transformation_pipeline.to_json(),
                 },
             )
             assert response.status_code == status.HTTP_400_BAD_REQUEST
@@ -343,8 +476,99 @@ def test_opendp_query(self) -> None:
                 + "measurement. It cannot be processed in this server."
             }
 
+            # Test MAX_DIVERGENCE (pure DP)
+            md_pipeline = transformation_pipeline >> meas.then_laplace(
+                scale=5.0
+            )
+            response = client.post(
+                "/opendp_query",
+                json={
+                    "dataset_name": PENGUIN_DATASET,
+                    "opendp_json": md_pipeline.to_json(),
+                },
+            )
+            assert response.status_code == status.HTTP_200_OK
+
+            response_dict = json.loads(response.content.decode("utf8"))
+            assert response_dict["requested_by"] == self.user_name
+            assert response_dict["query_response"] > 0
+            assert response_dict["spent_epsilon"] > 0.1
+            assert response_dict["spent_delta"] == 0
+
+            # Test ZERO_CONCENTRATED_DIVERGENCE
+            zcd_pipeline = transformation_pipeline >> meas.then_gaussian(
+                scale=5.0
+            )
+            json_obj = {
+                "dataset_name": PENGUIN_DATASET,
+                "opendp_json": zcd_pipeline.to_json(),
+            }
+            # Should error because missing fixed_delta
+            response = client.post("/opendp_query", json=json_obj)
+            assert response.status_code == status.HTTP_400_BAD_REQUEST
+            assert response.json() == {
+                "InvalidQueryException": ""
+                + "fixed_delta must be set for smooth max divergence"
+                + " and zero concentrated divergence."
+            }
+            # Should work because fixed_delta is set
+            json_obj["fixed_delta"] = 1e-6
+            response = client.post("/opendp_query", json=json_obj)
+            assert response.status_code == status.HTTP_200_OK
+
+            response_dict = json.loads(response.content.decode("utf8"))
+            assert response_dict["requested_by"] == self.user_name
+            assert response_dict["query_response"] > 0
+            assert response_dict["spent_epsilon"] > 0.1
+            assert response_dict["spent_delta"] == 1e-6
+
+            # Test SMOOTHED_MAX_DIVERGENCE (approx DP)
+            sm_pipeline = comb.make_zCDP_to_approxDP(zcd_pipeline)
+            json_obj = {
+                "dataset_name": PENGUIN_DATASET,
+                "opendp_json": sm_pipeline.to_json(),
+            }
+            # Should error because missing fixed_delta
+            response = client.post("/opendp_query", json=json_obj)
+            assert response.status_code == status.HTTP_400_BAD_REQUEST
+            assert response.json() == {
+                "InvalidQueryException": ""
+                + "fixed_delta must be set for smooth max divergence"
+                + " and zero concentrated divergence."
+            }
+
+            # Should work because fixed_delta is set
+            json_obj["fixed_delta"] = 1e-6
+            response = client.post("/opendp_query", json=json_obj)
+            assert response.status_code == status.HTTP_200_OK
+            response_dict = json.loads(response.content.decode("utf8"))
+            assert response_dict["requested_by"] == self.user_name
+            assert response_dict["query_response"] > 0
+            assert response_dict["spent_epsilon"] > 0.1
+            assert response_dict["spent_delta"] == 1e-6
+
+            # Test FIXED_SMOOTHED_MAX_DIVERGENCE
+            fms_pipeline = (
+                trans.make_split_dataframe(separator=",", col_names=colnames)
+                >> trans.make_select_column(key="island", TOA=str)
+                >> trans.then_count_by(MO=dp_p.L1Distance[float], TV=float)
+                >> meas.then_base_laplace_threshold(scale=2.0, threshold=28.0)
+            )
+            json_obj = {
+                "dataset_name": PENGUIN_DATASET,
+                "opendp_json": fms_pipeline.to_json(),
+            }
+            # Should error because missing fixed_delta
+            response = client.post("/opendp_query", json=json_obj)
+            assert response.status_code == status.HTTP_200_OK
+            response_dict = json.loads(response.content.decode("utf8"))
+            assert response_dict["requested_by"] == self.user_name
+            assert isinstance(response_dict["query_response"], dict)
+            assert response_dict["spent_epsilon"] > 0.1
+            assert response_dict["spent_delta"] > 0
+
     def test_dummy_opendp_query(self) -> None:
-        """_summary_"""
+        """test_dummy_opendp_query"""
         with TestClient(app) as client:
             # Expect to work
             response = client.post(
@@ -355,7 +579,7 @@ def test_dummy_opendp_query(self) -> None:
             assert response_dict["query_response"] > 0
 
     def test_opendp_cost(self) -> None:
-        """_summary_"""
+        """test_opendp_cost"""
         with TestClient(app) as client:
             # Expect to work
             response = client.post(
@@ -368,7 +592,7 @@ def test_opendp_cost(self) -> None:
             assert response_dict["delta_cost"] == 0
 
     def test_get_initial_budget(self) -> None:
-        """_summary_"""
+        """test_get_initial_budget"""
         with TestClient(app, headers=self.headers) as client:
             # Expect to work
             response = client.post(
@@ -396,7 +620,7 @@ def test_get_initial_budget(self) -> None:
             assert response_dict_2 == response_dict
 
     def test_get_total_spent_budget(self) -> None:
-        """_summary_"""
+        """test_get_total_spent_budget"""
         with TestClient(app, headers=self.headers) as client:
             # Expect to work
             response = client.post(
@@ -432,7 +656,7 @@ def test_get_total_spent_budget(self) -> None:
             )
 
     def test_get_remaining_budget(self) -> None:
-        """_summary_"""
+        """test_get_remaining_budget"""
         with TestClient(app, headers=self.headers) as client:
             # Expect to work
             response = client.post(
@@ -469,7 +693,7 @@ def test_get_remaining_budget(self) -> None:
             )
 
     def test_get_previous_queries(self) -> None:
-        """_summary_"""
+        """test_get_previous_queries"""
         with TestClient(app, headers=self.headers) as client:
             # Expect to work
             response = client.post(
@@ -540,7 +764,7 @@ def test_get_previous_queries(self) -> None:
             )
 
     def test_budget_over_limit(self) -> None:
-        """_summary_"""
+        """test_budget_over_limit"""
         with TestClient(app, headers=self.headers) as client:
             # Should fail: too much budget on one go
             smartnoise_body = dict(example_smartnoise_sql)
@@ -559,7 +783,7 @@ def test_budget_over_limit(self) -> None:
             assert error["msg"] == "Input should be less than or equal to 5"
 
     def test_subsequent_budget_limit_logic(self) -> None:
-        """_summary_"""
+        """test_subsequent_budget_limit_logic"""
         with TestClient(app, headers=self.headers) as client:
             # Should fail: too much budget after three queries
             smartnoise_body = dict(example_smartnoise_sql)
@@ -585,7 +809,7 @@ def test_subsequent_budget_limit_logic(self) -> None:
             response_dict = json.loads(response.content.decode("utf8"))
             assert response_dict["requested_by"] == self.user_name
 
-            # spend 3*4.0 (total_spent = 12.0 > INTIAL_BUDGET = 10.0)
+            # spend 3*4.0 (total_spent = 12.0 > INITIAL_BUDGET = 10.0)
             response = client.post(
                 "/smartnoise_query",
                 json=smartnoise_body,
diff --git a/server/lomas_server/tests/test_data/local_db_file.yaml b/server/lomas_server/tests/test_data/local_db_file.yaml
index 8e1f6975..c064f953 100644
--- a/server/lomas_server/tests/test_data/local_db_file.yaml
+++ b/server/lomas_server/tests/test_data/local_db_file.yaml
@@ -11,6 +11,20 @@ datasets:
   metadata:
     database_type: "PATH_DB"
     metadata_path: "tests/test_data/metadata/penguin_metadata.yaml"
+- dataset_name: "TINTIN_S3_TEST"
+  database_type: "S3_DB"
+  s3_bucket: "example"
+  s3_key: "data/test_penguin.csv"
+  endpoint_url: "http://localhost:9000"
+  aws_access_key_id: "admin"
+  aws_secret_access_key: "admin123"
+  metadata:
+    database_type: "S3_DB"
+    s3_bucket: "example"
+    s3_key: "metadata/penguin_metadata.yaml"
+    endpoint_url: "http://localhost:9000"
+    aws_access_key_id: "admin"
+    aws_secret_access_key: "admin123"
 users:
 - user_name: Dr. Antartica
   may_query: True
@@ -20,4 +34,9 @@ users:
     initial_delta: 0.005
     total_spent_epsilon: 0
     total_spent_delta: 0
+  - dataset_name: "TINTIN_S3_TEST"
+    initial_epsilon: 10
+    initial_delta: 0.005
+    total_spent_epsilon: 0
+    total_spent_delta: 0
 queries: []
\ No newline at end of file
diff --git a/server/lomas_server/tests/test_data/test_datasets_with_s3.yaml b/server/lomas_server/tests/test_data/test_datasets_with_s3.yaml
new file mode 100644
index 00000000..652e2c2a
--- /dev/null
+++ b/server/lomas_server/tests/test_data/test_datasets_with_s3.yaml
@@ -0,0 +1,27 @@
+datasets:
+- dataset_name: "PENGUIN"
+  database_type: "PATH_DB"
+  dataset_path: "https://raw.githubusercontent.com/mwaskom/seaborn-data/master/penguins.csv"
+  metadata:
+    database_type: "PATH_DB"
+    metadata_path: "tests/test_data/metadata/penguin_metadata.yaml"
+- dataset_name: "IRIS"
+  database_type: "PATH_DB"
+  dataset_path: "https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv"
+  metadata:
+    database_type: "PATH_DB"
+    metadata_path: "tests/test_data/metadata/penguin_metadata.yaml"
+- dataset_name: "TINTIN_S3_TEST"
+  database_type: "S3_DB"
+  s3_bucket: "example"
+  s3_key: "data/test_penguin.csv"
+  endpoint_url: "http://localhost:9000"
+  aws_access_key_id: "admin"
+  aws_secret_access_key: "admin123"
+  metadata:
+    database_type: "S3_DB"
+    s3_bucket: "example"
+    s3_key: "metadata/penguin_metadata.yaml"
+    endpoint_url: "http://localhost:9000"
+    aws_access_key_id: "admin"
+    aws_secret_access_key: "admin123"
\ No newline at end of file
diff --git a/server/lomas_server/tests/test_data/test_penguin.csv b/server/lomas_server/tests/test_data/test_penguin.csv
new file mode 100644
index 00000000..37aaf5c3
--- /dev/null
+++ b/server/lomas_server/tests/test_data/test_penguin.csv
@@ -0,0 +1,6 @@
+species,island,bill_length_mm,bill_depth_mm,flipper_length_mm,body_mass_g,sex
+Adelie,Torgersen,39.1,18.7,181,3750,MALE
+Adelie,Torgersen,39.5,17.4,186,3800,FEMALE
+Adelie,Torgersen,40.3,18,195,3250,FEMALE
+Adelie,Torgersen,,,,,
+Adelie,Torgersen,36.7,19.3,193,3450,FEMALE
\ No newline at end of file
diff --git a/server/lomas_server/tests/test_data/test_user_collection.yaml b/server/lomas_server/tests/test_data/test_user_collection.yaml
index 3664911e..e7f0cab8 100644
--- a/server/lomas_server/tests/test_data/test_user_collection.yaml
+++ b/server/lomas_server/tests/test_data/test_user_collection.yaml
@@ -7,6 +7,11 @@ users:
     initial_delta: 0.005
     total_spent_epsilon: 0
     total_spent_delta: 0
+  - dataset_name: "TINTIN_S3_TEST"
+    initial_epsilon: 10
+    initial_delta: 0.005
+    total_spent_epsilon: 0
+    total_spent_delta: 0
 - user_name: "Tintin"
   may_query: True
   datasets_list:
diff --git a/server/lomas_server/tests/test_dummy_generation.py b/server/lomas_server/tests/test_dummy_generation.py
index a940ba8a..06746a95 100644
--- a/server/lomas_server/tests/test_dummy_generation.py
+++ b/server/lomas_server/tests/test_dummy_generation.py
@@ -10,29 +10,48 @@ class TestMakeDummyDataset(unittest.TestCase):
     """
 
     def test_cardinality_column(self) -> None:
-        """_summary_"""
+        """test_cardinality_column"""
         metadata = {
             "columns": {
-                "col_card": {
+                "col_card_cat": {  # cardinality + categories
                     "type": "string",
                     "cardinality": 3,
-                    "categories": ["a", "b", "c"],
-                }
+                    "categories": ["x", "y", "z"],
+                },
+                "col_card_no_cat": {  # cardinality, no categories
+                    "type": "string",
+                    "cardinality": 3,
+                },
+                "col_no_card": {  # no cardinality
+                    "type": "string",
+                },
             }
         }
         df = make_dummy_dataset(metadata)
 
-        # Test length
-        self.assertEqual(len(df), DUMMY_NB_ROWS)
+        # Test shape
+        self.assertEqual(df.shape[0], DUMMY_NB_ROWS)
+        self.assertEqual(df.shape[1], len(metadata["columns"].keys()))
 
         # Test cardinality type and categories
-        self.assertIn("col_card", df.columns)
-        self.assertEqual(df["col_card"].nunique(), 3)
-        self.assertEqual(set(df["col_card"].values), {"a", "b", "c"})
-        assert isinstance(df["col_card"], object)
+        self.assertIn("col_card_cat", df.columns)
+        self.assertEqual(df["col_card_cat"].nunique(), 3)
+        self.assertEqual(set(df["col_card_cat"].values), {"x", "y", "z"})
+        assert isinstance(df["col_card_cat"], object)
+
+        # Test cardinality type and no categories
+        self.assertIn("col_card_no_cat", df.columns)
+        self.assertEqual(set(df["col_card_no_cat"].values), {"a", "b", "c"})
+        assert isinstance(df["col_card_no_cat"], object)
+
+        # Test cardinality type and no categories
+        self.assertIn("col_no_card", df.columns)
+        self.assertTrue(
+            df["col_no_card"].apply(lambda x: isinstance(x, str)).all()
+        )
 
     def test_boolean_column(self) -> None:
-        """_summary_"""
+        """test_boolean_column"""
 
         # Test a boolean column
         metadata = {
@@ -48,7 +67,7 @@ def test_boolean_column(self) -> None:
         self.assertEqual(df.col_bool.dtypes.name, "boolean")
 
     def test_float_column(self) -> None:
-        """_summary_"""
+        """test_float_column"""
         lower_bound = 10.0
         upper_bound = 20.0
         metadata = {
@@ -70,7 +89,7 @@ def test_float_column(self) -> None:
         self.assertTrue((df["col_float"] <= upper_bound).all())
 
     def test_int_column(self) -> None:
-        """_summary_"""
+        """test_int_column"""
         lower_bound = 100
         upper_bound = 120
         metadata = {
@@ -92,7 +111,7 @@ def test_int_column(self) -> None:
         self.assertTrue((df["col_int"] <= upper_bound).all())
 
     def test_datetime_column(self) -> None:
-        """_summary_"""
+        """test_datetime_column"""
         metadata = {"columns": {"col_datetime": {"type": "datetime"}}}
         df = make_dummy_dataset(metadata)
 
@@ -103,7 +122,7 @@ def test_datetime_column(self) -> None:
         self.assertFalse(df.col_datetime.isnull().values.any())
 
     def test_nullable_column(self) -> None:
-        """_summary_"""
+        """test_nullable_column"""
         metadata = {
             "columns": {"col_nullable": {"type": "datetime", "nullable": True}}
         }
@@ -113,7 +132,7 @@ def test_nullable_column(self) -> None:
         self.assertTrue(df.col_nullable.isnull().values.any())
 
     def test_seed(self) -> None:
-        """_summary_"""
+        """test_seed"""
         # Test the behavior with different seeds
         metadata = {"columns": {"col_int": {"type": "int", "nullable": True}}}
         seed1 = DUMMY_SEED
@@ -128,3 +147,19 @@ def test_seed(self) -> None:
         # Check if datasets generated with the same seed are identical
         df1_copy = make_dummy_dataset(metadata, seed=seed1)
         self.assertTrue(df1.equals(df1_copy))
+
+    def test_unknown_column(self) -> None:
+        """test_unknown_column"""
+        metadata = {
+            "columns": {
+                "col_bool": {"type": "boolean", "nullable": True},
+                "col_unknown": {"type": "unknown"},
+            }
+        }
+        df = make_dummy_dataset(metadata)
+
+        # Test col generated
+        self.assertIn("col_bool", df.columns)
+
+        # Test col not generated
+        self.assertNotIn("col_unknown", df.columns)
diff --git a/server/lomas_server/tests/test_mongodb_admin.py b/server/lomas_server/tests/test_mongodb_admin.py
index 909538a8..b0cbad34 100644
--- a/server/lomas_server/tests/test_mongodb_admin.py
+++ b/server/lomas_server/tests/test_mongodb_admin.py
@@ -3,6 +3,7 @@
 from types import SimpleNamespace
 from typing import Dict
 
+import boto3
 import yaml
 from pymongo import MongoClient
 
@@ -30,13 +31,19 @@
     show_user,
 )
 from constants import PrivateDatabaseType
-from tests.constants import ENV_MONGO_INTEGRATION
+from tests.constants import (
+    ENV_MONGO_INTEGRATION,
+    ENV_S3_INTEGRATION,
+    TRUE_VALUES,
+    FALSE_VALUES,
+)
 from utils.config import CONFIG_LOADER, get_config
+from utils.utils import add_demo_data_to_admindb
 
 
 @unittest.skipIf(
     ENV_MONGO_INTEGRATION not in os.environ
-    and os.getenv(ENV_MONGO_INTEGRATION, "0").lower() in ("false", "0", "f"),
+    and os.getenv(ENV_MONGO_INTEGRATION, "0").lower() in FALSE_VALUES,
     f"""Not an MongoDB integration test: {ENV_MONGO_INTEGRATION}
         environment variable not set to True.""",
 )
@@ -534,6 +541,128 @@ def test_add_local_dataset(self) -> None:
         )[dataset]
         self.assertEqual(metadata_found, expected_metadata)
 
+        # Add already present dataset
+        with self.assertRaises(ValueError):
+            add_dataset(
+                self.db,
+                dataset,
+                database_type=database_type,
+                metadata_database_type=metadata_database_type,
+                dataset_path=dataset_path,
+                metadata_path=metadata_path,
+            )
+
+        # Add not already present dataset but present metadata
+        drop_collection(self.db, "datasets")
+        with self.assertRaises(ValueError):
+            add_dataset(
+                self.db,
+                dataset,
+                database_type=database_type,
+                metadata_database_type=metadata_database_type,
+                dataset_path=dataset_path,
+                metadata_path=metadata_path,
+            )
+
+        # Restart clean
+        drop_collection(self.db, "metadata")
+        drop_collection(self.db, "datasets")
+
+        # Unknown database type for dataset
+        with self.assertRaises(ValueError):
+            add_dataset(
+                self.db,
+                dataset,
+                database_type="type_that_does_not_exist",
+                metadata_database_type=metadata_database_type,
+                dataset_path=dataset_path,
+                metadata_path=metadata_path,
+            )
+
+        # Unknown database type for metadata
+        with self.assertRaises(ValueError):
+            add_dataset(
+                self.db,
+                dataset,
+                database_type=database_type,
+                metadata_database_type="type_that_does_not_exist",
+                dataset_path=dataset_path,
+                metadata_path=metadata_path,
+            )
+
+    @unittest.skipIf(
+        ENV_S3_INTEGRATION not in os.environ
+        and os.getenv(ENV_S3_INTEGRATION, "0").lower() in FALSE_VALUES,
+        f"""Not an S3 integration test: {ENV_S3_INTEGRATION}
+            environment variable not set to True.""",
+    )
+    def test_add_s3_dataset(self) -> None:  # pylint: disable=R0914
+        """Test adding a dataset stored on S3"""
+        dataset = "TINTIN_S3_TEST"
+        database_type = PrivateDatabaseType.S3
+        metadata_database_type = PrivateDatabaseType.S3
+        s3_bucket = "example"
+        endpoint_url = "http://localhost:9000"
+        aws_access_key_id = "admin"
+        aws_secret_access_key = "admin123"
+        s3_key_file = "data/test_penguin.csv"
+        s3_key_metadata = "metadata/penguin_metadata.yaml"
+
+        add_dataset(
+            self.db,
+            dataset,
+            database_type,
+            metadata_database_type,
+            s3_bucket=s3_bucket,
+            s3_key=s3_key_file,
+            endpoint_url=endpoint_url,
+            aws_access_key_id=aws_access_key_id,
+            aws_secret_access_key=aws_secret_access_key,
+            metadata_s3_bucket=s3_bucket,
+            metadata_s3_key=s3_key_metadata,
+            metadata_endpoint_url=endpoint_url,
+            metadata_aws_access_key_id=aws_access_key_id,
+            metadata_aws_secret_access_key=aws_secret_access_key,
+        )
+
+        # Check dataset collection
+        expected_dataset = {
+            "dataset_name": dataset,
+            "database_type": database_type,
+            "s3_bucket": s3_bucket,
+            "s3_key": s3_key_file,
+            "endpoint_url": endpoint_url,
+            "aws_access_key_id": aws_access_key_id,
+            "aws_secret_access_key": aws_secret_access_key,
+            "metadata": {
+                "database_type": metadata_database_type,
+                "s3_bucket": s3_bucket,
+                "s3_key": s3_key_metadata,
+                "endpoint_url": endpoint_url,
+                "aws_access_key_id": aws_access_key_id,
+                "aws_secret_access_key": aws_secret_access_key,
+            },
+        }
+
+        dataset_found = self.db.datasets.find_one({"dataset_name": dataset})
+        del dataset_found["_id"]
+        self.assertEqual(dataset_found, expected_dataset)
+
+        # Check metadata collection
+        s3_client = boto3.client(
+            "s3",
+            endpoint_url=endpoint_url,
+            aws_access_key_id=aws_access_key_id,
+            aws_secret_access_key=aws_secret_access_key,
+        )
+        response = s3_client.get_object(Bucket=s3_bucket, Key=s3_key_metadata)
+        expected_metadata = yaml.safe_load(response["Body"])
+
+        metadata_found = self.db.metadata.find_one(
+            {dataset: {"$exists": True}}
+        )[dataset]
+        self.assertEqual(metadata_found, expected_metadata)
+
     def test_add_datasets_via_yaml(self) -> None:
         """Test add datasets via a YAML file"""
         # Load reference data
@@ -615,6 +744,62 @@ def verify_datasets():
         )
         verify_datasets()
 
+        # Check no clean and overwrite metadata
+        add_datasets_via_yaml(
+            self.db,
+            path,
+            clean=False,
+            overwrite_datasets=True,
+            overwrite_metadata=True,
+        )
+        verify_datasets()
+
+    @unittest.skipIf(
+        ENV_S3_INTEGRATION not in os.environ
+        and os.getenv(ENV_S3_INTEGRATION, "0").lower() in FALSE_VALUES,
+        f"""Not an S3 integration test: {ENV_S3_INTEGRATION}
+            environment variable not set to True.""",
+    )
+    def test_add_s3_datasets_via_yaml(self) -> None:
+        """Test add datasets via a YAML file"""
+        # Load reference data
+        dataset_path = "./tests/test_data/test_datasets_with_s3.yaml"
+        with open(
+            dataset_path,
+            encoding="utf-8",
+        ) as f:
+            datasets = yaml.safe_load(f)
+            tintin = datasets["datasets"][2]
+
+        with open(
+            "./tests/test_data/metadata/penguin_metadata.yaml",
+            encoding="utf-8",
+        ) as f:
+            tintin_metadata = yaml.safe_load(f)
+
+        clean = False
+        overwrite_datasets = False
+        overwrite_metadata = False
+
+        add_datasets_via_yaml(
+            self.db,
+            dataset_path,
+            clean,
+            overwrite_datasets,
+            overwrite_metadata,
+        )
+
+        tintin_found = self.db.datasets.find_one(
+            {"dataset_name": "TINTIN_S3_TEST"}
+        )
+        del tintin_found["_id"]
+        self.assertEqual(tintin_found, tintin)
+
+        metadata_found = self.db.metadata.find_one(
+            {"TINTIN_S3_TEST": {"$exists": True}}
+        )["TINTIN_S3_TEST"]
+        self.assertEqual(metadata_found, tintin_metadata)
+
     def test_del_dataset(self) -> None:
         """Test dataset deletion"""
         # Setup: add one dataset
@@ -642,7 +827,20 @@ def test_del_dataset(self) -> None:
         nb_metadata = self.db.metadata.count_documents({})
         self.assertEqual(nb_metadata, 0)
 
-        # Delete non-existing dataset should trigger error
+        # Delete non-existing dataset should trigger decorator error
+        with self.assertRaises(ValueError):
+            del_dataset(self.db, dataset)
+
+        # Delete dataset with non-existing metadata should trigger decorator error
+        add_dataset(
+            self.db,
+            dataset,
+            database_type,
+            metadata_database_type,
+            dataset_path=dataset_path,
+            metadata_path=metadata_path,
+        )
+        self.db.metadata.delete_many({dataset: {"$exists": True}})
         with self.assertRaises(ValueError):
             del_dataset(self.db, dataset)
 
@@ -760,3 +958,28 @@ def test_show_collection(self) -> None:
         self.assertEqual(
             expected_dataset_collection["datasets"], dataset_collection
         )
+
+    def test_add_demo_data_to_admindb(self) -> None:
+        """Test add demo data to admin db"""
+
+        if os.getenv(ENV_S3_INTEGRATION, "0").lower() in TRUE_VALUES:
+            dataset_yaml = "tests/test_data/test_datasets_with_s3.yaml"
+        else:
+            dataset_yaml = "tests/test_data/test_datasets.yaml"
+
+        add_demo_data_to_admindb(
+            user_yaml="./tests/test_data/test_user_collection.yaml",
+            dataset_yaml=dataset_yaml,
+        )
+
+        users_list = get_list_of_users(self.db)
+        self.assertEqual(users_list, ["Dr. Antartica", "Tintin", "Milou"])
+
+        list_datasets = get_list_of_datasets(self.db)
+
+        if os.getenv(ENV_S3_INTEGRATION, "0").lower() in TRUE_VALUES:
+            self.assertEqual(
+                list_datasets, ["PENGUIN", "IRIS", "TINTIN_S3_TEST"]
+            )
+        else:
+            self.assertEqual(list_datasets, ["PENGUIN", "IRIS"])
diff --git a/server/run_tests_and_coverage.sh b/server/run_tests_and_coverage.sh
index 52e47c80..57c05e88 100644
--- a/server/run_tests_and_coverage.sh
+++ b/server/run_tests_and_coverage.sh
@@ -2,17 +2,20 @@
 
 docker compose -f docker-compose-test.yml up --detach
 
-sleep 3
+sleep 15
 
 cd ./lomas_server
 
+
 # "mongodb", "LRU_cache", production mode, "jitter"
-export LOMAS_TEST_MONGO_INTEGRATION=1
+export LOMAS_TEST_MONGO_INTEGRATION=1 # tests with mongodb available
+export LOMAS_TEST_S3_INTEGRATION=1 # tests with s3 minio available
 coverage run --source=. -m unittest discover -s .
 ret1=$?
 
 # "yaml", "basic", developer mode, "stall"
 export LOMAS_TEST_MONGO_INTEGRATION=0
+export LOMAS_TEST_S3_INTEGRATION=0
 coverage run -a --source=. -m unittest discover -s .
 ret2=$?