From 3bb8f70b409699ff66869ec6991fdd7daff84e05 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:45:28 +0000 Subject: [PATCH 1/4] Initial plan From 27711c62092d3b14e6f3f01d9f517447b800c49d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:55:01 +0000 Subject: [PATCH 2/4] Create new models, schemas, services and migration for preference refactoring Co-authored-by: zlendo1 <115471708+zlendo1@users.noreply.github.com> --- alembic/env.py | 2 + ...6cb_replace_preference_with_global_and_.py | 81 ++++++++++ app/api/v1/endpoints/preferences.py | 25 ++-- app/api/v1/endpoints/routes.py | 6 +- app/models/__init__.py | 4 + app/models/global_preference.py | 23 +++ app/models/route_preference.py | 39 +++++ app/models/user.py | 8 +- app/schemas/global_preference.py | 20 +++ app/schemas/route_preference.py | 24 +++ app/services/global_preference_service.py | 137 +++++++++++++++++ app/services/route_preference_service.py | 141 ++++++++++++++++++ tests/endpoints/test_preferences.py | 8 +- .../endpoints/test_routes_with_preferences.py | 8 +- tests/services/test_preference_service.py | 44 +++--- 15 files changed, 523 insertions(+), 47 deletions(-) create mode 100644 alembic/versions/172fc263f6cb_replace_preference_with_global_and_.py create mode 100644 app/models/global_preference.py create mode 100644 app/models/route_preference.py create mode 100644 app/schemas/global_preference.py create mode 100644 app/schemas/route_preference.py create mode 100644 app/services/global_preference_service.py create mode 100644 app/services/route_preference_service.py diff --git a/alembic/env.py b/alembic/env.py index 36a84c7..f5f2659 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -8,6 +8,8 @@ # Import your Base and all models from app.db.database import Base from app.models.user import User # noqa: F401 - Import to register with Base +from app.models.global_preference import GlobalPreference # noqa: F401 +from app.models.route_preference import RoutePreference # noqa: F401 # this is the Alembic Config object, which provides diff --git a/alembic/versions/172fc263f6cb_replace_preference_with_global_and_.py b/alembic/versions/172fc263f6cb_replace_preference_with_global_and_.py new file mode 100644 index 0000000..c6225ca --- /dev/null +++ b/alembic/versions/172fc263f6cb_replace_preference_with_global_and_.py @@ -0,0 +1,81 @@ +"""replace_preference_with_global_and_route_preferences + +Revision ID: 172fc263f6cb +Revises: ecd62863c9d0 +Create Date: 2025-11-13 11:51:48.867956 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '172fc263f6cb' +down_revision: Union[str, Sequence[str], None] = 'ecd62863c9d0' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # Drop the old preference table + op.drop_index(op.f('ix_preference_user_id'), table_name='preference') + op.drop_index(op.f('ix_preference_id'), table_name='preference') + op.drop_table('preference') + + # Create the new global_preferences table + op.create_table('global_preferences', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('prompt', sa.String(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_global_preferences_id'), 'global_preferences', ['id'], unique=False) + op.create_index(op.f('ix_global_preferences_user_id'), 'global_preferences', ['user_id'], unique=False) + + # Create the new route_preferences table + op.create_table('route_preferences', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('prompt', sa.String(), nullable=False), + sa.Column('from_latitude', sa.Float(), nullable=False), + sa.Column('from_longitude', sa.Float(), nullable=False), + sa.Column('to_latitude', sa.Float(), nullable=False), + sa.Column('to_longitude', sa.Float(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_route_preferences_id'), 'route_preferences', ['id'], unique=False) + op.create_index(op.f('ix_route_preferences_user_id'), 'route_preferences', ['user_id'], unique=False) + + +def downgrade() -> None: + """Downgrade schema.""" + # Drop the new tables + op.drop_index(op.f('ix_route_preferences_user_id'), table_name='route_preferences') + op.drop_index(op.f('ix_route_preferences_id'), table_name='route_preferences') + op.drop_table('route_preferences') + + op.drop_index(op.f('ix_global_preferences_user_id'), table_name='global_preferences') + op.drop_index(op.f('ix_global_preferences_id'), table_name='global_preferences') + op.drop_table('global_preferences') + + # Recreate the old preference table + op.create_table('preference', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('prompt', sa.String(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_preference_id'), 'preference', ['id'], unique=False) + op.create_index(op.f('ix_preference_user_id'), 'preference', ['user_id'], unique=False) diff --git a/app/api/v1/endpoints/preferences.py b/app/api/v1/endpoints/preferences.py index 92f233f..bc7f1df 100644 --- a/app/api/v1/endpoints/preferences.py +++ b/app/api/v1/endpoints/preferences.py @@ -5,37 +5,40 @@ from app.db.database import get_db from app.models.user import User -from app.schemas.preference import PreferenceCreate, PreferenceResponse +from app.schemas.global_preference import ( + GlobalPreferenceCreate, + GlobalPreferenceResponse, +) from app.services.auth_service import auth_service -from app.services.preference_service import preference_service +from app.services.global_preference_service import global_preference_service router = APIRouter() -@router.get("", response_model=List[PreferenceResponse]) +@router.get("", response_model=List[GlobalPreferenceResponse]) async def get_user_preferences( current_user: User = Depends(auth_service.get_current_user), db: Session = Depends(get_db), ) -> Any: """ - Get all preferences for the authenticated user. + Get all global preferences for the authenticated user. """ - preferences = preference_service.get_user_preferences( + preferences = global_preference_service.get_user_preferences( db, int(current_user.id) ) return preferences -@router.post("", response_model=PreferenceResponse, status_code=201) +@router.post("", response_model=GlobalPreferenceResponse, status_code=201) async def create_preference( - preference_in: PreferenceCreate, + preference_in: GlobalPreferenceCreate, current_user: User = Depends(auth_service.get_current_user), db: Session = Depends(get_db), ) -> Any: """ - Create a new preference for the authenticated user. + Create a new global preference for the authenticated user. """ - preference = preference_service.create_preference( + preference = global_preference_service.create_preference( db, int(current_user.id), preference_in ) return preference @@ -48,8 +51,8 @@ async def delete_preference( db: Session = Depends(get_db), ) -> None: """ - Delete a preference for the authenticated user. + Delete a global preference for the authenticated user. """ - preference_service.delete_preference( + global_preference_service.delete_preference( db, int(current_user.id), preference_id ) diff --git a/app/api/v1/endpoints/routes.py b/app/api/v1/endpoints/routes.py index 80244c3..d114447 100644 --- a/app/api/v1/endpoints/routes.py +++ b/app/api/v1/endpoints/routes.py @@ -18,7 +18,7 @@ from app.schemas.routes import RouteSearchRequest, RouteSearchResponse from app.services.ai_agents_service import ai_agents_service from app.services.auth_service import auth_service -from app.services.preference_service import preference_service +from app.services.global_preference_service import global_preference_service from app.services.routing_service import ( RoutingAPIError, RoutingDataError, @@ -76,9 +76,9 @@ async def search_routes( for pref in request.preferences: user_preferences.append({"prompt": pref}) - # 2. Add stored preferences from authenticated user + # 2. Add stored global preferences from authenticated user try: - stored_prefs = preference_service.get_user_preferences( + stored_prefs = global_preference_service.get_user_preferences( db, int(current_user.id) ) for pref in stored_prefs: diff --git a/app/models/__init__.py b/app/models/__init__.py index e69de29..d1d4436 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -0,0 +1,4 @@ +from app.models.global_preference import GlobalPreference +from app.models.route_preference import RoutePreference + +__all__ = ["GlobalPreference", "RoutePreference"] diff --git a/app/models/global_preference.py b/app/models/global_preference.py new file mode 100644 index 0000000..f1ea66e --- /dev/null +++ b/app/models/global_preference.py @@ -0,0 +1,23 @@ +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func + +from app.db.database import Base + + +class GlobalPreference(Base): + __tablename__ = "global_preferences" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column( + Integer, ForeignKey("users.id"), nullable=False, index=True + ) + prompt = Column(String, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + user = relationship("User", back_populates="global_preferences") + + def __init__(self, user_id, prompt): + self.user_id = user_id + self.prompt = prompt diff --git a/app/models/route_preference.py b/app/models/route_preference.py new file mode 100644 index 0000000..ab39334 --- /dev/null +++ b/app/models/route_preference.py @@ -0,0 +1,39 @@ +from sqlalchemy import Column, DateTime, Float, ForeignKey, Integer, String +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func + +from app.db.database import Base + + +class RoutePreference(Base): + __tablename__ = "route_preferences" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column( + Integer, ForeignKey("users.id"), nullable=False, index=True + ) + prompt = Column(String, nullable=False) + from_latitude = Column(Float, nullable=False) + from_longitude = Column(Float, nullable=False) + to_latitude = Column(Float, nullable=False) + to_longitude = Column(Float, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + user = relationship("User", back_populates="route_preferences") + + def __init__( + self, + user_id, + prompt, + from_latitude, + from_longitude, + to_latitude, + to_longitude, + ): + self.user_id = user_id + self.prompt = prompt + self.from_latitude = from_latitude + self.from_longitude = from_longitude + self.to_latitude = to_latitude + self.to_longitude = to_longitude diff --git a/app/models/user.py b/app/models/user.py index 540b3a5..35ccd6a 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -3,7 +3,6 @@ from sqlalchemy.sql import func from app.db.database import Base -from app.models.preference import Preference class User(Base): @@ -15,8 +14,11 @@ class User(Base): created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) - preferences = relationship( - Preference, back_populates="user", cascade="all, delete-orphan" + global_preferences = relationship( + "GlobalPreference", back_populates="user", cascade="all, delete-orphan" + ) + route_preferences = relationship( + "RoutePreference", back_populates="user", cascade="all, delete-orphan" ) def __init__(self, username, hashed_password): diff --git a/app/schemas/global_preference.py b/app/schemas/global_preference.py new file mode 100644 index 0000000..d72e848 --- /dev/null +++ b/app/schemas/global_preference.py @@ -0,0 +1,20 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict + + +class GlobalPreferenceBase(BaseModel): + prompt: str + + +class GlobalPreferenceCreate(GlobalPreferenceBase): + pass + + +class GlobalPreferenceResponse(GlobalPreferenceBase): + id: int + created_at: datetime + updated_at: Optional[datetime] = None + + model_config = ConfigDict(from_attributes=True) diff --git a/app/schemas/route_preference.py b/app/schemas/route_preference.py new file mode 100644 index 0000000..338e8a2 --- /dev/null +++ b/app/schemas/route_preference.py @@ -0,0 +1,24 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + + +class RoutePreferenceBase(BaseModel): + prompt: str + from_latitude: float = Field(..., ge=-90, le=90) + from_longitude: float = Field(..., ge=-180, le=180) + to_latitude: float = Field(..., ge=-90, le=90) + to_longitude: float = Field(..., ge=-180, le=180) + + +class RoutePreferenceCreate(RoutePreferenceBase): + pass + + +class RoutePreferenceResponse(RoutePreferenceBase): + id: int + created_at: datetime + updated_at: Optional[datetime] = None + + model_config = ConfigDict(from_attributes=True) diff --git a/app/services/global_preference_service.py b/app/services/global_preference_service.py new file mode 100644 index 0000000..b8e9ddb --- /dev/null +++ b/app/services/global_preference_service.py @@ -0,0 +1,137 @@ +""" +Global preference service for handling global preference-related business logic. +""" + +from typing import List, Optional + +from fastapi import HTTPException, status +from sqlalchemy.orm import Session + +from app.models.global_preference import GlobalPreference +from app.schemas.global_preference import GlobalPreferenceCreate + + +class GlobalPreferenceService: + """Service for handling global preference operations.""" + + @staticmethod + def get_user_preferences( + db: Session, user_id: int + ) -> List[GlobalPreference]: + """ + Get all global preferences for a user. + + Args: + db: Database session + user_id: User ID to get preferences for + + Returns: + List of GlobalPreference objects + """ + return ( + db.query(GlobalPreference) + .filter(GlobalPreference.user_id == user_id) + .all() + ) + + @staticmethod + def get_preference_by_id( + db: Session, preference_id: int + ) -> Optional[GlobalPreference]: + """ + Get a global preference by ID. + + Args: + db: Database session + preference_id: Preference ID to search for + + Returns: + GlobalPreference object if found, None otherwise + """ + return ( + db.query(GlobalPreference) + .filter(GlobalPreference.id == preference_id) + .first() + ) + + @staticmethod + def create_preference( + db: Session, user_id: int, preference_in: GlobalPreferenceCreate + ) -> GlobalPreference: + """ + Create a new global preference for a user. + + Args: + db: Database session + user_id: User ID to create preference for + preference_in: Preference creation data + + Returns: + Created GlobalPreference object + + Raises: + HTTPException: If validation fails + """ + # Validate prompt is not empty + if not preference_in.prompt.strip(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Preference prompt cannot be empty", + ) + + # Create preference + preference = GlobalPreference( + user_id=user_id, + prompt=preference_in.prompt.strip(), + ) + db.add(preference) + db.commit() + db.refresh(preference) + + return preference + + @staticmethod + def delete_preference( + db: Session, user_id: int, preference_id: int + ) -> bool: + """ + Delete a global preference for a user. + + Args: + db: Database session + user_id: User ID who owns the preference + preference_id: Preference ID to delete + + Returns: + True if deleted successfully + + Raises: + HTTPException: If preference not found or doesn't belong to user + """ + preference = ( + db.query(GlobalPreference) + .filter(GlobalPreference.id == preference_id) + .first() + ) + + if not preference: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Preference not found", + ) + + # Verify the preference belongs to the user + if preference.user_id != user_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not authorized to delete this preference", + ) + + db.delete(preference) + db.commit() + + return True + + +# Create a singleton instance +global_preference_service = GlobalPreferenceService() diff --git a/app/services/route_preference_service.py b/app/services/route_preference_service.py new file mode 100644 index 0000000..b736f2d --- /dev/null +++ b/app/services/route_preference_service.py @@ -0,0 +1,141 @@ +""" +Route preference service for handling route-specific preference business logic. +""" + +from typing import List, Optional + +from fastapi import HTTPException, status +from sqlalchemy.orm import Session + +from app.models.route_preference import RoutePreference +from app.schemas.route_preference import RoutePreferenceCreate + + +class RoutePreferenceService: + """Service for handling route preference operations.""" + + @staticmethod + def get_user_preferences( + db: Session, user_id: int + ) -> List[RoutePreference]: + """ + Get all route preferences for a user. + + Args: + db: Database session + user_id: User ID to get preferences for + + Returns: + List of RoutePreference objects + """ + return ( + db.query(RoutePreference) + .filter(RoutePreference.user_id == user_id) + .all() + ) + + @staticmethod + def get_preference_by_id( + db: Session, preference_id: int + ) -> Optional[RoutePreference]: + """ + Get a route preference by ID. + + Args: + db: Database session + preference_id: Preference ID to search for + + Returns: + RoutePreference object if found, None otherwise + """ + return ( + db.query(RoutePreference) + .filter(RoutePreference.id == preference_id) + .first() + ) + + @staticmethod + def create_preference( + db: Session, user_id: int, preference_in: RoutePreferenceCreate + ) -> RoutePreference: + """ + Create a new route preference for a user. + + Args: + db: Database session + user_id: User ID to create preference for + preference_in: Preference creation data + + Returns: + Created RoutePreference object + + Raises: + HTTPException: If validation fails + """ + # Validate prompt is not empty + if not preference_in.prompt.strip(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Preference prompt cannot be empty", + ) + + # Create preference + preference = RoutePreference( + user_id=user_id, + prompt=preference_in.prompt.strip(), + from_latitude=preference_in.from_latitude, + from_longitude=preference_in.from_longitude, + to_latitude=preference_in.to_latitude, + to_longitude=preference_in.to_longitude, + ) + db.add(preference) + db.commit() + db.refresh(preference) + + return preference + + @staticmethod + def delete_preference( + db: Session, user_id: int, preference_id: int + ) -> bool: + """ + Delete a route preference for a user. + + Args: + db: Database session + user_id: User ID who owns the preference + preference_id: Preference ID to delete + + Returns: + True if deleted successfully + + Raises: + HTTPException: If preference not found or doesn't belong to user + """ + preference = ( + db.query(RoutePreference) + .filter(RoutePreference.id == preference_id) + .first() + ) + + if not preference: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Preference not found", + ) + + # Verify the preference belongs to the user + if preference.user_id != user_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not authorized to delete this preference", + ) + + db.delete(preference) + db.commit() + + return True + + +# Create a singleton instance +route_preference_service = RoutePreferenceService() diff --git a/tests/endpoints/test_preferences.py b/tests/endpoints/test_preferences.py index ebcabff..9c0fe3d 100644 --- a/tests/endpoints/test_preferences.py +++ b/tests/endpoints/test_preferences.py @@ -1,7 +1,7 @@ from fastapi.testclient import TestClient from sqlalchemy.orm import Session -from app.models.preference import Preference +from app.models.global_preference import GlobalPreference from app.models.user import User from app.services.auth_service import auth_service @@ -26,9 +26,9 @@ def get_auth_header(user_id: int) -> dict: def create_test_preference( db: Session, user_id: int, prompt: str -) -> Preference: - """Helper function to create a test preference""" - preference = Preference(user_id=user_id, prompt=prompt) +) -> GlobalPreference: + """Helper function to create a test global preference""" + preference = GlobalPreference(user_id=user_id, prompt=prompt) db.add(preference) db.commit() db.refresh(preference) diff --git a/tests/endpoints/test_routes_with_preferences.py b/tests/endpoints/test_routes_with_preferences.py index 1d1e309..420856d 100644 --- a/tests/endpoints/test_routes_with_preferences.py +++ b/tests/endpoints/test_routes_with_preferences.py @@ -9,7 +9,7 @@ from fastapi.testclient import TestClient from sqlalchemy.orm import Session -from app.models.preference import Preference +from app.models.global_preference import GlobalPreference from app.models.user import User from app.schemas.geo import Coordinates from app.schemas.itinary import Itinerary, Leg, Route, TransportMode @@ -179,7 +179,7 @@ def test_search_routes_with_authenticated_user_preferences( "app.api.v1.endpoints.routes.ai_agents_service" ) as mock_ai_service: with patch( - "app.api.v1.endpoints.routes.preference_service" + "app.api.v1.endpoints.routes.global_preference_service" ) as mock_pref_service: mock_routing_service.get_itinaries = AsyncMock( return_value=sample_itineraries @@ -187,10 +187,10 @@ def test_search_routes_with_authenticated_user_preferences( # Mock stored preferences for the user stored_prefs = [ - Preference( + GlobalPreference( user_id=1, prompt="I prefer eco-friendly routes" ), - Preference(user_id=1, prompt="Avoid long walks"), + GlobalPreference(user_id=1, prompt="Avoid long walks"), ] mock_pref_service.get_user_preferences = MagicMock( return_value=stored_prefs diff --git a/tests/services/test_preference_service.py b/tests/services/test_preference_service.py index 6e8b8d1..072975d 100644 --- a/tests/services/test_preference_service.py +++ b/tests/services/test_preference_service.py @@ -2,11 +2,11 @@ from fastapi import HTTPException from sqlalchemy.orm import Session -from app.models.preference import Preference +from app.models.global_preference import GlobalPreference from app.models.user import User -from app.schemas.preference import PreferenceCreate +from app.schemas.global_preference import GlobalPreferenceCreate from app.services.auth_service import auth_service -from app.services.preference_service import preference_service +from app.services.global_preference_service import global_preference_service def create_test_user(db: Session) -> User: @@ -23,9 +23,9 @@ def create_test_user(db: Session) -> User: def create_test_preference( db: Session, user_id: int, prompt: str -) -> Preference: - """Helper function to create a test preference""" - preference = Preference(user_id=user_id, prompt=prompt) +) -> GlobalPreference: + """Helper function to create a test global preference""" + preference = GlobalPreference(user_id=user_id, prompt=prompt) db.add(preference) db.commit() db.refresh(preference) @@ -36,7 +36,7 @@ def test_get_user_preferences_empty(db: Session): """Test getting preferences when user has none""" user = create_test_user(db) - preferences = preference_service.get_user_preferences(db, user.id) + preferences = global_preference_service.get_user_preferences(db, user.id) assert preferences == [] @@ -47,7 +47,7 @@ def test_get_user_preferences(db: Session): pref1 = create_test_preference(db, user.id, "Prefer direct routes") pref2 = create_test_preference(db, user.id, "Avoid buses") - preferences = preference_service.get_user_preferences(db, user.id) + preferences = global_preference_service.get_user_preferences(db, user.id) assert len(preferences) == 2 assert preferences[0].id == pref1.id @@ -61,7 +61,7 @@ def test_get_preference_by_id(db: Session): user = create_test_user(db) pref = create_test_preference(db, user.id, "Test preference") - result = preference_service.get_preference_by_id(db, pref.id) + result = global_preference_service.get_preference_by_id(db, pref.id) assert result is not None assert result.id == pref.id @@ -70,7 +70,7 @@ def test_get_preference_by_id(db: Session): def test_get_preference_by_id_not_found(db: Session): """Test getting a non-existent preference""" - result = preference_service.get_preference_by_id(db, 99999) + result = global_preference_service.get_preference_by_id(db, 99999) assert result is None @@ -78,9 +78,9 @@ def test_get_preference_by_id_not_found(db: Session): def test_create_preference(db: Session): """Test creating a new preference""" user = create_test_user(db) - preference_in = PreferenceCreate(prompt="Prefer trains over buses") + preference_in = GlobalPreferenceCreate(prompt="Prefer trains over buses") - preference = preference_service.create_preference( + preference = global_preference_service.create_preference( db, user.id, preference_in ) @@ -93,9 +93,9 @@ def test_create_preference(db: Session): def test_create_preference_strips_whitespace(db: Session): """Test that creating a preference strips leading/trailing whitespace""" user = create_test_user(db) - preference_in = PreferenceCreate(prompt=" Avoid crowded routes ") + preference_in = GlobalPreferenceCreate(prompt=" Avoid crowded routes ") - preference = preference_service.create_preference( + preference = global_preference_service.create_preference( db, user.id, preference_in ) @@ -105,10 +105,10 @@ def test_create_preference_strips_whitespace(db: Session): def test_create_preference_empty_prompt(db: Session): """Test creating a preference with empty prompt raises error""" user = create_test_user(db) - preference_in = PreferenceCreate(prompt="") + preference_in = GlobalPreferenceCreate(prompt="") with pytest.raises(HTTPException) as exc_info: - preference_service.create_preference(db, user.id, preference_in) + global_preference_service.create_preference(db, user.id, preference_in) assert exc_info.value.status_code == 400 assert "cannot be empty" in exc_info.value.detail @@ -117,10 +117,10 @@ def test_create_preference_empty_prompt(db: Session): def test_create_preference_whitespace_only_prompt(db: Session): """Test creating a preference with whitespace-only prompt raises error""" user = create_test_user(db) - preference_in = PreferenceCreate(prompt=" ") + preference_in = GlobalPreferenceCreate(prompt=" ") with pytest.raises(HTTPException) as exc_info: - preference_service.create_preference(db, user.id, preference_in) + global_preference_service.create_preference(db, user.id, preference_in) assert exc_info.value.status_code == 400 assert "cannot be empty" in exc_info.value.detail @@ -131,11 +131,11 @@ def test_delete_preference(db: Session): user = create_test_user(db) pref = create_test_preference(db, user.id, "Test preference") - result = preference_service.delete_preference(db, user.id, pref.id) + result = global_preference_service.delete_preference(db, user.id, pref.id) assert result is True # Verify it's deleted - deleted_pref = preference_service.get_preference_by_id(db, pref.id) + deleted_pref = global_preference_service.get_preference_by_id(db, pref.id) assert deleted_pref is None @@ -144,7 +144,7 @@ def test_delete_preference_not_found(db: Session): user = create_test_user(db) with pytest.raises(HTTPException) as exc_info: - preference_service.delete_preference(db, user.id, 99999) + global_preference_service.delete_preference(db, user.id, 99999) assert exc_info.value.status_code == 404 assert "not found" in exc_info.value.detail @@ -164,7 +164,7 @@ def test_delete_preference_wrong_user(db: Session): pref = create_test_preference(db, user1.id, "User1's preference") with pytest.raises(HTTPException) as exc_info: - preference_service.delete_preference(db, user2.id, pref.id) + global_preference_service.delete_preference(db, user2.id, pref.id) assert exc_info.value.status_code == 403 assert "Not authorized" in exc_info.value.detail From f6009b0f089b7e662b6d4ea1e160bdd6513a510d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:57:09 +0000 Subject: [PATCH 3/4] Remove old preference files (replaced by global and route preferences) Co-authored-by: zlendo1 <115471708+zlendo1@users.noreply.github.com> --- app/models/preference.py | 23 ------ app/schemas/preference.py | 20 ----- app/services/preference_service.py | 127 ----------------------------- 3 files changed, 170 deletions(-) delete mode 100644 app/models/preference.py delete mode 100644 app/schemas/preference.py delete mode 100644 app/services/preference_service.py diff --git a/app/models/preference.py b/app/models/preference.py deleted file mode 100644 index 7c06524..0000000 --- a/app/models/preference.py +++ /dev/null @@ -1,23 +0,0 @@ -from sqlalchemy import Column, DateTime, ForeignKey, Integer, String -from sqlalchemy.orm import relationship -from sqlalchemy.sql import func - -from app.db.database import Base - - -class Preference(Base): - __tablename__ = "preference" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column( - Integer, ForeignKey("users.id"), nullable=False, index=True - ) - prompt = Column(String, nullable=False) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), onupdate=func.now()) - - user = relationship("User", back_populates="preferences") - - def __init__(self, user_id, prompt): - self.user_id = user_id - self.prompt = prompt diff --git a/app/schemas/preference.py b/app/schemas/preference.py deleted file mode 100644 index 02a1785..0000000 --- a/app/schemas/preference.py +++ /dev/null @@ -1,20 +0,0 @@ -from datetime import datetime -from typing import Optional - -from pydantic import BaseModel, ConfigDict - - -class PreferenceBase(BaseModel): - prompt: str - - -class PreferenceCreate(PreferenceBase): - pass - - -class PreferenceResponse(PreferenceBase): - id: int - created_at: datetime - updated_at: Optional[datetime] = None - - model_config = ConfigDict(from_attributes=True) diff --git a/app/services/preference_service.py b/app/services/preference_service.py deleted file mode 100644 index d4aec18..0000000 --- a/app/services/preference_service.py +++ /dev/null @@ -1,127 +0,0 @@ -""" -Preference service for handling preference-related business logic. -""" - -from typing import List, Optional - -from fastapi import HTTPException, status -from sqlalchemy.orm import Session - -from app.models.preference import Preference -from app.schemas.preference import PreferenceCreate - - -class PreferenceService: - """Service for handling preference operations.""" - - @staticmethod - def get_user_preferences(db: Session, user_id: int) -> List[Preference]: - """ - Get all preferences for a user. - - Args: - db: Database session - user_id: User ID to get preferences for - - Returns: - List of Preference objects - """ - return db.query(Preference).filter(Preference.user_id == user_id).all() - - @staticmethod - def get_preference_by_id( - db: Session, preference_id: int - ) -> Optional[Preference]: - """ - Get a preference by ID. - - Args: - db: Database session - preference_id: Preference ID to search for - - Returns: - Preference object if found, None otherwise - """ - return ( - db.query(Preference).filter(Preference.id == preference_id).first() - ) - - @staticmethod - def create_preference( - db: Session, user_id: int, preference_in: PreferenceCreate - ) -> Preference: - """ - Create a new preference for a user. - - Args: - db: Database session - user_id: User ID to create preference for - preference_in: Preference creation data - - Returns: - Created Preference object - - Raises: - HTTPException: If validation fails - """ - # Validate prompt is not empty - if not preference_in.prompt.strip(): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Preference prompt cannot be empty", - ) - - # Create preference - preference = Preference( - user_id=user_id, - prompt=preference_in.prompt.strip(), - ) - db.add(preference) - db.commit() - db.refresh(preference) - - return preference - - @staticmethod - def delete_preference( - db: Session, user_id: int, preference_id: int - ) -> bool: - """ - Delete a preference for a user. - - Args: - db: Database session - user_id: User ID who owns the preference - preference_id: Preference ID to delete - - Returns: - True if deleted successfully - - Raises: - HTTPException: If preference not found or doesn't belong to user - """ - preference = ( - db.query(Preference).filter(Preference.id == preference_id).first() - ) - - if not preference: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Preference not found", - ) - - # Verify the preference belongs to the user - if preference.user_id != user_id: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Not authorized to delete this preference", - ) - - db.delete(preference) - db.commit() - - return True - - -# Create a singleton instance -preference_service = PreferenceService() From 66cd76c14db1564c33f9d250b2b644c159258427 Mon Sep 17 00:00:00 2001 From: zlendo1 Date: Thu, 13 Nov 2025 14:13:19 +0200 Subject: [PATCH 4/4] fix: ran manual migration --- ...6cb_replace_preference_with_global_and_.py | 81 ------------------- alembic/versions/8f2fc156b3bb_.py | 76 +++++++++++++++++ 2 files changed, 76 insertions(+), 81 deletions(-) delete mode 100644 alembic/versions/172fc263f6cb_replace_preference_with_global_and_.py create mode 100644 alembic/versions/8f2fc156b3bb_.py diff --git a/alembic/versions/172fc263f6cb_replace_preference_with_global_and_.py b/alembic/versions/172fc263f6cb_replace_preference_with_global_and_.py deleted file mode 100644 index c6225ca..0000000 --- a/alembic/versions/172fc263f6cb_replace_preference_with_global_and_.py +++ /dev/null @@ -1,81 +0,0 @@ -"""replace_preference_with_global_and_route_preferences - -Revision ID: 172fc263f6cb -Revises: ecd62863c9d0 -Create Date: 2025-11-13 11:51:48.867956 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '172fc263f6cb' -down_revision: Union[str, Sequence[str], None] = 'ecd62863c9d0' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # Drop the old preference table - op.drop_index(op.f('ix_preference_user_id'), table_name='preference') - op.drop_index(op.f('ix_preference_id'), table_name='preference') - op.drop_table('preference') - - # Create the new global_preferences table - op.create_table('global_preferences', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('prompt', sa.String(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_global_preferences_id'), 'global_preferences', ['id'], unique=False) - op.create_index(op.f('ix_global_preferences_user_id'), 'global_preferences', ['user_id'], unique=False) - - # Create the new route_preferences table - op.create_table('route_preferences', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('prompt', sa.String(), nullable=False), - sa.Column('from_latitude', sa.Float(), nullable=False), - sa.Column('from_longitude', sa.Float(), nullable=False), - sa.Column('to_latitude', sa.Float(), nullable=False), - sa.Column('to_longitude', sa.Float(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_route_preferences_id'), 'route_preferences', ['id'], unique=False) - op.create_index(op.f('ix_route_preferences_user_id'), 'route_preferences', ['user_id'], unique=False) - - -def downgrade() -> None: - """Downgrade schema.""" - # Drop the new tables - op.drop_index(op.f('ix_route_preferences_user_id'), table_name='route_preferences') - op.drop_index(op.f('ix_route_preferences_id'), table_name='route_preferences') - op.drop_table('route_preferences') - - op.drop_index(op.f('ix_global_preferences_user_id'), table_name='global_preferences') - op.drop_index(op.f('ix_global_preferences_id'), table_name='global_preferences') - op.drop_table('global_preferences') - - # Recreate the old preference table - op.create_table('preference', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('prompt', sa.String(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_preference_id'), 'preference', ['id'], unique=False) - op.create_index(op.f('ix_preference_user_id'), 'preference', ['user_id'], unique=False) diff --git a/alembic/versions/8f2fc156b3bb_.py b/alembic/versions/8f2fc156b3bb_.py new file mode 100644 index 0000000..911c386 --- /dev/null +++ b/alembic/versions/8f2fc156b3bb_.py @@ -0,0 +1,76 @@ +""" + +Revision ID: 8f2fc156b3bb +Revises: ecd62863c9d0 +Create Date: 2025-11-13 14:11:13.445064 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '8f2fc156b3bb' +down_revision: Union[str, Sequence[str], None] = 'ecd62863c9d0' +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('global_preferences', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('prompt', sa.String(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_global_preferences_id'), 'global_preferences', ['id'], unique=False) + op.create_index(op.f('ix_global_preferences_user_id'), 'global_preferences', ['user_id'], unique=False) + op.create_table('route_preferences', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('prompt', sa.String(), nullable=False), + sa.Column('from_latitude', sa.Float(), nullable=False), + sa.Column('from_longitude', sa.Float(), nullable=False), + sa.Column('to_latitude', sa.Float(), nullable=False), + sa.Column('to_longitude', sa.Float(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_route_preferences_id'), 'route_preferences', ['id'], unique=False) + op.create_index(op.f('ix_route_preferences_user_id'), 'route_preferences', ['user_id'], unique=False) + op.drop_index(op.f('ix_preference_id'), table_name='preference') + op.drop_index(op.f('ix_preference_user_id'), table_name='preference') + op.drop_table('preference') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('preference', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('prompt', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), + sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('preference_user_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('preference_pkey')) + ) + op.create_index(op.f('ix_preference_user_id'), 'preference', ['user_id'], unique=False) + op.create_index(op.f('ix_preference_id'), 'preference', ['id'], unique=False) + op.drop_index(op.f('ix_route_preferences_user_id'), table_name='route_preferences') + op.drop_index(op.f('ix_route_preferences_id'), table_name='route_preferences') + op.drop_table('route_preferences') + op.drop_index(op.f('ix_global_preferences_user_id'), table_name='global_preferences') + op.drop_index(op.f('ix_global_preferences_id'), table_name='global_preferences') + op.drop_table('global_preferences') + # ### end Alembic commands ###