Repository: mathesar-foundation/mathesar

Issue:
Add support for Python versions 3.10 and above
## Problem
<!-- Please provide a clear and concise description of the problem that this feature request is designed to solve.-->
We currently publish supporting only python 3.9 in our [build from source documentation](https://docs.mathesar.org/installation/build-from-source/#software) and all of our docker images are published using the same. This is a problem since modern Debian versions now ship with Python 3.10+ natively installed.

## Proposed solution
<!-- A clear and concise description of your proposed solution or feature. -->
We should support Python versions from 3.9 all the way till 3.12(The latest Python version supported by Django)



Discussion:

Resolving patch:
diff --git a/mathesar/api/db/viewsets/tables.py b/mathesar/api/db/viewsets/tables.py
--- a/mathesar/api/db/viewsets/tables.py
+++ b/mathesar/api/db/viewsets/tables.py
@@ -110,7 +110,7 @@ def split_table(self, request, pk=None):
         table = self.get_object()
         column_names_id_map = table.get_column_name_id_bidirectional_map()
         serializer = SplitTableRequestSerializer(data=request.data, context={"request": request, 'table': table})
-        if serializer.is_valid(True):
+        if serializer.is_valid(raise_exception=True):
             # We need to get the column names before splitting the table,
             # as they are the only reference to the new column after it is moved to a new table
             columns_to_extract = serializer.validated_data['extract_columns']
@@ -128,14 +128,14 @@ def split_table(self, request, pk=None):
                 'fk_column': remainder_fk_column.id
             }
             response_serializer = SplitTableResponseSerializer(data=split_table_response)
-            response_serializer.is_valid(True)
+            response_serializer.is_valid(raise_exception=True)
             return Response(response_serializer.data, status=status.HTTP_201_CREATED)
 
     @action(methods=['post'], detail=True)
     def move_columns(self, request, pk=None):
         table = self.get_object()
         serializer = MoveTableRequestSerializer(data=request.data, context={"request": request, 'table': table})
-        if serializer.is_valid(True):
+        if serializer.is_valid(raise_exception=True):
             target_table = serializer.validated_data['target_table']
             move_columns = serializer.validated_data['move_columns']
             table.move_columns(
diff --git a/mathesar/api/serializers/dependents.py b/mathesar/api/serializers/dependents.py
--- a/mathesar/api/serializers/dependents.py
+++ b/mathesar/api/serializers/dependents.py
@@ -77,4 +77,4 @@ class DependentSerializer(serializers.Serializer):
 
 
 class DependentFilterSerializer(serializers.Serializer):
-    exclude = serializers.MultipleChoiceField(DATABASE_OBJECT_TYPES, required=False)
+    exclude = serializers.MultipleChoiceField(choices=DATABASE_OBJECT_TYPES, required=False)
diff --git a/mathesar/state/django.py b/mathesar/state/django.py
--- a/mathesar/state/django.py
+++ b/mathesar/state/django.py
@@ -145,7 +145,7 @@ def _invalidate_columns_with_incorrect_display_options(tables):
                 data=column.display_options,
                 context={DISPLAY_OPTIONS_SERIALIZER_MAPPING_KEY: column.db_type}
             )
-            if not serializer.is_valid(False):
+            if not serializer.is_valid(raise_exception=False):
                 columns_with_invalid_display_option.append(column.id)
     if len(columns_with_invalid_display_option) > 0:
         models.Column.current_objects.filter(id__in=columns_with_invalid_display_option).update(display_options=None)


Test patch:
diff --git a/mathesar/tests/api/test_record_api.py b/mathesar/tests/api/test_record_api.py
--- a/mathesar/tests/api/test_record_api.py
+++ b/mathesar/tests/api/test_record_api.py
@@ -1513,15 +1513,14 @@ def test_record_patch_unique_violation(create_patents_table, client):
     assert actual_constraint_details['name'] == 'NASA unique record PATCH_pkey'
 
 
-def test_record_post_exclusion_violation(reservations_table, engine, client):
-    table_name = 'Exclusion test post'
-    table = reservations_table(table_name)
+def test_record_post_exclusion_violation(create_reservations_table, engine, client):
+    table = create_reservations_table
     room_number_column_id = table.get_column_name_id_bidirectional_map()['room_number']
     columns_name_id_map = table.get_column_name_id_bidirectional_map()
     query = text(
         f"""CREATE EXTENSION IF NOT EXISTS btree_gist;
-        ALTER TABLE "Reservations"."{table_name}" DROP CONSTRAINT IF EXISTS room_overlap;
-        ALTER TABLE "Reservations"."{table_name}"
+        ALTER TABLE "Reservations"."{table.name}" DROP CONSTRAINT IF EXISTS room_overlap;
+        ALTER TABLE "Reservations"."{table.name}"
         ADD CONSTRAINT room_overlap
         EXCLUDE USING gist
         ("room_number" WITH =, TSRANGE("check_in_date", "check_out_date", '[]') WITH &&);"""
@@ -1541,15 +1540,14 @@ def test_record_post_exclusion_violation(reservations_table, engine, client):
     assert actual_exception['detail']['constraint_columns'] == [room_number_column_id]
 
 
-def test_record_patch_exclusion_violation(reservations_table, engine, client):
-    table_name = 'Exclusion test patch'
-    table = reservations_table(table_name)
+def test_record_patch_exclusion_violation(create_reservations_table, engine, client):
+    table = create_reservations_table
     room_number_column_id = table.get_column_name_id_bidirectional_map()['room_number']
     columns_name_id_map = table.get_column_name_id_bidirectional_map()
     query = text(
         f"""CREATE EXTENSION IF NOT EXISTS btree_gist;
-        ALTER TABLE "Reservations"."{table_name}" DROP CONSTRAINT IF EXISTS room_overlap;
-        ALTER TABLE "Reservations"."{table_name}"
+        ALTER TABLE "Reservations"."{table.name}" DROP CONSTRAINT IF EXISTS room_overlap;
+        ALTER TABLE "Reservations"."{table.name}"
         ADD CONSTRAINT room_overlap
         EXCLUDE USING gist
         ("room_number" WITH =, TSRANGE("check_in_date", "check_out_date", '[]') WITH &&);"""
diff --git a/mathesar/tests/conftest.py b/mathesar/tests/conftest.py
--- a/mathesar/tests/conftest.py
+++ b/mathesar/tests/conftest.py
@@ -9,6 +9,7 @@
 from django.core.files import File
 from django.core.cache import cache
 from django.conf import settings
+from django.db import connection as dj_connection
 from rest_framework.test import APIClient
 
 from sqlalchemy import Column, MetaData, Integer, Date
@@ -151,15 +152,9 @@ def add_db_to_dj_settings(request):
     added_dbs = set()
 
     def _add(db_name):
-        reference_entry = dj_databases["default"]
-        new_entry = dict(
-            USER=reference_entry['USER'],
-            PASSWORD=reference_entry['PASSWORD'],
-            HOST=reference_entry['HOST'],
-            PORT=reference_entry['PORT'],
-            NAME=db_name,
-        )
-        dj_databases[db_name] = new_entry
+        reference_entry = dj_connection.settings_dict
+        dj_databases[db_name] = reference_entry
+        dj_databases[db_name]['NAME'] = db_name
         cache.clear()
         added_dbs.add(db_name)
         return db_name
@@ -370,27 +365,26 @@ def _create_mathesar_table(
 
 
 @pytest.fixture
-def reservations_table(engine, reservations_schema):
+def create_reservations_table(engine_with_schema, reservations_schema):
+    engine, _ = engine_with_schema
+    table_name = 'Exclusion Check'
     schema_name = reservations_schema.name
-
-    def _create_test_table(table_name, schema_name=schema_name):
-        table_name = table_name or 'Exclusion Check'
-        schema_name = reservations_schema.name
-        cols = [
-            Column('id', Integer, primary_key=True),
-            Column('room_number', Integer),
-            Column('check_in_date', Date),
-            Column('check_out_date', Date)
-        ]
-        insert_data = [
-            (1, 1, '11/10/2023', '11/15/2023'),
-            (2, 1, '11/16/2023', '11/20/2023')
-        ]
-        sa_table = create_test_table(table_name, cols, insert_data, schema_name, engine)
-        table_oid = get_oid_from_table(sa_table.name, schema_name, engine)
-        table = Table.current_objects.create(oid=table_oid, schema=reservations_schema)
-        return table
-    return _create_test_table
+    cols = [
+        Column('id', Integer, primary_key=True),
+        Column('room_number', Integer),
+        Column('check_in_date', Date),
+        Column('check_out_date', Date)
+    ]
+    insert_data = [
+        (1, 1, '11/10/2023', '11/15/2023'),
+        (2, 1, '11/16/2023', '11/20/2023')
+    ]
+    sa_table = create_test_table(table_name, cols, insert_data, schema_name, engine)
+    table_oid = get_oid_from_table(sa_table.name, schema_name, engine)
+    table = Table.current_objects.create(oid=table_oid, schema=reservations_schema)
+    yield table
+    table.delete_sa_table()
+    table.delete()
 
 
 @pytest.fixture