2024년 2월 20일 화요일

python requests 사용시 multipart 로 사용하는 방법

python requests 를 사용해서 post를 사용하다면, 기본적으로 application/x-www-form-urlencoded 을 기본적으로 사용하게 됩니다.

그런데 간혹 multipart/form-data 를 원하는 경우가 있습니다.

기본 예제를 살펴 보겠습니다.

import requests

url = 'http://127.0.0.1:5000/funs'
data = {"key1": "value1", "key2": "value2"}

r = requests.post(url, data=data)
print(r.request.headers)
print(r.request.body)

위와 같이 사용하게 되면 'Content-Type': 'application/x-www-form-urlencoded' 이 됩니다.

{'User-Agent': 'python-requests/2.28.1', 'Accept-Encoding': 'gzip, deflate, br', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Length': '23', 'Content-Type': 'application/x-www-form-urlencoded'}
key1=value1&key2=value2

그렇다면 multipart 가 되는 조건은 뭘까요?

파일을 첨부 할때입니다.

파일 첨부 예를 만들어 보겠습니다.

import requests

url = 'http://127.0.0.1:5000/funs'
data = {"key1": "value1", "key2": "value2"}

files1 = {'fl': open('test.txt', 'rb')}
r = requests.post(url, files=files1)
print(r.request.headers)
print(r.request.body)


{'User-Agent': 'python-requests/2.28.1', 'Accept-Encoding': 'gzip, deflate, br', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Length': '149', 'Content-Type': 'multipart/form-data; boundary=40aac4e5ef113fc32999aeb43e78702e'}
b'--40aac4e5ef113fc32999aeb43e78702e\r\nContent-Disposition: form-data; name="fl"; filename="test.txt"\r\n\r\ntest123\r\n--40aac4e5ef113fc32999aeb43e78702e--\r\n'

결과가 알아보기 어렵게 되긴했지만 'Content-Type': 'multipart/form-data; 가 된것을 알 수 있습니다. 뒤에 있는 boundary는 여러개를 구분하는 용도로 사용하게 됩니다.

처음으로 돌아와서 그렇다면 어떻게 하면 data를 multipart로 전달이 가능할까요?

다른 패키지를 사용하는 방법도 있지만, requests에서는 files와 같이 보내는 방법이 있습니다.

그런데 파일을 보내지 않고 싶은데 어떻게 하냐고요? dummy로 임의의 변수를 넣어 주면 됩니다.

최종 해결책입니다.

import requests

url = 'http://127.0.0.1:5000/funs'
data = {"key1": "value1", "key2": "value2"}


files_dummy = {'fl': None}
r = requests.post(url, data=data, files=files_dummy)
print(r.request.headers)
print(r.request.body)

리턴값입니다.

{'User-Agent': 'python-requests/2.28.1', 'Accept-Encoding': 'gzip, deflate, br', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Length': '220', 'Content-Type': 'multipart/form-data; boundary=21825c01c73525d71646d70babe7f0b5'}
b'--21825c01c73525d71646d70babe7f0b5\r\nContent-Disposition: form-data; name="key1"\r\n\r\nvalue1\r\n--21825c01c73525d71646d70babe7f0b5\r\nContent-Disposition: form-data; name="key2"\r\n\r\nvalue2\r\n--21825c01c73525d71646d70babe7f0b5--\r\n'

위에서 'fl' 이란 부분을 보냈는데 requests body부분을 살펴보면 관련 내용이 없음을 알 수 있습니다.


여기에서 사용된 server 소스 입니다.

from flask import Flask, json
from flask import request
api = Flask(__name__)

@api.route('/funs', methods=['POST'])
def post_funs():
    print(request.form)
    print(request.headers)
    return json.dumps({"success": True}), 201

if __name__ == '__main__':
    api.run(debug=True)

 


2024년 2월 18일 일요일

unity c# callback 함수

callback 함수

 콜백 함수(callback function)란 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 말합니다. 콜백 함수는 특정 이벤트가 발생하거나 특정 조건이 발생할때 실행될 수 있습니다. 


사용처

버튼을 클릭하면 콜백 함수가 호출되어 원하는 기능을 수행할 수 있습니다. 콜백 함수는 프로그래밍에서 비동기적인 작업을 처리하거나 코드의 재사용성을 높이는 데 유용합니다.


yes/no 를 선택하는 다이얼 로그 예제

다이얼 로그를 띄워주는 class를 만들었습니다. 그런데 해당 class가 닫힐때 특별한 처리를 원하는 경우가 있습니다. 즉 사용자가 yes, no를 선택했는가에 따라 다른 처리를 하고 싶은 경우 입니다. yes no dialog를 여러가지 용도로 사용을 하고자 한다면 그때마다 yes no를 선택할때 다른 코드를 넣어 줘야 합니다.

그런데 그때 호출되어야 하는 함수를 callback 함수 형태로 제작해서 dialog를 생성할때 인자를 넘겨주게 되면 코드 재사용성을 높일 수 있습니다.


unity c# 에서 사용방법

c 언어에서는 보통 함수 포인터라는 것이 있습니다. 함수의 포인터를 함수의 인자로 넘겨서 해당 함수를 호출하는 방식입니다. 그러나 c# 의 경우 포인터라는 개념이 없기 때문에 다른 방법을 이용하게 됩니다. interface라는 방법입니다. 

아래와 같은 형태로 만듭니다. OnYesNo 이름은 나중에 호출을 위한 함수가 됩니다.

public interface DialogYesNoCB
{
    public void OnYesNo(int yesNo);
}

그리고 아래와 같이 다이얼로그가 닫힐때 CloseDialog 함수가 호출되도록 연결해줍니다.  

init 에서는 cb 인자로 interface 함수를 연결합니다. 그리고 실제는 CloseDialog() 함수는 다이얼로가 닫힐때 호출되도록 작업은 따로 해주어야 합니다.

public class DialogYesNo : MonoBehaviour
{
    private DialogYesNoCB cb;
    public void Init(string text, DialogYesNoCB cb)
    {
        GetComponentsInChildren<TMP_Text>(true)[0].enabled = true;
        GetComponentsInChildren<TMP_Text>(true)[0].text = text;
        GetComponentsInChildren<TMP_Text>(true)[1].enabled = false;
        GetComponentsInChildren<Image>(true)[2].enabled = false;
        this.cb = cb;
    }
    public void CloseDialog(int yesNo)
    {
        cb.OnYesNo(yesNo);
    }
}


실제 호출은 아래와 같은 형태로 불리게 됩니다.

아래 코드는 예제 코드로 참고만 하시기 바랍니다. 작업중인 코드에서 가져왔기 때문에 실행 가능한 코드는 아닙니다.

ButtonLevelUp() 함수를 호출하면 YesNo dialog가 뜨고 Yes no 버튼을 누르면 만들어 놓은 OnYesNo 함수가 호출 되도록 되어 있습니다.

public class FieldLevelUp : MonoBehaviour, DialogYesNoCB
{
    public void ButtonLevelUp()
    {
        ShowDialogYesNo("필드를 레벨업 하시겠습니까?", this);
    }
    public void OnYesNo(int yesNo)
    {
        if(yesNo == 1) {
            GameManager.instance.quest.fieldClearCountAfterLUP[(int)fieldName] = 0;
            GameManager.instance.quest.fieldLevel[(int)fieldName]++;
            LeftListUpdate();
        }
    }
    ...
    public void ShowDialogYesNo(string text, DialogYesNoCB cb)
    {

        yesNoDialogPanel.GetComponent<DialogYesNo>().Init(text, cb);
        yesNoDialogPanel.GetComponent<PanelHandler>().Show();
    }
    public void CloseDialogYesNo(int yesNo)
    {
        yesNoDialogPanel.GetComponent<DialogYesNo>().CloseDialog(yesNo);
        yesNoDialogPanel.GetComponent<PanelHandler>().Hide();
    }
}

CloseDialogYesNo는 object yes, no 버튼의 button on click() 함수에 넣도록 합니다.




2024년 2월 11일 일요일

addr2line 구하는 곳 window용

addr2line 이란

addr2line은 디버깅 목적으로 사용되는 편리한 명령 줄 도구입니다. 메모리 주소를 파일 이름과 라인 번호로 변환합니다. 

일반적으로 kernel의 오류를 디버깅 할때가 아니라 so 파일들을 디버깅 할때 사용합니다. 


주의할 점

디버깅 대상이 되는 파일은 디버깅 정보가 포함되도록 컴파일이 되어야 합니다.


실행 예제

-e 옵션을 파일을 지정합니다.

addr2line -e a.out 0x800001(주소)


구하는 방법

android sudio를 설치하면 Android SDK 를 다운로드 가능합니다.

https://developer.android.com/studio

SDK Manager에서 NDK를 선택해서 설치합니다.



경로는 버전에 따라 조금씩 다를 수 있지만 아래 폴더에 존재하게 됩니다.

ndk\26.1.10909125\toolchains\llvm\prebuilt\windows-x86_64\bin


















2024년 1월 24일 수요일

GameObject (creation order) 생성 순서에 대한 고찰

#unity change order GameObject

 Unity 를 사용하면서 Awake나 Start 함수에서 다른 GameObject 상태를 살피는 경우가 종종 있습니다.

그런데 간혹 잘되던 상태가 종종 오동작하는 경우가 있어서 Object 생성 순서와 관계가 있을것 같아서 조사해 보았습니다.

구글링을 했을때는 마땅한 결론이 없어서 이것 저것 테스트 해보았습니다.

Hierarchy 에서 게임 Object를 순서대로 만들었습니다. 이름은 혹시 몰라 A,B,C,1,2,3,c,b,a 순서로 지었습니다.

그리고 Test Code에서는 Awake 진입하면 Object 이름이 출력되도록 로그를 넣었습니다.


실제 테스트를 하게되면 아래와 같은 형태로 나옵니다.

순서는 오래전 생성된것이 처음에 나오고 마지막에 추가된 Obejct가 마지막에 나옵니다.

Hierarchy 에서 순서를 마구 섞어 보았습니다.


순서는 바뀌지 않았습니다.


혹시 몰라서 Alphanumeric Sorting 기능도 건들어봤습니다.


전혀 변화가 없습니다.


지금부터는 순서를 변화 하는 방법입니다.

동일한 기능을 하는 새 스크립트를 만들어서 추가했습니다.

수정된 Object가 가장 처음으로 올라오는 변화가 일어납니다.

그렇다면 여기에서 다시 예전 스크립트로 돌린다면 순서가 예전으로 돌아가는지 확인해 보았습니다.


한번 변화된 순번은 원복되지 않습니다.

이번에는 script component 를 삭제했다가 다시 추가해 보았습니다.


처음으로 올라갑니다.



결론 : 변화가 있는 GameObject가 제일 마지막에 생성됩니다. (여기에서는 컴포넌트 변화를 주었습니다.)
순서를 변경하려면 최악의 경우 위와 같이 해야하지만, 실제는 Object 생성 순서에 영향이 없도록 Awake와 Start를 조합해서 잘 설계하는 것을 추천합니다.



2024년 1월 12일 금요일

DOTween Time.timeScale = 0 인 경우 동작하는 방법, SetUpdate(true) 추가해도 동작이 안될때

 DOTween 에서 Time.timeScale = 0 일때 animation 을 사용 하는 방법

동작 뒤에 SetUpdate(true추가해 주면 됩니다.

그런데 추가해도 안되는 경우가 있었습니다.

아래와 같은 예제가 있습니다.

            var seq = DOTween.Sequence();
            
            transform.localScale = Vector3.one * 0.1f;

            seq.Append(transform.DOScale(1.1f, fadeTime).SetUpdate(true));
            seq.Append(transform.DOScale(1f, 0.1f).SetUpdate(true));

            seq.Play().OnComplete(() =>
            {
                transform.localScale = Vector3.one;
            });

어디가 잘못 되었는지 찾았나요?



수정한 코드입니다.

        var seq = DOTween.Sequence();
            
        transform.localScale = Vector3.one * 0.1f;

        seq.Append(transform.DOScale(1.1f, fadeTime));
        seq.Append(transform.DOScale(1f, 0.1f));

        seq.Play().SetUpdate(true).OnComplete(() =>
        {
            transform.localScale = Vector3.one;
        });

sequence 를 사용하고 있다면 Play() 에 SetUpdate(true)를 넣어야 합니다.




2024년 1월 8일 월요일

requests post 에서 streaming 사용방법

requests streaming 의 필요성

requests에서 streaming이란 조금씩 buffering 하는것을 의미합니다. 일반적으로 파일을 upload하게 된다면 requests 모듈안에서는 파일 통째로 메모리에 올려서 upload하게 됩니다.

만약 파일의 크기가 크다면 어떻게 될까요? 많은 메모리가 필요하게 됩니다. 심지어는 메모리 부족 사태까지 발생될 수 있습니다.


requests streaming 사용법

가이드 문서에서는 아래 링크를 참고 하면 됩니다.

https://requests.readthedocs.io/en/latest/user/advanced/#streaming-uploads

Streaming Uploads

Requests supports streaming uploads, which allow you to send large streams or files without reading them into memory. To stream and upload, simply provide a file-like object for your body:

with open('massive-body', 'rb') as f:
    requests.post('http://some.url/streamed', data=f)

Warning

It is strongly recommended that you open files in binary mode. This is because Requests may attempt to provide the Content-Length header for you, and if it does this value will be set to the number of bytes in the file. Errors may occur if you open the file in text mode.

위 용법을 보면 크게 기존 사용하는 방법과 큰 차이가 없어보입니다.

그렇다면 이미 streaming 하고 있는걸까요?

실제 테스트해보니 동작하고 있지 않았습니다.


코드 안쪽으로 디버깅 해보기

일반적으로 requests를 사용해서 파일을 업로드시 아래와 같은 방식으로 사용하게 됩니다.

https://requests.readthedocs.io/en/latest/user/quickstart/#post-a-multipart-encoded-file

POST a Multipart-Encoded File

Requests makes it simple to upload Multipart-encoded files:

>>> url = 'https://httpbin.org/post'
>>> files = {'file': open('report.xls', 'rb')}

>>> r = requests.post(url, files=files)
>>> r.text
{
  ...
  "files": {
    "file": "<censored...binary...data>"
  },
  ...
}

차이가 뭘까요? 

정답은 data=f , files=files 넘어가는 인자가 틀립니다. 

그렇습니다. 

!중요! requests에서 files로 넘어가는 인자에 대해서는 streaming 을 지원하지 않습니다.

엥 이게 무슨말이냐고요? 거짓말 아닌지 문의하실것 같은데요.

코드에서 로그를 넣어서 확인이 가능했습니다.

requets/models.py라는 코드를 살펴보다보면 아래와 같은 부분이 보입니다.

def prepare_body(self, data, files, json=None):
"""Prepares the given HTTP body data."""

# Check if file, fo, generator, iterator.
# If not, run through normal process.

# Nottin' on you.
body = None
content_type = None

if not data and json is not None:
# urllib3 requires a bytes-like body. Python 2's json.dumps
# provides this natively, but Python 3 gives a Unicode string.
content_type = "application/json"

try:
body = complexjson.dumps(json, allow_nan=False)
except ValueError as ve:
raise InvalidJSONError(ve, request=self)

if not isinstance(body, bytes):
body = body.encode("utf-8")

is_stream = all(
[
hasattr(data, "__iter__"),
not isinstance(data, (basestring, list, tuple, Mapping)),
]
)

if is_stream:
try:
length = super_len(data)
except (TypeError, AttributeError, UnsupportedOperation):
length = None

body = data

if getattr(body, "tell", None) is not None:
# Record the current file position before reading.
# This will allow us to rewind a file in the event
# of a redirect.
try:
self._body_position = body.tell()
except OSError:
# This differentiates from None, allowing us to catch
# a failed `tell()` later when trying to rewind the body
self._body_position = object()

if files:
raise NotImplementedError(
"Streamed bodies and files are mutually exclusive."
)


    is_stream = all(
[
hasattr(data, "__iter__"),
not isinstance(data, (basestring, list, tuple, Mapping)),
]
)

is_stream 이 True되는 조건자체가 data인자만 확인하도록 되어 있습니다. 즉 streaming은 file쪽은 확인하지도 않게되고, 만약 is_stream 으로 들어가서도 files가 존재하게된다면 아래 코드에 의해서 exception이 발생하도록 되어있습니다.

        if files:
raise NotImplementedError(
"Streamed bodies and files are mutually exclusive."
)


해결 방법

이미 다른 분들이 구현해 놓은 모듈이 존재합니다.

https://github.com/requests/toolbelt

사용법은 files로 넘어가는 부분을 data로 옮기는 작업을 해야합니다.

github의 예제를 참고해서 data+files를 MultipartEncoder 넣어서 작업하면 됩니다.

from requests_toolbelt import MultipartEncoder
import requests

m = MultipartEncoder(
    fields={'field0': 'value', 'field1': 'value',
            'field2': ('filename', open('file.py', 'rb'), 'text/plain')}
    )

r = requests.post('http://httpbin.org/post', data=m,
                  headers={'Content-Type': m.content_type})



2024년 1월 2일 화요일

unity background scrolling

Unity에서 백그라운드 스크롤을 할 수 있는 방법입니다.

여러장의 이미지를 이용해서도 가능하지만 여기에서는 Material 을 이용해서 하는 방법을 정리하였습니다. 실제 따라해 보니 동작이 안되던 부분도 있고 해서 여기 저기 내용을 합쳐서 정리해 보았습니다.

2D 기준으로 테스트 하였습니다.


1. 이미지 준비

스클롤을 하려면 적당한 이미지가 필요로 합니다.

좌우로 스크롤 하려면 당연히 좌우측 이미지가 연결되어야 합니다.



2. Unity 에서 이미지 Wrap Mode에서 Repeat 설정


3. Create -> Meterial 을 만들고 Mobile/Particles/Alpha Blended 설정을 합니다.


4. 1에서 준비한 background 이미지를 추가합니다.

5. 4번에서 만든 이미지에 Meterial 적용합니다.

Sprite Renderer 항목중 Meterial 항목이 있습니다. 거기에 3번에서 생성한 파일을 Drag&Drop 해주면 추가가 됩니다.


6. 코드 작업을 해서 컴포넌트에 추가합니다.

using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using UnityEngine;
using UnityEngine.UIElements;
 
public class ScrollBG : MonoBehaviour
{
    public float scrollspeed = 0.2f;
    public bool isScrollX = true;
    Material material;
 
 
    private void Start()
    {
        material = GetComponent<Renderer>().material;
    }
 
    void Update()
    {
        Vector2 newOffset = Vector2.zero;
 
        if (isScrollX)
        {
            float newOffSetX = material.mainTextureOffset.x + scrollspeed * Time.deltaTime;
            newOffset = new Vector2(newOffSetX, 0);
        }
        else
        {
            float newOffSetY = material.mainTextureOffset.y + scrollspeed * Time.deltaTime;
            newOffset = new Vector2(0, newOffSetY);
        }
 
        material.mainTextureOffset = newOffset;
    }
}
 

7. 스크롤 방향은 isScrollX 와 scrollspeed 음수 양수로 조절 하면 됩니다.


8. 동작이 안되는 경우

- 전혀 동작이 안된다... 이미지에 Meterial 설정이 제대로 반영되고 있는지 확인 바랍니다.

- 동작은 되는데 한번만 되고, 스크롤이 아래와 같은 현상이면 2번 설정을 확인해보시기 바랍니다.