[Django] apple social login 구현기 3일차 + async openai in django
생각보다 오래 걸리고 있다..
얼른 해결해보자...!
https://njw9108.tistory.com/36
[Flutter] Apple Social login 방법 및 설명(email null 해결)
1. IOS 앱 배포 할때 앱에서 소셜 로그인을 사용한다면 반드시 Apple 로그인을 포함해야 한다. 2. Apple 개발자 센터(https://developer.apple.com/) 3. 애플 개발자 회원 가입 및 플랜에 가입되어야함. (어차피
njw9108.tistory.com
일단 여기를 참고해봤는데 저기에 마지막즈음에 나오는 token을 프론트에서 줄 것 같다.
해당 토큰은 Jwt 방식이 되어 있는 듯 보였다.
근데 이런식으로 작성하면 내가 작성한 코드는 맞지 않는 코드인 것 같다.
이미 저 값을 decode 하면 값이 jwt 안에 들어있는거고, 적당히 decode 를 해서 email과 device token(있다면) 을 데려오면 되기 때문이다.
그래서 사실상 여기서 더 이상 진행을 못할 것 같고, 내일 담당 팀원분이 어떤 내용을 줄건지가 확정된 이후부터야 가능할 것 같다.
!!테스트실!!
따라서 저 내용이 확정되기 전까지는 간단하게 폴링과 기본 openai 테스트를 진행해볼 것 이다.
저번에 예측했던 사항은 한국 - 미국으로 이동하는 거리때문에 오래 걸린 것 이라고 생각했다.
그 예측이 맞는지, 그래서 결론적으로 시간을 줄일 수 있는지 확인해볼 것 이다!!
일단 그러기 위해서는 대략적인 테스트 환경이 필요하다.
그래서 간단하게 테스트 환경을 만들어볼 예정이다.
pytest 를 통해서 진행할 수 있는 듯 했다. 생각해보니 과제에서 본 것 같은 기분이 든다.
일단 pytest-benchmark 와 pytest-asyncio 를 설치해준다.
pip install pytest-asyncio pytest-benchmark
그 후에 다음과 같은 코드를 작성했다.
일단 위와 같이 작성하고 한번 돌려본다.
에러가 났다.
벤치마킹과 async 가 맞지 않는다는 의미같다. 그래서 아래와 같이 수정해주었다.
뭔가 나왔다...!
그런데 아직도 저 에러는 계속 발생한다.. 내가 볼때는 async 관련 문제인 듯 싶으니 일단 이 문제부터 해결하면 좋을 것 같다. recommend 는 비동기 뷰라 어차피 pytest 에서 계속 오류가 나기도 했었고..
다음의 링크들을 참고했다.
[Python] 비동기 테스트를 하려면? (a.k.a pytest-asyncio)
들어가며python은 async/await 문법과 single-thread 기반 event loop를 통해 비동기적인 작업을 지원합니다. 하지만, 비동기적으로 만들어진 코드를 테스트하기 위해서는 어떻게 해야 할까요? 이번 포스팅
libertbaek.tistory.com
https://pytest-asyncio.readthedocs.io/en/latest/reference/index.html
확인해보니 비동기에 대해서 좀 알아야할 것 같다. 근데 좀 급한 사항이라. 일단 밀어붙여보고 하다가 막히면 공부해보려고 한다.
@pytest.mark.django_db
@pytest.mark.asyncio
@patch("todos.views.client.chat.completions.create")
async def test_rate_limit_exceeded(
mock_llm, create_user, create_todo, recommend_result
):
mock_llm.return_value = recommend_result
url = "https://dev.stepby.one/todos/recommend/"
access_token = str(AccessToken.for_user(create_user))
print(recommend_result)
async with httpx.AsyncClient() as client:
# Attach the Authorization header with the Bearer token
client.headers.update(
{
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
)
response = await client.get(url, params={"todo_id": create_todo.id})
assert response.status_code == status.HTTP_200_OK
이런식으로 작성했는데 이 코드 자체에 문제보단 다른 곳의 로직에서 계속 문제가 발생하는 것 같다.
왜인지도 잘 모르겠다.
정말 왜인걸까...
400에러로 모델과 부르는게 맞지 않는다는 에러가 발생한다.
한번 디버깅을 진행해보았다.
그런데 동기적인 뷰와 다르게 디버깅이 안되었다. 그냥 결과만 나와서 사실상 print 문과 다른 역할이 없었다.
그래서 또 찾아봤다.
https://kwonkyo.tistory.com/497#gsc.tab=0
VSCode Python Extension에서 멀티 스레드 디버깅
파이썬 코딩에 Visual Studio Code(VSCode)를 많이들 사용하시는지 잘 모르겠는데, 저는 VSCode가 무겁지 않고 아두이노 스케치뿐만 아니라 공부하고 있는 다양한 언어를 사용할 수 있어서 All in one IDE 개
kwonkyo.tistory.com
여기에서 파악할 수 있었다. 역시 이미 한사람이 있었다. 최고최고
근데 저분이 찾은 공식문서 링크가 어디있는지는 못찾았다. 진짜 어디서 찾으신거지
일단 따라해보자
이걸 넣어보자
나는 todos 앱의 views.py 에 넣고, 바로 recommend 뷰에 넣을 예정이다.
이런식으로 넣고 계속 따라해보았다.
이 블로그 덕분에 디버그 하면서 왜 콜스택이 있는지 알 수 있었다. 제어권을 확인할 수 있었구나..
좀 더 찾아보면 좋을 듯 싶다.
음 계속 에러가 발생해서 혹시나 하고 뷰에 포스트맨으로 날려본 결과
다음과 같은 에러가 발생했다.
이런 에러였는데 챗지피티에게 물어본 결과는 await 를 붙이지 않아서가 이유였고 await 를 붙여도 문제가 해결되지 않았다.
그래서 찾아보니 openai의 함수도 비동기로 변환해야하는 듯 싶었다. 생각보다 큰 일이였다.
일단 해봐야하는데 궁금증이 생긴건 다음과 같다.
일단 async 를 붙이고 polling 도 같이 붙일 수 있는가? -> 일단 챗지피티는 가능하다고 한다.
그러면 순서는 다음과 같다. 지금 dev 도 같은 코드라 recommend 가 맛이 간 상태이니, 우선적으로 해결해주어야할 듯 싶다.
흠 그러면 흐름도는 다음과 같다.
openai async 도입
도입 후 pytest 정상화
정상화 후 시간 체크
시간 체크 후 pulling 도입
시간 체크
얼마나 차이나는지 비교
팀원들에게 전달
하면 될 것 같다.
그러면 일단 openai Async 를 도입해보자
https://github.com/openai/openai-python
GitHub - openai/openai-python: The official Python library for the OpenAI API
The official Python library for the OpenAI API. Contribute to openai/openai-python development by creating an account on GitHub.
github.com
이곳을 참고해서 진행해보았다.
이걸 참고해보았는데 나머지는 쉽게 해결했는데 asyncio.run 을 어떻게 해야하는지 좀 어려운 것 같다.
그래서 저기 나온 것 처럼 openai 에 요청보내고 받는 부분만 비동기로 진행해야 하나 고민이 된다.
일단 우리의 친구 코파일럿에게 물어보자.
코파일럿은 무시했다...?
딱히 필요 없는건가 하고 다른 친구인 챗지피티에게 물어보니 저거 event loop 라는 개념때문에 사용했다는 것 같다. 우리는 django 뷰로 사용하고 있어서 딱히 사용할 이유가 없는 것 이었다.
일단 다음에 공부해야할 것 같다.
그래서 client 변경 완료하고 await 를 붙여주고 테스트를 돌려보았다.
음 똑같은 에러가 발생한다.
그래서 db 쿼리를 동기적으로 불러서 문제라고 해서 다음처럼 바꿨는데
똑같은 에러가 발생한다.
그래서 다시 읽어보니까
manage.py runserver 를 사용하지 말고 uvicorn 을 사용하라는 말을 들었다. 일단 안되니까 진행해본다.
계속 안되다보니 이런 생각이 든다.
굳이 뷰 자체를 모두 async 로 만들어야하나?
사실상 오래 걸리는게 chatgpt 부분만 포함되는 거면 저거 받아오는 곳만 async 로 바꾸면 안되는 걸까?
일단 한번 물어본다
일단 일차적으로 해결이 된것 같다..
해결방법은 다음과 같은데,
class RecommendSubTodo(APIView):
permission_classes = [AllowAny]
@swagger_auto_schema(
tags=["RecommendSubTodo"],
manual_parameters=[
openapi.Parameter(
"todo_id",
openapi.IN_QUERY,
type=openapi.TYPE_INTEGER,
description="todo_id",
required=True,
),
],
operation_summary="Recommend subtodo",
responses={200: SubTodoSerializer},
)
def get(self, request):
"""
- 이 함수는 sub todo를 추천하는 함수입니다.
"""
set_sentry_user(request.user)
user_id = request.GET.get("user_id")
try:
# 비동기적으로 OpenAI API 호출 처리
completion = asyncio.run(self.get_openai_completion())
return Response(
json.loads(completion.choices[0].message.content),
status=status.HTTP_200_OK,
)
except Exception as e:
sentry_sdk.capture_exception(e)
return Response(
{"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
)
# 비동기적으로 OpenAI API를 호출하는 함수
async def get_openai_completion(self):
loop = asyncio.get_event_loop()
with ThreadPoolExecutor() as pool:
# OpenAI API 호출을 스레드 풀에서 실행
return await loop.run_in_executor(
pool,
lambda: openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "내가 저녁으로 먹을만한거 5개 정도 추천해줘",
},
],
response_format={"type": "json_object"},
),
)
이렇게 시도해봤다. 뷰자체는 동기적으로 하되, openai 를 호출하는 부분만 비동기로 했다.
그런데 이렇게 하면 비동기로 하는 이유가 있는지 갑자기 의문이 드는데, 한번 찾아보면 좋을 듯 싶다.
todo = Todo.objects.get_with_id(id=todo_id)
# 비동기적으로 OpenAI API 호출 처리
completion = asyncio.run(self.get_openai_completion(todo))
이렇게 작성하니까 이런 에러가 발생했다.
error You cannot call this from an async context - use a thread or sync_to_async.
그래서 느낀 점은 아마 저기서 get_oepnai_completion 일 때 todo 를 불러서 발생하는 에러로 짐작하고 있다.
이걸 참고해보자
https://docs.djangoproject.com/en/5.1/topics/async/
Asynchronous support | Django documentation
The web framework for perfectionists with deadlines.
docs.djangoproject.com
문제가 계속 돌고돌아서 정리해보자면
일단 await 를 두번해야하는상황이다. todo_id 로 쿼리문 날릴 때 한번, openai 사용할 때 한번
그래서 결국 get 자체를 async 로 처리해야하는 상황이다.
근데 이 경우에 계속 coroutine 관련으로 에러가 발생한다.
AssertionError: Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` to be returned from the view, but received a `<
I follow the instruction posted here Django has support for writing asynchronous (“async”) views, along with an entirely async-enabled request stack if you are running under ASGI. Async views will still work under WSGI, but with performance penalties,
forum.djangoproject.com
여기를 한번 참고해서 adrf 를 사용해보려고 했다
AssertionError: Expected a `Response`, `HttpResponse` or `StreamingHttpResponse` to be returned from the view, but received a `<
async def send_stock_updates(symbol): while True: stock_info = get_one_stock_info(symbol) # Send stock info over WebSocket channel_layer = get_channel_layer() a...
stackoverflow.com
근데 그러면 app을 따로 만들어야 할 것 같다....?
이름이 겹쳐서 가능할지모르겠다.
# 비동기적으로 OpenAI API를 호출하는 함수
async def get_openai_completion(self, todo_id):
return await openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": """너는 퍼스널 매니저야.
너가 하는 일은 이 사람이 할 이야기를 듣고 약 1시간 정도면 끝낼 수 있도록 작업을 나눠주는 식으로 진행할 거야.
아래는 너가 나눠줄 작업 형식이야.
{ id : 1, content: "3학년 2학기 운영체제 중간고사 준비", date="2024-09-01", due_time="2024-09-24"}
이런 형식으로 작성된 작업을 받았을 때 너는 이 작업을 어떻게 나눠줄 것인지를 알려주면 돼.
Output a JSON object structured like:
{id, content, date, due_time, category_id, order, is_completed, children : [
{content, date, todo(parent todo id)}, ... ,{content, date, todo(parent todo id)}]}
[조건]
- date 는 부모의 date를 따를 것
- 작업은 한 서브투두를 해결하는데 1시간 정도로 이루어지도록 제시할 것
- 언어는 주어진 todo content의 언어에 따를 것
""", # noqa: E501
},
# {
# "role": "user",
# "content": f"id: {todo.id}, \
# content: {todo.content}, \
# date: {todo.date}, \
# due_time: {todo.due_time}, \
# category_id: {todo.category_id}, \
# order: {todo.order}, \
# is_completed: {todo.is_completed}",
# },
{
"role": "user",
"content": "id: 1, \
content: 저녁으로 삼계탕먹어야지, \
date: 2024-02-02, \
due_time: None, \
category_id: 1, \
order: a|aaaaaa, \
is_completed: False",
},
],
response_format={"type": "json_object"},
)
db query 에서 발생하는 문제가 맞는 것 같다.
영롱하다.. 잘 나온다... 따이...
그래서 비동기로 db query를 받는 방법을 찾아봐야할 것 같다.
이거는 sync_to_async를 사용하면 되는 것 같긴한데 아래 에러가 발생한다.
error You cannot submit onto CurrentThreadExecutor from its own thread
쓰레드 관련 에러인 것 같다.
여러가지 시도를 해보고 느낀 결과는 저 함수를 잘못 사용하고 있었다. (그간 노력 : async 함수 하나더 만들기, 쓰레드 풀 늘리려고 했으나 아직 시도는 하지 않음)
그냥 저 todo 불러오는 부분에서 에러가 발생하고 있었다. 다른 걸 한번 사용해본다.
음 그냥 aget 을 사용하면 되었던 건가..? => 안된다.
음.. 아는 어떠한 모든 방식을 사용해도 안되길래 비동기뷰라고만 명시하고 안에는 response만주도록 변경해보니 그래도 coroutine 관련에러가 뜬다. 이러면 이제 uvicorn 문제인갑다.
async def get(self, request):
"""
- 이 함수는 sub todo를 추천하는 함수입니다.
"""
return Response(
{"error": "todo_id must be provided"},
status=status.HTTP_400_BAD_REQUEST,
)
모든 걸 날리고 이상태에서도
AssertionError: Expected a `Response`, `HttpResponse` or `StreamingHttpResponse` to be returned from the view, but received a `<class 'coroutine'>`
이 에러가 발생한다.
모르겠다....
해결했다.......으어어악
해결 방법은 다음과 같이 했다.
class RecommendSubTodo(APIView):
permission_classes = [AllowAny]
@swagger_auto_schema(
tags=["RecommendSubTodo"],
manual_parameters=[
openapi.Parameter(
"todo_id",
openapi.IN_QUERY,
type=openapi.TYPE_INTEGER,
description="todo_id",
required=True,
),
],
operation_summary="Recommend subtodo",
responses={200: SubTodoSerializer},
)
def get(self, request):
"""
- 이 함수는 sub todo를 추천하는 함수입니다.
"""
set_sentry_user(request.user)
user_id = request.GET.get("user_id")
flag, message = UserLastUsage.check_rate_limit(
user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS
)
if flag is False:
return Response(
{"error": message}, status=status.HTTP_429_TOO_MANY_REQUESTS
)
todo_id = request.GET.get("todo_id")
if todo_id is None:
sentry_sdk.capture_message("Todo_id not provided", level="error")
return Response(
{"error": "todo_id must be provided"},
status=status.HTTP_400_BAD_REQUEST,
)
try:
# 비동기적으로 OpenAI API 호출 처리
todo = Todo.objects.get_with_id(id=todo_id)
todo_data = {
"id": todo.id,
"content": todo.content,
"date": todo.date,
"due_time": todo.due_time,
"category_id": todo.category_id,
"rank": todo.rank,
"is_completed": todo.is_completed,
}
completion = asyncio.run(self.get_openai_completion(todo_data))
return Response(
json.loads(completion.choices[0].message.content),
status=status.HTTP_200_OK,
)
except Todo.DoesNotExist as e:
sentry_sdk.capture_exception(e)
return Response(
{"error": "Todo not found"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
sentry_sdk.capture_exception(e)
print("error", e)
return Response(
{"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
)
# 비동기적으로 OpenAI API를 호출하는 함수
async def get_openai_completion(self, todo):
return await openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": """너는 퍼스널 매니저야.
너가 하는 일은 이 사람이 할 이야기를 듣고 약 1시간 정도면 끝낼 수 있도록 작업을 나눠주는 식으로 진행할 거야.
아래는 너가 나눠줄 작업 형식이야.
{ id : 1, content: "3학년 2학기 운영체제 중간고사 준비", date="2024-09-01", due_time="2024-09-24"}
이런 형식으로 작성된 작업을 받았을 때 너는 이 작업을 어떻게 나눠줄 것인지를 알려주면 돼.
Output a JSON object structured like:
{id, content, date, due_time, category_id, rank, is_completed, children : [
{content, date, todo(parent todo id)}, ... ,{content, date, todo(parent todo id)}]}
[조건]
- date 는 부모의 date를 따를 것
- 작업은 한 서브투두를 해결하는데 1시간 정도로 이루어지도록 제시할 것
- 언어는 주어진 todo content의 언어에 따를 것
""", # noqa: E501
},
{
"role": "user",
"content": f"id: {todo["id"]}, \
content: {todo["content"]}, \
date: {todo["date"]}, \
due_time: {todo["due_time"]}, \
category_id: {todo["category_id"]}, \
rank: {todo["rank"]}, \
is_completed: {todo["is_completed"]}",
},
],
response_format={"type": "json_object"},
)
이유는 모르겠지만 get은 뭘해도 비동기가 안되는 거였던 것 같다.
그래서 todo db에서 가져올때 처리한 것을 보내주도록 바꿔줬다...
일단 이렇게 처리하고 나중에 다시 생각해야겠다.... 리팩토링도 나중에....
이제 하나 처리했따....
테스트코드변경하고 다음에 해야할 것 같다..
TypeError("object Mock can't be used in 'await' expression")
오.....
해결해야지
해결했따이....
mock 관련 문제는 간단했는데
뒤에 저 new_callable 붙여주니까 해결되었다.
일단 하나라도 해결되었으니까 행복해야지....
일단 담당 맡으신분이 오늘 저녁에서야 가능하시다고 해서 혹시 저번에 맡으셨던 분에게 어떤 게 올지 한번만 확인해달라고 했다.
그러니까 약 2분만에 이럴 것 같다고 답장이 오셨다. 최고최고
https://docs.expo.dev/versions/latest/sdk/apple-authentication/
AppleAuthentication
A library that provides Sign-in with Apple capability for iOS.
docs.expo.dev
여기를 사용할 것 같은데,
여기에서 보아하니 email과 jwt 를 줄 수 있는 것 같다.
그러니 email 과 추가적인 정보를 통해서 구글과 마찬가지로 진행하고, jwt 를 검증하면 될 것 같다고 하셨다.
음. 어제 만든 코드는 일단 전부 커밋만 해놓고 다 날려버려야 할 듯 싶다.
# 공부할 것
- async event loop
- Pytest를 사용한 기본적인 테스트 코드 작성 방법
- pytest scope에 대한 이해
- Python의 비동기(Async) 개념(async/await, 코루틴, event loop)
- 동기 사이에 있는 비동기 함수가 영향이 있을까...?
오늘은 여기까지밖에 못할 것 같다...
내일열심히 더 해야지.... 으억억