From 69c0a9e87be4ad9aca5ce24a9c7a4b55ff44a6c9 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 24 Jul 2025 15:13:51 -0600 Subject: [PATCH 01/42] fix: import all models in db package for Alembic --- .pre-commit-config.yaml | 1 + ...n.py => 42f7242f188d_initial_migration.py} | 176 +++++++++++------- .../versions/842535e6433f_contact_change.py | 57 ------ .../8447b9ebaf29_changes_to_pre_production.py | 87 --------- db/__init__.py | 8 +- tests/__init__.py | 11 +- 6 files changed, 121 insertions(+), 219 deletions(-) rename alembic/versions/{5901f059248a_initial_migration.py => 42f7242f188d_initial_migration.py} (90%) delete mode 100644 alembic/versions/842535e6433f_contact_change.py delete mode 100644 alembic/versions/8447b9ebaf29_changes_to_pre_production.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index de1f0749c..608a59e9b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,6 +14,7 @@ repos: '--show-source', '--statistics' ] + exclude: ^db/__init__.py$ # all models need to be imported for Alembic, but are not used directly # - repo: https://github.com/pre-commit/mirrors-mypy # rev: v1.10.0 # Use the latest stable version or pin to your preference diff --git a/alembic/versions/5901f059248a_initial_migration.py b/alembic/versions/42f7242f188d_initial_migration.py similarity index 90% rename from alembic/versions/5901f059248a_initial_migration.py rename to alembic/versions/42f7242f188d_initial_migration.py index 7a8ee7cb9..764f919e6 100644 --- a/alembic/versions/5901f059248a_initial_migration.py +++ b/alembic/versions/42f7242f188d_initial_migration.py @@ -1,8 +1,8 @@ -"""Initial migration +"""initial migration -Revision ID: 5901f059248a -Revises: -Create Date: 2025-07-22 11:32:48.826352 +Revision ID: 42f7242f188d +Revises: +Create Date: 2025-07-24 15:12:10.195332 """ @@ -15,7 +15,7 @@ # revision identifiers, used by Alembic. -revision: str = "5901f059248a" +revision: str = "42f7242f188d" down_revision: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -49,6 +49,19 @@ def upgrade() -> None: unique=False, postgresql_using="gin", ) + op.create_table( + "group", + sa.Column("name", sa.String(length=100), nullable=False), + sa.Column("description", sa.String(length=255), nullable=True), + sa.Column("parent_group_id", sa.Integer(), nullable=True), + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column( + "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + sa.ForeignKeyConstraint(["parent_group_id"], ["group.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name"), + ) op.create_table( "lexicon_category", sa.Column("name", sa.String(length=100), nullable=False), @@ -225,7 +238,6 @@ def upgrade() -> None: ), sa.PrimaryKeyConstraint("id"), ) - op.execute("DROP INDEX IF EXISTS idx_location_point;") op.create_index( "idx_location_point", "location", @@ -268,6 +280,21 @@ def upgrade() -> None: op.create_table( "thing", sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("description", sa.String(length=500), nullable=True), + sa.Column("thing_type", sa.String(length=100), nullable=True), + sa.Column("spring_type", sa.String(length=100), nullable=True), + sa.Column("well_depth", sa.Float(), nullable=True), + sa.Column("hole_depth", sa.Float(), nullable=True), + sa.Column("well_type", sa.String(length=100), nullable=True), + sa.Column("well_casing_diameter", sa.Float(), nullable=True), + sa.Column("well_casing_depth", sa.Float(), nullable=True), + sa.Column("well_casing_description", sa.String(length=50), nullable=True), + sa.Column("well_construction_notes", sa.String(length=250), nullable=True), + sa.Column( + "search_vector", + sqlalchemy_utils.types.ts_vector.TSVectorType(), + nullable=True, + ), sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), sa.Column( "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False @@ -277,8 +304,27 @@ def upgrade() -> None: ["release_status"], ["lexicon_term.term"], ), + sa.ForeignKeyConstraint( + ["spring_type"], + ["lexicon_term.term"], + ), + sa.ForeignKeyConstraint( + ["thing_type"], + ["lexicon_term.term"], + ), + sa.ForeignKeyConstraint( + ["well_type"], + ["lexicon_term.term"], + ), sa.PrimaryKeyConstraint("id"), ) + op.create_index( + "ix_thing_search_vector", + "thing", + ["search_vector"], + unique=False, + postgresql_using="gin", + ) op.create_table( "address", sa.Column("contact_id", sa.Integer(), nullable=False), @@ -287,8 +333,13 @@ def upgrade() -> None: sa.Column("city", sa.String(length=100), nullable=False), sa.Column("state", sa.String(length=50), nullable=False), sa.Column("postal_code", sa.String(length=20), nullable=False), - sa.Column("country", sa.String(length=100), nullable=True), - sa.Column("address_type", sa.String(length=100), nullable=True), + sa.Column("country", sa.String(length=100), nullable=False), + sa.Column("address_type", sa.String(length=100), nullable=False), + sa.Column( + "search_vector", + sqlalchemy_utils.types.ts_vector.TSVectorType(), + nullable=True, + ), sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), sa.Column( "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False @@ -298,8 +349,19 @@ def upgrade() -> None: ["lexicon_term.term"], ), sa.ForeignKeyConstraint(["contact_id"], ["contact.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["country"], + ["lexicon_term.term"], + ), sa.PrimaryKeyConstraint("id"), ) + op.create_index( + "ix_address_search_vector", + "address", + ["search_vector"], + unique=False, + postgresql_using="gin", + ) op.create_table( "asset_thing_association", sa.Column("asset_id", sa.Integer(), nullable=False), @@ -312,11 +374,22 @@ def upgrade() -> None: sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("id"), ) + op.create_table( + "collaborative_network_well", + sa.Column("actively_monitored", sa.Boolean(), nullable=False), + sa.Column("thing_id", sa.Integer(), nullable=False), + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column( + "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( "email", sa.Column("contact_id", sa.Integer(), nullable=False), sa.Column("email", sa.String(length=100), nullable=False), - sa.Column("email_type", sa.String(length=100), nullable=True), + sa.Column("email_type", sa.String(length=100), nullable=False), sa.Column( "search_vector", sqlalchemy_utils.types.ts_vector.TSVectorType(), @@ -340,6 +413,18 @@ def upgrade() -> None: unique=False, postgresql_using="gin", ) + op.create_table( + "group_thing_association", + sa.Column("group_id", sa.Integer(), nullable=False), + sa.Column("thing_id", sa.Integer(), nullable=False), + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column( + "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + sa.ForeignKeyConstraint(["group_id"], ["group.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( "location_thing_association", sa.Column("location_id", sa.Integer(), nullable=False), @@ -363,7 +448,7 @@ def upgrade() -> None: "phone", sa.Column("contact_id", sa.Integer(), nullable=False), sa.Column("phone_number", sa.String(length=20), nullable=False), - sa.Column("phone_type", sa.String(length=100), nullable=True), + sa.Column("phone_type", sa.String(length=100), nullable=False), sa.Column( "search_vector", sqlalchemy_utils.types.ts_vector.TSVectorType(), @@ -441,18 +526,6 @@ def upgrade() -> None: ), sa.PrimaryKeyConstraint("id"), ) - op.create_table( - "spring_thing", - sa.Column("description", sa.String(length=255), nullable=True), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("thing_id", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("thing_id"), - ) op.create_table( "thing_contact_association", sa.Column("thing_id", sa.Integer(), nullable=False), @@ -493,36 +566,20 @@ def upgrade() -> None: sa.PrimaryKeyConstraint("id"), ) op.create_table( - "well_thing", - sa.Column("well_depth", sa.Float(), nullable=True), - sa.Column("hole_depth", sa.Float(), nullable=True), - sa.Column("well_type", sa.String(length=100), nullable=True), - sa.Column("casing_diameter", sa.Float(), nullable=True), - sa.Column("casing_depth", sa.Float(), nullable=True), - sa.Column("casing_description", sa.String(length=50), nullable=True), - sa.Column("construction_notes", sa.String(length=250), nullable=True), + "well_screen", + sa.Column("thing_id", sa.Integer(), nullable=False), + sa.Column("screen_depth_top", sa.Float(), nullable=False), + sa.Column("screen_depth_bottom", sa.Float(), nullable=False), + sa.Column("screen_type", sa.String(length=100), nullable=True), sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), sa.Column( "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False ), - sa.Column("thing_id", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint( - ["well_type"], + ["screen_type"], ["lexicon_term.term"], ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("thing_id"), - ) - op.create_table( - "collaborative_network_well", - sa.Column("actively_monitored", sa.Boolean(), nullable=False), - sa.Column("well_id", sa.Integer(), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["well_id"], ["well_thing.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("id"), ) op.create_table( @@ -558,48 +615,34 @@ def upgrade() -> None: sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("series_id"), ) - op.create_table( - "well_screen", - sa.Column("well_id", sa.Integer(), nullable=False), - sa.Column("screen_depth_top", sa.Float(), nullable=False), - sa.Column("screen_depth_bottom", sa.Float(), nullable=False), - sa.Column("screen_type", sa.String(length=100), nullable=True), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["screen_type"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint(["well_id"], ["well_thing.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) # ### end Alembic commands ### def downgrade() -> None: """Downgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("well_screen") op.drop_table("groundwater_level_series") op.drop_table("geothermal_series") op.drop_table("geochemical_series") - op.drop_table("collaborative_network_well") - op.drop_table("well_thing") + op.drop_table("well_screen") op.drop_table("thing_id_link") op.drop_table("thing_contact_association") - op.drop_table("spring_thing") op.drop_table("series") op.drop_table("pub_author_publication_association") op.drop_table("pub_author_contact_association") op.drop_index("ix_phone_search_vector", table_name="phone", postgresql_using="gin") op.drop_table("phone") op.drop_table("location_thing_association") + op.drop_table("group_thing_association") op.drop_index("ix_email_search_vector", table_name="email", postgresql_using="gin") op.drop_table("email") + op.drop_table("collaborative_network_well") op.drop_table("asset_thing_association") + op.drop_index( + "ix_address_search_vector", table_name="address", postgresql_using="gin" + ) op.drop_table("address") + op.drop_index("ix_thing_search_vector", table_name="thing", postgresql_using="gin") op.drop_table("thing") op.drop_index( "ix_publication_search_vector", table_name="publication", postgresql_using="gin" @@ -623,6 +666,7 @@ def downgrade() -> None: op.drop_table("pub_author") op.drop_table("lexicon_term") op.drop_table("lexicon_category") + op.drop_table("group") op.drop_index("ix_asset_search_vector", table_name="asset", postgresql_using="gin") op.drop_table("asset") # ### end Alembic commands ### diff --git a/alembic/versions/842535e6433f_contact_change.py b/alembic/versions/842535e6433f_contact_change.py deleted file mode 100644 index 29be0d363..000000000 --- a/alembic/versions/842535e6433f_contact_change.py +++ /dev/null @@ -1,57 +0,0 @@ -"""contact change - -Revision ID: 842535e6433f -Revises: 8447b9ebaf29 -Create Date: 2025-07-23 08:28:14.874799 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = "842535e6433f" -down_revision: Union[str, Sequence[str], None] = "8447b9ebaf29" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column( - "address", "country", existing_type=sa.VARCHAR(length=100), nullable=False - ) - op.alter_column( - "address", "address_type", existing_type=sa.VARCHAR(length=100), nullable=False - ) - op.create_foreign_key(None, "address", "lexicon_term", ["country"], ["term"]) - op.alter_column( - "email", "email_type", existing_type=sa.VARCHAR(length=100), nullable=False - ) - op.alter_column( - "phone", "phone_type", existing_type=sa.VARCHAR(length=100), nullable=False - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column( - "phone", "phone_type", existing_type=sa.VARCHAR(length=100), nullable=True - ) - op.alter_column( - "email", "email_type", existing_type=sa.VARCHAR(length=100), nullable=True - ) - op.drop_constraint(None, "address", type_="foreignkey") - op.alter_column( - "address", "address_type", existing_type=sa.VARCHAR(length=100), nullable=True - ) - op.alter_column( - "address", "country", existing_type=sa.VARCHAR(length=100), nullable=True - ) - # ### end Alembic commands ### diff --git a/alembic/versions/8447b9ebaf29_changes_to_pre_production.py b/alembic/versions/8447b9ebaf29_changes_to_pre_production.py deleted file mode 100644 index 956add039..000000000 --- a/alembic/versions/8447b9ebaf29_changes_to_pre_production.py +++ /dev/null @@ -1,87 +0,0 @@ -"""changes to pre-production - -Revision ID: 8447b9ebaf29 -Revises: 5901f059248a -Create Date: 2025-07-22 12:17:19.076090 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import sqlalchemy_utils - - -# revision identifiers, used by Alembic. -revision: str = "8447b9ebaf29" -down_revision: Union[str, Sequence[str], None] = "5901f059248a" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column( - "address", - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - ) - op.create_index( - "ix_address_search_vector", - "address", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.add_column( - "thing", - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - ) - op.create_index( - "ix_thing_search_vector", - "thing", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.add_column( - "well_thing", - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - ) - op.create_index( - "ix_well_thing_search_vector", - "well_thing", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index( - "ix_well_thing_search_vector", table_name="well_thing", postgresql_using="gin" - ) - op.drop_column("well_thing", "search_vector") - op.drop_index("ix_thing_search_vector", table_name="thing", postgresql_using="gin") - op.drop_column("thing", "search_vector") - op.drop_index( - "ix_address_search_vector", table_name="address", postgresql_using="gin" - ) - op.drop_column("address", "search_vector") - # ### end Alembic commands ### diff --git a/db/__init__.py b/db/__init__.py index 7c87ebb26..1a0cc2d49 100644 --- a/db/__init__.py +++ b/db/__init__.py @@ -14,10 +14,13 @@ # limitations under the License. # =============================================================================== +# import all models from db package so that Alembic can discover them from db.asset import * -from db.base import Base from db.collabnet import * +from db.contact import * from db.geochronology import * +from db.geothermal import * +from db.group import * from db.lexicon import * from db.location import * from db.observation import * @@ -26,8 +29,7 @@ from db.sensor import * from db.series import * from db.thing import * -from db.contact import * - +from db.base import Base from sqlalchemy import ( func, diff --git a/tests/__init__.py b/tests/__init__.py index 6af3b115d..d8e89e2bf 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -17,16 +17,15 @@ from fastapi.testclient import TestClient from sqlalchemy.orm import configure_mappers -from core.app import init_lexicon, init_hypertables +from core.app import init_lexicon from main import app -from db.base import Base -from db import * -from db.engine import engine, session_ctx +from db import Thing +from db.engine import session_ctx configure_mappers() -Base.metadata.drop_all(engine) -Base.metadata.create_all(engine) +# Base.metadata.drop_all(engine) +# Base.metadata.create_all(engine) # init_hypertables() init_lexicon() From 6bece1b859e6cff7dcc8e38d48fa539eb7949fec Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 24 Jul 2025 15:16:45 -0600 Subject: [PATCH 02/42] fix: drop idx_location_point index This commit drops the `idx_location_point` index from the `location` table. The index was causing issues with the database schema because it is automatically created by SQLAlchemy when the `location` table is defined. --- alembic/versions/42f7242f188d_initial_migration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/alembic/versions/42f7242f188d_initial_migration.py b/alembic/versions/42f7242f188d_initial_migration.py index 764f919e6..b28833262 100644 --- a/alembic/versions/42f7242f188d_initial_migration.py +++ b/alembic/versions/42f7242f188d_initial_migration.py @@ -238,6 +238,7 @@ def upgrade() -> None: ), sa.PrimaryKeyConstraint("id"), ) + op.execute("DROP INDEX IF EXISTS idx_location_point") op.create_index( "idx_location_point", "location", From a40605412c4b6efd8e5b077581f6d103b905a3c5 Mon Sep 17 00:00:00 2001 From: jacob-a-brown Date: Thu, 24 Jul 2025 21:18:09 +0000 Subject: [PATCH 03/42] Formatting changes --- alembic/versions/42f7242f188d_initial_migration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alembic/versions/42f7242f188d_initial_migration.py b/alembic/versions/42f7242f188d_initial_migration.py index b28833262..8e3ab0512 100644 --- a/alembic/versions/42f7242f188d_initial_migration.py +++ b/alembic/versions/42f7242f188d_initial_migration.py @@ -1,7 +1,7 @@ """initial migration Revision ID: 42f7242f188d -Revises: +Revises: Create Date: 2025-07-24 15:12:10.195332 """ From c348dc64acc9334019c316bfe44fa2c21ee4ca94 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 24 Jul 2025 16:07:13 -0600 Subject: [PATCH 04/42] feat: Refactor imports and update Alembic migration for all models --- ...n.py => b9de5401f56e_initial_migration.py} | 79 ++++++++++++++++++- db/__init__.py | 14 +++- tests/__init__.py | 17 +++- 3 files changed, 101 insertions(+), 9 deletions(-) rename alembic/versions/{42f7242f188d_initial_migration.py => b9de5401f56e_initial_migration.py} (88%) diff --git a/alembic/versions/42f7242f188d_initial_migration.py b/alembic/versions/b9de5401f56e_initial_migration.py similarity index 88% rename from alembic/versions/42f7242f188d_initial_migration.py rename to alembic/versions/b9de5401f56e_initial_migration.py index b28833262..03ef66454 100644 --- a/alembic/versions/42f7242f188d_initial_migration.py +++ b/alembic/versions/b9de5401f56e_initial_migration.py @@ -1,8 +1,8 @@ """initial migration -Revision ID: 42f7242f188d +Revision ID: b9de5401f56e Revises: -Create Date: 2025-07-24 15:12:10.195332 +Create Date: 2025-07-24 15:46:30.425985 """ @@ -15,7 +15,7 @@ # revision identifiers, used by Alembic. -revision: str = "42f7242f188d" +revision: str = "b9de5401f56e" down_revision: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -238,7 +238,7 @@ def upgrade() -> None: ), sa.PrimaryKeyConstraint("id"), ) - op.execute("DROP INDEX IF EXISTS idx_location_point") + op.execute("DROP INDEX IF EXISTS idx_location_point;") op.create_index( "idx_location_point", "location", @@ -616,12 +616,83 @@ def upgrade() -> None: sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("series_id"), ) + op.create_table( + "observation", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("series_id", sa.Integer(), nullable=False), + sa.Column("observation_timestamp", sa.TIMESTAMP(), nullable=False), + sa.Column("release_status", sa.String(length=100), nullable=True), + sa.ForeignKeyConstraint( + ["release_status"], + ["lexicon_term.term"], + ), + sa.ForeignKeyConstraint(["series_id"], ["series.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id", "observation_timestamp"), + ) + op.create_table( + "geochemical_observation", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column( + "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + sa.Column("observation_id", sa.Integer(), nullable=False), + sa.Column("observation_timestamp", sa.TIMESTAMP(), nullable=False), + sa.ForeignKeyConstraint( + ["observation_id", "observation_timestamp"], + ["observation.id", "observation.observation_timestamp"], + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "geothermal_observation", + sa.Column("depth", sa.Float(), nullable=False), + sa.Column("temperature", sa.Float(), nullable=False), + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column( + "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + sa.Column("observation_id", sa.Integer(), nullable=False), + sa.Column("observation_timestamp", sa.TIMESTAMP(), nullable=False), + sa.ForeignKeyConstraint( + ["observation_id", "observation_timestamp"], + ["observation.id", "observation.observation_timestamp"], + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "groundwater_level_observation", + sa.Column("depth_to_water", sa.Float(), nullable=False), + sa.Column("measuring_point_height", sa.Float(), nullable=False), + sa.Column("level_status", sa.String(length=100), nullable=True), + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column( + "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + sa.Column("observation_id", sa.Integer(), nullable=False), + sa.Column("observation_timestamp", sa.TIMESTAMP(), nullable=False), + sa.ForeignKeyConstraint( + ["level_status"], + ["lexicon_term.term"], + ), + sa.ForeignKeyConstraint( + ["observation_id", "observation_timestamp"], + ["observation.id", "observation.observation_timestamp"], + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("id"), + ) # ### end Alembic commands ### def downgrade() -> None: """Downgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("groundwater_level_observation") + op.drop_table("geothermal_observation") + op.drop_table("geochemical_observation") + op.drop_table("observation") op.drop_table("groundwater_level_series") op.drop_table("geothermal_series") op.drop_table("geochemical_series") diff --git a/db/__init__.py b/db/__init__.py index 1a0cc2d49..aee3938c1 100644 --- a/db/__init__.py +++ b/db/__init__.py @@ -16,6 +16,7 @@ # import all models from db package so that Alembic can discover them from db.asset import * +from db.base import * from db.collabnet import * from db.contact import * from db.geochronology import * @@ -23,11 +24,18 @@ from db.group import * from db.lexicon import * from db.location import * -from db.observation import * +from db.observation.geochemical import * +from db.observation.geothermal import * +from db.observation.groundwaterlevel import * +from db.observation.observation import * from db.publication import * from db.sample import * -from db.sensor import * -from db.series import * +from db.sensor.groundwaterlevel import * +from db.sensor.sensor import * +from db.series.geochemical import * +from db.series.geothermal import * +from db.series.groundwaterlevel import * +from db.series.series import * from db.thing import * from db.base import Base diff --git a/tests/__init__.py b/tests/__init__.py index d8e89e2bf..8fdf4fa56 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # =============================================================================== +from alembic.config import Config +from alembic import command import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import configure_mappers @@ -24,8 +26,19 @@ configure_mappers() -# Base.metadata.drop_all(engine) -# Base.metadata.create_all(engine) + +def run_alembic_upgrade(): + alembic_cfg = Config("alembic.ini") + command.upgrade(alembic_cfg, "head") + + +def run_alembic_downgrade(): + alembic_cfg = Config("alembic.ini") + command.downgrade(alembic_cfg, "base") + + +run_alembic_downgrade() +run_alembic_upgrade() # init_hypertables() init_lexicon() From 56e5adecd28291f26328025ef08fdce04d3428a4 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 24 Jul 2025 17:05:10 -0600 Subject: [PATCH 05/42] fix: version work in docker database --- alembic/env.py | 2 +- ...itial_migration.py => 0ca8b417fea8_initial_migration.py} | 6 +++--- db/__init__.py | 6 ++++-- 3 files changed, 8 insertions(+), 6 deletions(-) rename alembic/versions/{b9de5401f56e_initial_migration.py => 0ca8b417fea8_initial_migration.py} (99%) diff --git a/alembic/env.py b/alembic/env.py index 89ba72be3..1bfa152fe 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -46,7 +46,7 @@ def include_object(object, name, type_, reflected, compare_to): # only include tables in sql alchemy model, not auto-generated tables from PostGIS or TIGER - if type_ == "table": + if type_ == "table" or name.endswith("_version") or name == "transaction": return name in model_tables return True diff --git a/alembic/versions/b9de5401f56e_initial_migration.py b/alembic/versions/0ca8b417fea8_initial_migration.py similarity index 99% rename from alembic/versions/b9de5401f56e_initial_migration.py rename to alembic/versions/0ca8b417fea8_initial_migration.py index 03ef66454..1f307981d 100644 --- a/alembic/versions/b9de5401f56e_initial_migration.py +++ b/alembic/versions/0ca8b417fea8_initial_migration.py @@ -1,8 +1,8 @@ """initial migration -Revision ID: b9de5401f56e +Revision ID: 0ca8b417fea8 Revises: -Create Date: 2025-07-24 15:46:30.425985 +Create Date: 2025-07-24 17:02:46.216216 """ @@ -15,7 +15,7 @@ # revision identifiers, used by Alembic. -revision: str = "b9de5401f56e" +revision: str = "0ca8b417fea8" down_revision: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None diff --git a/db/__init__.py b/db/__init__.py index aee3938c1..3de2a2693 100644 --- a/db/__init__.py +++ b/db/__init__.py @@ -15,8 +15,11 @@ # =============================================================================== # import all models from db package so that Alembic can discover them -from db.asset import * + from db.base import * +from db.base import Base + +from db.asset import * from db.collabnet import * from db.contact import * from db.geochronology import * @@ -37,7 +40,6 @@ from db.series.groundwaterlevel import * from db.series.series import * from db.thing import * -from db.base import Base from sqlalchemy import ( func, From 6b3e83bf453b6e1a81336eb5309d49d529cf809b Mon Sep 17 00:00:00 2001 From: jacob-a-brown Date: Thu, 24 Jul 2025 23:06:05 +0000 Subject: [PATCH 06/42] Formatting changes --- alembic/versions/0ca8b417fea8_initial_migration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alembic/versions/0ca8b417fea8_initial_migration.py b/alembic/versions/0ca8b417fea8_initial_migration.py index 1f307981d..7f8d375c7 100644 --- a/alembic/versions/0ca8b417fea8_initial_migration.py +++ b/alembic/versions/0ca8b417fea8_initial_migration.py @@ -1,7 +1,7 @@ """initial migration Revision ID: 0ca8b417fea8 -Revises: +Revises: Create Date: 2025-07-24 17:02:46.216216 """ From 70c211cd080749be4c631b6ee546369e3f15bb2d Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 28 Jul 2025 09:13:00 -0600 Subject: [PATCH 07/42] fix: create migration script with sa continuum tables configure_mappers() to ensure all models are registered and ready for migration. --- ...n.py => dcd4ba0a63c6_initial_migration.py} | 145 +++++++++++++++++- db/__init__.py | 3 + 2 files changed, 144 insertions(+), 4 deletions(-) rename alembic/versions/{0ca8b417fea8_initial_migration.py => dcd4ba0a63c6_initial_migration.py} (85%) diff --git a/alembic/versions/0ca8b417fea8_initial_migration.py b/alembic/versions/dcd4ba0a63c6_initial_migration.py similarity index 85% rename from alembic/versions/0ca8b417fea8_initial_migration.py rename to alembic/versions/dcd4ba0a63c6_initial_migration.py index 1f307981d..f2912b5d8 100644 --- a/alembic/versions/0ca8b417fea8_initial_migration.py +++ b/alembic/versions/dcd4ba0a63c6_initial_migration.py @@ -1,8 +1,8 @@ """initial migration -Revision ID: 0ca8b417fea8 +Revision ID: dcd4ba0a63c6 Revises: -Create Date: 2025-07-24 17:02:46.216216 +Create Date: 2025-07-28 09:10:27.082507 """ @@ -15,7 +15,7 @@ # revision identifiers, used by Alembic. -revision: str = "0ca8b417fea8" +revision: str = "dcd4ba0a63c6" down_revision: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -84,6 +84,100 @@ def upgrade() -> None: sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("term"), ) + op.create_table( + "location_version", + sa.Column("name", sa.String(length=255), autoincrement=False, nullable=True), + sa.Column("notes", sa.Text(), autoincrement=False, nullable=True), + sa.Column( + "point", + geoalchemy2.types.Geometry( + geometry_type="POINT", + srid=4326, + from_text="ST_GeomFromEWKT", + name="geometry", + nullable=False, + ), + autoincrement=False, + nullable=False, + ), + sa.Column("id", sa.Integer(), autoincrement=False, nullable=False), + sa.Column( + "created_at", + sa.DateTime(), + server_default=sa.text("now()"), + autoincrement=False, + nullable=True, + ), + sa.Column( + "release_status", sa.String(length=100), autoincrement=False, nullable=True + ), + sa.Column( + "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False + ), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint("id", "transaction_id"), + ) + op.create_index( + "idx_location_version_point", + "location_version", + ["point"], + unique=False, + postgresql_using="gist", + ) + op.create_index( + op.f("ix_location_version_end_transaction_id"), + "location_version", + ["end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_location_version_operation_type"), + "location_version", + ["operation_type"], + unique=False, + ) + op.create_index( + op.f("ix_location_version_transaction_id"), + "location_version", + ["transaction_id"], + unique=False, + ) + op.create_table( + "observation_version", + sa.Column("id", sa.Integer(), autoincrement=False, nullable=False), + sa.Column("series_id", sa.Integer(), autoincrement=False, nullable=True), + sa.Column( + "observation_timestamp", sa.TIMESTAMP(), autoincrement=False, nullable=False + ), + sa.Column( + "release_status", sa.String(length=100), autoincrement=False, nullable=True + ), + sa.Column( + "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False + ), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint("id", "observation_timestamp", "transaction_id"), + ) + op.create_index( + op.f("ix_observation_version_end_transaction_id"), + "observation_version", + ["end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_observation_version_operation_type"), + "observation_version", + ["operation_type"], + unique=False, + ) + op.create_index( + op.f("ix_observation_version_transaction_id"), + "observation_version", + ["transaction_id"], + unique=False, + ) op.create_table( "pub_author", sa.Column("name", sa.String(), nullable=False), @@ -215,6 +309,7 @@ def upgrade() -> None: ) op.create_table( "location", + sa.Column("name", sa.String(length=255), nullable=True), sa.Column("notes", sa.Text(), nullable=True), sa.Column( "point", @@ -238,7 +333,6 @@ def upgrade() -> None: ), sa.PrimaryKeyConstraint("id"), ) - op.execute("DROP INDEX IF EXISTS idx_location_point;") op.create_index( "idx_location_point", "location", @@ -326,6 +420,21 @@ def upgrade() -> None: unique=False, postgresql_using="gin", ) + op.create_table( + "transaction", + sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column("remote_addr", sa.String(length=50), nullable=True), + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("issued_at", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["user_id"], + ["user.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_transaction_user_id"), "transaction", ["user_id"], unique=False + ) op.create_table( "address", sa.Column("contact_id", sa.Integer(), nullable=False), @@ -714,6 +823,8 @@ def downgrade() -> None: "ix_address_search_vector", table_name="address", postgresql_using="gin" ) op.drop_table("address") + op.drop_index(op.f("ix_transaction_user_id"), table_name="transaction") + op.drop_table("transaction") op.drop_index("ix_thing_search_vector", table_name="thing", postgresql_using="gin") op.drop_table("thing") op.drop_index( @@ -736,6 +847,32 @@ def downgrade() -> None: "ix_pub_author_search_vector", table_name="pub_author", postgresql_using="gin" ) op.drop_table("pub_author") + op.drop_index( + op.f("ix_observation_version_transaction_id"), table_name="observation_version" + ) + op.drop_index( + op.f("ix_observation_version_operation_type"), table_name="observation_version" + ) + op.drop_index( + op.f("ix_observation_version_end_transaction_id"), + table_name="observation_version", + ) + op.drop_table("observation_version") + op.drop_index( + op.f("ix_location_version_transaction_id"), table_name="location_version" + ) + op.drop_index( + op.f("ix_location_version_operation_type"), table_name="location_version" + ) + op.drop_index( + op.f("ix_location_version_end_transaction_id"), table_name="location_version" + ) + op.drop_index( + "idx_location_version_point", + table_name="location_version", + postgresql_using="gist", + ) + op.drop_table("location_version") op.drop_table("lexicon_term") op.drop_table("lexicon_category") op.drop_table("group") diff --git a/db/__init__.py b/db/__init__.py index 3de2a2693..59e5d5588 100644 --- a/db/__init__.py +++ b/db/__init__.py @@ -52,6 +52,9 @@ inspect_search_vectors, search_manager, ) +from sqlalchemy.orm import configure_mappers + +configure_mappers() def adder(session, table, model, **kwargs): From f4c1b537f72c9ef708b49933e6c3ccc8a7e4429b Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 28 Jul 2025 11:22:29 -0600 Subject: [PATCH 08/42] fix: drop indexes before creation --- .../versions/dcd4ba0a63c6_initial_migration.py | 2 ++ things.dbf | Bin 0 -> 201 bytes things.shp | Bin 0 -> 156 bytes things.shx | Bin 0 -> 116 bytes things.zip | Bin 0 -> 783 bytes 5 files changed, 2 insertions(+) create mode 100644 things.dbf create mode 100644 things.shp create mode 100644 things.shx create mode 100644 things.zip diff --git a/alembic/versions/dcd4ba0a63c6_initial_migration.py b/alembic/versions/dcd4ba0a63c6_initial_migration.py index f2912b5d8..00c736784 100644 --- a/alembic/versions/dcd4ba0a63c6_initial_migration.py +++ b/alembic/versions/dcd4ba0a63c6_initial_migration.py @@ -118,6 +118,7 @@ def upgrade() -> None: sa.Column("operation_type", sa.SmallInteger(), nullable=False), sa.PrimaryKeyConstraint("id", "transaction_id"), ) + op.drop_index("idx_location_point", table_name="location_version", if_exists=True) op.create_index( "idx_location_version_point", "location_version", @@ -333,6 +334,7 @@ def upgrade() -> None: ), sa.PrimaryKeyConstraint("id"), ) + op.drop_index("idx_location_point", table_name="location", if_exists=True) op.create_index( "idx_location_point", "location", diff --git a/things.dbf b/things.dbf new file mode 100644 index 0000000000000000000000000000000000000000..d7ebc7e6f2da2c6a1f2c8d887e84054e2f45d583 GIT binary patch literal 201 zcmZRsWtU-MU|>jOFaeU7ATtFn<^y6e!nqJeUSe)4RLB`b8$mfRidR7)BqK8~UBOU+ L6adjml5SL_R=A{?w6=xJMFsOqBumQgp%nS^SKpA5& zP;r3K5XB}As5FF!g9I7Fg~Kc+m>fhe!rTB9Ke+8-bSnWW0bvwBRN(Q06&3wp1hv=! zM5DTG^NtsvP6CBN7|CrZNomZr>@pw^CNh`+NlcKL0vGcEu|Q#j4Du3lQ=xok5Djt! zGT>EE2mwW=f}sK_0HVi;1T6vHj7%cTxRVsjQA-*@EJOlB*M}aB2z^J8^;MwjLl0tv dz64;HLG1$ucz`!68%T~B2v-2 Date: Mon, 28 Jul 2025 17:23:44 +0000 Subject: [PATCH 09/42] Formatting changes --- alembic/versions/dcd4ba0a63c6_initial_migration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alembic/versions/dcd4ba0a63c6_initial_migration.py b/alembic/versions/dcd4ba0a63c6_initial_migration.py index 00c736784..8406210a1 100644 --- a/alembic/versions/dcd4ba0a63c6_initial_migration.py +++ b/alembic/versions/dcd4ba0a63c6_initial_migration.py @@ -1,7 +1,7 @@ """initial migration Revision ID: dcd4ba0a63c6 -Revises: +Revises: Create Date: 2025-07-28 09:10:27.082507 """ From d471487b3588617255507213b4de153b57590ab5 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 28 Jul 2025 11:25:51 -0600 Subject: [PATCH 10/42] fix: drop index before creating if exists --- alembic/versions/dcd4ba0a63c6_initial_migration.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/alembic/versions/dcd4ba0a63c6_initial_migration.py b/alembic/versions/dcd4ba0a63c6_initial_migration.py index 00c736784..dd942386a 100644 --- a/alembic/versions/dcd4ba0a63c6_initial_migration.py +++ b/alembic/versions/dcd4ba0a63c6_initial_migration.py @@ -118,7 +118,9 @@ def upgrade() -> None: sa.Column("operation_type", sa.SmallInteger(), nullable=False), sa.PrimaryKeyConstraint("id", "transaction_id"), ) - op.drop_index("idx_location_point", table_name="location_version", if_exists=True) + op.drop_index( + "idx_location_version_point", table_name="location_version", if_exists=True + ) op.create_index( "idx_location_version_point", "location_version", From 5c111dfabef0522b54ac704f5ad5c6fc12316af3 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 28 Jul 2025 11:32:57 -0600 Subject: [PATCH 11/42] fix: remove material created by tests --- things.dbf | Bin 201 -> 0 bytes things.shp | Bin 156 -> 0 bytes things.shx | Bin 116 -> 0 bytes things.zip | Bin 783 -> 0 bytes 4 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 things.dbf delete mode 100644 things.shp delete mode 100644 things.shx delete mode 100644 things.zip diff --git a/things.dbf b/things.dbf deleted file mode 100644 index d7ebc7e6f2da2c6a1f2c8d887e84054e2f45d583..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 201 zcmZRsWtU-MU|>jOFaeU7ATtFn<^y6e!nqJeUSe)4RLB`b8$mfRidR7)BqK8~UBOU+ L6adjml5SL_R=A{?w6=xJMFsOqBumQgp%nS^SKpA5& zP;r3K5XB}As5FF!g9I7Fg~Kc+m>fhe!rTB9Ke+8-bSnWW0bvwBRN(Q06&3wp1hv=! zM5DTG^NtsvP6CBN7|CrZNomZr>@pw^CNh`+NlcKL0vGcEu|Q#j4Du3lQ=xok5Djt! zGT>EE2mwW=f}sK_0HVi;1T6vHj7%cTxRVsjQA-*@EJOlB*M}aB2z^J8^;MwjLl0tv dz64;HLG1$ucz`!68%T~B2v-2 Date: Tue, 29 Jul 2025 15:24:30 -0600 Subject: [PATCH 12/42] docs: document work needed to be done by alembic migrations --- .../1f2adf9e4454_initial_migration.py | 885 ------------------ .../66ac1af4ba69_initial_migration.py | 22 +- .../dcd4ba0a63c6_initial_migration.py | 885 ------------------ 3 files changed, 18 insertions(+), 1774 deletions(-) delete mode 100644 alembic/versions/1f2adf9e4454_initial_migration.py delete mode 100644 alembic/versions/dcd4ba0a63c6_initial_migration.py diff --git a/alembic/versions/1f2adf9e4454_initial_migration.py b/alembic/versions/1f2adf9e4454_initial_migration.py deleted file mode 100644 index 66767063a..000000000 --- a/alembic/versions/1f2adf9e4454_initial_migration.py +++ /dev/null @@ -1,885 +0,0 @@ -"""initial migration - -Revision ID: 1f2adf9e4454 -Revises: -Create Date: 2025-07-28 11:52:48.695008 - -""" - -from typing import Sequence, Union - -from alembic import op -import geoalchemy2 -import sqlalchemy as sa -import sqlalchemy_utils - - -# revision identifiers, used by Alembic. -revision: str = "1f2adf9e4454" -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "asset", - sa.Column("filename", sa.String(), nullable=True), - sa.Column("storage_service", sa.String(), nullable=True), - sa.Column("storage_path", sa.String(), nullable=True), - sa.Column("mime_type", sa.String(), nullable=True), - sa.Column("size", sa.Integer(), nullable=True), - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "ix_asset_search_vector", - "asset", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.create_table( - "group", - sa.Column("name", sa.String(length=100), nullable=False), - sa.Column("description", sa.String(length=255), nullable=True), - sa.Column("parent_group_id", sa.Integer(), nullable=True), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["parent_group_id"], ["group.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("name"), - ) - op.create_table( - "lexicon_category", - sa.Column("name", sa.String(length=100), nullable=False), - sa.Column("description", sa.String(length=255), nullable=True), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("name"), - ) - op.create_table( - "lexicon_term", - sa.Column("term", sa.String(length=100), nullable=False), - sa.Column("definition", sa.String(length=255), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("term"), - ) - op.create_table( - "location_version", - sa.Column("name", sa.String(length=255), autoincrement=False, nullable=True), - sa.Column("notes", sa.Text(), autoincrement=False, nullable=True), - sa.Column( - "point", - geoalchemy2.types.Geometry( - geometry_type="POINT", - srid=4326, - from_text="ST_GeomFromEWKT", - name="geometry", - nullable=False, - ), - autoincrement=False, - nullable=False, - ), - sa.Column("id", sa.Integer(), autoincrement=False, nullable=False), - sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("now()"), - autoincrement=False, - nullable=True, - ), - sa.Column( - "release_status", sa.String(length=100), autoincrement=False, nullable=True - ), - sa.Column( - "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False - ), - sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), - sa.Column("operation_type", sa.SmallInteger(), nullable=False), - sa.PrimaryKeyConstraint("id", "transaction_id"), - ) - op.drop_index( - "idx_location_version_point", table_name="location_version", if_exists=True - ) - op.create_index( - "idx_location_version_point", - "location_version", - ["point"], - unique=False, - postgresql_using="gist", - ) - op.create_index( - op.f("ix_location_version_end_transaction_id"), - "location_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_location_version_operation_type"), - "location_version", - ["operation_type"], - unique=False, - ) - op.create_index( - op.f("ix_location_version_transaction_id"), - "location_version", - ["transaction_id"], - unique=False, - ) - op.create_table( - "observation_version", - sa.Column("id", sa.Integer(), autoincrement=False, nullable=False), - sa.Column("series_id", sa.Integer(), autoincrement=False, nullable=True), - sa.Column( - "observation_timestamp", sa.TIMESTAMP(), autoincrement=False, nullable=False - ), - sa.Column( - "release_status", sa.String(length=100), autoincrement=False, nullable=True - ), - sa.Column( - "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False - ), - sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), - sa.Column("operation_type", sa.SmallInteger(), nullable=False), - sa.PrimaryKeyConstraint("id", "observation_timestamp", "transaction_id"), - ) - op.create_index( - op.f("ix_observation_version_end_transaction_id"), - "observation_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_observation_version_operation_type"), - "observation_version", - ["operation_type"], - unique=False, - ) - op.create_index( - op.f("ix_observation_version_transaction_id"), - "observation_version", - ["transaction_id"], - unique=False, - ) - op.create_table( - "pub_author", - sa.Column("name", sa.String(), nullable=False), - sa.Column("affiliation", sa.String(), nullable=True), - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "ix_pub_author_search_vector", - "pub_author", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.create_table( - "sensor", - sa.Column("name", sa.String(length=255), nullable=False), - sa.Column("model", sa.String(length=50), nullable=True), - sa.Column("serial_no", sa.String(length=50), nullable=True), - sa.Column("date_installed", sa.DateTime(), nullable=True), - sa.Column("date_removed", sa.DateTime(), nullable=True), - sa.Column("recording_interval", sa.Integer(), nullable=True), - sa.Column("notes", sa.String(length=50), nullable=True), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "user", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("username", sa.String(length=255), nullable=False), - sa.Column("password", sa.String(length=255), nullable=False), - sa.Column("is_superuser", sa.Boolean(), nullable=False), - sa.Column("is_active", sa.Boolean(), nullable=False), - sa.Column("avatar_url", sa.Text(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "contact", - sa.Column("name", sa.String(length=100), nullable=False), - sa.Column("role", sa.String(length=100), nullable=False), - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["role"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "ix_contact_search_vector", - "contact", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.create_table( - "geochronology_age", - sa.Column("location_id", sa.Integer(), nullable=False), - sa.Column("age", sa.Float(), nullable=False), - sa.Column("age_error", sa.Float(), nullable=True), - sa.Column("method", sa.String(length=100), nullable=True), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["method"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "groundwater_level_sensor", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("sensor_id", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(["sensor_id"], ["sensor.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("sensor_id"), - ) - op.create_table( - "lexicon_term_category_association", - sa.Column("lexicon_term", sa.String(length=100), nullable=False), - sa.Column("category_name", sa.String(length=255), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["category_name"], ["lexicon_category.name"], ondelete="CASCADE" - ), - sa.ForeignKeyConstraint( - ["lexicon_term"], ["lexicon_term.term"], ondelete="CASCADE" - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "lexicon_triple", - sa.Column("subject", sa.String(length=100), nullable=False), - sa.Column("predicate", sa.String(length=100), nullable=False), - sa.Column("object_", sa.String(length=100), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["object_"], ["lexicon_term.term"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["subject"], ["lexicon_term.term"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "location", - sa.Column("name", sa.String(length=255), nullable=True), - sa.Column("notes", sa.Text(), nullable=True), - sa.Column( - "point", - geoalchemy2.types.Geometry( - geometry_type="POINT", - srid=4326, - from_text="ST_GeomFromEWKT", - name="geometry", - nullable=False, - ), - nullable=False, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("release_status", sa.String(length=100), nullable=True), - sa.ForeignKeyConstraint( - ["release_status"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.drop_index("idx_location_point", table_name="location", if_exists=True) - op.create_index( - "idx_location_point", - "location", - ["point"], - unique=False, - postgresql_using="gist", - ) - op.create_table( - "publication", - sa.Column("title", sa.Text(), nullable=False), - sa.Column("abstract", sa.Text(), nullable=True), - sa.Column("doi", sa.String(), nullable=True), - sa.Column("year", sa.Integer(), nullable=True), - sa.Column("publisher", sa.String(), nullable=True), - sa.Column("url", sa.String(), nullable=True), - sa.Column("publication_type", sa.String(length=100), nullable=False), - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["publication_type"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("doi"), - ) - op.create_index( - "ix_publication_search_vector", - "publication", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.create_table( - "thing", - sa.Column("name", sa.String(length=255), nullable=False), - sa.Column("description", sa.String(length=500), nullable=True), - sa.Column("thing_type", sa.String(length=100), nullable=True), - sa.Column("spring_type", sa.String(length=100), nullable=True), - sa.Column("well_depth", sa.Float(), nullable=True), - sa.Column("hole_depth", sa.Float(), nullable=True), - sa.Column("well_type", sa.String(length=100), nullable=True), - sa.Column("well_casing_diameter", sa.Float(), nullable=True), - sa.Column("well_casing_depth", sa.Float(), nullable=True), - sa.Column("well_casing_description", sa.String(length=50), nullable=True), - sa.Column("well_construction_notes", sa.String(length=250), nullable=True), - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("release_status", sa.String(length=100), nullable=True), - sa.ForeignKeyConstraint( - ["release_status"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint( - ["spring_type"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint( - ["thing_type"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint( - ["well_type"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "ix_thing_search_vector", - "thing", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.create_table( - "transaction", - sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column("remote_addr", sa.String(length=50), nullable=True), - sa.Column("user_id", sa.Integer(), nullable=True), - sa.Column("issued_at", sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint( - ["user_id"], - ["user.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_transaction_user_id"), "transaction", ["user_id"], unique=False - ) - op.create_table( - "address", - sa.Column("contact_id", sa.Integer(), nullable=False), - sa.Column("address_line_1", sa.String(length=255), nullable=False), - sa.Column("address_line_2", sa.String(length=255), nullable=True), - sa.Column("city", sa.String(length=100), nullable=False), - sa.Column("state", sa.String(length=50), nullable=False), - sa.Column("postal_code", sa.String(length=20), nullable=False), - sa.Column("country", sa.String(length=100), nullable=False), - sa.Column("address_type", sa.String(length=100), nullable=False), - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["address_type"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint(["contact_id"], ["contact.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint( - ["country"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "ix_address_search_vector", - "address", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.create_table( - "asset_thing_association", - sa.Column("asset_id", sa.Integer(), nullable=False), - sa.Column("thing_id", sa.Integer(), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["asset_id"], ["asset.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "collaborative_network_well", - sa.Column("actively_monitored", sa.Boolean(), nullable=False), - sa.Column("thing_id", sa.Integer(), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "email", - sa.Column("contact_id", sa.Integer(), nullable=False), - sa.Column("email", sa.String(length=100), nullable=False), - sa.Column("email_type", sa.String(length=100), nullable=False), - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["contact_id"], ["contact.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint( - ["email_type"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "ix_email_search_vector", - "email", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.create_table( - "group_thing_association", - sa.Column("group_id", sa.Integer(), nullable=False), - sa.Column("thing_id", sa.Integer(), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["group_id"], ["group.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "location_thing_association", - sa.Column("location_id", sa.Integer(), nullable=False), - sa.Column("thing_id", sa.Integer(), nullable=False), - sa.Column( - "effective_start", - sa.DateTime(), - server_default=sa.text("now()"), - nullable=False, - ), - sa.Column("effective_end", sa.DateTime(), nullable=True), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["location_id"], ["location.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("location_id", "thing_id", "id"), - ) - op.create_table( - "phone", - sa.Column("contact_id", sa.Integer(), nullable=False), - sa.Column("phone_number", sa.String(length=20), nullable=False), - sa.Column("phone_type", sa.String(length=100), nullable=False), - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["contact_id"], ["contact.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint( - ["phone_type"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "ix_phone_search_vector", - "phone", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.create_table( - "pub_author_contact_association", - sa.Column("author_id", sa.Integer(), nullable=False), - sa.Column("contact_id", sa.Integer(), nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["author_id"], ["pub_author.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["contact_id"], ["contact.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("author_id", "contact_id"), - ) - op.create_table( - "pub_author_publication_association", - sa.Column("publication_id", sa.Integer(), nullable=False), - sa.Column("author_id", sa.Integer(), nullable=False), - sa.Column("author_order", sa.Integer(), nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["author_id"], ["pub_author.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint( - ["publication_id"], ["publication.id"], ondelete="CASCADE" - ), - sa.PrimaryKeyConstraint("publication_id", "author_id"), - ) - op.create_table( - "series", - sa.Column("observed_property", sa.String(length=100), nullable=False), - sa.Column("unit", sa.String(length=100), nullable=False), - sa.Column("name", sa.String(length=255), nullable=False), - sa.Column("description", sa.Text(), nullable=True), - sa.Column("sensor_id", sa.Integer(), nullable=True), - sa.Column("thing_id", sa.Integer(), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("release_status", sa.String(length=100), nullable=True), - sa.ForeignKeyConstraint( - ["observed_property"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint( - ["release_status"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint(["sensor_id"], ["sensor.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint( - ["unit"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "thing_contact_association", - sa.Column("thing_id", sa.Integer(), nullable=False), - sa.Column("contact_id", sa.Integer(), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["contact_id"], - ["contact.id"], - ), - sa.ForeignKeyConstraint( - ["thing_id"], - ["thing.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "thing_id_link", - sa.Column("thing_id", sa.Integer(), nullable=True), - sa.Column("relation", sa.String(length=100), nullable=False), - sa.Column("alternate_id", sa.String(length=100), nullable=False), - sa.Column("alternate_organization", sa.String(length=100), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["alternate_organization"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint( - ["relation"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "well_screen", - sa.Column("thing_id", sa.Integer(), nullable=False), - sa.Column("screen_depth_top", sa.Float(), nullable=False), - sa.Column("screen_depth_bottom", sa.Float(), nullable=False), - sa.Column("screen_type", sa.String(length=100), nullable=True), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["screen_type"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "geochemical_series", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("series_id", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(["series_id"], ["series.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("series_id"), - ) - op.create_table( - "geothermal_series", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("series_id", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(["series_id"], ["series.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("series_id"), - ) - op.create_table( - "groundwater_level_series", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("series_id", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(["series_id"], ["series.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("series_id"), - ) - op.create_table( - "observation", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column("series_id", sa.Integer(), nullable=False), - sa.Column("observation_timestamp", sa.TIMESTAMP(), nullable=False), - sa.Column("release_status", sa.String(length=100), nullable=True), - sa.ForeignKeyConstraint( - ["release_status"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint(["series_id"], ["series.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id", "observation_timestamp"), - ) - op.create_table( - "geochemical_observation", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("observation_id", sa.Integer(), nullable=False), - sa.Column("observation_timestamp", sa.TIMESTAMP(), nullable=False), - sa.ForeignKeyConstraint( - ["observation_id", "observation_timestamp"], - ["observation.id", "observation.observation_timestamp"], - ondelete="CASCADE", - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "geothermal_observation", - sa.Column("depth", sa.Float(), nullable=False), - sa.Column("temperature", sa.Float(), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("observation_id", sa.Integer(), nullable=False), - sa.Column("observation_timestamp", sa.TIMESTAMP(), nullable=False), - sa.ForeignKeyConstraint( - ["observation_id", "observation_timestamp"], - ["observation.id", "observation.observation_timestamp"], - ondelete="CASCADE", - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "groundwater_level_observation", - sa.Column("depth_to_water", sa.Float(), nullable=False), - sa.Column("measuring_point_height", sa.Float(), nullable=False), - sa.Column("level_status", sa.String(length=100), nullable=True), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("observation_id", sa.Integer(), nullable=False), - sa.Column("observation_timestamp", sa.TIMESTAMP(), nullable=False), - sa.ForeignKeyConstraint( - ["level_status"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint( - ["observation_id", "observation_timestamp"], - ["observation.id", "observation.observation_timestamp"], - ondelete="CASCADE", - ), - sa.PrimaryKeyConstraint("id"), - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("groundwater_level_observation") - op.drop_table("geothermal_observation") - op.drop_table("geochemical_observation") - op.drop_table("observation") - op.drop_table("groundwater_level_series") - op.drop_table("geothermal_series") - op.drop_table("geochemical_series") - op.drop_table("well_screen") - op.drop_table("thing_id_link") - op.drop_table("thing_contact_association") - op.drop_table("series") - op.drop_table("pub_author_publication_association") - op.drop_table("pub_author_contact_association") - op.drop_index("ix_phone_search_vector", table_name="phone", postgresql_using="gin") - op.drop_table("phone") - op.drop_table("location_thing_association") - op.drop_table("group_thing_association") - op.drop_index("ix_email_search_vector", table_name="email", postgresql_using="gin") - op.drop_table("email") - op.drop_table("collaborative_network_well") - op.drop_table("asset_thing_association") - op.drop_index( - "ix_address_search_vector", table_name="address", postgresql_using="gin" - ) - op.drop_table("address") - op.drop_index(op.f("ix_transaction_user_id"), table_name="transaction") - op.drop_table("transaction") - op.drop_index("ix_thing_search_vector", table_name="thing", postgresql_using="gin") - op.drop_table("thing") - op.drop_index( - "ix_publication_search_vector", table_name="publication", postgresql_using="gin" - ) - op.drop_table("publication") - op.drop_index("idx_location_point", table_name="location", postgresql_using="gist") - op.drop_table("location") - op.drop_table("lexicon_triple") - op.drop_table("lexicon_term_category_association") - op.drop_table("groundwater_level_sensor") - op.drop_table("geochronology_age") - op.drop_index( - "ix_contact_search_vector", table_name="contact", postgresql_using="gin" - ) - op.drop_table("contact") - op.drop_table("user") - op.drop_table("sensor") - op.drop_index( - "ix_pub_author_search_vector", table_name="pub_author", postgresql_using="gin" - ) - op.drop_table("pub_author") - op.drop_index( - op.f("ix_observation_version_transaction_id"), table_name="observation_version" - ) - op.drop_index( - op.f("ix_observation_version_operation_type"), table_name="observation_version" - ) - op.drop_index( - op.f("ix_observation_version_end_transaction_id"), - table_name="observation_version", - ) - op.drop_table("observation_version") - op.drop_index( - op.f("ix_location_version_transaction_id"), table_name="location_version" - ) - op.drop_index( - op.f("ix_location_version_operation_type"), table_name="location_version" - ) - op.drop_index( - op.f("ix_location_version_end_transaction_id"), table_name="location_version" - ) - op.drop_index( - "idx_location_version_point", - table_name="location_version", - postgresql_using="gist", - ) - op.drop_table("location_version") - op.drop_table("lexicon_term") - op.drop_table("lexicon_category") - op.drop_table("group") - op.drop_index("ix_asset_search_vector", table_name="asset", postgresql_using="gin") - op.drop_table("asset") - # ### end Alembic commands ### diff --git a/alembic/versions/66ac1af4ba69_initial_migration.py b/alembic/versions/66ac1af4ba69_initial_migration.py index 7bd24acdf..97a31f5d5 100644 --- a/alembic/versions/66ac1af4ba69_initial_migration.py +++ b/alembic/versions/66ac1af4ba69_initial_migration.py @@ -8,10 +8,7 @@ from typing import Sequence, Union -from alembic import op -import geoalchemy2 -import sqlalchemy as sa -import sqlalchemy_utils +# from alembic import op from sqlalchemy.orm import configure_mappers # revision identifiers, used by Alembic. @@ -39,6 +36,23 @@ def upgrade() -> None: # It is here as a record of the initial database state. # Actual initial database creation should be done through the Base.metadata.create_all(engine) call above. + """ + TODO + The following code will need to be regenerated by Alembic since configure_mappers() is now called + in db/__init__.py to ensure all models are loaded before creating the database schema. This is + require for SQL Alchemy continuum. + + The following code will also need to be added: + + - op.drop_index("idx_location_version_point", table_name="location_version", if_exists=True) + - before calling op.create_index("idx_location_version_point", "location_version", ["point"], unique=False, postgresql_using="gist",) + - op.drop_index("idx_location_point", table_name="location", if_exists=True) + - before calling op.create_index("idx_location_point", "location", ["point"], unique=False, postgresql_using="gist",) + + We will also need to figure out how to handle the SQL Alchemy searchable columns in the models, as they are not currently handled by Alembic. + There is some documentation about sync_triggers, but that has not yet been tested. + """ + # ### commands auto generated by Alembic - please adjust! ### # op.create_table('asset', # sa.Column('name', sa.String(), nullable=False), diff --git a/alembic/versions/dcd4ba0a63c6_initial_migration.py b/alembic/versions/dcd4ba0a63c6_initial_migration.py deleted file mode 100644 index 1e08aa477..000000000 --- a/alembic/versions/dcd4ba0a63c6_initial_migration.py +++ /dev/null @@ -1,885 +0,0 @@ -"""initial migration - -Revision ID: dcd4ba0a63c6 -Revises: -Create Date: 2025-07-28 09:10:27.082507 - -""" - -from typing import Sequence, Union - -from alembic import op -import geoalchemy2 -import sqlalchemy as sa -import sqlalchemy_utils - - -# revision identifiers, used by Alembic. -revision: str = "dcd4ba0a63c6" -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "asset", - sa.Column("filename", sa.String(), nullable=True), - sa.Column("storage_service", sa.String(), nullable=True), - sa.Column("storage_path", sa.String(), nullable=True), - sa.Column("mime_type", sa.String(), nullable=True), - sa.Column("size", sa.Integer(), nullable=True), - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "ix_asset_search_vector", - "asset", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.create_table( - "group", - sa.Column("name", sa.String(length=100), nullable=False), - sa.Column("description", sa.String(length=255), nullable=True), - sa.Column("parent_group_id", sa.Integer(), nullable=True), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["parent_group_id"], ["group.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("name"), - ) - op.create_table( - "lexicon_category", - sa.Column("name", sa.String(length=100), nullable=False), - sa.Column("description", sa.String(length=255), nullable=True), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("name"), - ) - op.create_table( - "lexicon_term", - sa.Column("term", sa.String(length=100), nullable=False), - sa.Column("definition", sa.String(length=255), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("term"), - ) - op.create_table( - "location_version", - sa.Column("name", sa.String(length=255), autoincrement=False, nullable=True), - sa.Column("notes", sa.Text(), autoincrement=False, nullable=True), - sa.Column( - "point", - geoalchemy2.types.Geometry( - geometry_type="POINT", - srid=4326, - from_text="ST_GeomFromEWKT", - name="geometry", - nullable=False, - ), - autoincrement=False, - nullable=False, - ), - sa.Column("id", sa.Integer(), autoincrement=False, nullable=False), - sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("now()"), - autoincrement=False, - nullable=True, - ), - sa.Column( - "release_status", sa.String(length=100), autoincrement=False, nullable=True - ), - sa.Column( - "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False - ), - sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), - sa.Column("operation_type", sa.SmallInteger(), nullable=False), - sa.PrimaryKeyConstraint("id", "transaction_id"), - ) - op.drop_index( - "idx_location_version_point", table_name="location_version", if_exists=True - ) - op.create_index( - "idx_location_version_point", - "location_version", - ["point"], - unique=False, - postgresql_using="gist", - ) - op.create_index( - op.f("ix_location_version_end_transaction_id"), - "location_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_location_version_operation_type"), - "location_version", - ["operation_type"], - unique=False, - ) - op.create_index( - op.f("ix_location_version_transaction_id"), - "location_version", - ["transaction_id"], - unique=False, - ) - op.create_table( - "observation_version", - sa.Column("id", sa.Integer(), autoincrement=False, nullable=False), - sa.Column("series_id", sa.Integer(), autoincrement=False, nullable=True), - sa.Column( - "observation_timestamp", sa.TIMESTAMP(), autoincrement=False, nullable=False - ), - sa.Column( - "release_status", sa.String(length=100), autoincrement=False, nullable=True - ), - sa.Column( - "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False - ), - sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), - sa.Column("operation_type", sa.SmallInteger(), nullable=False), - sa.PrimaryKeyConstraint("id", "observation_timestamp", "transaction_id"), - ) - op.create_index( - op.f("ix_observation_version_end_transaction_id"), - "observation_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_observation_version_operation_type"), - "observation_version", - ["operation_type"], - unique=False, - ) - op.create_index( - op.f("ix_observation_version_transaction_id"), - "observation_version", - ["transaction_id"], - unique=False, - ) - op.create_table( - "pub_author", - sa.Column("name", sa.String(), nullable=False), - sa.Column("affiliation", sa.String(), nullable=True), - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "ix_pub_author_search_vector", - "pub_author", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.create_table( - "sensor", - sa.Column("name", sa.String(length=255), nullable=False), - sa.Column("model", sa.String(length=50), nullable=True), - sa.Column("serial_no", sa.String(length=50), nullable=True), - sa.Column("date_installed", sa.DateTime(), nullable=True), - sa.Column("date_removed", sa.DateTime(), nullable=True), - sa.Column("recording_interval", sa.Integer(), nullable=True), - sa.Column("notes", sa.String(length=50), nullable=True), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "user", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("username", sa.String(length=255), nullable=False), - sa.Column("password", sa.String(length=255), nullable=False), - sa.Column("is_superuser", sa.Boolean(), nullable=False), - sa.Column("is_active", sa.Boolean(), nullable=False), - sa.Column("avatar_url", sa.Text(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "contact", - sa.Column("name", sa.String(length=100), nullable=False), - sa.Column("role", sa.String(length=100), nullable=False), - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["role"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "ix_contact_search_vector", - "contact", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.create_table( - "geochronology_age", - sa.Column("location_id", sa.Integer(), nullable=False), - sa.Column("age", sa.Float(), nullable=False), - sa.Column("age_error", sa.Float(), nullable=True), - sa.Column("method", sa.String(length=100), nullable=True), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["method"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "groundwater_level_sensor", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("sensor_id", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(["sensor_id"], ["sensor.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("sensor_id"), - ) - op.create_table( - "lexicon_term_category_association", - sa.Column("lexicon_term", sa.String(length=100), nullable=False), - sa.Column("category_name", sa.String(length=255), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["category_name"], ["lexicon_category.name"], ondelete="CASCADE" - ), - sa.ForeignKeyConstraint( - ["lexicon_term"], ["lexicon_term.term"], ondelete="CASCADE" - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "lexicon_triple", - sa.Column("subject", sa.String(length=100), nullable=False), - sa.Column("predicate", sa.String(length=100), nullable=False), - sa.Column("object_", sa.String(length=100), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["object_"], ["lexicon_term.term"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["subject"], ["lexicon_term.term"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "location", - sa.Column("name", sa.String(length=255), nullable=True), - sa.Column("notes", sa.Text(), nullable=True), - sa.Column( - "point", - geoalchemy2.types.Geometry( - geometry_type="POINT", - srid=4326, - from_text="ST_GeomFromEWKT", - name="geometry", - nullable=False, - ), - nullable=False, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("release_status", sa.String(length=100), nullable=True), - sa.ForeignKeyConstraint( - ["release_status"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.drop_index("idx_location_point", table_name="location", if_exists=True) - op.create_index( - "idx_location_point", - "location", - ["point"], - unique=False, - postgresql_using="gist", - ) - op.create_table( - "publication", - sa.Column("title", sa.Text(), nullable=False), - sa.Column("abstract", sa.Text(), nullable=True), - sa.Column("doi", sa.String(), nullable=True), - sa.Column("year", sa.Integer(), nullable=True), - sa.Column("publisher", sa.String(), nullable=True), - sa.Column("url", sa.String(), nullable=True), - sa.Column("publication_type", sa.String(length=100), nullable=False), - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["publication_type"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("doi"), - ) - op.create_index( - "ix_publication_search_vector", - "publication", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.create_table( - "thing", - sa.Column("name", sa.String(length=255), nullable=False), - sa.Column("description", sa.String(length=500), nullable=True), - sa.Column("thing_type", sa.String(length=100), nullable=True), - sa.Column("spring_type", sa.String(length=100), nullable=True), - sa.Column("well_depth", sa.Float(), nullable=True), - sa.Column("hole_depth", sa.Float(), nullable=True), - sa.Column("well_type", sa.String(length=100), nullable=True), - sa.Column("well_casing_diameter", sa.Float(), nullable=True), - sa.Column("well_casing_depth", sa.Float(), nullable=True), - sa.Column("well_casing_description", sa.String(length=50), nullable=True), - sa.Column("well_construction_notes", sa.String(length=250), nullable=True), - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("release_status", sa.String(length=100), nullable=True), - sa.ForeignKeyConstraint( - ["release_status"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint( - ["spring_type"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint( - ["thing_type"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint( - ["well_type"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "ix_thing_search_vector", - "thing", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.create_table( - "transaction", - sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column("remote_addr", sa.String(length=50), nullable=True), - sa.Column("user_id", sa.Integer(), nullable=True), - sa.Column("issued_at", sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint( - ["user_id"], - ["user.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_transaction_user_id"), "transaction", ["user_id"], unique=False - ) - op.create_table( - "address", - sa.Column("contact_id", sa.Integer(), nullable=False), - sa.Column("address_line_1", sa.String(length=255), nullable=False), - sa.Column("address_line_2", sa.String(length=255), nullable=True), - sa.Column("city", sa.String(length=100), nullable=False), - sa.Column("state", sa.String(length=50), nullable=False), - sa.Column("postal_code", sa.String(length=20), nullable=False), - sa.Column("country", sa.String(length=100), nullable=False), - sa.Column("address_type", sa.String(length=100), nullable=False), - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["address_type"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint(["contact_id"], ["contact.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint( - ["country"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "ix_address_search_vector", - "address", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.create_table( - "asset_thing_association", - sa.Column("asset_id", sa.Integer(), nullable=False), - sa.Column("thing_id", sa.Integer(), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["asset_id"], ["asset.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "collaborative_network_well", - sa.Column("actively_monitored", sa.Boolean(), nullable=False), - sa.Column("thing_id", sa.Integer(), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "email", - sa.Column("contact_id", sa.Integer(), nullable=False), - sa.Column("email", sa.String(length=100), nullable=False), - sa.Column("email_type", sa.String(length=100), nullable=False), - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["contact_id"], ["contact.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint( - ["email_type"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "ix_email_search_vector", - "email", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.create_table( - "group_thing_association", - sa.Column("group_id", sa.Integer(), nullable=False), - sa.Column("thing_id", sa.Integer(), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["group_id"], ["group.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "location_thing_association", - sa.Column("location_id", sa.Integer(), nullable=False), - sa.Column("thing_id", sa.Integer(), nullable=False), - sa.Column( - "effective_start", - sa.DateTime(), - server_default=sa.text("now()"), - nullable=False, - ), - sa.Column("effective_end", sa.DateTime(), nullable=True), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["location_id"], ["location.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("location_id", "thing_id", "id"), - ) - op.create_table( - "phone", - sa.Column("contact_id", sa.Integer(), nullable=False), - sa.Column("phone_number", sa.String(length=20), nullable=False), - sa.Column("phone_type", sa.String(length=100), nullable=False), - sa.Column( - "search_vector", - sqlalchemy_utils.types.ts_vector.TSVectorType(), - nullable=True, - ), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["contact_id"], ["contact.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint( - ["phone_type"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "ix_phone_search_vector", - "phone", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) - op.create_table( - "pub_author_contact_association", - sa.Column("author_id", sa.Integer(), nullable=False), - sa.Column("contact_id", sa.Integer(), nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["author_id"], ["pub_author.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["contact_id"], ["contact.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("author_id", "contact_id"), - ) - op.create_table( - "pub_author_publication_association", - sa.Column("publication_id", sa.Integer(), nullable=False), - sa.Column("author_id", sa.Integer(), nullable=False), - sa.Column("author_order", sa.Integer(), nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint(["author_id"], ["pub_author.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint( - ["publication_id"], ["publication.id"], ondelete="CASCADE" - ), - sa.PrimaryKeyConstraint("publication_id", "author_id"), - ) - op.create_table( - "series", - sa.Column("observed_property", sa.String(length=100), nullable=False), - sa.Column("unit", sa.String(length=100), nullable=False), - sa.Column("name", sa.String(length=255), nullable=False), - sa.Column("description", sa.Text(), nullable=True), - sa.Column("sensor_id", sa.Integer(), nullable=True), - sa.Column("thing_id", sa.Integer(), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("release_status", sa.String(length=100), nullable=True), - sa.ForeignKeyConstraint( - ["observed_property"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint( - ["release_status"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint(["sensor_id"], ["sensor.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint( - ["unit"], - ["lexicon_term.term"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "thing_contact_association", - sa.Column("thing_id", sa.Integer(), nullable=False), - sa.Column("contact_id", sa.Integer(), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["contact_id"], - ["contact.id"], - ), - sa.ForeignKeyConstraint( - ["thing_id"], - ["thing.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "thing_id_link", - sa.Column("thing_id", sa.Integer(), nullable=True), - sa.Column("relation", sa.String(length=100), nullable=False), - sa.Column("alternate_id", sa.String(length=100), nullable=False), - sa.Column("alternate_organization", sa.String(length=100), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["alternate_organization"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint( - ["relation"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "well_screen", - sa.Column("thing_id", sa.Integer(), nullable=False), - sa.Column("screen_depth_top", sa.Float(), nullable=False), - sa.Column("screen_depth_bottom", sa.Float(), nullable=False), - sa.Column("screen_type", sa.String(length=100), nullable=True), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["screen_type"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint(["thing_id"], ["thing.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "geochemical_series", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("series_id", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(["series_id"], ["series.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("series_id"), - ) - op.create_table( - "geothermal_series", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("series_id", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(["series_id"], ["series.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("series_id"), - ) - op.create_table( - "groundwater_level_series", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("series_id", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(["series_id"], ["series.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("series_id"), - ) - op.create_table( - "observation", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column("series_id", sa.Integer(), nullable=False), - sa.Column("observation_timestamp", sa.TIMESTAMP(), nullable=False), - sa.Column("release_status", sa.String(length=100), nullable=True), - sa.ForeignKeyConstraint( - ["release_status"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint(["series_id"], ["series.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id", "observation_timestamp"), - ) - op.create_table( - "geochemical_observation", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("observation_id", sa.Integer(), nullable=False), - sa.Column("observation_timestamp", sa.TIMESTAMP(), nullable=False), - sa.ForeignKeyConstraint( - ["observation_id", "observation_timestamp"], - ["observation.id", "observation.observation_timestamp"], - ondelete="CASCADE", - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "geothermal_observation", - sa.Column("depth", sa.Float(), nullable=False), - sa.Column("temperature", sa.Float(), nullable=False), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("observation_id", sa.Integer(), nullable=False), - sa.Column("observation_timestamp", sa.TIMESTAMP(), nullable=False), - sa.ForeignKeyConstraint( - ["observation_id", "observation_timestamp"], - ["observation.id", "observation.observation_timestamp"], - ondelete="CASCADE", - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "groundwater_level_observation", - sa.Column("depth_to_water", sa.Float(), nullable=False), - sa.Column("measuring_point_height", sa.Float(), nullable=False), - sa.Column("level_status", sa.String(length=100), nullable=True), - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.Column("observation_id", sa.Integer(), nullable=False), - sa.Column("observation_timestamp", sa.TIMESTAMP(), nullable=False), - sa.ForeignKeyConstraint( - ["level_status"], - ["lexicon_term.term"], - ), - sa.ForeignKeyConstraint( - ["observation_id", "observation_timestamp"], - ["observation.id", "observation.observation_timestamp"], - ondelete="CASCADE", - ), - sa.PrimaryKeyConstraint("id"), - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("groundwater_level_observation") - op.drop_table("geothermal_observation") - op.drop_table("geochemical_observation") - op.drop_table("observation") - op.drop_table("groundwater_level_series") - op.drop_table("geothermal_series") - op.drop_table("geochemical_series") - op.drop_table("well_screen") - op.drop_table("thing_id_link") - op.drop_table("thing_contact_association") - op.drop_table("series") - op.drop_table("pub_author_publication_association") - op.drop_table("pub_author_contact_association") - op.drop_index("ix_phone_search_vector", table_name="phone", postgresql_using="gin") - op.drop_table("phone") - op.drop_table("location_thing_association") - op.drop_table("group_thing_association") - op.drop_index("ix_email_search_vector", table_name="email", postgresql_using="gin") - op.drop_table("email") - op.drop_table("collaborative_network_well") - op.drop_table("asset_thing_association") - op.drop_index( - "ix_address_search_vector", table_name="address", postgresql_using="gin" - ) - op.drop_table("address") - op.drop_index(op.f("ix_transaction_user_id"), table_name="transaction") - op.drop_table("transaction") - op.drop_index("ix_thing_search_vector", table_name="thing", postgresql_using="gin") - op.drop_table("thing") - op.drop_index( - "ix_publication_search_vector", table_name="publication", postgresql_using="gin" - ) - op.drop_table("publication") - op.drop_index("idx_location_point", table_name="location", postgresql_using="gist") - op.drop_table("location") - op.drop_table("lexicon_triple") - op.drop_table("lexicon_term_category_association") - op.drop_table("groundwater_level_sensor") - op.drop_table("geochronology_age") - op.drop_index( - "ix_contact_search_vector", table_name="contact", postgresql_using="gin" - ) - op.drop_table("contact") - op.drop_table("user") - op.drop_table("sensor") - op.drop_index( - "ix_pub_author_search_vector", table_name="pub_author", postgresql_using="gin" - ) - op.drop_table("pub_author") - op.drop_index( - op.f("ix_observation_version_transaction_id"), table_name="observation_version" - ) - op.drop_index( - op.f("ix_observation_version_operation_type"), table_name="observation_version" - ) - op.drop_index( - op.f("ix_observation_version_end_transaction_id"), - table_name="observation_version", - ) - op.drop_table("observation_version") - op.drop_index( - op.f("ix_location_version_transaction_id"), table_name="location_version" - ) - op.drop_index( - op.f("ix_location_version_operation_type"), table_name="location_version" - ) - op.drop_index( - op.f("ix_location_version_end_transaction_id"), table_name="location_version" - ) - op.drop_index( - "idx_location_version_point", - table_name="location_version", - postgresql_using="gist", - ) - op.drop_table("location_version") - op.drop_table("lexicon_term") - op.drop_table("lexicon_category") - op.drop_table("group") - op.drop_index("ix_asset_search_vector", table_name="asset", postgresql_using="gin") - op.drop_table("asset") - # ### end Alembic commands ### From 5b223bcc9f8a21b24895e21aa682f1cd77de33f7 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 29 Jul 2025 15:37:04 -0600 Subject: [PATCH 13/42] refactor: call configure_mappers() in db/__init__.py --- tests/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 6990113ff..d1813ef08 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -17,15 +17,12 @@ from alembic import command import pytest from fastapi.testclient import TestClient -from sqlalchemy.orm import configure_mappers from core.app import init_lexicon from main import app from db import * from db.engine import session_ctx -configure_mappers() - def run_alembic_upgrade(): alembic_cfg = Config("alembic.ini") From d43365d30428d05b33d2a63a2b5543db2c2f758b Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 29 Jul 2025 15:51:46 -0600 Subject: [PATCH 14/42] feat: add sample fixture for sample tests --- tests/__init__.py | 2 +- tests/test_sample.py | 25 +++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index d1813ef08..34d4ef50b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -52,7 +52,7 @@ def thing(): session.add(thing) session.commit() yield thing - + session.delete(thing) session.close() diff --git a/tests/test_sample.py b/tests/test_sample.py index 2661a7a65..37b48f4bc 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -15,7 +15,28 @@ # =============================================================================== import pytest -from tests import client +from tests import client, thing # noqa: F401 + + +import pytest +from db.engine import session_ctx +from db.sample import Sample + + +@pytest.fixture +def sample_fixture(thing): + with session_ctx() as session: + sample = Sample( + thing_id=thing.id, + collection_timestamp="2025-01-01T00:00:00+00:00", + collection_method="manual", + ) + session.add(sample) + session.commit() + session.refresh(sample) + yield sample + session.delete(sample) + session.commit() def test_add_sample(): @@ -72,7 +93,7 @@ def test_add_geothermal_sample(): # ============= Get tests for samples ============================================= -def test_get_samples(): +def test_get_samples(sample_fixture): """ Test retrieving samples from the collaborative network. """ From 56a62293dbc031025de4e9e04b9b4d8bfff28a74 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 29 Jul 2025 15:54:30 -0600 Subject: [PATCH 15/42] fix: delete thing after creation in fixture --- tests/__init__.py | 1 + things.dbf | Bin 0 -> 201 bytes things.shp | Bin 0 -> 156 bytes things.shx | Bin 0 -> 116 bytes things.zip | Bin 0 -> 783 bytes 5 files changed, 1 insertion(+) create mode 100644 things.dbf create mode 100644 things.shp create mode 100644 things.shx create mode 100644 things.zip diff --git a/tests/__init__.py b/tests/__init__.py index 34d4ef50b..26dfaa85f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -51,6 +51,7 @@ def thing(): thing.thing_type = "water well" session.add(thing) session.commit() + session.refresh(thing) yield thing session.delete(thing) session.close() diff --git a/things.dbf b/things.dbf new file mode 100644 index 0000000000000000000000000000000000000000..b613053d1ea72a5c668198ad8e96eaa25199f4c4 GIT binary patch literal 201 zcmZRsWtU}QU|>jOFaeU7ATtFn<^y6e!nqJeUSe)4RLB`b8$mfRidR7)BqK8~UBOU+ L6adjC!tfilKm zpyB|dA&N~LP-zGc2MIEU3x`=uFgb``gt-AIesJ5v=vD$$0>UVMsKDa~D=PZI2x_qd zh(>i=X4w0hlR#k*Msiz9QW|qDyDZ3qi3}z{5))*mz{Pw(EKnFBgS^DtR4CsWM1vfG z40sh3LO_wJV5mR}faozIK}&!)Ba;XN?j!|s)V4+t3z5Lk^`S>2Lf;W&eHG~X(1RGE cF98^4Q2T%Z9^lQ&29je2!WBR|5ES|h05tAuA^-pY literal 0 HcmV?d00001 From 5fc4016c68ce279b9e4d8ac72e5ed28a3523c760 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 29 Jul 2025 16:16:41 -0600 Subject: [PATCH 16/42] feat: create general ResourceNotFoundResponse model for 404 errors This is to be used by all endpoints where a resource is not found when an id is given as part of a path parameter --- schemas_v2/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/schemas_v2/__init__.py b/schemas_v2/__init__.py index 8e546ddc2..a117e7097 100644 --- a/schemas_v2/__init__.py +++ b/schemas_v2/__init__.py @@ -13,5 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. # =============================================================================== +from pydantic import BaseModel + + +class ResourceNotFoundResponse(BaseModel): + detail: str + # ============= EOF ============================================= From ded3c542afa771295d24cf9b0af99aaf6a0615ab Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 29 Jul 2025 17:34:20 -0600 Subject: [PATCH 17/42] feat: create sample patch and get by id endpoints --- api/sample.py | 51 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/api/sample.py b/api/sample.py index c7a4f2928..4efa270f5 100644 --- a/api/sample.py +++ b/api/sample.py @@ -14,20 +14,19 @@ # limitations under the License. # =============================================================================== -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session -from starlette.status import HTTP_201_CREATED +from starlette.status import HTTP_201_CREATED, HTTP_404_NOT_FOUND from api.pagination import CustomPage from core.dependencies import session_dependency from db import adder from db.engine import get_db_session from db.sample import Sample -from schemas_v2.sample import ( - SampleResponse, - CreateSample, -) +from schemas_v2 import ResourceNotFoundResponse +from schemas_v2.sample import SampleResponse, CreateSample, UpdateSample from services.query_helper import paginated_all_getter +from services.crud_helper import model_patcher router = APIRouter( prefix="/sample", @@ -69,6 +68,33 @@ def add_sample(sample_data: CreateSample, session: Session = Depends(get_db_sess # return adder(session, GeothermalSample, sample_data) +# ============= Update ============================================= +@router.patch("/{sample_id}", summary="Update Sample") +def update_sample( + sample_id: int, + sample_data: UpdateSample, + session: Session = Depends(get_db_session), +) -> SampleResponse | ResourceNotFoundResponse: + """ + Endpoint to update a sample. + """ + + """ + Development notes: + + What do we do if the field is nullable and the schema defaults to None? + If that occurs, then we update the field to None, which may not have + been the intension of the user. We could set some string to indicate + DO NOT UPDATE. Perhaps coordination between the front and backends? + """ + if session.get(Sample, sample_id) is None: + raise HTTPException( + status_code=HTTP_404_NOT_FOUND, + detail=f"Sample with ID {sample_id} not found.", + ) + return model_patcher(session, Sample, sample_id, sample_data) + + # ============= Get ============================================= @router.get("", summary="Get Samples") def get_samples(session: session_dependency) -> CustomPage[SampleResponse]: @@ -102,11 +128,20 @@ def get_samples(session: session_dependency) -> CustomPage[SampleResponse]: # ============= Get by ID ============================================= @router.get("/{sample_id}", summary="Get Sample by ID") -def get_sample_by_id(sample_id: int, session: session_dependency) -> SampleResponse: +def get_sample_by_id( + sample_id: int, session: session_dependency +) -> SampleResponse | ResourceNotFoundResponse: """ Endpoint to retrieve a sample by its ID. """ - return session.get(Sample, sample_id) + sample = session.get(Sample, sample_id) + if sample is None: + raise HTTPException( + status_code=HTTP_404_NOT_FOUND, + detail=f"Sample with ID {sample_id} not found.", + ) + else: + return sample # @router.get("/{sample_id}", summary="Get Geochemical Sample by ID") From 7ef271fefe52df2adf0a091768bfec7d461220d3 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 29 Jul 2025 17:35:02 -0600 Subject: [PATCH 18/42] feat: add update schema for sample and update sample response schema --- schemas_v2/sample.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/schemas_v2/sample.py b/schemas_v2/sample.py index d0cec20b9..efe8b71f4 100644 --- a/schemas_v2/sample.py +++ b/schemas_v2/sample.py @@ -14,8 +14,9 @@ # limitations under the License. # =============================================================================== from datetime import datetime +from pydantic import BaseModel, field_validator -from pydantic import BaseModel +from db.engine import get_db_session # -------- CREATE ---------- @@ -44,8 +45,38 @@ class CreateGeothermalSample(BaseModel): # -------- RESPONSE ---------- class SampleResponse(BaseModel): id: int + collection_timestamp: datetime + collection_method: str + thing_id: int # -------- UPDATE ---------- +class UpdateSample(BaseModel): + collection_timestamp: datetime | None = None + collection_method: str | None = None + thing_id: int | None = None + + @field_validator("thing_id") + def validate_thing_id_exists(cls, thing_id: int) -> int: + """ + Validate that the thing_id exists in the database. + """ + session = get_db_session() + thing = session.get("Thing", thing_id) + if not thing: + raise ValueError(f"Thing with ID {thing_id} does not exist.") + return thing_id + + @field_validator("collection_timestamp") + def validate_collection_timestamp(cls, collection_timestamp: datetime) -> datetime: + """ + Validate that the collection_timestamp is not in the future. + """ + if collection_timestamp > datetime.now(): + raise ValueError( + f"Collection timestamp {collection_timestamp} cannot be in the future." + ) + return collection_timestamp + # ============= EOF ============================================= From 2d40298e95780a1effc9d9cc20b12cb29eecf1e5 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 29 Jul 2025 17:35:56 -0600 Subject: [PATCH 19/42] WIP: work on sample endpoint tests --- tests/test_sample.py | 58 ++++++++++++++++++++++++++++++++++++++----- things.dbf | Bin 201 -> 0 bytes things.shp | Bin 156 -> 0 bytes things.shx | Bin 116 -> 0 bytes things.zip | Bin 783 -> 0 bytes 5 files changed, 52 insertions(+), 6 deletions(-) delete mode 100644 things.dbf delete mode 100644 things.shp delete mode 100644 things.shx delete mode 100644 things.zip diff --git a/tests/test_sample.py b/tests/test_sample.py index 37b48f4bc..037b19fde 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -34,11 +34,12 @@ def sample_fixture(thing): session.add(sample) session.commit() session.refresh(sample) - yield sample + yield thing, sample session.delete(sample) session.commit() +# ============= Post tests for samples ============================================= def test_add_sample(): """ Test adding a sample to the collaborative network. @@ -49,7 +50,6 @@ def test_add_sample(): "thing_id": 1, "collection_timestamp": "2025-01-01T00:00:00Z", "collection_method": "manual", - "release_status": "draft", }, ) data = response.json() @@ -92,16 +92,46 @@ def test_add_geothermal_sample(): assert data["sample_id"] == 1 +# ============= Patch tests for samples ============================================= +def test_patch_sample(sample_fixture): + """ + Test updating a sample in the collaborative network. + """ + thing, sample = sample_fixture + collection_method_patch = "automated" + response = client.patch( + f"/sample/{sample.id}", + json={ + "collection_method": collection_method_patch, + }, + ) + assert response.status_code == 200 + data = response.json() + assert data == { + "id": sample.id, + "collection_timestamp": sample.collection_timestamp.isoformat(), + "collection_method": collection_method_patch, + "thing_id": thing.id, + } + + # ============= Get tests for samples ============================================= def test_get_samples(sample_fixture): """ Test retrieving samples from the collaborative network. """ + thing, sample = sample_fixture response = client.get("/sample") assert response.status_code == 200 data = response.json() - assert "items" in data - assert len(data["items"]) > 0 + assert data["items"] == [ + { + "id": sample.id, + "collection_timestamp": sample.collection_timestamp.isoformat(), + "collection_method": sample.collection_method, + "thing_id": thing.id, + } + ] @pytest.mark.skip(reason="Geochemical samples endpoint not implemented yet") @@ -128,14 +158,30 @@ def test_get_geothermal_samples(): assert len(data["items"]) > 0 -def test_get_sample_by_id(): +def test_get_sample_by_id_200(sample_fixture): """ Test retrieving a sample from the collaborative network. """ + thing, sample = sample_fixture response = client.get("/sample/1") assert response.status_code == 200 data = response.json() - assert data["id"] == 1 + assert data == { + "id": sample.id, + "collection_timestamp": sample.collection_timestamp.isoformat(), + "collection_method": sample.collection_method, + "thing_id": thing.id, + } + + +def test_get_sample_by_id_404_not_found(sample_fixture): + """ + Test retrieving a sample from the collaborative network. + """ + response = client.get("/sample/999") + assert response.status_code == 404 + data = response.json() + assert data["detail"] == "Sample with ID 999 not found." # ============= EOF ============================================= diff --git a/things.dbf b/things.dbf deleted file mode 100644 index b613053d1ea72a5c668198ad8e96eaa25199f4c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 201 zcmZRsWtU}QU|>jOFaeU7ATtFn<^y6e!nqJeUSe)4RLB`b8$mfRidR7)BqK8~UBOU+ L6adjC!tfilKm zpyB|dA&N~LP-zGc2MIEU3x`=uFgb``gt-AIesJ5v=vD$$0>UVMsKDa~D=PZI2x_qd zh(>i=X4w0hlR#k*Msiz9QW|qDyDZ3qi3}z{5))*mz{Pw(EKnFBgS^DtR4CsWM1vfG z40sh3LO_wJV5mR}faozIK}&!)Ba;XN?j!|s)V4+t3z5Lk^`S>2Lf;W&eHG~X(1RGE cF98^4Q2T%Z9^lQ&29je2!WBR|5ES|h05tAuA^-pY From 706006f88eb0e206172d6fda946e2bc187470b6a Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 29 Jul 2025 18:56:06 -0600 Subject: [PATCH 20/42] fix: implement successful and 404 sample patch endpoints --- schemas_v2/sample.py | 11 ++++++----- tests/test_sample.py | 22 ++++++++++++++++++++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/schemas_v2/sample.py b/schemas_v2/sample.py index efe8b71f4..fe2dd14c1 100644 --- a/schemas_v2/sample.py +++ b/schemas_v2/sample.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # =============================================================================== -from datetime import datetime +from datetime import datetime, timezone from pydantic import BaseModel, field_validator from db.engine import get_db_session @@ -72,10 +72,11 @@ def validate_collection_timestamp(cls, collection_timestamp: datetime) -> dateti """ Validate that the collection_timestamp is not in the future. """ - if collection_timestamp > datetime.now(): - raise ValueError( - f"Collection timestamp {collection_timestamp} cannot be in the future." - ) + if collection_timestamp: + if collection_timestamp > datetime.now(tz=timezone.utc): + raise ValueError( + f"Collection timestamp {collection_timestamp} cannot be in the future." + ) return collection_timestamp diff --git a/tests/test_sample.py b/tests/test_sample.py index 037b19fde..5c418ed69 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -98,23 +98,41 @@ def test_patch_sample(sample_fixture): Test updating a sample in the collaborative network. """ thing, sample = sample_fixture - collection_method_patch = "automated" + collection_method_patch = "continuous" + collection_timestamp_patch = "2025-01-02T00:00:00+00:00" response = client.patch( f"/sample/{sample.id}", json={ "collection_method": collection_method_patch, + "collection_timestamp": collection_timestamp_patch, }, ) assert response.status_code == 200 data = response.json() assert data == { "id": sample.id, - "collection_timestamp": sample.collection_timestamp.isoformat(), + "collection_timestamp": collection_timestamp_patch.split("+")[0], "collection_method": collection_method_patch, "thing_id": thing.id, } +def test_patch_sample_404_not_found(sample_fixture): + """ + Test updating a sample that does not exist in the collaborative network. + """ + collection_method_patch = "continuous" + response = client.patch( + "/sample/999", + json={ + "collection_method": collection_method_patch, + }, + ) + assert response.status_code == 404 + data = response.json() + assert data["detail"] == "Sample with ID 999 not found." + + # ============= Get tests for samples ============================================= def test_get_samples(sample_fixture): """ From e22f7e8228f9390ee69c65bb9d4216fa1c91bd29 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 29 Jul 2025 19:01:01 -0600 Subject: [PATCH 21/42] WIP: test sample custom field validators --- tests/test_sample.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_sample.py b/tests/test_sample.py index 5c418ed69..3c925b228 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -133,6 +133,40 @@ def test_patch_sample_404_not_found(sample_fixture): assert data["detail"] == "Sample with ID 999 not found." +def test_patch_sample_422_thing_id_not_found(sample_fixture): + """ + Test updating a sample with a thing_id that does not exist + """ + response = client.patch( + f"/sample/{sample_fixture[1].id}", + json={ + "thing_id": 999, + }, + ) + assert response.status_code == 422 + data = response.json() + assert data["detail"] == "Thing with ID 999 does not exist." + + +def test_patch_sample_422_invalid_timestamp(sample_fixture): + """ + Test updating a sample with an invalid collection timestamp. + """ + bad_collection_timestamp = "3500-01-01T00:00:00Z" + response = client.patch( + f"/sample/{sample_fixture[1].id}", + json={ + "collection_timestamp": bad_collection_timestamp, # Invalid date + }, + ) + assert response.status_code == 422 + data = response.json() + assert ( + data["detail"] + == f"Collection timestamp {bad_collection_timestamp} cannot be in the future." + ) + + # ============= Get tests for samples ============================================= def test_get_samples(sample_fixture): """ From 9f58da381d880b34832fd73b2fdc6f4274af6d5b Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 30 Jul 2025 11:09:38 -0600 Subject: [PATCH 22/42] docs: note where code can be refactored --- services/query_helper.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/services/query_helper.py b/services/query_helper.py index 350276fbc..373c543df 100644 --- a/services/query_helper.py +++ b/services/query_helper.py @@ -18,7 +18,7 @@ from fastapi import HTTPException from fastapi_pagination.ext.sqlalchemy import paginate -from sqlalchemy import select, Float, Integer, Column, Select, func +from sqlalchemy import select, Float, Integer, Column, Select from sqlalchemy.orm import DeclarativeBase from sqlalchemy.sql.elements import OperatorExpression @@ -100,6 +100,12 @@ def simple_get_by_id(session, table, item_id) -> object | None: """ Helper function to get a record by ID from the database. """ + """ + REFACTOR NOTE/TODO: this function replicates the functionality of + session.get(table, item_id), which is a SQL Alchemy method to retrieve + a record by its primary key. This function can be replaced with + session.get(table, item_id). + """ sql = select(table).where(table.id == item_id) result = session.execute(sql) return result.scalar_one_or_none() From 315f407bb5f388ce53af9cf05221133784e7131b Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 30 Jul 2025 11:48:39 -0600 Subject: [PATCH 23/42] docs: note fixture management --- tests/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/__init__.py b/tests/__init__.py index 26dfaa85f..0bd404a51 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -42,6 +42,12 @@ def run_alembic_downgrade(): client = TestClient(app) +""" +REFACTOR TODO: put all fixtures here or in a separate fixtures file. Some fixtures are dependent on others, +such as `sample_fixture` which requires `thing`. By putting them all in one place, we can ensure that +they are properly managed and avoid potential issues with fixture scope and lifecycle. +""" + @pytest.fixture(scope="function") def thing(): From f892b3c1b6fc58596f0cc8a6d0b2b65519f61297 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 30 Jul 2025 11:52:08 -0600 Subject: [PATCH 24/42] fix: update add sample test to use thing fixture --- tests/test_sample.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_sample.py b/tests/test_sample.py index 3c925b228..15b2df743 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -40,14 +40,14 @@ def sample_fixture(thing): # ============= Post tests for samples ============================================= -def test_add_sample(): +def test_add_sample(thing): """ Test adding a sample to the collaborative network. """ response = client.post( "/sample", json={ - "thing_id": 1, + "thing_id": thing.id, "collection_timestamp": "2025-01-01T00:00:00Z", "collection_method": "manual", }, @@ -55,7 +55,7 @@ def test_add_sample(): data = response.json() assert response.status_code == 201 assert data["id"] is not None - assert data["thing_id"] == 1 + assert data["thing_id"] == thing.id @pytest.mark.skip(reason="Geochemical sample endpoint not implemented yet") From e3b127df510970ff3158d24053fe9e30ea5c0693 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 30 Jul 2025 11:59:59 -0600 Subject: [PATCH 25/42] fix: use sample fixture's in get get sample test The id changes because of autoincrement in the database, so use the fixture's sample id instead of a hardcoded value. --- tests/test_sample.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_sample.py b/tests/test_sample.py index 15b2df743..2f505074e 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -215,7 +215,7 @@ def test_get_sample_by_id_200(sample_fixture): Test retrieving a sample from the collaborative network. """ thing, sample = sample_fixture - response = client.get("/sample/1") + response = client.get(f"/sample/{sample.id}") assert response.status_code == 200 data = response.json() assert data == { From d54d5ce04821c440b5c41a24fd794f2fad73776c Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 30 Jul 2025 12:04:49 -0600 Subject: [PATCH 26/42] fix: fix use of session to validate Thing existence for UpdateSample --- schemas_v2/sample.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/schemas_v2/sample.py b/schemas_v2/sample.py index fe2dd14c1..2d9dc4072 100644 --- a/schemas_v2/sample.py +++ b/schemas_v2/sample.py @@ -17,6 +17,7 @@ from pydantic import BaseModel, field_validator from db.engine import get_db_session +from db import Thing # -------- CREATE ---------- @@ -61,10 +62,10 @@ def validate_thing_id_exists(cls, thing_id: int) -> int: """ Validate that the thing_id exists in the database. """ - session = get_db_session() - thing = session.get("Thing", thing_id) - if not thing: - raise ValueError(f"Thing with ID {thing_id} does not exist.") + with next(get_db_session()) as session: + thing = session.get(Thing, thing_id) + if not thing: + raise ValueError(f"Thing with ID {thing_id} does not exist.") return thing_id @field_validator("collection_timestamp") From b3011035fe6f31b4252504dd6bd6fd8b5cd66d8f Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 30 Jul 2025 12:16:32 -0600 Subject: [PATCH 27/42] fix: fix errors with 422 custom validation update sample tests --- tests/test_sample.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/tests/test_sample.py b/tests/test_sample.py index 2f505074e..47f72c37e 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -14,6 +14,7 @@ # limitations under the License. # =============================================================================== import pytest +from datetime import datetime from tests import client, thing # noqa: F401 @@ -137,15 +138,24 @@ def test_patch_sample_422_thing_id_not_found(sample_fixture): """ Test updating a sample with a thing_id that does not exist """ + bad_thing_id = 999 response = client.patch( f"/sample/{sample_fixture[1].id}", json={ - "thing_id": 999, + "thing_id": bad_thing_id, }, ) assert response.status_code == 422 data = response.json() - assert data["detail"] == "Thing with ID 999 does not exist." + assert data["detail"] == [ + { + "type": "value_error", + "loc": ["body", "thing_id"], + "msg": f"Value error, Thing with ID {bad_thing_id} does not exist.", + "input": bad_thing_id, + "ctx": {"error": {}}, + } + ] def test_patch_sample_422_invalid_timestamp(sample_fixture): @@ -153,6 +163,9 @@ def test_patch_sample_422_invalid_timestamp(sample_fixture): Test updating a sample with an invalid collection timestamp. """ bad_collection_timestamp = "3500-01-01T00:00:00Z" + bad_collection_timestamp_dt = datetime.fromisoformat( + bad_collection_timestamp.replace("Z", "+00:00") + ) response = client.patch( f"/sample/{sample_fixture[1].id}", json={ @@ -161,10 +174,15 @@ def test_patch_sample_422_invalid_timestamp(sample_fixture): ) assert response.status_code == 422 data = response.json() - assert ( - data["detail"] - == f"Collection timestamp {bad_collection_timestamp} cannot be in the future." - ) + assert data["detail"] == [ + { + "type": "value_error", + "loc": ["body", "collection_timestamp"], + "msg": f"Value error, Collection timestamp {bad_collection_timestamp_dt} cannot be in the future.", + "input": bad_collection_timestamp, + "ctx": {"error": {}}, + } + ] # ============= Get tests for samples ============================================= From 7e73e473850bf5858f6a5c9525154ca18b80cb47 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 30 Jul 2025 12:51:55 -0600 Subject: [PATCH 28/42] fix: cleanup sample after add test --- tests/test_sample.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_sample.py b/tests/test_sample.py index 47f72c37e..4362237b1 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -58,6 +58,10 @@ def test_add_sample(thing): assert data["id"] is not None assert data["thing_id"] == thing.id + with session_ctx() as session: + session.query(Sample).delete() + session.commit() + @pytest.mark.skip(reason="Geochemical sample endpoint not implemented yet") def test_add_geochemical_sample(): From fed04a743cd686229b5d36b14200ec7ac24d941a Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 30 Jul 2025 17:12:43 -0600 Subject: [PATCH 29/42] refactor: remove outdated note about fixtures This note has been addressed in another development branch and is no longer relevant in this context. --- tests/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 0bd404a51..26dfaa85f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -42,12 +42,6 @@ def run_alembic_downgrade(): client = TestClient(app) -""" -REFACTOR TODO: put all fixtures here or in a separate fixtures file. Some fixtures are dependent on others, -such as `sample_fixture` which requires `thing`. By putting them all in one place, we can ensure that -they are properly managed and avoid potential issues with fixture scope and lifecycle. -""" - @pytest.fixture(scope="function") def thing(): From a35b9d166b38095b0d3ee7e8d032dc8c805445a3 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 30 Jul 2025 17:14:22 -0600 Subject: [PATCH 30/42] refactor: put sample fixture in tests/__init__.py for organization --- tests/__init__.py | 16 ++++++++++++++++ tests/test_sample.py | 18 +----------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 26dfaa85f..845f83b32 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -57,4 +57,20 @@ def thing(): session.close() +@pytest.fixture +def sample_fixture(thing): + with session_ctx() as session: + sample = Sample( + thing_id=thing.id, + collection_timestamp="2025-01-01T00:00:00+00:00", + collection_method="manual", + ) + session.add(sample) + session.commit() + session.refresh(sample) + yield thing, sample + session.delete(sample) + session.commit() + + # ============= EOF ============================================= diff --git a/tests/test_sample.py b/tests/test_sample.py index 4362237b1..b25e35864 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -16,7 +16,7 @@ import pytest from datetime import datetime -from tests import client, thing # noqa: F401 +from tests import client, thing, sample_fixture # noqa: F401 import pytest @@ -24,22 +24,6 @@ from db.sample import Sample -@pytest.fixture -def sample_fixture(thing): - with session_ctx() as session: - sample = Sample( - thing_id=thing.id, - collection_timestamp="2025-01-01T00:00:00+00:00", - collection_method="manual", - ) - session.add(sample) - session.commit() - session.refresh(sample) - yield thing, sample - session.delete(sample) - session.commit() - - # ============= Post tests for samples ============================================= def test_add_sample(thing): """ From d484f2c2a2a78b42827fe836f5f2a71eab54c6e0 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 31 Jul 2025 14:54:23 -0600 Subject: [PATCH 31/42] fix: fix artifact from merge conflict --- db/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/db/__init__.py b/db/__init__.py index d69dbb568..40af68323 100644 --- a/db/__init__.py +++ b/db/__init__.py @@ -32,10 +32,6 @@ from db.sample import * from db.sensor.groundwaterlevel import * from db.sensor.sensor import * -from db.series.geochemical import * -from db.series.geothermal import * -from db.series.groundwaterlevel import * -from db.series.series import * from db.thing import * from sqlalchemy import ( From b14a5363de6622a49655992723a83a61822ce61e Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 31 Jul 2025 15:13:08 -0600 Subject: [PATCH 32/42] refactor: move fixtures to conftest.py fixtures were moved to conftest.py to allow for better organization and reuse across multiple test files. This change helps in maintaining cleaner test code and improves the overall structure of the test suite. --- tests/__init__.py | 83 ----------------------------------------------- tests/conftest.py | 62 +++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 83 deletions(-) create mode 100644 tests/conftest.py diff --git a/tests/__init__.py b/tests/__init__.py index 1e4f958e3..c82325097 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -15,15 +15,11 @@ # =============================================================================== from alembic.config import Config from alembic import command -import uuid -import pytest from fastapi.testclient import TestClient from core.app import init_lexicon from main import app -from db import * -from db.engine import session_ctx def run_alembic_upgrade(): @@ -44,83 +40,4 @@ def run_alembic_downgrade(): client = TestClient(app) -@pytest.fixture(scope="session") -def location(): - with session_ctx() as session: - loc = Location(point="SRID=4326;POINT(0 0)") - session.add(loc) - session.commit() - session.refresh(loc) - yield loc - - session.close() - - -@pytest.fixture(scope="session") -def thing(location): - with session_ctx() as session: - # loc = Location(point='SRID=4326;POINT(0 0)') - # session.add(loc) - # session.commit() - # session.refresh(loc) - - wt = add_thing( - session, - { - "location_id": location.id, - "name": "Test Well", - }, - "water well", - ) - - yield wt - - session.close() - - -@pytest.fixture(scope="session") -def sample(thing): - with session_ctx() as session: - sample = Sample( - collection_timestamp="2025-01-01T00:00:00Z", - collection_method="manual", - thing_id=thing.id, - sample_type="groundwater", - sampler="Test Sampler", - ) - session.add(sample) - session.commit() - yield sample - - session.close() - - -@pytest.fixture(scope="session") -def sensor(): - with session_ctx() as session: - sensor = Sensor(name=f"Test Sensor {uuid.uuid4()}") - session.add(sensor) - session.commit() - session.refresh(thing) - yield thing - session.delete(thing) - session.close() - - -@pytest.fixture -def sample_fixture(thing): - with session_ctx() as session: - sample = Sample( - thing_id=thing.id, - collection_timestamp="2025-01-01T00:00:00+00:00", - collection_method="manual", - ) - session.add(sample) - session.commit() - session.refresh(sample) - yield thing, sample - session.delete(sample) - session.commit() - - # ============= EOF ============================================= diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..7c14541df --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,62 @@ +import pytest +import uuid + +from db import * +from db.engine import session_ctx +from services.thing_helper import add_thing + + +@pytest.fixture(scope="session") +def location(): + with session_ctx() as session: + loc = Location(point="SRID=4326;POINT(0 0)") + session.add(loc) + session.commit() + session.refresh(loc) + yield loc + + session.close() + + +@pytest.fixture(scope="session") +def thing(location): + with session_ctx() as session: + wt = add_thing( + session, + { + "location_id": location.id, + "name": "Test Well", + }, + "water well", + ) + + yield wt + + session.close() + + +@pytest.fixture(scope="session") +def sample(thing): + with session_ctx() as session: + sample = Sample( + collection_timestamp="2025-01-01T00:00:00", + collection_method="manual", + thing_id=thing.id, + sample_type="groundwater", + sampler="Test Sampler", + ) + session.add(sample) + session.commit() + yield sample + + session.close() + + +@pytest.fixture(scope="session") +def sensor(): + with session_ctx() as session: + sensor = Sensor(name=f"Test Sensor {uuid.uuid4()}") + session.add(sensor) + session.commit() + yield sensor + session.close() From 1fe76a1beb534de740c8d597dfdb3b63fa9bd952 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 31 Jul 2025 15:13:43 -0600 Subject: [PATCH 33/42] fix: update and clean samples tests for session fixtures Tests that post and patch data need to be undone for fixtures that are used in the full session --- tests/test_sample.py | 51 +++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/tests/test_sample.py b/tests/test_sample.py index e7dce42bd..2eca1de87 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -16,12 +16,9 @@ import pytest from datetime import datetime -from tests import client, thing, sample_fixture # noqa: F401 - - -import pytest from db.engine import session_ctx from db.sample import Sample +from tests import client # ============= Post tests for samples ============================================= @@ -45,8 +42,10 @@ def test_add_sample(thing): assert data["id"] is not None assert data["thing_id"] == thing.id + # cleanup after adding the sample + sample_id = data["id"] with session_ctx() as session: - session.query(Sample).delete() + session.query(Sample).where(Sample.id == sample_id).delete() session.commit() @@ -85,11 +84,13 @@ def test_add_geothermal_sample(): # ============= Patch tests for samples ============================================= -def test_patch_sample(sample_fixture): +def test_patch_sample(sample): """ Test updating a sample in the collaborative network. """ - thing, sample = sample_fixture + original_method_patch = sample.collection_method + original_timestamp_patch = sample.collection_timestamp + collection_method_patch = "continuous" collection_timestamp_patch = "2025-01-02T00:00:00+00:00" response = client.patch( @@ -105,11 +106,18 @@ def test_patch_sample(sample_fixture): "id": sample.id, "collection_timestamp": collection_timestamp_patch.split("+")[0], "collection_method": collection_method_patch, - "thing_id": thing.id, + "thing_id": sample.thing_id, } + # cleanup after patching the sample + with session_ctx() as session: + updated_sample = session.query(Sample).filter(Sample.id == sample.id).one() + updated_sample.collection_method = original_method_patch + updated_sample.collection_timestamp = original_timestamp_patch + session.commit() + -def test_patch_sample_404_not_found(sample_fixture): +def test_patch_sample_404_not_found(sample): """ Test updating a sample that does not exist in the collaborative network. """ @@ -125,13 +133,13 @@ def test_patch_sample_404_not_found(sample_fixture): assert data["detail"] == "Sample with ID 999 not found." -def test_patch_sample_422_thing_id_not_found(sample_fixture): +def test_patch_sample_422_thing_id_not_found(sample): """ Test updating a sample with a thing_id that does not exist """ bad_thing_id = 999 response = client.patch( - f"/sample/{sample_fixture[1].id}", + f"/sample/{sample.id}", json={ "thing_id": bad_thing_id, }, @@ -149,7 +157,7 @@ def test_patch_sample_422_thing_id_not_found(sample_fixture): ] -def test_patch_sample_422_invalid_timestamp(sample_fixture): +def test_patch_sample_422_invalid_timestamp(sample): """ Test updating a sample with an invalid collection timestamp. """ @@ -158,7 +166,7 @@ def test_patch_sample_422_invalid_timestamp(sample_fixture): bad_collection_timestamp.replace("Z", "+00:00") ) response = client.patch( - f"/sample/{sample_fixture[1].id}", + f"/sample/{sample.id}", json={ "collection_timestamp": bad_collection_timestamp, # Invalid date }, @@ -177,20 +185,20 @@ def test_patch_sample_422_invalid_timestamp(sample_fixture): # ============= Get tests for samples ============================================= -def test_get_samples(sample_fixture): +def test_get_samples(sample): """ Test retrieving samples from the collaborative network. """ - thing, sample = sample_fixture response = client.get("/sample") assert response.status_code == 200 data = response.json() + print(data) assert data["items"] == [ { "id": sample.id, - "collection_timestamp": sample.collection_timestamp.isoformat(), + "collection_timestamp": sample.collection_timestamp, "collection_method": sample.collection_method, - "thing_id": thing.id, + "thing_id": sample.thing_id, } ] @@ -219,23 +227,22 @@ def test_get_geothermal_samples(): assert len(data["items"]) > 0 -def test_get_sample_by_id_200(sample_fixture): +def test_get_sample_by_id(sample): """ Test retrieving a sample from the collaborative network. """ - thing, sample = sample_fixture response = client.get(f"/sample/{sample.id}") assert response.status_code == 200 data = response.json() assert data == { "id": sample.id, - "collection_timestamp": sample.collection_timestamp.isoformat(), + "collection_timestamp": sample.collection_timestamp, "collection_method": sample.collection_method, - "thing_id": thing.id, + "thing_id": sample.thing_id, } -def test_get_sample_by_id_404_not_found(sample_fixture): +def test_get_sample_by_id_404_not_found(sample): """ Test retrieving a sample from the collaborative network. """ From 20b4a12e30d9e332189d77fef80be86c60d8467b Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 31 Jul 2025 15:42:07 -0600 Subject: [PATCH 34/42] fix: remove debugging print statement --- tests/test_sample.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_sample.py b/tests/test_sample.py index 2eca1de87..76c7ab81b 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -192,7 +192,6 @@ def test_get_samples(sample): response = client.get("/sample") assert response.status_code == 200 data = response.json() - print(data) assert data["items"] == [ { "id": sample.id, From 3ba7dfa896919312a37ca64afe853448403ae2fb Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 31 Jul 2025 15:45:55 -0600 Subject: [PATCH 35/42] fix: remove fixture imports now they are in conftest --- tests/test_asset.py | 2 +- tests/test_group.py | 2 +- tests/test_observation.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_asset.py b/tests/test_asset.py index b5dfcf9e4..90803ea72 100644 --- a/tests/test_asset.py +++ b/tests/test_asset.py @@ -15,7 +15,7 @@ # =============================================================================== from api.asset import get_storage_bucket from core.app import app -from tests import client, thing, location +from tests import client class MockBlob: diff --git a/tests/test_group.py b/tests/test_group.py index 800fa78b1..6b7d88e71 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -1,5 +1,5 @@ import pytest -from tests import client, thing, location +from tests import client # ADD tests ====================================================== diff --git a/tests/test_observation.py b/tests/test_observation.py index 8dc31499e..cd43afba2 100644 --- a/tests/test_observation.py +++ b/tests/test_observation.py @@ -14,7 +14,7 @@ # limitations under the License. # =============================================================================== -from tests import client, sample, sensor, thing, location +from tests import client import pytest From 2d08c7437cf28c9655877edc1f783d23a98a2e94 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 31 Jul 2025 15:50:01 -0600 Subject: [PATCH 36/42] fix: fix search test cases --- tests/test_search.py | 4 +--- things.dbf | Bin 0 -> 253 bytes things.shp | Bin 0 -> 184 bytes things.shx | Bin 0 -> 124 bytes things.zip | Bin 0 -> 871 bytes 5 files changed, 1 insertion(+), 3 deletions(-) create mode 100644 things.dbf create mode 100644 things.shp create mode 100644 things.shx create mode 100644 things.zip diff --git a/tests/test_search.py b/tests/test_search.py index 9f1927de7..047f83564 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # =============================================================================== -import pprint - import pytest from sqlalchemy import select @@ -30,7 +28,7 @@ def test_search_api(): data = response.json() assert isinstance(data, list) - assert len(data) == 5 + assert len(data) == 3 @pytest.mark.skip(reason="This test is not working .") diff --git a/things.dbf b/things.dbf new file mode 100644 index 0000000000000000000000000000000000000000..c3931d4e302a37a5780544e2bb4f8bdac4da9518 GIT binary patch literal 253 zcmZRsWtV4WU|>jOFaeU7ATtFn<^y6e!nqJeUSe)4RLB`b8$mfRidP{dB(=CiAv`rF ZM}c?{l98F0u3$*K2B1oa9wQR;0089*9XkL3 literal 0 HcmV?d00001 diff --git a/things.shp b/things.shp new file mode 100644 index 0000000000000000000000000000000000000000..f7b0ec16609de19ce98aa013f1f8a08c11b00127 GIT binary patch literal 184 zcmZQzQ0HR64q{#~GcYh>mjjBLI6$OeG){#e36L>dILu;#$r*!ziUW)WiUVDMtPf_F Gi30%H(gvLX literal 0 HcmV?d00001 diff --git a/things.shx b/things.shx new file mode 100644 index 0000000000000000000000000000000000000000..8fc8ccaa384a42c7db05f31344cec2f7ca9d2254 GIT binary patch literal 124 rcmZQzQ0HR64(whqGcYh>mjjBLI6$OeG){#e2_ql|+2a7E{XjGT$zcN` literal 0 HcmV?d00001 diff --git a/things.zip b/things.zip new file mode 100644 index 0000000000000000000000000000000000000000..957d447971b4875f5e18d4f975569db79d2158dd GIT binary patch literal 871 zcmWIWW@Zs#00Hy5|52e+KkeNCWP>ml5SL_R=A{?w6=xJMFsOqBuz{Et%nS^S*kr(B zCJqoO7>!dQNCIRG7Y?(SU~-8r6;4j!HZH1qy>Ok{eT!(wJ-6@F+F6IMbp@(B$ zVs0u_g)@i-IR+Wi_@% literal 0 HcmV?d00001 From 8406c8c415a1753d71547c57cc278fb6286e62af Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 31 Jul 2025 16:02:49 -0600 Subject: [PATCH 37/42] fix: delete shapefile files created by geospatial test --- tests/test_geospatial.py | 4 ++++ things.dbf | Bin 253 -> 0 bytes things.shp | Bin 184 -> 0 bytes things.shx | Bin 124 -> 0 bytes things.zip | Bin 871 -> 0 bytes 5 files changed, 4 insertions(+) delete mode 100644 things.dbf delete mode 100644 things.shp delete mode 100644 things.shx delete mode 100644 things.zip diff --git a/tests/test_geospatial.py b/tests/test_geospatial.py index ba1c5bec0..9f54bedd3 100644 --- a/tests/test_geospatial.py +++ b/tests/test_geospatial.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # =============================================================================== +from pathlib import Path import pytest from db import Thing, Location, LocationThingAssociation @@ -76,6 +77,9 @@ def test_get_shapefile(): 'attachment; filename="things.zip"' == response.headers["Content-Disposition"] ) + for shapefile_ending in [".shp", ".shx", ".dbf", ".prj", ".zip"]: + Path(f"things{shapefile_ending}").unlink(missing_ok=True) + @pytest.mark.skip def test_get_locations_expand(): diff --git a/things.dbf b/things.dbf deleted file mode 100644 index c3931d4e302a37a5780544e2bb4f8bdac4da9518..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 253 zcmZRsWtV4WU|>jOFaeU7ATtFn<^y6e!nqJeUSe)4RLB`b8$mfRidP{dB(=CiAv`rF ZM}c?{l98F0u3$*K2B1oa9wQR;0089*9XkL3 diff --git a/things.shp b/things.shp deleted file mode 100644 index f7b0ec16609de19ce98aa013f1f8a08c11b00127..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 184 zcmZQzQ0HR64q{#~GcYh>mjjBLI6$OeG){#e36L>dILu;#$r*!ziUW)WiUVDMtPf_F Gi30%H(gvLX diff --git a/things.shx b/things.shx deleted file mode 100644 index 8fc8ccaa384a42c7db05f31344cec2f7ca9d2254..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 124 rcmZQzQ0HR64(whqGcYh>mjjBLI6$OeG){#e2_ql|+2a7E{XjGT$zcN` diff --git a/things.zip b/things.zip deleted file mode 100644 index 957d447971b4875f5e18d4f975569db79d2158dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 871 zcmWIWW@Zs#00Hy5|52e+KkeNCWP>ml5SL_R=A{?w6=xJMFsOqBuz{Et%nS^S*kr(B zCJqoO7>!dQNCIRG7Y?(SU~-8r6;4j!HZH1qy>Ok{eT!(wJ-6@F+F6IMbp@(B$ zVs0u_g)@i-IR+Wi_@% From 21e6c9a4563bd093e65ebd286ac30ed5d2e9db4b Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 31 Jul 2025 16:09:51 -0600 Subject: [PATCH 38/42] fix: use session fixtures for contact tests | cleanup post tests --- tests/test_contact.py | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/tests/test_contact.py b/tests/test_contact.py index 33bfcbdd7..f35f4c9f6 100644 --- a/tests/test_contact.py +++ b/tests/test_contact.py @@ -1,10 +1,8 @@ # from fastapi.testclient import TestClient # from main import app # from models import Base, engine -import pytest - -from db import Thing -from db.engine import get_db_session, session_ctx +from db import Contact +from db.engine import session_ctx # Base.metadata.drop_all(engine) # Base.metadata.create_all(engine) @@ -17,24 +15,13 @@ # ADD tests ====================================================== -@pytest.fixture(scope="function") -def thing(): - with session_ctx() as session: - thing = Thing(name="Test Thing", thing_type="water well") - session.add(thing) - session.commit() - yield - - session.close() - - def test_add_contact(thing): response = client.post( "/contact", json={ "name": "Test Contact", "role": "Owner", - "thing_id": 1, + "thing_id": thing.id, "emails": [{"email": "fasdfasdf@gmail.com", "email_type": "Primary"}], "phones": [{"phone_number": "+12345678901", "phone_type": "Primary"}], "addresses": [ @@ -63,6 +50,12 @@ def test_add_contact(thing): assert len(data["addresses"]) == 1 assert data["addresses"][0]["address_line_1"] == "123 Main St" + # cleanup after adding the contact + contact_id = data["id"] + with session_ctx() as session: + session.select(Contact).where(Contact.id == contact_id).delete() + session.commit() + # assert data["email"] == "fasdfasdf@gmail.com" # for i in range(2, 5): @@ -83,7 +76,7 @@ def test_add_contact(thing): # assert data["phone"] == f"+1234567890{i}" -def test_phone_validation_fail(): +def test_phone_validation_fail(thing): for phone in [ "definitely not a phone", # "1234567890", @@ -100,7 +93,7 @@ def test_phone_validation_fail(): "/contact", json={ "name": "Test Contact 2", - "thing_id": 1, + "thing_id": thing.id, "role": "Primary", "emails": [{"email": "fasdfasdf@gmail.com", "email_type": "Primary"}], "phones": [{"phone_number": phone, "phone_type": "Primary"}], @@ -124,7 +117,7 @@ def test_phone_validation_fail(): assert detail["msg"] == f"Value error, Invalid phone number. {phone}" -def test_email_validation_fail(): +def test_email_validation_fail(thing): for email in [ "", @@ -137,7 +130,7 @@ def test_email_validation_fail(): "/contact", json={ "name": "Test ContactX", - "thing_id": 1, + "thing_id": thing.id, "role": "Primary", "emails": [{"email": email, "email_type": "Primary"}], "phones": [{"phone_number": "+12345678901", "phone_type": "Primary"}], From eb0d1fa4a2c3790bec9fd6d9953688877aaf0fb4 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 31 Jul 2025 16:12:45 -0600 Subject: [PATCH 39/42] fix: run search test on relevant fixtures --- tests/test_search.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_search.py b/tests/test_search.py index 047f83564..cb4364d70 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -22,13 +22,13 @@ from tests import client -def test_search_api(): +def test_search_api(thing, sample): response = client.get("/search", params={"q": "Test"}) assert response.status_code == 200 data = response.json() assert isinstance(data, list) - assert len(data) == 3 + assert len(data) == 2 @pytest.mark.skip(reason="This test is not working .") From 6aa3e8fda4565f58d2de8bf8cc2a57f78f26f3e3 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 31 Jul 2025 16:23:16 -0600 Subject: [PATCH 40/42] fix: temporary fix until all fixtures are used --- tests/test_contact.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/test_contact.py b/tests/test_contact.py index f35f4c9f6..7657fbb27 100644 --- a/tests/test_contact.py +++ b/tests/test_contact.py @@ -1,9 +1,6 @@ # from fastapi.testclient import TestClient # from main import app # from models import Base, engine -from db import Contact -from db.engine import session_ctx - # Base.metadata.drop_all(engine) # Base.metadata.create_all(engine) @@ -50,12 +47,6 @@ def test_add_contact(thing): assert len(data["addresses"]) == 1 assert data["addresses"][0]["address_line_1"] == "123 Main St" - # cleanup after adding the contact - contact_id = data["id"] - with session_ctx() as session: - session.select(Contact).where(Contact.id == contact_id).delete() - session.commit() - # assert data["email"] == "fasdfasdf@gmail.com" # for i in range(2, 5): From 717dde8a96496d56119d47166544ce6eb233b272 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 31 Jul 2025 17:19:58 -0600 Subject: [PATCH 41/42] refactor: drop and create all tables for API testing suite These tests are for the API, not for the database, so no need to use Alembic migrations --- tests/__init__.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index c82325097..87d334000 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -13,27 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # =============================================================================== -from alembic.config import Config -from alembic import command - from fastapi.testclient import TestClient from core.app import init_lexicon +from db import Base +from db.engine import engine from main import app -def run_alembic_upgrade(): - alembic_cfg = Config("alembic.ini") - command.upgrade(alembic_cfg, "head") - - -def run_alembic_downgrade(): - alembic_cfg = Config("alembic.ini") - command.downgrade(alembic_cfg, "base") - - -run_alembic_downgrade() -run_alembic_upgrade() +Base.metadata.drop_all(engine) +Base.metadata.create_all(engine) init_lexicon() From 57b937ae9068c65fe0ceb994f80ea94d01f3d7ae Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 31 Jul 2025 17:24:19 -0600 Subject: [PATCH 42/42] refactor: address PR 49 feedback and use session_dependency Use this instead of session: Depends(get_db_session) in POST /sample endpoint to keep a consistent style. --- api/sample.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/sample.py b/api/sample.py index 4efa270f5..e36daafd6 100644 --- a/api/sample.py +++ b/api/sample.py @@ -35,7 +35,7 @@ # ============= Post ============================================= @router.post("", status_code=HTTP_201_CREATED) -def add_sample(sample_data: CreateSample, session: Session = Depends(get_db_session)): +def add_sample(sample_data: CreateSample, session: session_dependency): """ Endpoint to add a sample. """