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
65 changes: 65 additions & 0 deletions .claude/hooks/lint-python.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""
Claude Code Hook: Python 파일 자동 lint/fix
- ruff check --fix: 린트 오류 자동 수정
- ruff format: 코드 포매팅
- ty check: 타입 체크 (오류 시 exit code 1로 Claude에게 수정 요청)
"""
import json
import os
import subprocess
import sys


def main():
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError:
return 0

Comment on lines +18 to +19

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If json.load(sys.stdin) fails, the hook currently returns 0 (success). This could lead to silent failures where Claude believes the linting/fixing process was successful, but it actually failed to even parse the input. It would be more robust to return 1 (failure) in this case.

Suggested change
return 0
except json.JSONDecodeError:
print("Error: Invalid JSON input to lint-python hook.", file=sys.stderr)
return 1

file_path = input_data.get("tool_input", {}).get("file_path", "")

# Python 파일만 처리
if not file_path or not file_path.endswith(".py"):
return 0

project_dir = os.environ.get("CLAUDE_PROJECT_DIR", "")
if not project_dir:
return 0

os.chdir(project_dir)

# 1. ruff check --fix
subprocess.run(
["ruff", "check", "--fix", file_path],
capture_output=True,
text=True,
timeout=30,
)

# 2. ruff format
subprocess.run(
["ruff", "format", file_path],
capture_output=True,
text=True,
timeout=30,
)

# 3. ty check (타입 오류 시 차단)
result = subprocess.run(
["ty", "check", file_path],
capture_output=True,
text=True,
timeout=60,
)
if result.returncode != 0:
output = result.stdout.strip() or result.stderr.strip()
if output:
print(f"ty type error:\n{output}", file=sys.stderr)
return 1 # 타입 오류 시 Hook 실패 → Claude가 수정하도록 함

return 0


if __name__ == "__main__":
sys.exit(main())
15 changes: 15 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|NotebookEdit",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/lint-python.py\""
}
]
}
]
}
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@ debug

# ruff
.ruff_cache/

# omc
.omc/
156 changes: 156 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# SOLAPI-PYTHON KNOWLEDGE BASE

**Generated:** 2026-01-21
**Commit:** b77fdd9
**Branch:** main

## OVERVIEW

Python SDK for SOLAPI messaging platform. Sends SMS/LMS/MMS/Kakao/Naver/RCS messages in Korea. Thin wrapper around REST API using httpx + Pydantic v2.

## STRUCTURE

```
solapi-python/
├── solapi/ # Main package (single export: SolapiMessageService)
│ ├── services/ # message_service.py - all API operations
│ ├── model/ # Pydantic models (see solapi/model/AGENTS.md)
│ ├── lib/ # authenticator.py, fetcher.py
│ └── error/ # MessageNotReceivedError only
├── tests/ # pytest integration tests
├── examples/ # Feature-based usage examples
└── debug/ # Dev test scripts (not part of package)
```

## WHERE TO LOOK

| Task | Location | Notes |
|------|----------|-------|
| Send messages | `solapi/services/message_service.py` | All 10 API methods in single class |
| Request models | `solapi/model/request/` | Pydantic BaseModel with validators |
| Response models | `solapi/model/response/` | Separate from request models |
| Kakao/Naver/RCS | `solapi/model/{kakao,naver,rcs}/` | Domain-specific models |
| Authentication | `solapi/lib/authenticator.py` | HMAC-SHA256 signature |
| HTTP client | `solapi/lib/fetcher.py` | httpx with 3 retries |
| Test fixtures | `tests/conftest.py` | env-based credentials |
| Usage examples | `examples/simple/` | Copy-paste ready |

## CONVENTIONS

### Pydantic Everywhere
- ALL models extend `BaseModel`
- Field aliases: `Field(alias="camelCase")` for API compatibility
- Validators: `@field_validator` for normalization (e.g., phone numbers)

### Model Organization (Domain-Driven)
```
model/
├── request/ # Outbound API payloads
├── response/ # Inbound API responses
├── kakao/ # Kakao-specific (option, button)
├── naver/ # Naver-specific
├── rcs/ # RCS-specific
└── webhook/ # Delivery reports
```

### Naming
- Files: `snake_case.py`
- Classes: `PascalCase`
- Request suffix: `*Request` (e.g., `SendMessageRequest`)
- Response suffix: `*Response` (e.g., `SendMessageResponse`)

### Code Style (Ruff)
- Line length: 88
- Quote style: double
- Import sorting: isort (I rule)
- Target: Python 3.9+

### Tidy First Principles
- Never mix refactoring and feature changes in the same commit
- Tidy related code before making behavioral changes
- Tidying: guard clauses, dead code removal, rename, extract conditionals
- Separate tidying commits from feature commits

## ANTI-PATTERNS (THIS PROJECT)

### NEVER
- Add CLI/console scripts - this is library-only
- Create multiple service classes - all goes in `SolapiMessageService`
- Mix request/response models - they're deliberately separate
- Use dataclasses or TypedDict for API models - Pydantic only
- Hardcode credentials - use env vars

### VERSION SYNC REQUIRED
```python
# solapi/model/request/__init__.py
VERSION = "python/5.0.3" # MUST update on every release!
```
Also update `pyproject.toml` version.

## UNIQUE PATTERNS

### Single Service Class
```python
# All API methods in one class (318 lines)
class SolapiMessageService:
def send(...) # SMS/LMS/MMS/Kakao/Naver/RCS
def upload_file(...) # Storage
def get_balance(...) # Account
def get_groups(...) # Message groups
def get_messages(...) # Message history
def cancel_scheduled_message(...)
```

### Minimal Error Handling
- Only `MessageNotReceivedError` exists
- API errors raised as generic `Exception` with errorCode, errorMessage

### Authentication Flow
```
SolapiMessageService.__init__(api_key, api_secret)
→ Authenticator.get_auth_info()
→ HMAC-SHA256 signature
→ Authorization header
```

## COMMANDS

```bash
# Install
pip install solapi

# Dev setup
pip install -e ".[dev]"

# Lint & format
ruff check --fix .
ruff format .

# Test (requires env vars)
export SOLAPI_API_KEY="..."
export SOLAPI_API_SECRET="..."
export SOLAPI_SENDER="..."
export SOLAPI_RECIPIENT="..."
pytest

# Build
python -m build
```

## ENV VARS (Testing)

| Variable | Purpose |
|----------|---------|
| `SOLAPI_API_KEY` | API authentication |
| `SOLAPI_API_SECRET` | API authentication |
| `SOLAPI_SENDER` | Registered sender number |
| `SOLAPI_RECIPIENT` | Test recipient number |
| `SOLAPI_KAKAO_PF_ID` | Kakao business channel |
| `SOLAPI_KAKAO_TEMPLATE_ID` | Kakao template |

## NOTES

- No CI/CD pipeline - testing/linting is local only
- uv workspace includes Django webhook example
- Tests are integration tests (hit real API)
- Korean comments in some files (i18n TODO exists)
124 changes: 124 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Python SDK for SOLAPI messaging platform. Sends SMS/LMS/MMS/Kakao/Naver/RCS messages in Korea. Thin wrapper around REST API using httpx + Pydantic v2.

## Commands

```bash
# Dev setup
pip install -e ".[dev]"

# Lint & format
ruff check --fix .
ruff format .

# Test (requires env vars - see below)
pytest
pytest tests/test_balance.py # Single file
pytest -v # Verbose

# Build
python -m build
```

## Testing Environment Variables

Tests are integration tests that hit the real API:

| Variable | Purpose |
|----------|---------|
| `SOLAPI_API_KEY` | API authentication |
| `SOLAPI_API_SECRET` | API authentication |
| `SOLAPI_SENDER` | Registered sender number |
| `SOLAPI_RECIPIENT` | Test recipient number |
| `SOLAPI_KAKAO_PF_ID` | Kakao business channel |
| `SOLAPI_KAKAO_TEMPLATE_ID` | Kakao template |

## Architecture

### Package Structure
```
solapi/
├── services/ # message_service.py - single SolapiMessageService class
├── model/ # Pydantic models (see solapi/model/AGENTS.md)
│ ├── request/ # Outbound API payloads
│ ├── response/ # Inbound API responses (deliberately separate)
│ ├── kakao/ # Kakao channel models
│ ├── naver/ # Naver channel models
│ ├── rcs/ # RCS channel models
│ └── webhook/ # Delivery reports
├── lib/ # authenticator.py, fetcher.py
└── error/ # MessageNotReceivedError only
```

### Key Design Decisions

**Single Service Class**: All 10 API methods live in `SolapiMessageService` - do not create additional service classes.

**Request/Response Separation**: Request and response models are deliberately separate and should never be shared, even for similar fields.

**Pydantic Everywhere**: All API models use Pydantic BaseModel with field aliases for camelCase API compatibility:
```python
pf_id: str = Field(alias="pfId")
```

**Phone Number Normalization**: Use `@field_validator` to strip dashes from phone numbers.

### Version Sync Required

When releasing, update version in BOTH locations:
- `pyproject.toml` → `version = "X.Y.Z"`
- `solapi/model/request/__init__.py` → `VERSION = "python/X.Y.Z"`

## Code Style

- **Linter**: Ruff (line-length: 88, double quotes, isort)
- **Target**: Python 3.9+
- **Files**: `snake_case.py`
- **Classes**: `PascalCase`
- **Request/Response suffixes**: `*Request`, `*Response`

## Tidy First Principles

Follow Kent Beck's "Tidy First?" principles:

### Separate Changes
- Never mix **structural changes** (refactoring) with **behavioral changes** (features/fixes) in the same commit
- Order: tidying commit → feature commit

### Tidy First
Tidy the relevant code area before making behavioral changes:
- Use guard clauses to reduce nesting
- Remove dead code
- Rename for clarity
- Extract complex conditionals

### Small Steps
- Keep tidying changes small and safe
- One tidying per commit
- Maintain passing tests

## Key Locations

| Task | Location |
|------|----------|
| Send messages | `solapi/services/message_service.py` |
| Request models | `solapi/model/request/` |
| Response models | `solapi/model/response/` |
| Kakao/Naver/RCS | `solapi/model/{kakao,naver,rcs}/` |
| Authentication | `solapi/lib/authenticator.py` |
| HTTP client | `solapi/lib/fetcher.py` |
| Test fixtures | `tests/conftest.py` |
| Usage examples | `examples/simple/` |

## Anti-Patterns

- Do not add CLI/console scripts - this is library-only
- Do not create multiple service classes
- Do not mix request/response models
- Do not use dataclasses or TypedDict for API models - Pydantic only
- Do not hardcode credentials
Comment on lines +1 to +124

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Much of the information in CLAUDE.md (e.g., Project Overview, Commands, Testing Environment Variables, Architecture, Code Style, Anti-Patterns) appears to be duplicated from AGENTS.md. To improve maintainability and avoid inconsistencies, consider consolidating this information into a single source or clearly defining which document serves what purpose and linking between them where appropriate. For example, CLAUDE.md could focus solely on Claude-specific instructions and refer to AGENTS.md for general project details.

Binary file added examples/images/example_square.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/images/example_wide.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading