-
Riot API - Configuration, Request & Exception Handler, Error ModuleData Analysis/League of Legends 2020. 5. 20. 18:11
좋은 프로그래머라면 예외 처리를 고려하면서 프로그램을 짜야 합니다.
이에 development API key를 가지고 본격적으로 API를 사용해보기 전에, Riot API를 사용(request)하고 데이터를 얻는 모듈을 짜면서, 그 안에서 예외 처리(exception handling)를 해봅시다. 또한, 이를 위해 지역(region)과 API key를 세팅하는 configuration 모듈도 짜야겠죠.
모든 코드는 Python 3.6이상을 대상으로 합니다.
HTTP 형식의 GET, PUT, UPDATE, DELETE 네 종류의 명령어를 이용하는 API를 REST API라고 하는데요, Riot API도 이러한 REST API에 해당합니다. 또한, 대부분의 API는 GET 명령어를 사용하죠.
다음 API는 특정 랭크 게임 매치의 결과를 가져오는 주소입니다.
url = "https://kr.api.riotgames.com/lol/match/v4/matches/12345678?api_key=RGAPI-xxx..."
여기서, kr은 한국 지역(region)을 의미하고, 12345678은 matchId를 의미하고, api_key에 해당하는 값은 우리가 현재 지고 있는 development API key입니다.
중요한 점은 https://(kr).api.riotgames.com 까지의 주소는 kr에 해당하는 지역 정보만 제외하면 항상 동일하다는 것입니다. 또한, 주소의 뒷부분에 있는 '?' 뒤에 나타나는 a=b에 해당하는 요소들을 query라고 하는데요, api_key에 해당하는 query도 항상 포함해야 결과가 제대로 전달됨을 알 수 있습니다.
그렇다면 우리는 이제 지역 정보와 API key를 configure해야 합니다. 이를 위해 저번 글에서 만든 LolMaster폴더 안에 config.py를 다음과 같이 작성해 줍니다.
from LolMaster.error import KeyNotSetError, RegionNotSetError from configparser import ConfigParser import logging import sys import os base = os.path.expanduser('~/.lolMaster') if not os.path.exists(base): os.mkdir(base) fileName = os.path.join(base, 'config') config = ConfigParser() if not os.path.exists(fileName): with open(fileName, 'w+'): pass config.read(fileName) if not config.has_section('main'): config.add_section('main') def set_key(key: str): config.set('main', 'key', key) with open(fileName, 'w+') as fp: config.write(fp) def get_key() -> str: config.read(fileName) if config.has_option('main', 'key'): return config.get('main', 'key') else: raise KeyNotSetError("You should set key by config.store_key().") def set_region(region: str): config.set('main', 'region', region.lower()) with open(fileName, 'w+') as fp: config.write(fp) def get_region() -> str: config.read(fileName) if config.has_option('main', 'region'): return config.get('main', 'region') else: raise RegionNotSetError("You should set region by config.store_region().")
다만 아직 KeyNotSetError와 RegionNotSetError를 정의하지 않았습니다. 에러 모듈은 본 글의 맨 아래에서 짜도록 하죠.
이제 우리는 set_key, set_region 함수를 이용해 key와 region을 세팅할 수 있습니다. LolMaster의 상위 폴더에서 다음 코드를 작성해서 실행해보세요.
from LolMaster import config config.set_key("RGAPI-123") # Your API key config.set_region("kr") # Your interest region
위에서 특정 랭크 게임 매치를 가져오는 API 예시를 보여드렸죠. 바로 아래 API인데요,
url = "https://kr.api.riotgames.com/lol/match/v4/matches/12345678?api_key=RGAPI-xxx..."
우리는 위 url을 이용하여 GET 명령어를 보내면 이에 해당하는 게임 데이터가 포함된 결과를 받을 수 있겠지요. 심플하게는 다음과 같습니다.
import requests res = requests.get(url)
여기서, requests 모듈은 http/https 통신을 위한 Python package이고, res는 response의 약자로 사용했습니다.
다만, 모든 경우가 이처럼 깔끔하지는 않습니다. Riot API 정책 상 최대 요청 수 제한도 있고, 예기치 못한 에러가 발생할 수 있습니다. 또한, 우리가 설정한 region과 api_key를 자동으로 적절하게 붙여서 사용하면 좋겠죠.
지금부터 RiotURL class를 만들어서, 기본 url을 설정하고, 각종 query (url에서 &뒤에 붙는 변수들)를 설정하고, relative url을 Riot API의 region을 포함하는 absolute url로 변환하는 함수를 짭니다. 마지막으로, 가장 중요한 request 함수를 짭니다.
이를 위해 LolMaster 폴더 안에 api.py 파일을 만들고 다음과 같이 class를 정의해 줍니다.
from LolMaster.error import KeyNotValidError, NotReachableError from LolMaster import config from typing import Iterable, Union, Dict import requests import time class RiotURL: def __init__(self, url: str): self.url = url self.queries = {}
여기서 url은 'https://kr.api.riotgames.com'과 과 'api_key=RGAPI-xxx'이 제외된 상대주소입니다. 이는 코딩의 편의성 때문이기도 하고, 우리가 설정한 region과 API key를 적절히 이용하기 위함이기도 합니다.
따라서 url이 넘어왔을 때 상대주소이면 적절히 절대주소로 변환해줍시다.
class RiotURL: def __init__(self, url: str): self.url = url self.queries = {} if self.url[0] == '/': self.url = RiotURL.absolute_url(self.url) @staticmethod def absolute_url(url: str) -> str: region = config.get_region() base = "https://%s.api.riotgames.com" % region return base + url
API key는 왜 넣지 않았을까요? API key는 query에 포함되지만, query가 꼭 하나라는 법은 없습니다. 가령 게임ID도 포함될 수 있고, 이 경우 여러 query들을 연결해야 합니다. 이를 효율적으로 처리하고자 self.queries에 key:value 쌍으로 저장해 두고 마지막에 한 번에 처리합시다. 다음 함수로 말이죠.
class RiotURL: # ... def set_query(self, query_name: str, queries: Union[str, Iterable]) -> 'RiotURL': new_queries = [] if type(queries) is str: new_queries = [queries] for query in queries: new_queries.append(str(query)) self.queries[query_name] = new_queries return self def get_url_with_query(self) -> str: final_url = self.url first = True for query_name in self.queries.keys(): if first: final_url += '?' first = False else: final_url += '&' final_url += "%s=%s" % (query_name, ','.join(self.queries[query_name])) return final_url
이제 핵심인 request함수를 짜봅시다. 어떤 기능이 필요할까요? 1) API key를 query에 넣고, 2) query가 포함된 url을 가져오며, 3) request 모듈로 GET 요청을 보내고, 4) 응답 메세지를 적절히 처리해야 합니다. 이를 담은 "가장 간단한" 코드는 아래와 같습니다. (아직 완성된 함수가 아닙니다 !!)
class RiotURL: # ... def request(self) -> Union[None, Dict]: self.set_query('api_key', config.get_key()) url = self.get_url_with_query() r = requests.get(url) if r.status_code == 200: return r.json() else: # 예외처리 raise Exception
하지만 우리는 API call이 실패했다고 해서 바로 포기하는 코드를 원하지 않습니다. 이를 위해 status_code값에 따라 잘 처리할 필요가 있습니다.
status_code에 따른 의미는 다음과 같습니다.
- 429: 최근에 메세지를 너무 많이 보냈습니다.
- 404: 당신이 찾으려고 하는 데이터는 존재하지 않습니다.
- 401: 주소에 API token을 포함하지 않았습니다.
- 403: API token이나 주소가 유효하지 않습니다.
- 이외: 일시적인 오류일 가능성이 높습니다.
여기서 핵심은 status_code가 429일 때, header에 포함된 Retry-After 필드값을 이용하여 그 시간만큼 대기(time.sleep)하는 것입니다. 해당 필드값을 이용하지 않고 임의의 값만큼 쉬게 되면 불필요한 wakeup이 더 일어나거나, 필요한 시간보다 더 많이 sleep하게 되어 코드가 비효율적이죠.
(다만, 항상 Retry-After 필드가 존재하지는 않습니다. 이 경우에는 임의의 값만큼 쉴수밖에 없죠.)
또한, 일시적인 오류들에 한해서는 다시 request를 보내도록 합시다. 한 5번 정도면 충분하겠죠 ?
이러한 예외 처리가 모두 포함된 코드는 다음과 같습니다.
def RiotAPI: # ... def request(self, max_retry: int = 5) -> Union[None, Dict]: self.set_query('api_key', config.get_key()) url = self.get_url_with_query() while max_retry > 0: r = requests.get(url) if r.status_code == 200: return r.json() elif r.status_code == 404: return None elif r.status_code == 429: backoff = r.headers.get('Retry-After') if backoff is None: backoff = 30 backoff = int(backoff) time.sleep(backoff) continue else: if r.status_code == 401: raise NotReachableError elif r.status_code == 403: raise KeyNotValidError else: max_retry -= 1 continue return None
여기까지 코드를 작성하셨으면 코드에 오류가 뜰겁니다. NotReachableError와 KeyNotValidError를 정의하지 않았기 때문입니다. 또한, 아까 위에서 작성한 config.py에도 정의하지 않은 타입의 에러가 있었죠.
이를 위해 LolMaster 폴더에 error.py를 만들어 아래 내용을 작성합니다.
class LolMasterException(Exception): pass class KeyNotSetError(LolMasterException): pass class RegionNotSetError(LolMasterException): pass class KeyNotValidError(LolMasterException): pass class MaxRetryError(LolMasterException): pass class NotReachableError(LolMasterException): """ You should not reach this error. """ pass
다른 에러는 다 이해가 되시겠지만, NotRechableError는 왜 필요할까 하고 생각하실 수 있습니다. 이를 넣어놓은 이유는 우리가 항상 API key를 url에 넣어서 전달하고, 만약 프로그래머가 API key를 설정하지 않았으면 항상 KeyNotSetError를 raise할 것이므로, url에 API key가 포함되지 않았다는 401 Error는 로직 상 발생할 수 없기 때문이죠. 이를 직관적으로 표기하기 위해 해당 코드 블럭이 실제로는 발생할 수 없음을 나타내는 에러입니다.
궁금한 점 있으시면 질문 주세요. 감사합니다.
'Data Analysis > League of Legends' 카테고리의 다른 글
Riot API - Summoner (0) 2020.05.26 Riot API - League (0) 2020.05.20 Riot API - Environment Setting (0) 2020.05.20 Riot API - Basic (0) 2020.05.19 Before we start (0) 2020.05.19