-
Notifications
You must be signed in to change notification settings - Fork 0
Implement agents CRUD endpoints (DEV-70) #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sonya-q
wants to merge
12
commits into
main
Choose a base branch
from
feature/DEV-70/agents-crud
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+165
−9
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
dcad66c
Implement agents CRUD endpoints (DEV-71)
sonya-q 3d11499
Merge branch 'main' into feature/DEV-70/agents-crud
sonya-q 61b671d
Fix linting errors (DEV-70)
sonya-q abb3bc8
style: apply Black formatting (DEV-70)
sonya-q 0588b9b
fix: resolve merge conflict in agents.py (DEV-70)
sonya-q e999306
fix: resolve merge conflict in agents.py (DEV-70)
sonya-q 01f0c2d
Reformat, again, with Black (DEV-70)
sonya-q 9cafd34
Refactor agents into services layer (DEV-70)
sonya-q 93b66ac
style: apply Black formatting (DEV-70)
sonya-q fdc7d14
add import
sonya-q 5e9512b
remove unnecessary import (agency) (DEV-70)
sonya-q 031dc03
fix: handle IntegrityError and raise ValueError (DEV-70)
sonya-q File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,91 @@ | ||
| """Agents REST API. | ||
|
|
||
| Endpoints for Agency agents (Supabase Auth; link via supabase_user_id). | ||
| Full CRUD for agents resource. | ||
| """ | ||
|
|
||
| from fastapi import APIRouter, Response, status | ||
| from fastapi import APIRouter, Depends, HTTPException, status | ||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
|
|
||
| from ..database import get_db | ||
| from ..models import Agent | ||
| from ..schemas import AgentCreate, AgentUpdate, Agent as AgentSchema | ||
| from ..services import agents as agent_service | ||
|
|
||
| router = APIRouter() | ||
|
|
||
| # Helpers | ||
kenzysoror marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| async def get_agent_or_404(agent_id: str, db: AsyncSession) -> Agent: | ||
| """ | ||
| Reusable helper: looks up agent by ID | ||
| If not found, raise a 404 immediately | ||
| """ | ||
| agent = await agent_service.get_agent(agent_id, db) | ||
| if not agent: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_404_NOT_FOUND, | ||
| detail=f"Agent with id '{agent_id}' not found.", | ||
| ) | ||
| return agent | ||
|
|
||
|
|
||
| # Endpoints | ||
|
|
||
|
|
||
| @router.get("", response_model=list[AgentSchema]) | ||
| async def list_agents(db: AsyncSession = Depends(get_db)): | ||
| """ | ||
| GET /api/agents | ||
| Returns list of all agents | ||
| """ | ||
| return await agent_service.list_agents(db) | ||
|
|
||
|
|
||
| @router.post("", response_model=AgentSchema, status_code=status.HTTP_201_CREATED) | ||
| async def create_agent(agent: AgentCreate, db: AsyncSession = Depends(get_db)): | ||
| """ | ||
| Creates a new agent. | ||
| """ | ||
| if not await agent_service.validate_agency_exists(agent.agency_id, db): | ||
| raise HTTPException( | ||
| status_code=status.HTTP_404_NOT_FOUND, | ||
| detail=f"Agency with id '{agent.agency_id}' not found.", | ||
| ) | ||
| return await agent_service.create_agent(agent, db) | ||
|
|
||
|
|
||
| @router.get("/{agent_id}", response_model=AgentSchema) | ||
| async def get_agent(agent_id: str, db: AsyncSession = Depends(get_db)): | ||
| """ | ||
| Returns a single agent. | ||
| """ | ||
| return await get_agent_or_404(agent_id, db) | ||
|
|
||
|
|
||
| @router.put("/{agent_id}", response_model=AgentSchema) | ||
| async def update_agent( | ||
| agent_id: str, | ||
| payload: AgentUpdate, | ||
| db: AsyncSession = Depends(get_db), | ||
| ): | ||
| """ | ||
| Updates an existing agent. | ||
| """ | ||
| agent = await get_agent_or_404(agent_id, db) | ||
| if "agency_id" in payload.model_dump(exclude_unset=True): | ||
| if not await agent_service.validate_agency_exists(payload.agency_id, db): | ||
| raise HTTPException( | ||
| status_code=status.HTTP_404_NOT_FOUND, | ||
| detail=f"Agency with id '{payload.agency_id}' not found.", | ||
| ) | ||
| return await agent_service.update_agent(agent, payload, db) | ||
|
|
||
|
|
||
| @router.get("") | ||
| async def list_agents(): | ||
| """List agents. Placeholder — implement with Agent model and schemas.""" | ||
| return Response( | ||
| content="Not implemented — see docs/STARTER_BACKEND_GUIDE.md", | ||
| status_code=status.HTTP_501_NOT_IMPLEMENTED, | ||
| ) | ||
| @router.delete("/{agent_id}", status_code=status.HTTP_204_NO_CONTENT) | ||
| async def delete_agent(agent_id: str, db: AsyncSession = Depends(get_db)): | ||
| """ | ||
| Deletes an agent. | ||
| """ | ||
| agent = await get_agent_or_404(agent_id, db) | ||
| await agent_service.delete_agent(agent, db) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from . import agents as agents_service | ||
|
|
||
| __all__ = ["agents_service"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| """Agents service.""" | ||
|
|
||
| from sqlalchemy import select | ||
| from sqlalchemy.exc import IntegrityError | ||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
|
|
||
| from ..models import Agency, Agent | ||
| from ..schemas import AgentCreate, AgentUpdate | ||
|
|
||
|
|
||
| async def validate_agency_exists(agency_id: str, db: AsyncSession) -> bool: | ||
| """ | ||
| Return True if the agency exists, False otherwise. | ||
| """ | ||
| result = await db.execute(select(Agency).where(Agency.id == agency_id)) | ||
| return result.scalar_one_or_none() is not None | ||
|
|
||
|
|
||
| async def list_agents(db: AsyncSession) -> list[Agent]: | ||
| """ | ||
| Return all agents ordered by last name, first name. | ||
| """ | ||
| result = await db.execute(select(Agent).order_by(Agent.last_name, Agent.first_name)) | ||
| return result.scalars().all() | ||
|
|
||
|
|
||
| async def get_agent(agent_id: str, db: AsyncSession) -> Agent | None: | ||
| """ | ||
| Return an agent by id, or None if not found. | ||
| """ | ||
| result = await db.execute(select(Agent).where(Agent.id == agent_id)) | ||
| return result.scalar_one_or_none() | ||
|
|
||
|
|
||
| async def create_agent(payload: AgentCreate, db: AsyncSession) -> Agent: | ||
| """ | ||
| Create and return a new agent. | ||
| """ | ||
| db_agent = Agent(**payload.model_dump()) | ||
| db.add(db_agent) | ||
| try: | ||
| await db.commit() | ||
| except IntegrityError as e: | ||
| await db.rollback() | ||
| raise ValueError(f"Unable to create agent: {str(e.orig)}") from e | ||
| await db.refresh(db_agent) | ||
| return db_agent | ||
|
|
||
|
|
||
| async def update_agent( | ||
| agent: Agent, | ||
| payload: AgentUpdate, | ||
| db: AsyncSession, | ||
| ) -> Agent: | ||
| """ | ||
| Update and return an existing agent. | ||
| """ | ||
| update_data = payload.model_dump(exclude_unset=True) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. potentially add a check like to avoid committing without modifying anything (#17 for reference) |
||
| for key, value in update_data.items(): | ||
| setattr(agent, key, value) | ||
| try: | ||
| await db.commit() | ||
| except IntegrityError as e: | ||
| await db.rollback() | ||
| raise ValueError(f"Unable to update agent: {str(e.orig)}") from e | ||
| await db.refresh(agent) | ||
| return agent | ||
|
|
||
|
|
||
| async def delete_agent(agent: Agent, db: AsyncSession) -> None: | ||
| """ | ||
| Delete an agent permanently. | ||
| """ | ||
| await db.delete(agent) | ||
| try: | ||
| await db.commit() | ||
| except IntegrityError as e: | ||
| await db.rollback() | ||
| raise ValueError(f"Unable to delete agent: {str(e.orig)}") from e | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.