instapush로 메시지 푸시 전송 요청을 보내보자

instapush라는 서비스가 있어서 예전에 그냥 시험삼아 끄적거려 봤는데, 너무 대충이라 간단히 레시피 식으로 어떻게 푸시를 보내는지 알아보는 시간을 가져보자. instapush는 홈페이지에서 계정을 생성한 후 자신의 애플리케이션을 등록하고 필요한 필드들을 설정한 다음, 요구되는 조건에 맞게 REST API를 호출해주면, instapush 앱이 설치된 스마트폰으로 푸시 메시지를 보내주는 서비스이다. 실제로 엄청나게 간단하기 때문에 쉽게 푸시 전송을 보낼 수 있다.

회원 가입 및 앱 생성

먼저 instapush 홈페이지에 가서 회원가입을 한다.  그런다음 Dashboard의 Apps 탭으로 가면 애플리케이션을 만들라는 버튼이 있다.

[+ Add Application] 버튼을 클릭해서 새로운 앱을 등록한다. 이건 뭐 따로 만들어둔 앱을 선택한다기 보다는 일종의 푸시를 구분해서 받을 채널을 만드는 거라 생각하면 된다. 앱을 생성할 때는 이름만 주면 된다.

여기서는 간단하게 TestApp이라고 이름을 정했다. 사실 앱이름은 지금은 하나도 안 중요하니까 아무렇게나 줘도 상관없다.

이벤트 세팅

각각의 앱에 대해서 다시 이벤트를 설정한다. 이벤트는 푸시 메시지의 템플릿이라 생각하면 된다. 이벤트를 만들기 위해서는 이벤트 이름, 트래커 (메시지 파라미터) 그리고 트래커를 이용해서 실제 조합되는 문구의 포맷 템플릿을 정의해야 한다.

 

간단한 인사말을 전송하는 이벤트를 정의하자. 이벤트 이름은 Greetings라 하고 트래커로는 namemessage를 정의했다. 그리고 푸시메시지의 포맷은 Greet from {name} : "{message}" 라고 정했다.  이를테면 Greet from sooop: "Hello, world"` 와 같은 식으로 푸시 메시지가 전송될 것이다.

앱 아이디와 비밀코드

각각의 앱을 통해서 푸시를 발송하기 위해서는 앱아이디와 앱비밀코드(app secret)가 필요하다. 이는 앱을 처음 생성하면 자동으로 만들어진다. 대시보드에서 앱을 선택하고, 다시 [Basic Info] 탭으로 가면 이를 확인할 수 있다.

스마트폰 앱 설치

실제로 푸시를 수신할 스마트폰용 앱을 설치한다. 웹에서 회원 가입한 동일 계정으로 로그인해 두자.

푸시 전송하기

실제로 푸시를 전송해볼 차례이다.  사실 필요한 모든 정보는 API 페이지의 맨 위에 다 나와있다. 이 내용은 터미널 상에서 curl 이라는 툴을 사용해서 명령줄에서 직접 API 서버로 요청을 날리는 예를 설명한 것이다. (참고로 curl은 거의 모든 OS 용으로 배포되고 있으니, 관심 있는 사람이라면 이곳을 통해서 설치해볼 수도 있다.)

일단 위 내용의 파라미터를 설명하면 다음과 같다.

  1. curl 은 기본적으로 HTTP나 FTP 등의 인터넷 프로토콜을 이용해서 통신하는 프로그램이다.
  2. -X POST 는 HTTP 요청을 POST method를 이용해서 보낸다는 뜻이다.
  3. -H로 시작하는 값들은 모두 HTTP 요청의 헤더들이다. 헤더는 여러 개의 키:값 쌍으로 구분되며, : 을 통해서 구분된다.
  4. -d 로 시작된 이후 내용은 요청의 본체로 서버로 전송될 데이터, 즉 페이로드이다. JSON 포맷을 그대로 사용한다. (이 문자열은 curl 프로그램 내부에서 적절하게 인코딩되어 날아갈 것이다.)

사실, curl 이 시스템에 설치되어 있다면, 저 내용 그대로를 subprocess.check_call()을 사용해서 호출해도 된다. (물론 실제 그대로는 아니고 데이터 부분과 각 id, secret 정보는 자기 것으로 바꿔야지…) 결국 이 구현은 단순히 HTTP POST 요청하나에 JSON 데이터를 실어서 보내는 것 이상도 이하도 아니다.

urllib.request.Request

흔히 HTTP 요청을 통해서 웹페이지나 웹상의 리소스를 내려받을 때에는 urlopen() 함수를 사용한다. 이 함수만을 사용하는 것은 암시적으로 HTTP GET 요청을 보내는 것으로 일상적인 웹브라우저의 주소창을 이용한 웹페이지 접근과 동일하다고 볼 수 있다. HTTP POST는 폼을 통한 파일업로드나 로그인 등에 사용되는 통신 방식이다. 통신 방식이나 헤더 정보를 커스터마이징하여 통신하기 위해서는 별도의 HTTP 요청 객체를 생성해서, 이를 URL 대신에 urlopen() 함수에 전달해야 한다.

이 클래스의 인스턴스를 생성할 때, 앞서 언급한 대부분의 HTTP 요청의 구성 요소들을 넘겨줄 수 있다.

  • 맨 첫 인자는 URL이다.
  • headers= 인자는 헤더값을 정의한 dict 객체를 넘겨준다.
  • data= 인자는 요청의 페이로드에 대응하는 바이트배열이다.
  • method= 인자는 요청의 방식을 결정한다.

이제 실제 코드를 작성해보자. 위 인자를 사용해서 요청을 만들고 이걸 urlopen에 넘기면 끝.

from urllib.request import Request, urlopen
import json

def send_greet(name, message):
  url = 'https://api.instapush.im/v1/post'
  headers = {
    'x-instapush-appid' : '5###################',
    'x-instapuhs-appsecret' : '3####################',
    'Content-Type' : 'application/json'
  }
  payload = { 'event' : 'Test_Event',
              'trackers' : { 'name' : name, 'message' : message }
  }
  ## 페이로드는 json.dumps 를 사용해서 JSON 문자열로 바꾼다음, 다시 인코딩해서 바이트배열로 만든다.
  req = Request(url, headers=headers, data=json.dumps(payload).encode(), method='POST')
  res = urloepn(req)
  print(res.read().decode())  

이제 실제로 위 함수를 호출해서 스마트폰으로 푸시 메시지가 전송되는지 확인하면 된다.

Subprocess 모듈 사용법 – 파이썬에서 서브 프로세스를 생성하기

Subprocess 모듈 이란?

subprocess 모듈은 파이썬 프로그램 내에서 새로운 프로세스를 스폰하고 여기에 입출력 파이프를 연결하며 리턴코드를 획득할 수 있도록 하는 모듈로, 다른 언어로 만들어진 프로그램을 통합, 제어할 수 있게 만드는 모듈이다. 이 모듈은 기존에 오랜된 몇몇 모듈과 함수(os.system, os.spawn*)들을 대체하기 위해 만들어졌다. (혹은 os.popen 같은 함수도…)

사용방법

새로운 서브 프로세스를 만들기 위해 권장되는 방법은 다음의 편의 함수들을 사용하는 것이다. 보다 세부적인 제어를 위해서는 Popen 인터페이스를 직접 사용한다.

subprocess.call 함수 – 가장 간단한 서브 프로세스 실행

가장 기본적인 서브프로세스 호출명령이다. 인자값들을 사용하여 바로 명령을 실행한다. 이 함수는 블럭킹함수로 서브프로세스가 동작을 시작하면 이를 기다렸다가 서브 프로세스가 종료되면 해당 프로세스의 실행 리턴코드를 반환한다. 보통의 쉘에서 돌아가는 프로그램들은 정상 종료시 0을 리턴한다.

subprocess.call(args, *, stdin=None, stdout=None, stderr=None, shell=False, timeout=None)

call 함수는 Popen의 컨스트럭터와 비슷한 시그니쳐를 가지고 있다.(사실 subprocess 모듈의 거의 모든 함수들이 Popen의 인자중에서 일부를 갖는 형태로 디자인되어 있다.) 몇 가지 인자를 살펴보면 다음과 같다.

  • args는 명령행에 문자열 혹은 입력될 인자들을 공백으로 자른 리스트이다.
  • stdin, stdout, stderr 은 각각 표준입력, 표준출력, 표준에러의 표준입출력 리다이렉션에 대응한다. 파일디스크립터를 사용해서 파일의 내용을 표준입출력으로 대체할 수 있다. 보통은 생략되면 현재의 표준입출력이 그대로 적용된다.
  • shell=True 가 되면 서브 프로세스로 주어진 명령을 바로 실행하는 것이 아니라, 별도의 서브 쉘을 실행하고 해당 쉘 위에서 명령을 실행하도록 한다. 이 값이 True가 되는 경우에는 args가 리스트 보다는 하나의 문자열인 쪽이 좋다고 한다. 참고로 쉘을 사용하면 쉘이 제공하는 파이프, 리다이렉션과 쉘의 문자열 확장 (예를 들어 FILES* 라고 쓰면 FILES1 FILES2 FILES3 … 이렇게 만들어지는 것)을 함께 사용할 수 있게 된다.
  • 기본적으로 이 함수는 서브프로세스가 종료될 때까지 기다리는데 timeout 값을 설정하면 주어진 시간(초단위)만큼 대기한 후, 대기 시간을 초과하면 TimeoutExpired 예외를 일으키게 된다.
>>> subprocess.call(["ls", "-l"])
...파일 목록이 출력되고...
0  ## 성공했다면 0이 리턴값으로 나오게 될 것이다

subprocess.check_call – 결과값을 강제로 체크하기

call() 함수는 서브 프로세스를 통해서 주어진 명령을 실행하고 리턴한다. 이 과정에서 리턴 코드를 받아오긴 하지만, 해당 명령이 처리에 실패하고 비정상 종료를 하는 경우에도 이후의 코드가 무사히 실행이 될 것이다. (물론 부지러한 사람이라면 이 리턴코드가 0인지 비교해볼 것이다.)

check_call()은 서브 프로세스에 의한 처리가 성공하는 것이 보장되어야 하는 경우에 쓰인다. 즉 서브 프로세스의 명령이 비정상 종료하였다면 그 리턴코드가 0이 아닐 것이다. 이 때 다른 리턴 코드 값을 내놓는게 아니라 그 즉시 CalledProcessError 예외를 일으킨다. (이 예외 객체내에 returncode라는 속성이 있고, 따라서 예외처리 구문에서 리턴 코드를 확인할 수 있다.)

subprocess.check_output – 출력되는 문자열이 필요할 때

서브프로세스를 실행하고 그 출력 문자열을 리턴한다. 따라서 실행되어 출력된 결과를 문자열로 받고 싶을 때 사용한다. 스폰된 프로세스의 리턴코드가 0이 아닌 경우 CalledProcessError예외를 일으킨다. (그래서 이름에 check가 포함된다.)  이 버전은 Popen 객체를 사용함에 있어서 표준입출력을 문자열 데이터로 사용하는 버전에 가깝다. 즉 프로세스가 실행되고 종료되면 표준출력으로 넘어온 데이터를 읽어서 그 값을 리턴해주는 함수라 보면 되겠다. 프로세스스가 사용하는 입력의 경우에도 input= 인자를 통해서 미리 지정해 줄 수 있다. 

universal_newlines= 파라미터는 사실 그 이름과 의미가 약간 모호한데, 서브 프로세스와의 파이프 통신을 문자열을 주고받는 것으로 처리한다. (기본적으로 False 값일 때에는 파이프를 통해서 바이트스트림이 오간다.) 다음은 사용예이다.

>>> subprocess.check_output(["echo", "Hello World!"])
b'Hello World!\n'

### universal_newline을 쓰면 바이트가 아닌 문자열로 받게된다.
>>> subprocess.check_output(["echo", "Hello World!"], universal_newlines=True)
'Hello World!\n'

## input을 쓰면 표준입력을 통해서 입력될 내용을 인자로 넘겨줄 수 있다. 
>>> subprocess.check_output(["sed", "-e", "s/foo/bar"],
...                input=b"when in the course of fooman eventsn")
b'when in the course of barman eventsn'

## check_...가 들어있기 때문에 정상종료하지 않으면 예외처리된다.
>>> subprocess.check_output("exit 1", shell=True) # 리턴코드가 1이다.
Traceback ...
...
subprocess.CalledProcessError: command 'exit 1' returned non-zero exit status 1

기본적으로 리턴되는 출력 데이터는 인코딩된 바이트 스트림이다. 인코딩은 서브프로세스 프로그램에 의존하며, 따라서 이를 텍스트로 변환하기 위해서는 디코딩이 필요하다. 이 동작은 universal_newline=True이면 오버라이드 된다.

에러 출력을 잡기 위해서는 stderr=subprocess.PIPE라는 인자를 정의한다. 서브프로세스 상에서 에러메시지로 출력된 내용을 받아오게 된다.

>>> subprocess.check_output(
...    "ls non_existent_file; exit 0;",
...    stderr=subprocess.STDOUT,
...    shell=True)
'ls: non_existent_file: No such file or directoryn'

 

Popen 클래스

프로세스 생성과 관리 모듈은 내부적으로 Popen 클래스를 통해서 관리한다. 실질적으로 call-*류의 함수들은 이 클래스를 기반으로 하고 있다. Popen은 많은 유연성을 제공하기 때문에 기본적으로 제공되는 편의 함수들만으로 다룰 수 없는 케이스를 커버할 때, 세밀한 옵션들에 대해서 직접 제어하고자 할 때 사용할 수 있다.

Popen

Popen 클래스는 인스턴스 초기화시에 여러가지 옵션들을 인자로 받을 수 있다.  물론 여기서는 이걸 다 설명할 수 없다. 전체 내용은 파이썬 공식 문서를 참고하도록 하자.

class subprocess.Popen(args, bufsize=-1, excutable=None, 
                       stdin=None, stdout=None, stderr=None, 
                       preexec_fn=None, close_fds=None, shell=False, 
                       cwd=None, env=None, universal_newlines=False, 
                       startupinfo=None, creationflags=0, restoreflags=0, 
                       restore_signals=True, start_new_session=False, pass_fds=()
                      )

args는 프로그램 인자의 리스트이거나, 단일 문자열이다(만약 shell값이 True라면 단일 문자열이어도 된다.)

서브 프로세스의 입출력

stdin, stdout, stderr은 서브 프로세스의 표준 입출력 핸들러를 가리킨다. 이에 유효한 값은 PIPEDEVNULL 혹은 이미 존재하는 다른 파일 핸들러 및 파일 객체일 수 있다. PIPE를 넘기는 경우, 자식 프로세스에게 새로운 파이프가 생성되어 연결된다. 디폴트값은 None이며 이 경우 리다이렉션이 일어나지 않고, 부모 프로세스(파이썬 프로그램 자신)의 입출력을 상속받는다. 참고로 stderr에는 STDOUT을 줄 수 있는데, 이렇게 하여 에러 문구를 표준 출력으로 받을 수 있다.

문자열 출력에 대한 변환

universal_newlines는 라인엔딩 변환을 하느냐 하지 않느냐의 차이를 보이는데, 이 값이 True이면 문자열을 주고 받게 되므로, 파일 객체를 파이프연결한 경우 이 값은 False가 되어야 한다. 개행 문자는 플랫폼 마다 다른데, 이 값이 True로 넘어가는 경우, os.linesep을 사용하여 개행 문자를 변환하게 된다.

shell 파라미터 값이 True인 경우, 주어진 명령을 쉘을 통해서 처리한다. 이는 쉘 스크립트 대신 파이썬 스크립트를 통해서 배치 명령의 흐름을 제어하고자 할 때 유용하다. 이 기능을 사용하면 파일 이름 와일드카드나, 환경변수 확장 등의 쉘 기능을 이용할 수 있다. 단, 파이썬 역시 왠만한 쉘 관련 기능들을 제공한다. glob, fnmatch, os.walk(), os.path.expandvars(), os.path.expanduser(), shutil 등의 모듈, 함수를 참고할 것.

컨텍스트 매니저

Popen 객체는 컨텍스트 매니저 프로토콜을 구현하고 있어서 with 구문에 사용될 수 있다. 빠져나오는 경우 표준 파일 디스크립터는 닫히게 되고, 프로세스를 기다리게 된다.

with Popen(["ifconfig"], stdout=PIPE) as proc:
    log.write(proc.stdout.read())

 Popen 객체의 메소드들

다음 메소드들은 Popen 생성자를 이용해서 만든 객체가 제공하는 메소드들이다.

  • .poll(): 자식 프로세스가 종료되었는지를 확인한다.
  • .wait(timeout=None): 자식 프로세스가 종료되길 기다린다. 자식 프로세스의 리턴 코드를 돌려준다.
  • .communicate(input=None, timeout=None): 자식 프로세스의 표준 입력으로 데이터를 보낸다음, 표준 출력의 EOF르 만날 때까지 이를 읽어온다. (프로세스가 끝날 때까지 기다린다.) 이 함수는 (stdout_data, stderr_data)의 튜플을 리턴한다. 이 함수를 사용하려면 stdin=PIPE 옵션으로 자식 프로세스를 시작해야 한다.
  • Popen.send_signal(signal): 자식 프로세스에 시그널을 보낸다.
  • Popen.terminate(): 자식 프로세스에 종료 시그널을 보낸다.
  • Popen.kill(): 자식 프로세스를 강제로 죽인다.
  • Popen.args: 주어진 args 를 리턴한다.
  • Popen.stdin: 만약 PIPE라면 이 객체는 읽고 쓸 수 있는 스트림 객체이다.
  • Popen.stdout: PIPE라면 읽을 수 있는 객체이다.
  • Popen.stderr: PIPE라면 읽을 수 있는 객체이다.
  • Popen.pid: 자식 프로세스의 pid값
  • Popen.returncode: 자식 프로세스가 종료되었을 때 리턴코드 값을 가진다.