Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: pip install -e ".[dev]"
- name: Lint (ruff)
run: |
ruff check loftbox tests
ruff format --check loftbox tests
- name: Type check (mypy)
run: mypy loftbox
- name: Test
run: pytest -q
37 changes: 37 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Publish to PyPI

# v* 태그 푸시 시 PyPI 게시. 게시 권한은 PYPI_API_TOKEN 시크릿(레포 설정에
# caspar 가 주입). 태그 버전과 pyproject version 이 일치해야 함.
on:
push:
tags: ["v*"]

permissions:
contents: read

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install build tooling
run: pip install build twine
- name: Verify tag matches pyproject version
run: |
TAG="${GITHUB_REF_NAME#v}"
VER=$(python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
if [ "$TAG" != "$VER" ]; then
echo "tag $TAG != pyproject version $VER"; exit 1
fi
- name: Build
run: python -m build
- name: Check metadata
run: twine check dist/*
- name: Publish
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: twine upload dist/*
80 changes: 64 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,85 @@
# LoftBox Python SDK

AI 에이전트를 위한 이메일 인프라 SDK
AI 에이전트를 위한 이메일 인프라 SDK.

## 설치

```bash
pip install loftbox
```

## 사용법
요구사항: Python 3.9+.

## 빠른 시작

```python
from loftbox import LoftBox

client = LoftBox(api_key="lb_live_xxx")
client.messages.send(
mailbox_id="mb_xxx",
to="recipient@example.com",
subject="Hello",
body_text="World"
)
with LoftBox(api_key="lb_live_xxx") as client:
# 에이전트 + 메일박스
agent = client.agents.create(name="Support Bot", slug="support-bot")
mailbox = client.mailboxes.create(agent.id, local_part="support")

# 발송 (멱등 키로 중복 방지)
msg = client.messages.send(
mailbox_id=mailbox.id,
to=["recipient@example.com"],
subject="Hello",
body_text="World",
idempotency_key="welcome-42",
)

# 수신 폴링 → ack
inbox = client.mailboxes.list_inbox(mailbox.id)
client.mailboxes.ack_inbox(mailbox.id, [m.id for m in inbox.data])
```

## 프레임워크 통합
## 기능

### LangChain
- **발송**: `messages.send(...)` — 텍스트/HTML/Markdown 본문, 첨부, cc, 답장 헤더
- **예약 발송**: `send(..., send_at="2030-01-01T09:00:00Z")` (미래 RFC3339)
- **멱등 발송**: `send(..., idempotency_key="...")` — 중복 발송 방지
- **수신**: `mailboxes.list_inbox(...)` 폴링 + `ack_inbox(...)`. `message.extracted_text` 로 인용 제거된 답장 본문
- **라벨**: `messages.add_labels(...)`, `remove_label(...)`, `list(label=...)`
- **전문검색**: `messages.list(q="...")`, `threads.list(q="...")`
- **스레드**: `threads.list(...)`, `list_messages(...)`
- **승인 워크플로**: `messages.approve(id, reason=...)`, `reject(...)`
- **웹훅**: `webhooks.create(agent_id, url, event_types)`
- **도메인 / suppression**: `domains.*`, `suppressions.*`

```bash
pip install loftbox[langchain]
## 오류 처리

모든 호출은 실패 시 `LoftBoxError` 하위 예외를 던집니다:

```python
from loftbox import RateLimitError, NotFoundError, ValidationError

try:
client.messages.send(...)
except RateLimitError as e:
print(f"{e.retry_after_secs}s 후 재시도")
except (NotFoundError, ValidationError) as e:
print(e.status_code, e.message)
```

### CrewAI
## 페이지네이션

```bash
pip install loftbox[crewai]
목록 메서드는 `Page` 를 반환합니다 (`.data`, `.next_cursor`):

```python
page = client.messages.list(mailbox_id=mailbox.id, limit=50)
while True:
for m in page.data:
...
if not page.next_cursor:
break
page = client.messages.list(mailbox_id=mailbox.id, limit=50, cursor=page.next_cursor)
```

## 예제

`examples/quickstart.py` 참고.

## 라이선스

MIT
Empty file removed examples/.gitkeep
Empty file.
61 changes: 61 additions & 0 deletions examples/quickstart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""LoftBox Python SDK 퀵스타트.

실행:
export LOFTBOX_API_KEY=lb_live_xxx
python examples/quickstart.py
"""

import os

from loftbox import LoftBox, RateLimitError


def main() -> None:
api_key = os.environ["LOFTBOX_API_KEY"]

with LoftBox(api_key=api_key) as client:
# 1. 에이전트 + 메일박스 준비 (최초 1회).
agent = client.agents.create(name="Support Bot", slug="support-bot")
mailbox = client.mailboxes.create(agent.id, local_part="support")
print(f"mailbox: {mailbox.address}")

# 2. 발송 (멱등 키로 중복 방지).
try:
msg = client.messages.send(
mailbox_id=mailbox.id,
to=["customer@example.com"],
subject="안녕하세요",
body_text="LoftBox 에서 보냅니다.",
idempotency_key="welcome-customer-42",
)
print(f"sent: {msg.id} status={msg.status}")
except RateLimitError as e:
print(f"rate limited, retry after {e.retry_after_secs}s")

# 3. 예약 발송 (1시간 뒤).
from datetime import datetime, timedelta, timezone

client.messages.send(
mailbox_id=mailbox.id,
to=["customer@example.com"],
subject="리마인더",
body_text="예약 발송 메시지",
send_at=(datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),
)

# 4. 수신 폴링 → 처리 → ack.
inbox = client.mailboxes.list_inbox(mailbox.id, limit=20)
for incoming in inbox.data:
print(f"received: {incoming.subject} (extracted: {incoming.extracted_text!r})")
if inbox.data:
client.mailboxes.ack_inbox(mailbox.id, [m.id for m in inbox.data])

# 5. 라벨링 + 전문검색.
if inbox.data:
client.messages.add_labels(inbox.data[0].id, ["needs-reply", "vip"])
results = client.messages.list(q="invoice", label="vip", limit=10)
print(f"search hits: {len(results.data)}")


if __name__ == "__main__":
main()
47 changes: 44 additions & 3 deletions loftbox/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,48 @@
"""LoftBox Python SDK"""
"""LoftBox Python SDK — AI 에이전트를 위한 이메일 인프라."""

from .client import LoftBox
from .models import Message, Agent, Mailbox
from .errors import (
AuthenticationError,
ConflictError,
LoftBoxError,
NotFoundError,
PermissionError,
RateLimitError,
ValidationError,
)
from .models import (
Agent,
Attachment,
Domain,
DomainStatus,
Mailbox,
Message,
Page,
Suppression,
Thread,
Webhook,
)

__version__ = "0.1.0"
__all__ = ["LoftBox", "Message", "Agent", "Mailbox"]
__all__ = [
"LoftBox",
# models
"Agent",
"Attachment",
"Domain",
"DomainStatus",
"Mailbox",
"Message",
"Page",
"Suppression",
"Thread",
"Webhook",
# errors
"LoftBoxError",
"AuthenticationError",
"PermissionError",
"NotFoundError",
"ConflictError",
"RateLimitError",
"ValidationError",
]
Loading
Loading