Skip to content

Commit

Permalink
Merge pull request #155 from kreneskyp/fast_api_cleanup
Browse files Browse the repository at this point in the history
FastAPI hooks and cleanup
  • Loading branch information
kreneskyp authored Aug 8, 2023
2 parents e9f1109 + 7c67380 commit 44ebf61
Show file tree
Hide file tree
Showing 13 changed files with 328 additions and 42 deletions.
31 changes: 31 additions & 0 deletions frontend/utils/hooks/useCreateAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useCallback, useState } from "react";
import axios from "axios";

const useCreateAPI = (url) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

const create = useCallback(
async (data) => {
setIsLoading(true);
try {
const response = await axios.post(url, data);
setIsLoading(false);
return response.data;
} catch (err) {
setIsLoading(false);
setError(err);
throw err;
}
},
[url]
);

return {
create,
isLoading,
error,
};
};

export default useCreateAPI;
26 changes: 26 additions & 0 deletions frontend/utils/hooks/useCreateUpdateAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import useCreateAPI from "utils/hooks/useCreateAPI";
import useUpdateAPI from "utils/hooks/useUpdateAPI";

/**
* Hook that encapsulates both create and update APIs into a single save function.
* @param createURL
* @param updateURL
* @returns {{isLoading: boolean, save: ((function(*): Promise<any|undefined>)|*)}}
*/
export const useCreateUpdateAPI = (createURL, updateURL) => {
const { create, isLoading: isCreateLoading } = useCreateAPI(createURL);
const { update, isLoading: isUpdateLoading } = useUpdateAPI(updateURL);

const save = async (data) => {
if (data.id) {
return await update(data);
} else {
return await create(data);
}
};

return {
save,
isLoading: isCreateLoading || isUpdateLoading,
};
};
24 changes: 24 additions & 0 deletions frontend/utils/hooks/useDeleteAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useCallback, useState } from "react";
import axios from "axios";

export const useDeleteAPI = (endpoint) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

const deleteData = useCallback(async () => {
setIsLoading(true);
try {
await axios.delete(endpoint);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
}, [endpoint]);

return {
call: deleteData,
isLoading,
error,
};
};
33 changes: 33 additions & 0 deletions frontend/utils/hooks/useDetailAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useState, useEffect, useCallback } from "react";
import axios from "axios";

export const useDetailAPI = (endpoint, { load = true } = {}) => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

const loadData = useCallback(async () => {
setIsLoading(true);
try {
const response = await axios.get(endpoint);
setData(response.data);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
}, [endpoint]);

useEffect(() => {
if (load) {
loadData();
}
}, [endpoint, load]);

return {
data,
load: loadData,
isLoading,
error,
};
};
60 changes: 60 additions & 0 deletions frontend/utils/hooks/useObjectEditorView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useEffect, useState } from "react";
import { v4 as uuid4 } from "uuid";

/**
* Hook for handling the state of an object editor view.
* state for handling whether how to load the data (new vs existing)
* and when to reset the editor when opened. The cached state does not
* reset when the url changes as protection against reloading when
* creating new chains. This state tracks when to reset the cache.
*
* @param id - object id, or undefined/null if new
* @param load - function to load the object when needed
*/
export const useObjectEditorView = (id, load) => {
const [idRef, setIdRef] = useState(null);
const [isNew, setIsNew] = useState(null);
const [wasCreated, setWasCreated] = useState(null);
useEffect(() => {
const firstRender = isNew === null;
if (firstRender) {
// first render caches whether this started as a new chain
setIsNew(id === undefined);
} else {
// switch from existing to new
if (id === undefined && !isNew) {
setIsNew(true);
setWasCreated(false);
}
// a new chain was created
if (id !== undefined && isNew) {
setWasCreated(true);
}
// switch from created to new
if (id === undefined && wasCreated) {
setIsNew(true);
setWasCreated(false);
setIdRef(uuid4());
}
}
}, [id]);

useEffect(() => {
// load chain if id is provided on view load
// otherwise state will be handled internally by the editor
if (isNew === false) {
load();
setIdRef(id);
} else {
// create a uuid here to force a new editor. This helps detect
// creating a new object after a new object was just created.
setIdRef(uuid4());
}
}, [isNew]);

return {
isNew,
idRef,
wasCreated,
};
};
43 changes: 43 additions & 0 deletions frontend/utils/hooks/usePaginatedAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useCallback, useEffect, useState } from "react";
import axios from "axios";

export function usePaginatedAPI(
endpoint,
{ offset = 0, limit = 10, load = true, loadDependencies = [] } = {}
) {
const [page, setPage] = useState(null);
const [isLoading, setIsLoading] = useState(true);

const _load = useCallback(
async ({ search } = {}) => {
setIsLoading(true);
const params = { limit, offset };
if (search) {
params.search = search;
}
try {
const response = await axios.get(endpoint, {
params,
});
setPage(response.data);
} catch (error) {
console.error("Failed to fetch data:", error);
} finally {
setIsLoading(false);
}
},
[endpoint]
);

useEffect(() => {
if (load) {
_load();
}
}, [_load, ...loadDependencies]);

return {
page,
isLoading,
load: _load,
};
}
31 changes: 31 additions & 0 deletions frontend/utils/hooks/useUpdateAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useCallback, useState } from "react";
import axios from "axios";

const useUpdateAPI = (url, { onSuccess, onError } = {}) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

const update = useCallback(
async (data) => {
setIsLoading(true);
try {
const response = await axios.put(url, data);
setIsLoading(false);
return response.data;
} catch (err) {
setIsLoading(false);
setError(err);
throw err;
}
},
[url]
);

return {
update,
isLoading,
error,
};
};

export default useUpdateAPI;
15 changes: 10 additions & 5 deletions ix/api/agents/endpoints.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from asgiref.sync import sync_to_async
from django.db.models import Q
from fastapi import HTTPException, APIRouter
from typing import List, Optional
from typing import Optional
from pydantic import BaseModel
from uuid import UUID
from ix.agents.models import Agent
from ix.api.chains.endpoints import DeletedItem
from ix.api.agents.types import Agent as AgentPydantic
from ix.api.agents.types import Agent as AgentPydantic, AgentPage

__all__ = ["router", "AgentCreateUpdate"]

Expand Down Expand Up @@ -38,14 +39,18 @@ async def get_agent(agent_id: str):
return AgentPydantic.from_orm(agent)


@router.get("/agents/", response_model=List[AgentPydantic], tags=["Agents"])
async def get_agents(search: Optional[str] = None):
@router.get("/agents/", response_model=AgentPage, tags=["Agents"])
async def get_agents(search: Optional[str] = None, limit: int = 10, offset: int = 0):
query = (
Agent.objects.filter(Q(name__icontains=search) | Q(alias__icontains=search))
if search
else Agent.objects.all()
)
return [AgentPydantic.from_orm(agent) async for agent in query]

# punting on async implementation of pagination until later
return await sync_to_async(AgentPage.paginate)(
output_model=AgentPydantic, queryset=query, limit=limit, offset=offset
)


@router.put("/agents/{agent_id}", response_model=AgentPydantic, tags=["Agents"])
Expand Down
8 changes: 8 additions & 0 deletions ix/api/agents/types.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from datetime import datetime
from typing import List
from uuid import UUID

from pydantic import BaseModel, Field

import logging

from ix.utils.graphene.pagination import QueryPage

logger = logging.getLogger(__name__)


Expand All @@ -20,3 +23,8 @@ class Agent(BaseModel):

class Config:
orm_mode = True


class AgentPage(QueryPage[Agent]):
# override objects, FastAPI isn't detecting QueryPage type
objects: List[Agent]
Loading

0 comments on commit 44ebf61

Please sign in to comment.