Files
AutonetSellCar/agent/src/carmodoo_client.py
AutonetSellCar Deploy 1f0dcb1ddb Initial commit: AutonetSellCar platform with deployment system
- Frontend: Next.js 14 with TypeScript
- Backend: FastAPI with SQLAlchemy
- Agent: Carmodoo sync agent
- Deployment: Docker Compose based staging/production setup
- Scripts: Automated deployment with rollback support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 13:24:39 +09:00

295 lines
8.7 KiB
Python

"""
Carmodoo API Client - HTTP based car data extraction
"""
import asyncio
import re
import logging
from typing import Optional, Dict, List, Any
from dataclasses import dataclass, field
from datetime import datetime
import aiohttp
from lxml import etree
@dataclass
class CarmodooConfig:
base_url: str = "https://dealer.carmodoo.com"
check_url: str = "https://ck.carmodoo.com"
encoding: str = "euc-kr"
user_id: str = ""
password: str = ""
request_timeout: int = 30
request_delay: float = 0.5
max_retries: int = 3
retry_delay: int = 2
@dataclass
class CarMaker:
code: str
name: str
cho: str = ""
@dataclass
class CarModel:
code: str
name: str
maker_code: str
@dataclass
class CarDetail:
car_no: str
car_name: str
maker_code: str
model_code: str
year: int
month: Optional[int]
mileage: int
price: int
fuel: str
transmission: str
color: str
displacement: int
car_number: str
seize_count: int
collateral_count: int
options: List[str]
memo: str
dealer_memo: str
main_image: str
images: List[str]
thumbnails: List[str]
check_num: str
check_url: str
check_gubun: str
dealer_name: str
dealer_phone: str
shop_name: str
shop_tel: str
raw_data: Dict[str, Any] = field(default_factory=dict)
class CarmodooClient:
def __init__(self, config: CarmodooConfig):
self.config = config
self.logger = logging.getLogger('carmodoo')
self.session: Optional[aiohttp.ClientSession] = None
self.cookies: Dict[str, str] = {}
self.is_logged_in = False
self.last_session_refresh = None
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36',
'Accept-Language': 'ko-KR,ko;q=0.9',
}
async def __aenter__(self):
await self.create_session()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.close()
async def create_session(self):
if self.session is None or self.session.closed:
timeout = aiohttp.ClientTimeout(total=self.config.request_timeout)
self.session = aiohttp.ClientSession(timeout=timeout, headers=self.headers)
return self.session
async def close(self):
if self.session and not self.session.closed:
await self.session.close()
def _decode_response(self, content: bytes) -> str:
try:
return content.decode(self.config.encoding)
except UnicodeDecodeError:
try:
return content.decode('utf-8')
except UnicodeDecodeError:
return content.decode('latin-1')
def _clean_xml_bytes(self, content: bytes) -> bytes:
try:
text = content.decode(self.config.encoding)
except UnicodeDecodeError:
try:
text = content.decode('utf-8')
except UnicodeDecodeError:
text = content.decode('latin-1')
text = re.sub(r'^[0-9a-fA-F]+\r?\n', '', text, flags=re.MULTILINE)
text = text.strip()
if not text.startswith('<?xml'):
xml_start = text.find('<?xml')
if xml_start > 0:
text = text[xml_start:]
text = re.sub(r'encoding=["\'][^"\']*["\']', 'encoding="UTF-8"', text)
return text.encode('utf-8')
async def _request(self, method: str, url: str, **kwargs):
await self.create_session()
for attempt in range(self.config.max_retries):
try:
if attempt > 0:
await asyncio.sleep(self.config.retry_delay)
async with self.session.request(method, url, **kwargs) as response:
content = await response.read()
return response.status, content, dict(response.cookies)
except aiohttp.ClientError as e:
self.logger.warning(f"Request failed (attempt {attempt + 1}): {e}")
if attempt == self.config.max_retries - 1:
raise
raise Exception("Max retries exceeded")
async def login(self, user_id: Optional[str] = None, password: Optional[str] = None) -> bool:
user_id = user_id or self.config.user_id
password = password or self.config.password
if not user_id or not password:
self.logger.error("User ID and password are required")
return False
self.logger.info(f"Attempting login for user: {user_id}")
url = f"{self.config.base_url}/member/login_ok.html"
data = {
'prevURL': '',
'id': user_id,
'passwd': password,
'idSave': 'Y',
'button': 'LOGIN'
}
headers = {
**self.headers,
'Content-Type': 'application/x-www-form-urlencoded',
'Origin': self.config.base_url,
'Referer': f'{self.config.base_url}/member/login_v2.html',
}
try:
status, content, cookies = await self._request(
'POST', url, data=data, headers=headers, allow_redirects=False
)
text = self._decode_response(content)
if 'goMain' in text or 'PHPSESSID' in str(cookies):
self.cookies.update(cookies)
self.is_logged_in = True
self.last_session_refresh = datetime.now()
self.logger.info("Login successful")
return True
else:
self.logger.error("Login failed: unexpected response")
return False
except Exception as e:
self.logger.error(f"Login error: {e}")
return False
async def get_car_makers(self) -> List[CarMaker]:
url = f"{self.config.base_url}/common/ajax/AutoDBCode.html"
params = {'mode': 'getCarInit', 'ctl': 'car'}
headers = {
**self.headers,
'Accept': 'application/xml, text/xml, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest',
}
try:
status, content, _ = await self._request(
'GET', url, params=params, headers=headers, cookies=self.cookies
)
if status != 200:
return []
return self._parse_car_makers(content)
except Exception as e:
self.logger.error(f"Error getting car makers: {e}")
return []
def _parse_car_makers(self, content: bytes) -> List[CarMaker]:
makers = []
try:
xml_bytes = self._clean_xml_bytes(content)
root = etree.fromstring(xml_bytes)
for item in root.findall('.//item'):
key = item.findtext('key', '')
name = item.findtext('name', '')
cho = item.findtext('cho', '')
if key and name:
makers.append(CarMaker(code=key, name=name, cho=cho))
except Exception as e:
self.logger.error(f"Error parsing car makers: {e}")
return makers
async def get_car_models(self, maker_code: str) -> List[CarModel]:
url = f"{self.config.base_url}/common/ajax/AutoDBCode.html"
params = {
'mode': 'getCarModelInit',
'ctl': 'car',
'company': maker_code,
'selected': '',
}
headers = {
**self.headers,
'Accept': 'application/xml, text/xml, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest',
}
try:
status, content, _ = await self._request(
'GET', url, params=params, headers=headers, cookies=self.cookies
)
if status != 200:
return []
return self._parse_car_models(content, maker_code)
except Exception as e:
self.logger.error(f"Error getting car models: {e}")
return []
def _parse_car_models(self, content: bytes, maker_code: str) -> List[CarModel]:
models = []
try:
xml_bytes = self._clean_xml_bytes(content)
root = etree.fromstring(xml_bytes)
for item in root.findall('.//item'):
key = item.findtext('key', '')
name = item.findtext('name', '')
if key and name:
models.append(CarModel(code=key, name=name, maker_code=maker_code))
except Exception as e:
self.logger.error(f"Error parsing car models: {e}")
return models
def get_image_url(self, car_no: str, index: int = 0) -> str:
padded = car_no.zfill(9)
folder = f"{padded[0:3]}/{padded[3:6]}/{padded[6:9]}"
return f"{self.config.base_url}/data/__carPhoto/{folder}/cmcar_{index}.jpg"