Metadata-Version: 2.4
Name: dooray-sdk
Version: 0.0.1a1
Summary: Python SDK for Dooray.com with Socket Mode support
Author-email: Dooray SDK Team <dooray-sdk@nhndooray.com>
License: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.25.0
Requires-Dist: websocket-client>=1.0.0
Requires-Dist: python-dotenv>=1.0.0
Provides-Extra: aiohttp
Requires-Dist: aiohttp>=3.8.0; extra == "aiohttp"
Provides-Extra: websockets
Requires-Dist: websockets>=10.0; extra == "websockets"
Dynamic: license-file

# Dooray SDK - Python

Python Dooray SDK의 주요 클래스, 데코레이터 및 사용법을 설명합니다.

---

## 설치

### pip으로 설치

```bash
pip install dooray-sdk
```

### aiohttp 포함 설치 (권장)

```bash
pip install dooray-sdk[aiohttp]
```

### 가상환경 사용 (권장)

```bash
# 가상환경 생성
python3 -m venv .venv

# 활성화 (Linux/macOS)
source .venv/bin/activate

# 활성화 (Windows)
.venv\Scripts\activate

# 설치
pip install dooray-sdk[aiohttp]
```

### 필수 의존성

| 패키지 | 버전 | 설명 |
| :----- | :--- | :--- |
| `aiohttp` | >= 3.8.0 | 비동기 HTTP/WebSocket 클라이언트 |
| `requests` | >= 2.28.0 | 동기 HTTP 클라이언트 |

## 환경변수

SDK는 다음 환경변수를 사용합니다:

| 환경변수 | 필수 | 설명 | 예시 |
| :------- | :--- | :--- | :--- |
| `DOORAY_AGENT_TOKEN` | 예 | 에이전트 인증 토큰 | `your_agent_token` |
| `DOORAY_DOMAIN` | 예 | Dooray 도메인 | `company.dooray.com` |

## 클래스

### `Agent`

Dooray 에이전트를 만들기 위한 핵심 클래스입니다. 메시지 수신, 자동 응답, 이벤트 처리 등을 지원합니다.

```python
class Agent:
    def __init__(
        self,
        token: str = None,
        domain: str = None,
        services: List[str] = None,
        **kwargs
    )
```

#### 생성자 매개변수

| 매개변수 | 타입 | 기본값 | 설명 |
| :------- | :--- | :----- | :--- |
| `token` | `str \| None` | `None` | 에이전트 토큰 (환경변수 `DOORAY_AGENT_TOKEN` 대체) |
| `domain` | `str \| None` | `None` | Dooray 도메인 (환경변수 `DOORAY_DOMAIN` 대체) |
| `services` | `List[str] \| None` | `["messenger"]` | 연결할 서비스 목록 |

#### 속성

| 속성 | 타입 | 설명 |
| :--- | :--- | :--- |
| `token` | `str` | 에이전트 인증 토큰 |
| `domain` | `str` | Dooray 도메인 |
| `services` | `List[str]` | 연결된 서비스 목록 |
| `base_url` | `str` | API 기본 URL |
| `is_connected` | `bool` | 연결 상태 |

#### 메서드

| 메서드 | 설명 |
| :----- | :--- |
| `run()` | 에이전트 실행 (블로킹) |
| `on(event_type, action, service)` | 이벤트 핸들러 데코레이터 |
| `add_handler(event_type, handler, action, service)` | 핸들러 등록 (비데코레이터) |
| `reply_to(trigger, response)` | 단순 응답 매핑 |
| `send_message(channel, text)` | 메시지 전송 |

#### 예제 - 기본 사용

```python
from dooray_sdk import Agent

agent = Agent()

@agent.messenger
def handle_message(req):
    """메신저 메시지 처리."""
    if req.text:
        req.reply(f"받은 메시지: {req.text}")

agent.run()
```

#### 예제 - 멀티 서비스

```python
from dooray_sdk import Agent

agent = Agent(services=["messenger", "task", "wiki"])

@agent.messenger
def handle_messenger(req):
    req.reply("메신저 메시지입니다")

@agent.task
def handle_task(req):
    print(f"업무 이벤트: {req.action}")

@agent.wiki
def handle_wiki(req):
    print(f"위키 이벤트: {req.action}")

agent.run()
```

### `SocketModeRequest`

WebSocket으로 수신된 요청을 나타내는 클래스입니다.

```python
@dataclass
class SocketModeRequest:
    envelope_id: str
    type: str
    payload: Dict[str, Any]
    service: str = ""
    action: str = ""
    entity: EntityWrapper = None
    actor: ActorWrapper = None
    action_data: ActionWrapper = None
```

#### 속성

| 속성 | 타입 | 설명 |
| :--- | :--- | :--- |
| `envelope_id` | `str` | 메시지 고유 ID |
| `type` | `str` | 이벤트 타입 (`message`, `task`, `page` 등) |
| `payload` | `Dict[str, Any]` | 원본 페이로드 데이터 |
| `service` | `str` | 서비스 이름 (`messenger`, `task`, `wiki`) |
| `action` | `str` | 액션 타입 (`create`, `update`, `delete`) |
| `entity` | `EntityWrapper` | 엔티티 정보 래퍼 |
| `actor` | `ActorWrapper` | 액터 정보 래퍼 |
| `action_data` | `ActionWrapper` | 액션 데이터 래퍼 |

#### 편의 속성

| 속성 | 타입 | 설명 |
| :--- | :--- | :--- |
| `is_message` | `bool` | 메시지 타입 여부 |
| `text` | `str \| None` | 메시지 텍스트 |
| `channel` | `str \| None` | 채널 ID |
| `data` | `Dict[str, Any]` | 원본 데이터 (payload 별칭) |

#### 메서드

| 메서드 | 설명 |
| :----- | :--- |
| `reply(text)` | 동기/비동기 응답 전송 |
| `is_type(types)` | 타입 확인 |
| `is_action(actions)` | 액션 확인 |
| `is_service(services)` | 서비스 확인 |
| `to_dict()` | 딕셔너리 변환 |
| `from_dict(data, default_service)` | 딕셔너리에서 생성 (클래스 메서드) |

#### 예제 - 요청 처리

```python
@agent.messenger
def handle(req):
    # 메시지 타입 확인
    if req.is_message:
        print(f"텍스트: {req.text}")
        print(f"채널: {req.channel}")

    # 액션 확인
    if req.is_action("create"):
        print("새 메시지입니다")

    # 서비스 확인
    if req.is_service("messenger"):
        req.reply("메신저에서 응답합니다")
```

### `WebClient`

동기 REST API 클라이언트입니다.

```python
class WebClient:
    def __init__(
        self,
        token: str,
        base_url: str = "https://api.dooray.com/",
        timeout: int = 30
    )
```

#### 메서드

| 메서드 | 반환 타입 | 설명 |
| :----- | :-------- | :--- |
| `send_message(channel, text, **kwargs)` | `Dict` | 메시지 전송 |
| `get_member(member_id)` | `Dict` | 조직 멤버 정보 조회 |

### `AsyncWebClient`

비동기 REST API 클라이언트입니다.

```python
class AsyncWebClient:
    def __init__(
        self,
        token: str,
        base_url: str = "https://api.dooray.com/"
    )
```

#### 메서드

| 메서드 | 반환 타입 | 설명 |
| :----- | :-------- | :--- |
| `send_message(channel, text, **kwargs)` | `Dict` | 메시지 전송 |
| `get_member(member_id)` | `Dict` | 조직 멤버 정보 조회 |

## 데코레이터

### 서비스별 데코레이터

각 서비스에 대한 이벤트 핸들러를 등록합니다.

```python
@agent.messenger   # 메신저 서비스 전체 이벤트
@agent.task        # 업무 서비스 전체 이벤트
@agent.wiki        # 위키 서비스 전체 이벤트
```

### `@agent.on()` 데코레이터

통합 이벤트 핸들러 데코레이터입니다.

```python
def on(
    event_type: Union[str, List[str]] = "all",
    action: Union[str, List[str]] = None,
    service: Union[str, List[str]] = None
) -> Callable
```

#### 매개변수

| 매개변수 | 타입 | 기본값 | 설명 |
| :------- | :--- | :----- | :--- |
| `event_type` | `str \| List[str]` | `"all"` | 이벤트 타입 필터 |
| `action` | `str \| List[str] \| None` | `None` | 액션 필터 |
| `service` | `str \| List[str] \| None` | `None` | 서비스 필터 |

#### 예제

```python
# 메시지 이벤트만 처리
@agent.on("message")
def handle_message(req):
    pass

# 특정 액션만 처리
@agent.on("message", action="create")
def handle_new_message(req):
    pass

# 복수 타입 처리
@agent.on(["task", "page"])
def handle_task_or_page(req):
    pass

# 서비스 + 액션 조합
@agent.on(service="wiki", action=["create", "update"])
def handle_wiki_changes(req):
    pass
```

## 요청 데이터 구조

SDK는 딕셔너리 데이터를 점 표기법(dot notation)으로 접근할 수 있게 해주는 래퍼 타입을 제공합니다.

### 타입 구조 개요

```
SocketModeRequest
├── entity: EntityWrapper      # 엔티티 정보 (task, page 등)
│   ├── type: str              # 엔티티 타입
│   └── data: DataWrapper      # 엔티티 데이터
├── actor: ActorWrapper        # 액션 수행자 정보
│   ├── type: str              # 액터 타입
│   └── data: DataWrapper      # 액터 데이터
└── action_data: ActionWrapper # 액션 상세 정보
    ├── type: str              # 액션 타입
    └── data: DataWrapper      # 액션 데이터
```

### `DataWrapper`

모든 래퍼 타입의 기반이 되는 클래스입니다. 딕셔너리 데이터를 점 표기법으로 접근할 수 있게 합니다.

```python
class DataWrapper:
    def __getattr__(self, name: str) -> Any   # 점 표기법 접근
    def __getitem__(self, key: str) -> Any    # 딕셔너리 접근
    def get(self, key: str, default=None)     # 안전한 접근
    def to_dict(self) -> Dict[str, Any]       # 원본 딕셔너리 반환
```

**사용 예시:**

```python
# 점 표기법 접근
data.id
data.subject
data.nested.value

# 딕셔너리 접근
data["id"]
data.get("subject", "기본값")

# 원본 딕셔너리
data.to_dict()
```

### `EntityWrapper`

엔티티(업무, 페이지 등) 정보를 래핑합니다.

| 속성 | 타입 | 설명 |
| :--- | :--- | :--- |
| `type` | `str` | 엔티티 타입 (예: `"task"`, `"page"`) |
| `data` | `DataWrapper` | 엔티티 상세 데이터 |

**사용 예시:**

```python
@agent.on("task")
def handle_task(req):
    print(req.entity.type)           # "task"
    print(req.entity.data.id)        # 업무 ID
    print(req.entity.data.subject)   # 업무 제목
```

#### 메시지 데이터

메신저 메시지의 경우 `entity.type`이 `"message"`입니다.

| 속성 | 타입 | 설명 |
| :--- | :--- | :--- |
| `id` | `str` | 메시지 ID |
| `channelId` | `str` | 채널 ID |
| `senderId` | `str` | 보낸 사람 멤버 ID |
| `text` | `str` | 메시지 텍스트 |
| `sentAt` | `int` | 보낸 시간 (timestamp ms) |
| `seq` | `int` | 메시지 시퀀스 번호 |
| `directMemberId` | `str` | DM 상대방 멤버 ID |
| `parentChannelId` | `str` | 부모 채널 ID (스레드) |

```python
@agent.messenger
def handle(req):
    print(req.entity.message.text)
    print(req.entity.message.channelId)
    print(req.entity.message.senderId)
    print(req.entity.message.sentAt)
```

### `ActorWrapper`

액션을 수행한 사용자 정보를 래핑합니다.

| 속성 | 타입 | 설명 |
| :--- | :--- | :--- |
| `type` | `str` | 액터 타입 (예: `"organizationMember"`) |
| `data` | `DataWrapper` | 액터 상세 데이터 |
| `organizationMember` | `LazyMemberData \| None` | 조직 멤버 정보 (lazy loading 지원) |

**사용 예시:**

```python
@agent.on("message")
def handle(req):
    if req.actor:
        print(req.actor.type)         # "organizationMember"

        # organizationMember로 접근 (권장)
        member = req.actor.organizationMember
        print(member.id)              # 멤버 ID (로컬 데이터)
        print(member.name)            # 멤버 이름 (lazy loading)
```

#### organizationMember 필드

| 속성 | 타입 | 설명 |
| :--- | :--- | :--- |
| `id` | `str` | 멤버 ID |
| `name` | `str` | 멤버 이름 |
| `externalEmailAddress` | `str` | 이메일 주소 |
| `nickname` | `str` | 닉네임 |
| `englishName` | `str` | 영문 이름 |
| `nativeName` | `str` | 현지 이름 |
| `userCode` | `str` | 사용자 코드 |
| `locale` | `str` | 로케일 |
| `timezoneName` | `str` | 타임존 |

#### Lazy Loading

`organizationMember`는 WebSocket 메시지에 포함된 데이터는 즉시 반환하고, 없는 필드는 API를 호출하여 가져옵니다.

```python
@agent.messenger
async def handle(req):
    member = req.actor.organizationMember

    # 로컬 데이터 (API 호출 없음)
    member_id = member.id

    # Async 접근 - 필요시 API 호출
    name = await member.name
    email = await member.externalEmailAddress

    # Sync 접근 (print, 비교 등)
    print(member.name)
    if member.name == "홍길동":
        await req.reply("안녕하세요!")

    # 전체 데이터 조회
    full_data = await member
```

### `ActionWrapper`

액션의 상세 정보를 래핑합니다.

| 속성 | 타입 | 설명 |
| :--- | :--- | :--- |
| `type` | `str` | 액션 타입 (예: `"create"`, `"update"`) |
| `data` | `DataWrapper` | 액션 상세 데이터 |

## 사용 예시

### 에코 에이전트

```python
from dooray_sdk import Agent

agent = Agent()

@agent.messenger
def echo(req):
    if req.text:
        req.reply(f"에코: {req.text}")

agent.run()
```

### 자동 응답 에이전트

```python
from dooray_sdk import Agent

agent = Agent()

# 단순 매핑
agent.reply_to("ping", "pong")
agent.reply_to("hello", "안녕하세요!")

# 커스텀 핸들러
@agent.messenger
def handle(req):
    text = req.text or ""
    if text.startswith("help"):
        req.reply("사용 가능한 명령: ping, hello, help")

agent.run()
```

### 멀티 서비스 에이전트

```python
from dooray_sdk import Agent

agent = Agent(services=["messenger", "task", "wiki"])

@agent.messenger
def on_message(req):
    print(f"[메신저] {req.text}")
    req.reply("메시지 받음!")

@agent.task
def on_task(req):
    print(f"[업무] {req.action}: {req.entity.data.subject}")

@agent.wiki
def on_wiki(req):
    print(f"[위키] {req.action}: {req.entity.data.title}")

@agent.on("all")
def on_any(req):
    print(f"[전체] {req.service}/{req.type}/{req.action}")

agent.run()
```

## 오류 처리

### 일반적인 예외

| 예외 | 원인 | 해결 방법 |
| :--- | :--- | :------- |
| `ValueError` | 환경변수 미설정 | `DOORAY_AGENT_TOKEN`, `DOORAY_DOMAIN` 설정 |
| `ConnectionError` | WebSocket 연결 실패 | 네트워크 및 토큰 확인 |
| `RuntimeError` | 에이전트 미실행 상태에서 reply 호출 | `agent.run()` 후 사용 |

### 예외 처리 예시

```python
from dooray_sdk import Agent

try:
    agent = Agent()
except ValueError as e:
    print(f"설정 오류: {e}")
    print("DOORAY_AGENT_TOKEN과 DOORAY_DOMAIN을 설정하세요")
    exit(1)

@agent.messenger
def handle(req):
    try:
        # 비즈니스 로직
        process_message(req)
    except Exception as e:
        # 에러 로깅 (사용자에게 에러 노출 방지)
        logger.error(f"처리 오류: {e}")

agent.run()
```

## License

MIT License
