Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ jobs:
MODE: development
POSTGRES_HOST: localhost
POSTGRES_PORT: 5432
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
DB_DRIVER: postgres
# SPATIALITE_LIBRARY_PATH: /usr/lib/x86_64-linux-gnu/mod_spatialite.so
Expand Down
7 changes: 5 additions & 2 deletions db/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
# ===============================================================================

import asyncio
from dotenv import load_dotenv
import getpass
import os
from contextlib import contextmanager

from dotenv import load_dotenv
from sqlalchemy import (
create_engine,
)
Expand Down Expand Up @@ -108,7 +110,8 @@ def getconn():
password = os.environ.get("POSTGRES_PASSWORD", "")
host = os.environ.get("POSTGRES_HOST", "localhost")
port = os.environ.get("POSTGRES_PORT", "5432")
user = os.environ.get("POSTGRES_USER", "postgres")
# Default to current OS user if POSTGRES_USER not set or empty
user = os.environ.get("POSTGRES_USER", "").strip() or getpass.getuser()
Comment thread
kbighorse marked this conversation as resolved.
name = os.environ.get("POSTGRES_DB", "postgres")

auth = f"{user}:{password}@" if user and password else ""
Expand Down
4 changes: 2 additions & 2 deletions db/geochronology.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
# limitations under the License.
# ===============================================================================
from db.base import AutoBaseMixin, Base, lexicon_term
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Float, Boolean
from sqlalchemy.orm import relationship, backref, mapped_column
from sqlalchemy import Integer, Float
from sqlalchemy.orm import mapped_column


class GeochronologyAge(Base, AutoBaseMixin):
Expand Down
1 change: 0 additions & 1 deletion manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

import click
from core.initializers import init_lexicon
from db.engine import session_ctx


# from migration.migration2 import migrate_wells, migrate_water_levels
Expand Down
41 changes: 40 additions & 1 deletion schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ===============================================================================
from datetime import datetime, timezone
from pydantic import BaseModel, ConfigDict, AwareDatetime
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import core_schema


class ResourceNotFoundResponse(BaseModel):
Expand All @@ -28,9 +31,45 @@ class BaseUpdateModel(BaseCreateModel):
release_status: str | None = None


# Custom type for UTC datetime serialization
class UTCAwareDatetime(AwareDatetime):
"""Custom datetime type that always serializes to UTC with 'Z' suffix."""

@classmethod
def __get_pydantic_core_schema__(
cls, source_type, handler
) -> core_schema.CoreSchema:
def serialize_dt(value: datetime) -> str:
"""Serialize datetime to UTC format with Z suffix."""
if value is None:
return None
# Convert to UTC if not already
if value.tzinfo != timezone.utc:
value = value.astimezone(timezone.utc)
# Format with Z suffix
return value.strftime("%Y-%m-%dT%H:%M:%SZ")

# Use generate_schema instead of calling handler directly
python_schema = handler.generate_schema(datetime)
return core_schema.no_info_after_validator_function(
lambda x: x,
python_schema,
serialization=core_schema.plain_serializer_function_ser_schema(
serialize_dt,
return_schema=core_schema.str_schema(),
),
)

@classmethod
def __get_pydantic_json_schema__(
cls, core_schema: core_schema.CoreSchema, handler
) -> JsonSchemaValue:
return handler(core_schema)


class BaseResponseModel(BaseModel):
id: int # every ORM model should have an id field
created_at: AwareDatetime
created_at: UTCAwareDatetime
release_status: str

model_config = ConfigDict(
Expand Down
5 changes: 3 additions & 2 deletions schemas/lexicon.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ===============================================================================
from pydantic import BaseModel, ConfigDict, AwareDatetime
from pydantic import BaseModel, ConfigDict
from typing import List
from schemas import UTCAwareDatetime


# -------- CREATE ----------
Expand Down Expand Up @@ -74,7 +75,7 @@ class UpdateLexiconTriple(BaseModel):

class BaseLexiconResponse(BaseModel):
id: int
created_at: AwareDatetime
created_at: UTCAwareDatetime

model_config = ConfigDict(
from_attributes=True,
Expand Down
9 changes: 7 additions & 2 deletions schemas/observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@
from typing import Annotated
from typing_extensions import Self

from schemas import BaseCreateModel, BaseUpdateModel, BaseResponseModel
from schemas import (
BaseCreateModel,
BaseUpdateModel,
BaseResponseModel,
UTCAwareDatetime,
)
from schemas.parameter import ParameterResponse


Expand Down Expand Up @@ -103,7 +108,7 @@ class UpdateWaterChemistryObservation(UpdateBaseObservation):
class BaseObservationResponse(BaseResponseModel):
sample_id: int
sensor_id: int | None
observation_datetime: AwareDatetime
observation_datetime: UTCAwareDatetime
parameter: ParameterResponse
release_status: str
value: float | None
Expand Down
9 changes: 7 additions & 2 deletions schemas/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@
from typing import Annotated
from typing_extensions import Self

from schemas import BaseCreateModel, BaseUpdateModel, BaseResponseModel
from schemas import (
BaseCreateModel,
BaseUpdateModel,
BaseResponseModel,
UTCAwareDatetime,
)
from schemas.thing import ThingResponse
from schemas.field import FieldEventResponse, FieldActivityResponse
from schemas.contact import ContactResponse
Expand Down Expand Up @@ -124,7 +129,7 @@ class SampleResponse(BaseResponseModel):
field_event: FieldEventResponse
field_activity: FieldActivityResponse
contact: ContactResponse
sample_date: Annotated[AwareDatetime, PastDatetime()]
sample_date: UTCAwareDatetime
sample_name: str
sample_matrix: str
sample_method: str
Expand Down
4 changes: 0 additions & 4 deletions services/validation/chemistry.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ===============================================================================
from db.engine import database_sessionmaker
from db.lexicon import Lexicon

# from schemas.create.chemistry import CreateWaterChemistryAnalysis
from services.validation import get_category


# async def validate_analyte(analysis_data: CreateWaterChemistryAnalysis):
Expand Down
27 changes: 26 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,40 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ===============================================================================
import os

# Load .env file BEFORE importing anything else
# Use override=True to override conflicting shell environment variables
from dotenv import load_dotenv

load_dotenv(override=True)

# Set timezone to UTC for consistent datetime handling in tests
os.environ["TZ"] = "UTC"

# Also set time.tzset() to apply the timezone change
import time

time.tzset()

from fastapi.testclient import TestClient
from sqlalchemy import text

from core.initializers import init_lexicon, init_parameter
from db import Base, Parameter
from db.engine import engine, session_ctx
from main import app


Base.metadata.drop_all(engine)
# Force clean database state by dropping and recreating schema
# This ensures test isolation similar to Docker environment
with engine.connect() as conn:
# Drop all tables with CASCADE to handle foreign key dependencies
conn.execute(text("DROP SCHEMA IF EXISTS public CASCADE"))
conn.execute(text("CREATE SCHEMA public"))
conn.execute(text("CREATE EXTENSION IF NOT EXISTS postgis"))
conn.commit()

Base.metadata.create_all(engine)

init_lexicon()
Expand Down
15 changes: 10 additions & 5 deletions tests/test_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from tests import client, cleanup_post_test, override_authentication, cleanup_patch_test

import pytest
from datetime import timezone
from unittest.mock import patch

# CLASSES, FIXTURES, AND FUNCTIONS =============================================
Expand Down Expand Up @@ -147,9 +148,9 @@ def test_get_assets(asset, asset_with_associated_thing):
data = response.json()
assert data["total"] == 2
assert data["items"][0]["id"] == asset.id
assert data["items"][0]["created_at"] == asset.created_at.isoformat().replace(
"+00:00", "Z"
)
assert data["items"][0]["created_at"] == asset.created_at.astimezone(
timezone.utc
).strftime("%Y-%m-%dT%H:%M:%SZ")
assert data["items"][0]["release_status"] == asset.release_status
assert data["items"][0]["name"] == asset.name
assert data["items"][0]["label"] == asset.label
Expand All @@ -163,7 +164,9 @@ def test_get_assets(asset, asset_with_associated_thing):
assert data["items"][1]["id"] == asset_with_associated_thing.id
assert data["items"][1][
"created_at"
] == asset_with_associated_thing.created_at.isoformat().replace("+00:00", "Z")
] == asset_with_associated_thing.created_at.astimezone(timezone.utc).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
assert (
data["items"][1]["release_status"] == asset_with_associated_thing.release_status
)
Expand Down Expand Up @@ -199,7 +202,9 @@ def test_get_asset_by_id(asset):
assert response.status_code == 200
data = response.json()
assert data["id"] == asset.id
assert data["created_at"] == asset.created_at.isoformat().replace("+00:00", "Z")
assert data["created_at"] == asset.created_at.astimezone(timezone.utc).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
assert data["release_status"] == asset.release_status
assert data["name"] == asset.name
assert data["label"] == asset.label
Expand Down
Loading
Loading