Ver3.5 ๐ Python์ผ๋ก ์ธ์๊ณผ ์ฐ๊ฒฐํ๊ธฐ: requests์ ์น API ํด๋ผ์ด์ธํธ๋ก ์ธ๋ถ ์๋น์ค ์์ ์ ๋ณต!

๐ Python์ผ๋ก ์ธ์๊ณผ ์ฐ๊ฒฐํ๊ธฐ: requests์ ์น API ํด๋ผ์ด์ธํธ๋ก ์ธ๋ถ ์๋น์ค ์์ ์ ๋ณต!
์ผ, ์์งํ ๋งํด๋ด. ํ์ด์ฌ ๋ฐฐ์ฐ๋ฉด์ "์ด๊ฑธ๋ก ๋ญ ๋ง๋ค ์ ์์ง?" ๋ผ๋ ์๊ฐ ํ ๋ฒ์ฏค ํด๋ดค์ง? ๐ค
๊ทธ ๋ต ์ค ํ๋๊ฐ ๋ฐ๋ก ์ธ๋ถ ์๋น์ค ์ฐ๋์ด์ผ. ๋ ์จ ์ฑ, ํ์จ ๊ณ์ฐ๊ธฐ, ์ ํ๋ธ ๋ฐ์ดํฐ ๋ถ์, ์นด์นด์ค ๋ก๊ทธ์ธ... ์ด ๋ชจ๋ ๊ฒ ๋ค ์น API๋ฅผ ํตํด์ ์ด๋ฃจ์ด์ง๊ฑฐ๋ .
์ค๋์ ํ์ด์ฌ์ ๊ตญ๋ฏผ ๋ผ์ด๋ธ๋ฌ๋ฆฌ requests๋ฅผ ๋ค๊ณ , ์ค์ ๋ก ์ธ๋ถ ์๋น์ค๋ค๊ณผ ์ด๋ป๊ฒ ๋ํํ๋์ง ์ฒ์๋ถํฐ ๋๊น์ง ํํค์ณ๋ณผ ๊ฑฐ์ผ. ์ฝ๋ ํ ์ค ํ ์ค ๊ฐ์ด ๋ฏ์ด๋ณด๋ฉด์, ์ง์ง ์ค๋ฌด์์ ์ฐ์ด๋ ํจํด๊น์ง ๋ค ์๋ ค์ค๊ฒ. ์ค๋น๋์ด? ๊ทธ๋ผ ๊ณ ๊ณ ! ๐ฅ
API๋ Application Programming Interface์ ์ฝ์์ผ. ์ด๋ ต๊ฒ ๋ค๋ฆฌ์ง? ๊ทธ๋ฅ ์ฝ๊ฒ ๋งํ๋ฉด "ํ๋ก๊ทธ๋จ๋ผ๋ฆฌ ๋ํํ๋ ๋ฐฉ๋ฒ"์ด์ผ.
์๋น์ผ๋ก ๋น์ ํ๋ฉด ์ด๋. ๋(ํด๋ผ์ด์ธํธ)๊ฐ ์๋น(์๋ฒ)์ ๊ฐ์ ๋ฉ๋ดํ(API ๋ฌธ์)์ ๋ณด๊ณ , ์จ์ดํฐ(API)ํํ
"๊น์น์ฐ๊ฐ ์ฃผ์ธ์"(์์ฒญ)๋ผ๊ณ ํ๋ฉด, ์ฃผ๋ฐฉ์์ ์๋ฆฌํด์ ์์(์๋ต)์ ๊ฐ์ ธ๋ค์ฃผ๋ ๊ฑฐ์ผ. ๋๋ ์ฃผ๋ฐฉ์์ ์ด๋ป๊ฒ ์๋ฆฌํ๋์ง ๋ชฐ๋ผ๋ ๋ผ. ๊ทธ๋ฅ ์ฃผ๋ฌธ๋ง ํ๋ฉด ๋๋ ๊ฑฐ์ง! ๐ฒ
์์ฆ ๋๋ถ๋ถ์ ์น API๋ REST(Representational State Transfer) ๋ฐฉ์์ ์จ. REST API๋ HTTP ํ๋กํ ์ฝ์ ๊ธฐ๋ฐ์ผ๋ก ํ๊ณ , ์๋ 4๊ฐ์ง ๋ฉ์๋๋ฅผ ์ฃผ๋ก ์ฌ์ฉํด:
| HTTP ๋ฉ์๋ | ์ญํ | ์์ |
|---|---|---|
| GET | ๋ฐ์ดํฐ ์กฐํ | ๋ ์จ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ |
| POST | ๋ฐ์ดํฐ ์์ฑ | ์ ๊ฒ์๊ธ ์์ฑ |
| PUT/PATCH | ๋ฐ์ดํฐ ์์ | ํ๋กํ ์ ๋ณด ์ ๋ฐ์ดํธ |
| DELETE | ๋ฐ์ดํฐ ์ญ์ | ๊ฒ์๊ธ ์ญ์ |
๊ทธ๋ฆฌ๊ณ ์๋ฒ๋ ์๋ตํ ๋ HTTP ์ํ ์ฝ๋๋ฅผ ๊ฐ์ด ๋ณด๋ด์ค. ์ด๊ฒ ๋๊ฒ ์ค์ํด:
| ์ํ ์ฝ๋ | ์๋ฏธ |
|---|---|
| 200 OK | ์ฑ๊ณต! ์์ฒญํ ๋๋ก ์ ๋์ด โ |
| 201 Created | ์์ฑ ์ฑ๊ณต! ์ ๋ฆฌ์์ค๊ฐ ๋ง๋ค์ด์ก์ด ๐ |
| 400 Bad Request | ์์ฒญ์ด ์๋ชป๋์ด. ํ๋ผ๋ฏธํฐ ํ์ธํด๋ด ๐ค |
| 401 Unauthorized | ์ธ์ฆ์ด ํ์ํด. API ํค ํ์ธํด๋ด ๐ |
| 403 Forbidden | ๊ถํ ์์. ์ ๊ทผ ๋ถ๊ฐ ๐ซ |
| 404 Not Found | ๊ทธ๋ฐ ๊ฑฐ ์์ด. URL ํ์ธํด๋ด ๐ |
| 429 Too Many Requests | ์์ฒญ์ด ๋๋ฌด ๋ง์. ์ ๊น ์ฌ์ด โณ |
| 500 Internal Server Error | ์๋ฒ ๋ฌธ์ ์ผ. ๋ด ์๋ชป ์๋ ๐ |
ํ์ด์ฌ ํ์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ์๋ urllib์ด ์๊ธด ํ๋ฐ, ์์งํ ์ฐ๊ธฐ ๋ถํธํด. requests๋ "HTTP for Humans"๋ผ๋ ์ฌ๋ก๊ฑด์ฒ๋ผ ์ฌ๋์ด ์ฐ๊ธฐ ํธํ๊ฒ ๋ง๋ค์ด์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ. ํ์ด์ฌ ์ํ๊ณ์์ ๊ฐ์ฅ ๋ง์ด ๋ค์ด๋ก๋๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค ํ๋๊ณ , ์ค๋ฌด์์๋ ๊ฑฐ์ ํ์ค์ฒ๋ผ ์ฐ์ฌ.
pip install requests
๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ GET ์์ฒญ๋ถํฐ ํด๋ณด์. JSONPlaceholder๋ผ๋ ๋ฌด๋ฃ ํ ์คํธ API๋ฅผ ์จ๋ณผ๊ฒ:
import requests
# GET ์์ฒญ ๋ณด๋ด๊ธฐ
response = requests.get('https://jsonplaceholder.typicode.com/posts/1')
# ์ํ ์ฝ๋ ํ์ธ
print(response.status_code) # 200
# JSON ์๋ต ํ์ฑ
data = response.json()
print(data)
# {'userId': 1, 'id': 1, 'title': '...', 'body': '...'}
๋ฑ 3์ค์ด์ผ! requests.get()์ผ๋ก ์์ฒญ ๋ณด๋ด๊ณ , .json()์ผ๋ก ์๋ต ํ์ฑ. ์ด๊ฒ ๋์ด์ผ. ์ง์ง ๊ฐ๋จํ์ง? ๐
์๋ต ๊ฐ์ฒด์๋ ๋ค์ํ ์ ๋ณด๊ฐ ๋ด๊ฒจ ์์ด:
response = requests.get('https://jsonplaceholder.typicode.com/posts/1')
print(response.status_code) # 200 (์ํ ์ฝ๋)
print(response.headers) # ์๋ต ํค๋ ๋์
๋๋ฆฌ
print(response.text) # ์๋ต ๋ณธ๋ฌธ (๋ฌธ์์ด)
print(response.json()) # JSON ํ์ฑ ๊ฒฐ๊ณผ (๋์
๋๋ฆฌ)
print(response.content) # ์๋ต ๋ณธ๋ฌธ (๋ฐ์ดํธ)
print(response.url) # ์ต์ข
์์ฒญ URL
print(response.elapsed) # ์๋ต ์๊ฐ
print(response.encoding) # ์ธ์ฝ๋ฉ ๋ฐฉ์
# ์ฑ๊ณต ์ฌ๋ถ ํ์ธ (200~299๋ฉด True)
print(response.ok) # True
response.raise_for_status()๋ฅผ ํธ์ถํ๋ฉด 4xx, 5xx ์๋ฌ ์ ์๋์ผ๋ก ์์ธ๋ฅผ ๋ฐ์์์ผ์ค. ์๋ฌ ์ฒ๋ฆฌํ ๋ ์ง์ง ์ ์ฉํด!
URL์ ?key=value&key2=value2 ํํ๋ก ๋ถ์ด๋ ํ๋ผ๋ฏธํฐ์ผ. ๊ฒ์ ์กฐ๊ฑด์ด๋ ํํฐ๋ง์ ๋ง์ด ์จ. requests์์๋ ๋์
๋๋ฆฌ๋ก ๊น๋ํ๊ฒ ์ฒ๋ฆฌํ ์ ์์ด:
import requests
# ๋ฐฉ๋ฒ 1: URL์ ์ง์ ๋ถ์ด๊ธฐ (๋น์ถ์ฒ)
response = requests.get('https://api.example.com/search?q=python&page=1&limit=10')
# ๋ฐฉ๋ฒ 2: params ๋์
๋๋ฆฌ ์ฌ์ฉ (์ถ์ฒ! โ
)
params = {
'q': 'python',
'page': 1,
'limit': 10,
'sort': 'latest'
}
response = requests.get('https://api.example.com/search', params=params)
# ์ค์ ์์ฒญ URL ํ์ธ
print(response.url)
# https://api.example.com/search?q=python&page=1&limit=10&sort=latest
params ๋์ ๋๋ฆฌ๋ฅผ ์ฐ๋ฉด URL ์ธ์ฝ๋ฉ๋ ์๋์ผ๋ก ํด์ค์ ํน์๋ฌธ์ ๊ฑฑ์ ์์ด. ์ง์ง ํธ๋ฆฌํ์ง! ๐
ํค๋๋ ์์ฒญ์ ๋ํ ๋ฉํ ์ ๋ณด๋ฅผ ๋ด์. API ํค, ์ธ์ฆ ํ ํฐ, ์ฝํ ์ธ ํ์ ๋ฑ์ ์ฌ๊ธฐ์ ๋ฃ์ด:
import requests
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'MyApp/1.0',
'Authorization': 'Bearer your_token_here'
}
response = requests.get(
'https://api.example.com/data',
headers=headers
)
print(response.json())
API ์ธ์ฆ์ ํฌ๊ฒ 3๊ฐ์ง ๋ฐฉ์์ด ์์ด:
import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
# 1. Basic ์ธ์ฆ (์์ด๋/๋น๋ฐ๋ฒํธ)
response = requests.get(
'https://api.example.com/data',
auth=HTTPBasicAuth('username', 'password')
)
# ๋๋ ๊ฐ๋จํ๊ฒ
response = requests.get(
'https://api.example.com/data',
auth=('username', 'password')
)
# 2. API Key ์ธ์ฆ (ํค๋์ ๋ฃ๊ธฐ)
headers = {'X-API-Key': 'your_api_key_here'}
response = requests.get('https://api.example.com/data', headers=headers)
# 3. Bearer Token ์ธ์ฆ (OAuth 2.0)
headers = {'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...'}
response = requests.get('https://api.example.com/data', headers=headers)
import os
import requests
# ํ๊ฒฝ๋ณ์์์ API ํค ๊ฐ์ ธ์ค๊ธฐ (์์ ํ ๋ฐฉ๋ฒ โ
)
api_key = os.environ.get('MY_API_KEY')
headers = {'Authorization': f'Bearer {api_key}'}
response = requests.get('https://api.example.com/data', headers=headers)
POST ์์ฒญ์ ์๋ฒ์ ์ ๋ฐ์ดํฐ๋ฅผ ๋ง๋ค ๋ ์จ. JSON ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ด๋ ๊ฒ ์์ฆ ํ์ค์ด์ผ:
import requests
# JSON ๋ฐ์ดํฐ ๋ณด๋ด๊ธฐ
url = 'https://jsonplaceholder.typicode.com/posts'
payload = {
'title': 'ํ์ด์ฌ์ผ๋ก API ์ฐ๋ํ๊ธฐ',
'body': '์ค๋ ๋ฐฐ์ด ๋ด์ฉ์ ์ ๋ฆฌํด๋ดค์ด์!',
'userId': 1
}
# json ํ๋ผ๋ฏธํฐ ์ฌ์ฉ (Content-Type ์๋ ์ค์ )
response = requests.post(url, json=payload)
print(response.status_code) # 201 Created
print(response.json())
# {'id': 101, 'title': 'ํ์ด์ฌ์ผ๋ก API ์ฐ๋ํ๊ธฐ', ...}
json=payload๋ฅผ ์ฐ๋ฉด ์๋์ผ๋ก JSON ์ง๋ ฌํ + Content-Type: application/json ํค๋ ์ค์ ๊น์ง ํด์ค. data=json.dumps(payload)๋ณด๋ค ํจ์ฌ ํธํด!
import requests
# application/x-www-form-urlencoded ํ์
form_data = {
'username': 'pythonlover',
'password': 'secret123'
}
response = requests.post('https://api.example.com/login', data=form_data)
# multipart/form-data (ํ์ผ ์
๋ก๋)
files = {
'profile_image': ('avatar.png', open('avatar.png', 'rb'), 'image/png')
}
response = requests.post('https://api.example.com/upload', files=files)
import requests
# PUT: ์ ์ฒด ๋ฆฌ์์ค ๊ต์ฒด
response = requests.put(
'https://jsonplaceholder.typicode.com/posts/1',
json={
'id': 1,
'title': '์์ ๋ ์ ๋ชฉ',
'body': '์์ ๋ ๋ด์ฉ',
'userId': 1
}
)
# PATCH: ์ผ๋ถ ํ๋๋ง ์์
response = requests.patch(
'https://jsonplaceholder.typicode.com/posts/1',
json={'title': '์ ๋ชฉ๋ง ์์ '}
)
print(response.status_code) # 200
print(response.json())
import requests
response = requests.delete('https://jsonplaceholder.typicode.com/posts/1')
print(response.status_code) # 200 ๋๋ 204 No Content
API ํธ์ถ์ด ํญ์ ์ฑ๊ณตํ๋ฉด ์ผ๋ง๋ ์ข๊ฒ ์ด. ๊ทผ๋ฐ ํ์ค์ ๋ฌ๋ผ. ๋คํธ์ํฌ ๋๊ธฐ๊ณ , ์๋ฒ ๋ค์ด๋๊ณ , ์ธ์ฆ ๋ง๋ฃ๋๊ณ ... ์ด๋ฐ ์ํฉ์ ์ฐ์ํ๊ฒ ์ฒ๋ฆฌํ๋ ๊ฒ ์ง์ง ์ค๋ ฅ์ด์ผ! ๐ช
import requests
from requests.exceptions import (
ConnectionError,
Timeout,
HTTPError,
RequestException
)
def fetch_data(url, params=None):
try:
response = requests.get(
url,
params=params,
timeout=10 # 10์ด ํ์์์ ์ค์
)
# HTTP ์๋ฌ (4xx, 5xx) ๋ฐ์ ์ ์์ธ raise
response.raise_for_status()
return response.json()
except Timeout:
print("โฐ ์์ฒญ ์๊ฐ์ด ์ด๊ณผ๋์ด์. ์๋ฒ๊ฐ ๋๋ฆฐ๊ฐ๋ด์.")
return None
except ConnectionError:
print("๐ ์ฐ๊ฒฐ ์คํจ! ์ธํฐ๋ท ์ฐ๊ฒฐ์ ํ์ธํด์ฃผ์ธ์.")
return None
except HTTPError as e:
status_code = e.response.status_code
if status_code == 401:
print("๐ ์ธ์ฆ ์คํจ! API ํค๋ฅผ ํ์ธํด์ฃผ์ธ์.")
elif status_code == 403:
print("๐ซ ์ ๊ทผ ๊ถํ์ด ์์ด์.")
elif status_code == 404:
print("๐ ์์ฒญํ ๋ฆฌ์์ค๋ฅผ ์ฐพ์ ์ ์์ด์.")
elif status_code == 429:
print("โณ ์์ฒญ์ด ๋๋ฌด ๋ง์์. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.")
elif status_code >= 500:
print(f"๐ฅ ์๋ฒ ์๋ฌ ๋ฐ์! ์ํ ์ฝ๋: {status_code}")
return None
except RequestException as e:
print(f"โ ์ ์ ์๋ ์ค๋ฅ: {e}")
return None
# ์ฌ์ฉ
data = fetch_data('https://jsonplaceholder.typicode.com/posts/1')
if data:
print(data)
์ผ์์ ์ธ ์ค๋ฅ๋ ์ฌ์๋ํ๋ฉด ํด๊ฒฐ๋๋ ๊ฒฝ์ฐ๊ฐ ๋ง์. requests์ Session๊ณผ HTTPAdapter๋ฅผ ํ์ฉํ๋ฉด ์๋ ์ฌ์๋๋ฅผ ๊ตฌํํ ์ ์์ด:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def create_session_with_retry():
session = requests.Session()
retry_strategy = Retry(
total=3, # ์ต๋ 3๋ฒ ์ฌ์๋
backoff_factor=1, # ์ฌ์๋ ๊ฐ๊ฒฉ: 1์ด, 2์ด, 4์ด...
status_forcelist=[429, 500, 502, 503, 504], # ์ฌ์๋ํ ์ํ ์ฝ๋
allowed_methods=["GET", "POST"] # ์ฌ์๋ํ ๋ฉ์๋
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
# ์ฌ์ฉ
session = create_session_with_retry()
response = session.get('https://api.example.com/data', timeout=10)
print(response.json())
๋งค๋ฒ requests.get()์ ํธ์ถํ๋ฉด ๋งค ์์ฒญ๋ง๋ค ์ TCP ์ฐ๊ฒฐ์ ๋งบ์ด. ๊ฐ์ ์๋ฒ์ ์ฌ๋ฌ ๋ฒ ์์ฒญํ ๋๋ Session์ ์ฐ๋ฉด ์ฐ๊ฒฐ์ ์ฌ์ฌ์ฉํด์ ํจ์ฌ ๋น ๋ฅด๊ณ ํจ์จ์ ์ด์ผ!
import requests
# Session ์์ฑ
session = requests.Session()
# ๊ณตํต ํค๋ ์ค์ (๋ชจ๋ ์์ฒญ์ ์๋ ์ ์ฉ)
session.headers.update({
'Authorization': 'Bearer your_token',
'Content-Type': 'application/json',
'User-Agent': 'MyApp/1.0'
})
# ๊ณตํต ํ๋ผ๋ฏธํฐ ์ค์
session.params = {'api_version': 'v2'}
# ์ด์ ๋ชจ๋ ์์ฒญ์ ์ ์ค์ ์ด ์๋ ์ ์ฉ๋ผ!
response1 = session.get('https://api.example.com/users')
response2 = session.get('https://api.example.com/posts')
response3 = session.post('https://api.example.com/comments', json={'text': '์ข์์!'})
# Session ์ข
๋ฃ (with ๋ฌธ ์ฌ์ฉ ๊ถ์ฅ)
session.close()
# ๋ ์ข์ ๋ฐฉ๋ฒ: with ๋ฌธ์ผ๋ก ์๋ ์ข
๋ฃ
with requests.Session() as session:
session.headers.update({'Authorization': 'Bearer your_token'})
users = session.get('https://api.example.com/users').json()
posts = session.get('https://api.example.com/posts').json()
print(f"์ ์ ์: {len(users)}")
print(f"ํฌ์คํธ ์: {len(posts)}")
Session์ ์ฟ ํค๋ ์๋์ผ๋ก ๊ด๋ฆฌํด์ค. ๋ก๊ทธ์ธ์ด ํ์ํ ์๋น์ค๋ฅผ ์คํฌ๋ํํ ๋ ํนํ ์ ์ฉํด:
with requests.Session() as session:
# ๋ก๊ทธ์ธ
login_response = session.post(
'https://example.com/login',
data={'username': 'user', 'password': 'pass'}
)
# ๋ก๊ทธ์ธ ํ ์ฟ ํค๊ฐ ์๋์ผ๋ก ์ธ์
์ ์ ์ฅ๋จ
# ์ดํ ์์ฒญ์ ์๋์ผ๋ก ์ฟ ํค๊ฐ ํฌํจ๋จ
profile = session.get('https://example.com/profile')
print(profile.json())
OpenWeatherMap์์ ๋ฌด๋ฃ๋ก API ํค๋ฅผ ๋ฐ๊ธ๋ฐ์์ ํ์ฌ ๋ ์จ๋ฅผ ๊ฐ์ ธ์ค๋ ์์ ์ผ:
import requests
import os
def get_weather(city: str, api_key: str) -> dict:
"""๋์ ์ด๋ฆ์ผ๋ก ํ์ฌ ๋ ์จ ์กฐํ"""
url = "https://api.openweathermap.org/data/2.5/weather"
params = {
'q': city,
'appid': api_key,
'units': 'metric', # ์ญ์จ ์จ๋
'lang': 'kr' # ํ๊ตญ์ด ์ค๋ช
}
try:
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
return {
'๋์': data['name'],
'๋ ์จ': data['weather'][0]['description'],
'์จ๋': f"{data['main']['temp']}ยฐC",
'์ฒด๊ฐ์จ๋': f"{data['main']['feels_like']}ยฐC",
'์ต๋': f"{data['main']['humidity']}%",
'ํ์': f"{data['wind']['speed']}m/s"
}
except requests.exceptions.HTTPError as e:
if e.response.status_code == 401:
return {'์๋ฌ': 'API ํค๊ฐ ์ ํจํ์ง ์์์!'}
elif e.response.status_code == 404:
return {'์๋ฌ': f'"{city}" ๋์๋ฅผ ์ฐพ์ ์ ์์ด์!'}
return {'์๋ฌ': str(e)}
except Exception as e:
return {'์๋ฌ': str(e)}
# ์ฌ์ฉ ์์
api_key = os.environ.get('OPENWEATHER_API_KEY')
weather = get_weather('Seoul', api_key)
for key, value in weather.items():
print(f"{key}: {value}")
# ์ถ๋ ฅ ์์:
# ๋์: Seoul
# ๋ ์จ: ๋ง์
# ์จ๋: 22.5ยฐC
# ์ฒด๊ฐ์จ๋: 21.8ยฐC
# ์ต๋: 65%
# ํ์: 3.2m/s
GitHub API๋ ์ธ์ฆ ์์ด๋ ๊ธฐ๋ณธ ์กฐํ๊ฐ ๊ฐ๋ฅํด. ํน์ ์ฌ์ฉ์์ ๋ ํฌ์งํ ๋ฆฌ ๋ชฉ๋ก์ ๊ฐ์ ธ์๋ณด์:
import requests
class GitHubClient:
"""GitHub API ํด๋ผ์ด์ธํธ"""
BASE_URL = "https://api.github.com"
def __init__(self, token: str = None):
self.session = requests.Session()
self.session.headers.update({
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'MyGitHubApp/1.0'
})
# ํ ํฐ์ด ์์ผ๋ฉด ์ธ์ฆ ํค๋ ์ถ๊ฐ
if token:
self.session.headers.update({
'Authorization': f'token {token}'
})
def get_user(self, username: str) -> dict:
"""์ฌ์ฉ์ ์ ๋ณด ์กฐํ"""
response = self.session.get(f"{self.BASE_URL}/users/{username}")
response.raise_for_status()
return response.json()
def get_repos(self, username: str, sort: str = 'updated') -> list:
"""์ฌ์ฉ์ ๋ ํฌ์งํ ๋ฆฌ ๋ชฉ๋ก ์กฐํ"""
params = {
'sort': sort,
'per_page': 10
}
response = self.session.get(
f"{self.BASE_URL}/users/{username}/repos",
params=params
)
response.raise_for_status()
return response.json()
def get_repo_info(self, owner: str, repo: str) -> dict:
"""ํน์ ๋ ํฌ์งํ ๋ฆฌ ์ ๋ณด ์กฐํ"""
response = self.session.get(f"{self.BASE_URL}/repos/{owner}/{repo}")
response.raise_for_status()
return response.json()
def create_issue(self, owner: str, repo: str, title: str, body: str) -> dict:
"""์ด์ ์์ฑ (ํ ํฐ ํ์)"""
payload = {'title': title, 'body': body}
response = self.session.post(
f"{self.BASE_URL}/repos/{owner}/{repo}/issues",
json=payload
)
response.raise_for_status()
return response.json()
def __del__(self):
self.session.close()
# ์ฌ์ฉ ์์
client = GitHubClient() # ๊ณต๊ฐ API๋ ํ ํฐ ์์ด๋ ๊ฐ๋ฅ
# ์ฌ์ฉ์ ์ ๋ณด ์กฐํ
user = client.get_user('torvalds')
print(f"์ด๋ฆ: {user['name']}")
print(f"ํ๋ก์: {user['followers']:,}๋ช
")
print(f"๊ณต๊ฐ ๋ ํฌ: {user['public_repos']}๊ฐ")
# ๋ ํฌ์งํ ๋ฆฌ ๋ชฉ๋ก
repos = client.get_repos('torvalds')
print("\n์ต๊ทผ ์
๋ฐ์ดํธ๋ ๋ ํฌ์งํ ๋ฆฌ:")
for repo in repos[:5]:
stars = repo['stargazers_count']
print(f" โญ {repo['name']} ({stars:,} stars)")
์นด์นด์ค Developers์์ ์ฑ์ ๋ง๋ค๊ณ REST API ํค๋ฅผ ๋ฐ๊ธ๋ฐ์ผ๋ฉด ๋ผ. ์ฃผ์ ๊ฒ์ ๊ธฐ๋ฅ์ ๊ตฌํํด๋ณด์:
import requests
import os
def search_address(query: str, api_key: str) -> list:
"""์นด์นด์ค ์ฃผ์ ๊ฒ์ API"""
url = "https://dapi.kakao.com/v2/local/search/address.json"
headers = {
'Authorization': f'KakaoAK {api_key}'
}
params = {
'query': query,
'size': 5 # ์ต๋ 5๊ฐ ๊ฒฐ๊ณผ
}
response = requests.get(url, headers=headers, params=params, timeout=10)
response.raise_for_status()
data = response.json()
results = []
for doc in data['documents']:
results.append({
'์ฃผ์': doc['address_name'],
'์๋': doc['y'],
'๊ฒฝ๋': doc['x'],
'์ฃผ์ํ์
': doc['address_type']
})
return results
def search_keyword(keyword: str, api_key: str, category: str = None) -> list:
"""์นด์นด์ค ํค์๋ ์ฅ์ ๊ฒ์ API"""
url = "https://dapi.kakao.com/v2/local/search/keyword.json"
headers = {'Authorization': f'KakaoAK {api_key}'}
params = {
'query': keyword,
'size': 5
}
if category:
params['category_group_code'] = category
response = requests.get(url, headers=headers, params=params, timeout=10)
response.raise_for_status()
data = response.json()
results = []
for place in data['documents']:
results.append({
'์ฅ์๋ช
': place['place_name'],
'์ฃผ์': place['road_address_name'],
'์ ํ๋ฒํธ': place.get('phone', '์์'),
'์นดํ
๊ณ ๋ฆฌ': place['category_name']
})
return results
# ์ฌ์ฉ ์์
kakao_key = os.environ.get('KAKAO_REST_API_KEY')
# ์ฃผ์ ๊ฒ์
addresses = search_address('๊ฐ๋จ๊ตฌ ํ
ํค๋๋ก 152', kakao_key)
for addr in addresses:
print(f"๐ {addr['์ฃผ์']} (์๋: {addr['์๋']}, ๊ฒฝ๋: {addr['๊ฒฝ๋']})")
# ์นดํ ๊ฒ์
cafes = search_keyword('๊ฐ๋จ์ญ ์นดํ', kakao_key, 'CE7')
for cafe in cafes:
print(f"โ {cafe['์ฅ์๋ช
']} - {cafe['์ฃผ์']}")
์ค๋ฌด์์๋ API ํธ์ถ ์ฝ๋๋ฅผ ์ฌ๊ธฐ์ ๊ธฐ ํฉ์ด๋์ผ๋ฉด ์ ์ง๋ณด์๊ฐ ์ง์ฅ์ด ๋ผ. ํด๋์ค๋ก ๊น๋ํ๊ฒ ์บก์ํํ๋ ๊ฒ ์ ์์ด์ผ. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ์ธ๋ถ ๊ฒฐ์ API๋ ์์ ๋ก๊ทธ์ธ API๋ฅผ ์ฐ๋ํ ๋ ์ด๋ฐ ํจํด์ ๋ง์ด ์จ!
import requests
import time
import logging
from typing import Optional, Dict, Any
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# ๋ก๊น
์ค์
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class BaseAPIClient:
"""์ฌ์ฌ์ฉ ๊ฐ๋ฅํ API ํด๋ผ์ด์ธํธ ๋ฒ ์ด์ค ํด๋์ค"""
def __init__(
self,
base_url: str,
api_key: str = None,
timeout: int = 10,
max_retries: int = 3
):
self.base_url = base_url.rstrip('/')
self.timeout = timeout
self.session = self._create_session(api_key, max_retries)
def _create_session(self, api_key: str, max_retries: int) -> requests.Session:
"""์ฌ์๋ ๋ก์ง์ด ํฌํจ๋ ์ธ์
์์ฑ"""
session = requests.Session()
# ๊ธฐ๋ณธ ํค๋ ์ค์
session.headers.update({
'Content-Type': 'application/json',
'Accept': 'application/json'
})
# API ํค ์ค์
if api_key:
session.headers.update({'Authorization': f'Bearer {api_key}'})
# ์ฌ์๋ ์ ๋ต ์ค์
retry = Retry(
total=max_retries,
backoff_factor=0.5,
status_forcelist=[429, 500, 502, 503, 504]
)
adapter = HTTPAdapter(max_retries=retry)
session.mount('https://', adapter)
session.mount('http://', adapter)
return session
def _request(
self,
method: str,
endpoint: str,
**kwargs
) -> Optional[Dict[str, Any]]:
"""๊ณตํต ์์ฒญ ๋ฉ์๋"""
url = f"{self.base_url}/{endpoint.lstrip('/')}"
try:
logger.info(f"[{method.upper()}] {url}")
start_time = time.time()
response = self.session.request(
method,
url,
timeout=self.timeout,
**kwargs
)
elapsed = time.time() - start_time
logger.info(f"์๋ต: {response.status_code} ({elapsed:.2f}์ด)")
response.raise_for_status()
# ์๋ต ๋ณธ๋ฌธ์ด ์๋ ๊ฒฝ์ฐ ์ฒ๋ฆฌ (204 No Content ๋ฑ)
if response.status_code == 204 or not response.content:
return {}
return response.json()
except requests.exceptions.HTTPError as e:
logger.error(f"HTTP ์๋ฌ: {e.response.status_code} - {e.response.text}")
raise
except requests.exceptions.Timeout:
logger.error(f"ํ์์์: {url}")
raise
except requests.exceptions.RequestException as e:
logger.error(f"์์ฒญ ์คํจ: {e}")
raise
def get(self, endpoint: str, params: dict = None) -> dict:
return self._request('GET', endpoint, params=params)
def post(self, endpoint: str, data: dict = None) -> dict:
return self._request('POST', endpoint, json=data)
def put(self, endpoint: str, data: dict = None) -> dict:
return self._request('PUT', endpoint, json=data)
def patch(self, endpoint: str, data: dict = None) -> dict:
return self._request('PATCH', endpoint, json=data)
def delete(self, endpoint: str) -> dict:
return self._request('DELETE', endpoint)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.session.close()
# ์ค์ ์๋น์ค ํด๋ผ์ด์ธํธ ๊ตฌํ ์์
class TodoAPIClient(BaseAPIClient):
"""ํ ์ผ ๊ด๋ฆฌ API ํด๋ผ์ด์ธํธ"""
def __init__(self, api_key: str = None):
super().__init__(
base_url='https://jsonplaceholder.typicode.com',
api_key=api_key
)
def get_todos(self, user_id: int = None) -> list:
"""ํ ์ผ ๋ชฉ๋ก ์กฐํ"""
params = {'userId': user_id} if user_id else None
return self.get('/todos', params=params)
def get_todo(self, todo_id: int) -> dict:
"""ํน์ ํ ์ผ ์กฐํ"""
return self.get(f'/todos/{todo_id}')
def create_todo(self, title: str, user_id: int) -> dict:
"""ํ ์ผ ์์ฑ"""
return self.post('/todos', data={
'title': title,
'userId': user_id,
'completed': False
})
def complete_todo(self, todo_id: int) -> dict:
"""ํ ์ผ ์๋ฃ ์ฒ๋ฆฌ"""
return self.patch(f'/todos/{todo_id}', data={'completed': True})
def delete_todo(self, todo_id: int) -> dict:
"""ํ ์ผ ์ญ์ """
return self.delete(f'/todos/{todo_id}')
# ์ฌ์ฉ ์์
with TodoAPIClient() as client:
# ํ ์ผ ๋ชฉ๋ก ์กฐํ
todos = client.get_todos(user_id=1)
print(f"์ด {len(todos)}๊ฐ์ ํ ์ผ")
# ์ ํ ์ผ ์์ฑ
new_todo = client.create_todo('ํ์ด์ฌ API ๊ณต๋ถํ๊ธฐ', user_id=1)
print(f"์์ฑ๋จ: {new_todo}")
# ์๋ฃ ์ฒ๋ฆฌ
completed = client.complete_todo(1)
print(f"์๋ฃ: {completed}")
BaseAPIClient์ ๊ณตํต ๋ก์ง(์ฌ์๋, ๋ก๊น , ์๋ฌ์ฒ๋ฆฌ)์ ๋ฃ๊ณ , ์ค์ ์๋น์ค ํด๋ผ์ด์ธํธ๋ ์์๋ฐ์์ ๋น์ฆ๋์ค ๋ก์ง๋ง ๊ตฌํํด. ์ด๋ ๊ฒ ํ๋ฉด ์๋ก์ด API๋ฅผ ์ถ๊ฐํ ๋ ๊ณตํต ์ฝ๋๋ฅผ ๋ฐ๋ณต ์์ฑํ ํ์๊ฐ ์์ด!
API๋ ํ ๋ฒ์ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ์ฃผ์ง ์์. ๋ณดํต ํ์ด์ง ๋จ์๋ก ๋๋ ์ ์ค. ์ด๊ฑธ ํ์ด์ง๋ค์ด์ ์ด๋ผ๊ณ ํด. ์ฒ๋ฆฌ ๋ฐฉ๋ฒ์ ์์๋ณด์:
import requests
import time
def fetch_all_pages(base_url: str, params: dict = None, max_pages: int = 10) -> list:
"""๋ชจ๋ ํ์ด์ง์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ํจ์"""
all_data = []
params = params or {}
params['page'] = 1
params['per_page'] = 100 # ํ์ด์ง๋น ์ต๋ ํญ๋ชฉ ์
for page_num in range(1, max_pages + 1):
params['page'] = page_num
response = requests.get(base_url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
# ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด ์ข
๋ฃ
if not data:
print(f"โ
์ด {page_num - 1}ํ์ด์ง ์๋ฃ!")
break
all_data.extend(data)
print(f"๐ {page_num}ํ์ด์ง ๋ก๋: {len(data)}๊ฐ ํญ๋ชฉ")
# API ๋ ์ดํธ ๋ฆฌ๋ฐ ๋ฐฉ์ง: ์์ฒญ ๊ฐ ๋๋ ์ด
time.sleep(0.5)
# ๋ง์ง๋ง ํ์ด์ง ๊ฐ์ง (Link ํค๋ ๋ฐฉ์)
link_header = response.headers.get('Link', '')
if 'rel="next"' not in link_header:
print(f"โ
๋ง์ง๋ง ํ์ด์ง ๋๋ฌ!")
break
return all_data
# GitHub API ํ์ด์ง๋ค์ด์
์์
def get_all_repos(username: str, token: str = None) -> list:
"""์ฌ์ฉ์์ ๋ชจ๋ ๋ ํฌ์งํ ๋ฆฌ ๊ฐ์ ธ์ค๊ธฐ"""
headers = {'Accept': 'application/vnd.github.v3+json'}
if token:
headers['Authorization'] = f'token {token}'
all_repos = []
page = 1
while True:
response = requests.get(
f'https://api.github.com/users/{username}/repos',
headers=headers,
params={'page': page, 'per_page': 30, 'sort': 'updated'},
timeout=10
)
response.raise_for_status()
repos = response.json()
if not repos:
break
all_repos.extend(repos)
# Link ํค๋๋ก ๋ค์ ํ์ด์ง ํ์ธ
if 'rel="next"' not in response.headers.get('Link', ''):
break
page += 1
time.sleep(0.3) # ๋ ์ดํธ ๋ฆฌ๋ฐ ๋ฐฉ์ง
return all_repos
# ์ฌ์ฉ
repos = get_all_repos('octocat')
print(f"์ด ๋ ํฌ์งํ ๋ฆฌ: {len(repos)}๊ฐ")
for repo in repos:
print(f" ๐ {repo['name']} โญ{repo['stargazers_count']}")
์นด์นด์ค ๋ก๊ทธ์ธ, ๊ตฌ๊ธ ๋ก๊ทธ์ธ ๊ฐ์ ์์ ๋ก๊ทธ์ธ์ OAuth 2.0์ ์จ. ์ด๊ฒ ์ฒ์์ ๋ณต์กํด ๋ณด์ด๋๋ฐ, ํ๋ฆ์ ์ดํดํ๋ฉด ๋ณ๊ฑฐ ์๋์ผ!
import requests
import os
# ์นด์นด์ค OAuth 2.0 ์์
# 1๋จ๊ณ: ์ธ์ฆ URL ์์ฑ (์ฌ์ฉ์๋ฅผ ์ด URL๋ก ๋ฆฌ๋ค์ด๋ ํธ)
def get_kakao_auth_url():
client_id = os.environ.get('KAKAO_CLIENT_ID')
redirect_uri = 'https://myapp.com/callback'
return (
f"https://kauth.kakao.com/oauth/authorize"
f"?client_id={client_id}"
f"&redirect_uri={redirect_uri}"
f"&response_type=code"
)
# 2๋จ๊ณ: ์ธ์ฆ ์ฝ๋๋ก ์ก์ธ์ค ํ ํฐ ๋ฐ๊ธ
def get_kakao_token(auth_code: str) -> dict:
client_id = os.environ.get('KAKAO_CLIENT_ID')
client_secret = os.environ.get('KAKAO_CLIENT_SECRET')
redirect_uri = 'https://myapp.com/callback'
response = requests.post(
'https://kauth.kakao.com/oauth/token',
data={
'grant_type': 'authorization_code',
'client_id': client_id,
'client_secret': client_secret,
'redirect_uri': redirect_uri,
'code': auth_code
}
)
response.raise_for_status()
return response.json()
# {'access_token': '...', 'token_type': 'bearer', 'expires_in': 21599, ...}
# 3๋จ๊ณ: ์ก์ธ์ค ํ ํฐ์ผ๋ก ์ฌ์ฉ์ ์ ๋ณด ์กฐํ
def get_kakao_user_info(access_token: str) -> dict:
response = requests.get(
'https://kapi.kakao.com/v2/user/me',
headers={'Authorization': f'Bearer {access_token}'}
)
response.raise_for_status()
return response.json()
# ์ฌ์ฉ ํ๋ฆ
auth_url = get_kakao_auth_url()
print(f"์ด URL๋ก ์ฌ์ฉ์๋ฅผ ๋ฆฌ๋ค์ด๋ ํธ: {auth_url}")
# ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ํ ์ฝ๋ฐฑ์ผ๋ก code ํ๋ผ๋ฏธํฐ๊ฐ ์ด
auth_code = "๋ฐ์_์ธ์ฆ_์ฝ๋"
token_data = get_kakao_token(auth_code)
access_token = token_data['access_token']
user_info = get_kakao_user_info(access_token)
print(f"์ฌ์ฉ์ ID: {user_info['id']}")
print(f"๋๋ค์: {user_info['kakao_account']['profile']['nickname']}")
import requests
# ์ฐ๊ฒฐ ํ์์์ + ์ฝ๊ธฐ ํ์์์ ๋ถ๋ฆฌ ์ค์
response = requests.get(
'https://api.example.com/data',
timeout=(3.05, 27) # (์ฐ๊ฒฐ ํ์์์, ์ฝ๊ธฐ ํ์์์) ์ด
)
# ๋จ์ผ ํ์์์ (์ฐ๊ฒฐ+์ฝ๊ธฐ ํฉ์ฐ)
response = requests.get('https://api.example.com/data', timeout=10)
import requests
proxies = {
'http': 'http://proxy.example.com:8080',
'https': 'http://proxy.example.com:8080'
}
response = requests.get(
'https://api.example.com/data',
proxies=proxies
)
import requests
# SSL ์ธ์ฆ์ ๊ฒ์ฆ ๋นํ์ฑํ (๊ฐ๋ฐ ํ๊ฒฝ์์๋ง! ํ๋ก๋์
์์๋ ์ ๋ ๊ธ์ง)
response = requests.get('https://api.example.com/data', verify=False)
# ์ปค์คํ
CA ์ธ์ฆ์ ์ฌ์ฉ
response = requests.get(
'https://api.example.com/data',
verify='/path/to/ca-bundle.crt'
)
import requests
def download_file(url: str, filename: str):
"""๋์ฉ๋ ํ์ผ์ ์ฒญํฌ ๋จ์๋ก ๋ค์ด๋ก๋"""
with requests.get(url, stream=True, timeout=30) as response:
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
downloaded = 0
with open(filename, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk: # ๋น ์ฒญํฌ ํํฐ๋ง
f.write(chunk)
downloaded += len(chunk)
if total_size:
progress = (downloaded / total_size) * 100
print(f"\r๋ค์ด๋ก๋: {progress:.1f}%", end='', flush=True)
print(f"\nโ
๋ค์ด๋ก๋ ์๋ฃ: {filename}")
# ์ฌ์ฉ
download_file(
'https://example.com/large-file.zip',
'downloaded-file.zip'
)
์ค๋ฌด์์ API ํด๋ผ์ด์ธํธ ์ฝ๋๋ฅผ ํ ์คํธํ ๋๋ ์ค์ API๋ฅผ ํธ์ถํ๋ฉด ์ ๋ผ. ๋น์ฉ์ด ๋ฐ์ํ๊ฑฐ๋ ๋ ์ดํธ ๋ฆฌ๋ฐ์ ๊ฑธ๋ฆด ์ ์๊ฑฐ๋ . Mock์ ์ฌ์ฉํด์ ํ ์คํธํ๋ ๊ฒ ์ ์์ด์ผ!
import unittest
from unittest.mock import patch, MagicMock
import requests
# ํ
์คํธํ ํจ์
def get_user_info(user_id: int) -> dict:
response = requests.get(f'https://api.example.com/users/{user_id}')
response.raise_for_status()
return response.json()
# ํ
์คํธ ์ฝ๋
class TestAPIClient(unittest.TestCase):
@patch('requests.get')
def test_get_user_success(self, mock_get):
"""์ฑ๊ณต ์ผ์ด์ค ํ
์คํธ"""
# Mock ์๋ต ์ค์
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
'id': 1,
'name': 'ํ๊ธธ๋',
'email': 'hong@example.com'
}
mock_get.return_value = mock_response
# ํจ์ ํธ์ถ
result = get_user_info(1)
# ๊ฒ์ฆ
self.assertEqual(result['name'], 'ํ๊ธธ๋')
mock_get.assert_called_once_with('https://api.example.com/users/1')
@patch('requests.get')
def test_get_user_not_found(self, mock_get):
"""404 ์๋ฌ ์ผ์ด์ค ํ
์คํธ"""
mock_response = MagicMock()
mock_response.status_code = 404
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
response=mock_response
)
mock_get.return_value = mock_response
# ์์ธ ๋ฐ์ ํ์ธ
with self.assertRaises(requests.exceptions.HTTPError):
get_user_info(999)
@patch('requests.get')
def test_timeout(self, mock_get):
"""ํ์์์ ์ผ์ด์ค ํ
์คํธ"""
mock_get.side_effect = requests.exceptions.Timeout()
with self.assertRaises(requests.exceptions.Timeout):
get_user_info(1)
if __name__ == '__main__':
unittest.main()
pip install responses๋ก ์ค์นํ๋ฉด ๋ ์ง๊ด์ ์ผ๋ก HTTP ์๋ต์ ๋ชจํนํ ์ ์์ด. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์ธ๋ถ ๊ฒฐ์ API๋ ์๋ฆผ API๋ฅผ ํ
์คํธํ ๋ ์ ๋ง ์ ์ฉํ๊ฒ ์ฐ์ฌ!
๋๋ถ๋ถ์ API๋ ์ผ์ ์๊ฐ ๋ด ์์ฒญ ํ์๋ฅผ ์ ํํด. ์ด๊ฑธ ๋ ์ดํธ ๋ฆฌ๋ฐ์ด๋ผ๊ณ ํด. ์ด๊ฑธ ๋ฌด์ํ๋ฉด 429 ์๋ฌ ๋ง๊ณ IP ์ฐจ๋จ๋นํ ์๋ ์์ด! ๐ฑ
import requests
import time
from functools import wraps
def rate_limited(max_calls: int, period: float):
"""๋ ์ดํธ ๋ฆฌ๋ฐ ๋ฐ์ฝ๋ ์ดํฐ"""
calls = []
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
# ๊ธฐ๊ฐ ๋ด ํธ์ถ ๊ธฐ๋ก ํํฐ๋ง
calls[:] = [c for c in calls if now - c < period]
if len(calls) >= max_calls:
# ๋๊ธฐ ์๊ฐ ๊ณ์ฐ
sleep_time = period - (now - calls[0])
if sleep_time > 0:
print(f"โณ ๋ ์ดํธ ๋ฆฌ๋ฐ ๋๊ธฐ: {sleep_time:.2f}์ด")
time.sleep(sleep_time)
calls.append(time.time())
return func(*args, **kwargs)
return wrapper
return decorator
# ์ด๋น ์ต๋ 5๋ฒ ํธ์ถ ์ ํ
@rate_limited(max_calls=5, period=1.0)
def call_api(endpoint: str) -> dict:
response = requests.get(endpoint, timeout=10)
response.raise_for_status()
return response.json()
# ์๋ต ํค๋์์ ๋ ์ดํธ ๋ฆฌ๋ฐ ์ ๋ณด ์ฝ๊ธฐ
def smart_request(url: str) -> dict:
response = requests.get(url, timeout=10)
# GitHub API ๋ ์ดํธ ๋ฆฌ๋ฐ ํค๋ ์์
remaining = int(response.headers.get('X-RateLimit-Remaining', 999))
reset_time = int(response.headers.get('X-RateLimit-Reset', 0))
if response.status_code == 429:
# ๋ฆฌ์
์๊ฐ๊น์ง ๋๊ธฐ
wait_time = reset_time - time.time() + 1
print(f"โณ ๋ ์ดํธ ๋ฆฌ๋ฐ ์ด๊ณผ! {wait_time:.0f}์ด ๋๊ธฐ...")
time.sleep(max(wait_time, 0))
return smart_request(url) # ์ฌ์๋
if remaining < 10:
print(f"โ ๏ธ ๋จ์ ์์ฒญ ํ์: {remaining}ํ")
response.raise_for_status()
return response.json()
์ง๊ธ๊น์ง ๋ฐฐ์ด ๊ฑธ ๋ค ํฉ์ณ์ ์ค์ฉ์ ์ธ ํ์จ ๋ชจ๋ํฐ๋ง ๋ด์ ๋ง๋ค์ด๋ณด์! ๋ชฉํ ํ์จ์ ๋๋ฌํ๋ฉด ์๋ฆผ์ ๋ณด๋ด๋ ํ๋ก๊ทธ๋จ์ด์ผ:
import requests
import time
import os
from datetime import datetime
from typing import Optional
class ExchangeRateMonitor:
"""ํ์จ ๋ชจ๋ํฐ๋ง ํด๋ผ์ด์ธํธ"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = f"https://v6.exchangerate-api.com/v6/{api_key}"
self.session = requests.Session()
self.session.headers.update({'Accept': 'application/json'})
def get_rate(self, from_currency: str, to_currency: str) -> Optional[float]:
"""ํ์ฌ ํ์จ ์กฐํ"""
try:
response = self.session.get(
f"{self.base_url}/pair/{from_currency}/{to_currency}",
timeout=10
)
response.raise_for_status()
data = response.json()
if data['result'] == 'success':
return data['conversion_rate']
return None
except Exception as e:
print(f"โ ํ์จ ์กฐํ ์คํจ: {e}")
return None
def get_all_rates(self, base_currency: str = 'USD') -> dict:
"""๊ธฐ์ค ํตํ ๋๋น ๋ชจ๋ ํ์จ ์กฐํ"""
try:
response = self.session.get(
f"{self.base_url}/latest/{base_currency}",
timeout=10
)
response.raise_for_status()
data = response.json()
if data['result'] == 'success':
return data['conversion_rates']
return {}
except Exception as e:
print(f"โ ํ์จ ์กฐํ ์คํจ: {e}")
return {}
def send_slack_alert(self, webhook_url: str, message: str):
"""์ฌ๋ ์นํ
์ผ๋ก ์๋ฆผ ์ ์ก"""
try:
response = requests.post(
webhook_url,
json={'text': message},
timeout=10
)
response.raise_for_status()
print("โ
์ฌ๋ ์๋ฆผ ์ ์ก ์๋ฃ!")
except Exception as e:
print(f"โ ์ฌ๋ ์๋ฆผ ์คํจ: {e}")
def monitor(
self,
from_currency: str,
to_currency: str,
target_rate: float,
condition: str = 'below', # 'below' ๋๋ 'above'
check_interval: int = 60, # ์ด
webhook_url: str = None
):
"""ํ์จ ๋ชจ๋ํฐ๋ง ์์"""
print(f"๐ ํ์จ ๋ชจ๋ํฐ๋ง ์์!")
print(f" {from_currency} โ {to_currency}")
print(f" ๋ชฉํ: {condition} {target_rate}")
print(f" ์ฒดํฌ ๊ฐ๊ฒฉ: {check_interval}์ด")
print("-" * 40)
while True:
rate = self.get_rate(from_currency, to_currency)
if rate is None:
print("โ ๏ธ ํ์จ ์กฐํ ์คํจ, ์ฌ์๋...")
time.sleep(check_interval)
continue
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print(f"[{now}] {from_currency}/{to_currency} = {rate:.4f}")
# ๋ชฉํ ํ์จ ๋๋ฌ ํ์ธ
triggered = False
if condition == 'below' and rate <= target_rate:
triggered = True
msg = f"๐ฏ ๋ชฉํ ํ์จ ๋๋ฌ!\n{from_currency}/{to_currency} = {rate:.4f}\n๋ชฉํ: {target_rate} ์ดํ"
elif condition == 'above' and rate >= target_rate:
triggered = True
msg = f"๐ฏ ๋ชฉํ ํ์จ ๋๋ฌ!\n{from_currency}/{to_currency} = {rate:.4f}\n๋ชฉํ: {target_rate} ์ด์"
if triggered:
print(f"\n๐จ {msg}")
if webhook_url:
self.send_slack_alert(webhook_url, msg)
break
time.sleep(check_interval)
def __del__(self):
self.session.close()
# ์ฌ์ฉ ์์
api_key = os.environ.get('EXCHANGE_RATE_API_KEY')
monitor = ExchangeRateMonitor(api_key)
# ํ์ฌ ํ์จ ํ์ธ
usd_krw = monitor.get_rate('USD', 'KRW')
print(f"ํ์ฌ USD/KRW: {usd_krw:.2f}์")
# ์ฃผ์ ํตํ ํ์จ ์กฐํ
rates = monitor.get_all_rates('USD')
currencies = ['KRW', 'JPY', 'EUR', 'GBP', 'CNY']
print("\n๐ ์ฃผ์ ํตํ ํ์จ (USD ๊ธฐ์ค):")
for currency in currencies:
if currency in rates:
print(f" {currency}: {rates[currency]:.4f}")
# ํ์จ ๋ชจ๋ํฐ๋ง (1๋ฌ๋ฌ = 1300์ ์ดํ๊ฐ ๋๋ฉด ์๋ฆผ)
# monitor.monitor(
# from_currency='USD',
# to_currency='KRW',
# target_rate=1300,
# condition='below',
# check_interval=300, # 5๋ถ๋ง๋ค ์ฒดํฌ
# webhook_url=os.environ.get('SLACK_WEBHOOK_URL')
# )
1. ํ๊ฒฝ๋ณ์๋ก API ํค ๊ด๋ฆฌ ๐
์ ๋ ์ฝ๋์ ํ๋์ฝ๋ฉํ์ง ๋ง. python-dotenv ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก .env ํ์ผ ๊ด๋ฆฌํ๋ ๊ฒ ํธํด.
2. ํญ์ ํ์์์ ์ค์ โฑ๏ธ
timeout ์์ผ๋ฉด ์๋ต ์ ์ค๋ ์๋ฒ์ ์์ํ ๊ธฐ๋ค๋ฆด ์ ์์ด. ๋ฐ๋์ ์ค์ ํด!
3. ์๋ฌ ์ฒ๋ฆฌ๋ ๊ตฌ์ฒด์ ์ผ๋ก ๐ก๏ธ
except Exception์ผ๋ก ๋ค ์ก์ง ๋ง๊ณ , ๊ตฌ์ฒด์ ์ธ ์์ธ ํ์
๋ณ๋ก ์ฒ๋ฆฌํด.
4. Session ์ฌ์ฌ์ฉ โก
๊ฐ์ ์๋ฒ์ ์ฌ๋ฌ ๋ฒ ์์ฒญํ ๋๋ Session ์จ์ ์ฐ๊ฒฐ ์ฌ์ฌ์ฉํด.
5. ๋ ์ดํธ ๋ฆฌ๋ฐ ์กด์ค ๐ค
API ์ ๊ณต์์ ๋ ์ดํธ ๋ฆฌ๋ฐ์ ์ง์ผ. ์์ฒญ ๊ฐ ์ ์ ํ ๋๋ ์ด ์ถ๊ฐํด.
6. ์๋ต ์บ์ฑ ํ์ฉ ๐พ
์์ฃผ ๋ฐ๋์ง ์๋ ๋ฐ์ดํฐ๋ ์บ์ฑํด์ ๋ถํ์ํ API ํธ์ถ ์ค์ฌ.
7. ๋ก๊น
์ถ๊ฐ ๐
์์ฒญ/์๋ต ๋ก๊ทธ๋ฅผ ๋จ๊ฒจ๋๋ฉด ๋๋ฒ๊น
ํ ๋ ์์ฒญ ๋์๋ผ.
8. ํ
์คํธ ์์ฑ ๐งช
Mock์ ํ์ฉํด์ ์ค์ API ํธ์ถ ์์ด ํ
์คํธ ์ฝ๋ ์์ฑํด.
- API ํค๋ฅผ ๊นํ๋ธ์ ์ฌ๋ฆฌ๊ธฐ (์ง์ง ํฐ์ผ ๋จ!)
- ํ๋ก๋์ ์์
verify=False SSL ๊ฒ์ฆ ๋นํ์ฑํ- ํ์์์ ์์ด ๋ฌดํ ๋๊ธฐ
- ๋ ์ดํธ ๋ฆฌ๋ฐ ๋ฌด์ํ๊ณ ๋ฌดํ ๋ฃจํ ์์ฒญ
- ์๋ฌ ์ฒ๋ฆฌ ์์ด
.json() ๋ฐ๋ก ํธ์ถ
์, ์ฌ๊ธฐ๊น์ง ์์ด? ์ง์ง ๋๋จํด! ๐
์ค๋ ์ฐ๋ฆฌ๊ฐ ๋ค๋ฃฌ ๋ด์ฉ์ ์ ๋ฆฌํด๋ณด๋ฉด:
๐ ์น API ๊ธฐ๋ณธ ๊ฐ๋
- REST API, HTTP ๋ฉ์๋, ์ํ ์ฝ๋
๐ฆ requests ์ค์น์ ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ - GET/POST/PUT/DELETE
๐ฏ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ, ํค๋, ์ธ์ฆ - API Key, Bearer Token, OAuth
๐ก๏ธ ์๋ฌ ์ฒ๋ฆฌ์ ์ฌ์๋ ๋ก์ง - ์ค๋ฌด ํ์ ํจํด
โก Session ํ์ฉ - ์ฑ๋ฅ ์ต์ ํ
๐ ์ค์ API ์ฐ๋ - ๋ ์จ, GitHub, ์นด์นด์ค
๐๏ธ API ํด๋ผ์ด์ธํธ ํด๋์ค ์ค๊ณ - ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ๊ตฌ์กฐ
๐ ํ์ด์ง๋ค์ด์
์ฒ๋ฆฌ - ๋๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ
๐ OAuth 2.0 ํ๋ฆ - ์์
๋ก๊ทธ์ธ ์ดํด
โ๏ธ ๊ณ ๊ธ ๊ธฐ๋ฒ - ํ์์์, ์คํธ๋ฆฌ๋ฐ, ํ๋ก์
๐งช ํ
์คํธ ๋ฐฉ๋ฒ - Mock์ ํ์ฉํ ๋จ์ ํ
์คํธ
๐ ๋ ์ดํธ ๋ฆฌ๋ฐ ์ฒ๋ฆฌ - ์์ ๋ฐ๋ฅธ API ํธ์ถ
๐ ์ค์ ๋ฏธ๋ ํ๋ก์ ํธ - ํ์จ ๋ชจ๋ํฐ๋ง ๋ด
์ด์ ํ์ด์ฌ์ผ๋ก ๋ ์จ ์ฑ๋ ๋ง๋ค๊ณ , ํ์จ ๋ชจ๋ํฐ๋ง๋ ํ๊ณ , ๊นํ๋ธ ์๋ํ๋ ํ๊ณ , ์นด์นด์ค ๋ก๊ทธ์ธ๋ ๊ตฌํํ ์ ์์ด! ๐
API ์ฐ๋ ์ค๋ ฅ์ ๋ ํค์ฐ๊ณ ์ถ๋ค๋ฉด ์ฌ๋ฅ๋ท์์ ํ์ด์ฌ ์ ๋ฌธ๊ฐ๋ค์ ๋์์ ๋ฐ์๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ๋ค์ํ ์ค๋ฌด ๊ฒฝํ์ ๊ฐ์ง ๊ฐ๋ฐ์๋ค์ด API ์ฐ๋๋ถํฐ ์ค์ ์๋น์ค ๊ฐ๋ฐ๊น์ง ๋์์ค ์ ์๊ฑฐ๋ ! ๐ช
์, ์ด์ ์ฝ๋ ์๋ํฐ ์ด๊ณ ์ง์ ๋ง๋ค์ด๋ด. ์ฒ์์ JSONPlaceholder ๊ฐ์ ๋ฌด๋ฃ ํ
์คํธ API๋ก ์์ํด์, ์ ์ ์ค์ ์๋น์ค API๋ก ํ์ฅํด๋๊ฐ๋ฉด ๋ผ. ํ์ดํ
! ๐ฅ๐
๊ด๋ จ ํค์๋
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.

๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.