ff-digikey-api 0.0.4
DigiKey Product Information V4 API Client
Requires Python: >=3.10
Keywords: digikey, api, electronics, components, v4

# ff-digikey-api

DigiKey Product Information V4 API Python 클라이언트.

OAuth 2.0 인증, 토큰 자동 갱신, KRW/한국어 로케일을 지원합니다.

## Quick Start

### 1. 설치

```bash
pip install ff-digikey-api
```

### 2. DigiKey 개발자 등록

1. [DigiKey API Portal](https://developer.digikey.com/)에서 앱 생성
2. **Product Information V4** API를 구독
3. Callback URL을 `https://localhost:8139/digikey_callback`으로 설정

### 3. 환경변수 설정

`.env` 파일 생성:

```
DIGIKEY_CLIENT_ID=your_client_id
DIGIKEY_CLIENT_SECRET=your_client_secret
DIGIKEY_LANGUAGE=ko
DIGIKEY_CURRENCY=KRW
DIGIKEY_SITE=KR
```

### 4. 사용

```python
from ff_digikey_api import DigiKeyClient

client = DigiKeyClient.from_env(".env")

# 최초 1회: 브라우저에서 DigiKey 로그인 (토큰은 자동 저장/갱신)
client.authorize()

# 부품 검색
result = client.search("STM32F407", limit=5)
for p in result.products:
    print(p.manufacturer_product_number, p.unit_price, "KRW")

client.close()
```

> 최초 `authorize()` 시 브라우저에 "연결이 비공개" 경고가 뜹니다.
> 자체 서명 인증서 때문이며, "고급 → localhost로 계속"을 클릭하면 됩니다.

---

## 초기화

### 직접 전달

```python
from ff_digikey_api import DigiKeyClient, DigiKeyLocale

client = DigiKeyClient(
    client_id="...",
    client_secret="...",
    locale=DigiKeyLocale(language="ko", currency="KRW", site="KR"),
)
```

### 환경변수

```python
client = DigiKeyClient.from_env(".env")
```

### Context Manager

```python
with DigiKeyClient.from_env(".env") as client:
    result = client.search("LM358")
```

### 옵션

| 파라미터 | 기본값 | 설명 |
|---------|--------|------|
| `sandbox` | `False` | Sandbox API 사용 |
| `storage_path` | `"token_storage.json"` | 토큰 저장 파일 경로 |
| `callback_port` | `8139` | OAuth 콜백 포트 |
| `redirect_uri` | `"https://localhost:8139/digikey_callback"` | OAuth 리디렉트 URI |
| `auth_timeout` | `120` | 콜백 대기 타임아웃 (초) |

---

## API Reference

### `authorize()`

OAuth 2.0 최초 인증. 브라우저가 열리고, 로그인 후 토큰이 자동 저장됩니다.
이후 호출에서는 저장된 토큰을 사용하며, 만료 시 자동 갱신됩니다.

```python
client.authorize()
```

---

### `search(keywords, limit, offset, filters, sort)`

키워드로 부품을 검색합니다.

**Input**

| 파라미터 | 타입 | 기본값 | 설명 |
|---------|------|--------|------|
| `keywords` | `str` | (필수) | 검색 키워드 |
| `limit` | `int` | `50` | 반환 개수 (최대 50) |
| `offset` | `int` | `0` | 페이지 오프셋 |
| `filters` | `FilterOptions` | `None` | 필터 (제조사, 카테고리 등) |
| `sort` | `SortOptions` | `None` | 정렬 옵션 |

**Output** — `KeywordSearchResponse`

| 필드 | 타입 | 설명 |
|------|------|------|
| `products` | `list[Product]` | 검색 결과 목록 |
| `products_count` | `int` | 전체 결과 수 |
| `exact_matches` | `list[Product]` | 정확히 일치하는 결과 |
| `filter_options` | `dict` | 사용 가능한 필터 옵션 |
| `raw_data` | `dict` | API 원본 응답 |

**예시**

```python
from ff_digikey_api import FilterOptions, SortOptions

# 기본 검색
result = client.search("STM32F407", limit=10)
print(f"총 {result.products_count}건")
for p in result.products:
    print(f"{p.manufacturer_product_number} | {p.manufacturer.name} | {p.unit_price} KRW")

# 필터 + 정렬
result = client.search(
    "capacitor 100uF",
    limit=10,
    filters=FilterOptions(manufacturer_ids=[13]),
    sort=SortOptions(field="UnitPrice", sort_order="Ascending"),
)
```

---

### `product_details(product_number)`

제조사 부품번호(MPN)로 상세 정보를 조회합니다.

**Input**

| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `product_number` | `str` | 제조사 부품번호 |

**Output** — `Product`

| 필드 | 타입 | 설명 |
|------|------|------|
| `manufacturer_product_number` | `str` | MPN |
| `manufacturer` | `Manufacturer` | 제조사 (`.id`, `.name`) |
| `description` | `str` | 짧은 설명 |
| `detailed_description` | `str` | 상세 설명 |
| `unit_price` | `float` | 단가 |
| `product_url` | `str` | DigiKey 제품 페이지 URL |
| `datasheet_url` | `str \| None` | 데이터시트 URL |
| `photo_url` | `str \| None` | 제품 이미지 URL |
| `product_status` | `str` | 상태 (Active, Obsolete 등) |
| `category` | `Category \| None` | 카테고리 (`.id`, `.name`, `.children`) |
| `parameters` | `list[Parameter]` | 사양 목록 (`.name`, `.value`) |
| `product_variations` | `list[ProductVariation]` | 포장 변형 목록 |
| `raw_data` | `dict` | API 원본 응답 |

`ProductVariation` 필드:

| 필드 | 타입 | 설명 |
|------|------|------|
| `digi_key_product_number` | `str` | DigiKey 부품번호 |
| `package_type` | `str` | 포장 타입 (Cut Tape, Tape & Reel 등) |
| `standard_pricing` | `list[PriceBreak]` | 수량별 단가 |
| `quantity_available` | `int` | 재고 수량 |
| `min_order_quantity` | `int` | 최소 주문 수량 |

`PriceBreak` 필드: `break_quantity`, `unit_price`, `total_price`

**예시**

```python
detail = client.product_details("STM32F407VET6TR")
print(f"{detail.manufacturer_product_number} - {detail.description}")
print(f"제조사: {detail.manufacturer.name}")
print(f"상태: {detail.product_status}")
print(f"카테고리: {detail.category.name}")

for param in detail.parameters:
    print(f"  {param.name}: {param.value}")

for v in detail.product_variations:
    print(f"  {v.digi_key_product_number} | 재고: {v.quantity_available}")
    for pb in v.standard_pricing:
        print(f"    {pb.break_quantity}개: {pb.unit_price} KRW")
```

---

### `pricing(product_number)`

부품의 가격 정보만 조회합니다.

**Input**

| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `product_number` | `str` | 제조사 부품번호 |

**Output** — `ProductPricingResponse`

| 필드 | 타입 | 설명 |
|------|------|------|
| `products` | `list[Product]` | 가격 정보가 포함된 제품 목록 |
| `products_count` | `int` | 결과 수 |
| `raw_data` | `dict` | API 원본 응답 |

**예시**

```python
pricing = client.pricing("STM32F407VET6TR")
for p in pricing.products:
    for v in p.product_variations:
        print(f"{v.digi_key_product_number} ({v.package_type})")
        for pb in v.standard_pricing:
            print(f"  {pb.break_quantity}개: {pb.unit_price} KRW")
```

---

### `associations(product_number)`

연관 부품 목록을 조회합니다.

**Input**

| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `product_number` | `str` | 제조사 부품번호 |

**Output** — `list[Product]`

**예시**

```python
assocs = client.associations("STM32F407VET6TR")
for p in assocs:
    print(f"{p.manufacturer_product_number} | {p.description}")
```

---

### `substitutions(product_number)`

대체 가능한 부품 목록을 조회합니다.

**Input**

| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `product_number` | `str` | 제조사 부품번호 |

**Output** — `list[Product]`

**예시**

```python
subs = client.substitutions("STM32F407VET6TR")
for p in subs:
    print(f"{p.manufacturer_product_number} | {p.manufacturer.name}")
```

---

### `recommended_products(product_number)`

추천 부품 목록을 조회합니다.

**Input**

| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `product_number` | `str` | 제조사 부품번호 |

**Output** — `list[Product]`

**예시**

```python
recs = client.recommended_products("STM32F407VET6TR")
for p in recs:
    print(f"{p.manufacturer_product_number} | {p.unit_price} KRW")
```

---

### `alternate_packaging(product_number)`

동일 부품의 다른 포장 형태를 조회합니다.

**Input**

| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `product_number` | `str` | 제조사 부품번호 |

**Output** — `list[Product]`

**예시**

```python
alts = client.alternate_packaging("STM32F407VET6TR")
for p in alts:
    for v in p.product_variations:
        print(f"{v.digi_key_product_number} | {v.package_type} | 재고: {v.quantity_available}")
```

---

### `media(product_number)`

부품의 미디어 리소스(이미지, 동영상 등)를 조회합니다.

**Input**

| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `product_number` | `str` | 제조사 부품번호 |

**Output** — `dict` (API 원본 응답)

**예시**

```python
media = client.media("STM32F407VET6TR")
for link in media.get("MediaLinks", []):
    print(f"{link['MediaType']}: {link['Url']}")
```

---

### `manufacturers()`

DigiKey에 등록된 전체 제조사 목록을 조회합니다.

**Input** — 없음

**Output** — `list[Manufacturer]`

| 필드 | 타입 | 설명 |
|------|------|------|
| `id` | `int` | 제조사 ID |
| `name` | `str` | 제조사 이름 |

**예시**

```python
mfrs = client.manufacturers()
for m in mfrs[:10]:
    print(f"[{m.id}] {m.name}")
```

---

### `categories()`

전체 카테고리 트리를 조회합니다.

**Input** — 없음

**Output** — `list[Category]`

| 필드 | 타입 | 설명 |
|------|------|------|
| `id` | `int` | 카테고리 ID |
| `name` | `str` | 카테고리 이름 |
| `children` | `list[Category]` | 하위 카테고리 |

**예시**

```python
cats = client.categories()
for c in cats[:5]:
    print(f"[{c.id}] {c.name}")
    for child in c.children[:3]:
        print(f"  └ [{child.id}] {child.name}")
```

---

## 에러 처리

```python
from ff_digikey_api.Core.HttpClient.ApiError import ApiError
from ff_digikey_api.Core.HttpClient.RateLimitError import RateLimitError
from ff_digikey_api.Service.TokenManager.TokenExpiredError import TokenExpiredError

try:
    result = client.search("STM32")
except RateLimitError as e:
    print(f"요청 한도 초과. {e.retry_after}초 후 재시도")
except TokenExpiredError:
    print("토큰 만료. client.authorize() 재실행 필요")
except ApiError as e:
    print(f"API 에러 [{e.status_code}]: {e.message}")
```

## 모든 Struct에 `raw_data`

모든 응답 객체는 `raw_data` 필드에 API 원본 JSON을 보존합니다.
라이브러리가 파싱하지 않는 필드가 필요하면 `raw_data`에서 직접 접근할 수 있습니다.

```python
detail = client.product_details("STM32F407VET6TR")
# 라이브러리가 파싱하지 않는 필드
print(detail.raw_data["QuantityAvailable"])
print(detail.raw_data["ManufacturerLeadWeeks"])
```

## CLI (Agent-Oriented)

이 패키지는 Python API 라이브러리이면서, 주로 **AI 에이전트가 콘솔에서 DigiKey 부품 정보를 조회**하기 위해 만들어졌습니다.
설치 후 `ff-digikey` 명령으로 바로 사용할 수 있으며, 모든 출력은 에이전트가 파싱하기 쉬운 JSON 형식입니다.

```bash
pip install ff-digikey-api
```

### 최초 인증

```bash
ff-digikey authorize
```

브라우저가 열리고 DigiKey 로그인 후 토큰이 자동 저장됩니다.

### 부품 검색

```bash
ff-digikey search "STM32F407" --limit 5
ff-digikey search "capacitor 100uF" --limit 10 --offset 0
```

### 부품 상세 조회

```bash
ff-digikey details STM32F407VET6TR
```

### 가격 조회

```bash
ff-digikey pricing STM32F407VET6TR
```

### 에러 처리

에러 발생 시 stderr로 JSON 에러 객체가 출력되고 exit code 1로 종료됩니다.

```json
{"error": "TokenExpiredError", "message": "Refresh token is invalid. Re-authentication required."}
```

## License

MIT