Skip to content

Commit 43739b6

Browse files
Merge pull request #17 from Palbahngmiyine/main
SOLAPI Python SDK 5.0.3
2 parents b77fdd9 + c96af92 commit 43739b6

34 files changed

+2855
-7
lines changed

.claude/hooks/lint-python.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Claude Code Hook: Python 파일 자동 lint/fix
4+
- ruff check --fix: 린트 오류 자동 수정
5+
- ruff format: 코드 포매팅
6+
- ty check: 타입 체크 (오류 시 exit code 1로 Claude에게 수정 요청)
7+
"""
8+
import json
9+
import os
10+
import subprocess
11+
import sys
12+
13+
14+
def main():
15+
try:
16+
input_data = json.load(sys.stdin)
17+
except json.JSONDecodeError:
18+
return 0
19+
20+
file_path = input_data.get("tool_input", {}).get("file_path", "")
21+
22+
# Python 파일만 처리
23+
if not file_path or not file_path.endswith(".py"):
24+
return 0
25+
26+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", "")
27+
if not project_dir:
28+
return 0
29+
30+
os.chdir(project_dir)
31+
32+
# 1. ruff check --fix
33+
subprocess.run(
34+
["ruff", "check", "--fix", file_path],
35+
capture_output=True,
36+
text=True,
37+
timeout=30,
38+
)
39+
40+
# 2. ruff format
41+
subprocess.run(
42+
["ruff", "format", file_path],
43+
capture_output=True,
44+
text=True,
45+
timeout=30,
46+
)
47+
48+
# 3. ty check (타입 오류 시 차단)
49+
result = subprocess.run(
50+
["ty", "check", file_path],
51+
capture_output=True,
52+
text=True,
53+
timeout=60,
54+
)
55+
if result.returncode != 0:
56+
output = result.stdout.strip() or result.stderr.strip()
57+
if output:
58+
print(f"ty type error:\n{output}", file=sys.stderr)
59+
return 1 # 타입 오류 시 Hook 실패 → Claude가 수정하도록 함
60+
61+
return 0
62+
63+
64+
if __name__ == "__main__":
65+
sys.exit(main())

.claude/settings.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"hooks": {
3+
"PostToolUse": [
4+
{
5+
"matcher": "Edit|Write|NotebookEdit",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/lint-python.py\""
10+
}
11+
]
12+
}
13+
]
14+
}
15+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ debug
3737

3838
# ruff
3939
.ruff_cache/
40+
41+
# omc
42+
.omc/

AGENTS.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# SOLAPI-PYTHON KNOWLEDGE BASE
2+
3+
**Generated:** 2026-01-21
4+
**Commit:** b77fdd9
5+
**Branch:** main
6+
7+
## OVERVIEW
8+
9+
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.
10+
11+
## STRUCTURE
12+
13+
```
14+
solapi-python/
15+
├── solapi/ # Main package (single export: SolapiMessageService)
16+
│ ├── services/ # message_service.py - all API operations
17+
│ ├── model/ # Pydantic models (see solapi/model/AGENTS.md)
18+
│ ├── lib/ # authenticator.py, fetcher.py
19+
│ └── error/ # MessageNotReceivedError only
20+
├── tests/ # pytest integration tests
21+
├── examples/ # Feature-based usage examples
22+
└── debug/ # Dev test scripts (not part of package)
23+
```
24+
25+
## WHERE TO LOOK
26+
27+
| Task | Location | Notes |
28+
|------|----------|-------|
29+
| Send messages | `solapi/services/message_service.py` | All 10 API methods in single class |
30+
| Request models | `solapi/model/request/` | Pydantic BaseModel with validators |
31+
| Response models | `solapi/model/response/` | Separate from request models |
32+
| Kakao/Naver/RCS | `solapi/model/{kakao,naver,rcs}/` | Domain-specific models |
33+
| Authentication | `solapi/lib/authenticator.py` | HMAC-SHA256 signature |
34+
| HTTP client | `solapi/lib/fetcher.py` | httpx with 3 retries |
35+
| Test fixtures | `tests/conftest.py` | env-based credentials |
36+
| Usage examples | `examples/simple/` | Copy-paste ready |
37+
38+
## CONVENTIONS
39+
40+
### Pydantic Everywhere
41+
- ALL models extend `BaseModel`
42+
- Field aliases: `Field(alias="camelCase")` for API compatibility
43+
- Validators: `@field_validator` for normalization (e.g., phone numbers)
44+
45+
### Model Organization (Domain-Driven)
46+
```
47+
model/
48+
├── request/ # Outbound API payloads
49+
├── response/ # Inbound API responses
50+
├── kakao/ # Kakao-specific (option, button)
51+
├── naver/ # Naver-specific
52+
├── rcs/ # RCS-specific
53+
└── webhook/ # Delivery reports
54+
```
55+
56+
### Naming
57+
- Files: `snake_case.py`
58+
- Classes: `PascalCase`
59+
- Request suffix: `*Request` (e.g., `SendMessageRequest`)
60+
- Response suffix: `*Response` (e.g., `SendMessageResponse`)
61+
62+
### Code Style (Ruff)
63+
- Line length: 88
64+
- Quote style: double
65+
- Import sorting: isort (I rule)
66+
- Target: Python 3.9+
67+
68+
### Tidy First Principles
69+
- Never mix refactoring and feature changes in the same commit
70+
- Tidy related code before making behavioral changes
71+
- Tidying: guard clauses, dead code removal, rename, extract conditionals
72+
- Separate tidying commits from feature commits
73+
74+
## ANTI-PATTERNS (THIS PROJECT)
75+
76+
### NEVER
77+
- Add CLI/console scripts - this is library-only
78+
- Create multiple service classes - all goes in `SolapiMessageService`
79+
- Mix request/response models - they're deliberately separate
80+
- Use dataclasses or TypedDict for API models - Pydantic only
81+
- Hardcode credentials - use env vars
82+
83+
### VERSION SYNC REQUIRED
84+
```python
85+
# solapi/model/request/__init__.py
86+
VERSION = "python/5.0.3" # MUST update on every release!
87+
```
88+
Also update `pyproject.toml` version.
89+
90+
## UNIQUE PATTERNS
91+
92+
### Single Service Class
93+
```python
94+
# All API methods in one class (318 lines)
95+
class SolapiMessageService:
96+
def send(...) # SMS/LMS/MMS/Kakao/Naver/RCS
97+
def upload_file(...) # Storage
98+
def get_balance(...) # Account
99+
def get_groups(...) # Message groups
100+
def get_messages(...) # Message history
101+
def cancel_scheduled_message(...)
102+
```
103+
104+
### Minimal Error Handling
105+
- Only `MessageNotReceivedError` exists
106+
- API errors raised as generic `Exception` with errorCode, errorMessage
107+
108+
### Authentication Flow
109+
```
110+
SolapiMessageService.__init__(api_key, api_secret)
111+
→ Authenticator.get_auth_info()
112+
→ HMAC-SHA256 signature
113+
→ Authorization header
114+
```
115+
116+
## COMMANDS
117+
118+
```bash
119+
# Install
120+
pip install solapi
121+
122+
# Dev setup
123+
pip install -e ".[dev]"
124+
125+
# Lint & format
126+
ruff check --fix .
127+
ruff format .
128+
129+
# Test (requires env vars)
130+
export SOLAPI_API_KEY="..."
131+
export SOLAPI_API_SECRET="..."
132+
export SOLAPI_SENDER="..."
133+
export SOLAPI_RECIPIENT="..."
134+
pytest
135+
136+
# Build
137+
python -m build
138+
```
139+
140+
## ENV VARS (Testing)
141+
142+
| Variable | Purpose |
143+
|----------|---------|
144+
| `SOLAPI_API_KEY` | API authentication |
145+
| `SOLAPI_API_SECRET` | API authentication |
146+
| `SOLAPI_SENDER` | Registered sender number |
147+
| `SOLAPI_RECIPIENT` | Test recipient number |
148+
| `SOLAPI_KAKAO_PF_ID` | Kakao business channel |
149+
| `SOLAPI_KAKAO_TEMPLATE_ID` | Kakao template |
150+
151+
## NOTES
152+
153+
- No CI/CD pipeline - testing/linting is local only
154+
- uv workspace includes Django webhook example
155+
- Tests are integration tests (hit real API)
156+
- Korean comments in some files (i18n TODO exists)

CLAUDE.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
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.
8+
9+
## Commands
10+
11+
```bash
12+
# Dev setup
13+
pip install -e ".[dev]"
14+
15+
# Lint & format
16+
ruff check --fix .
17+
ruff format .
18+
19+
# Test (requires env vars - see below)
20+
pytest
21+
pytest tests/test_balance.py # Single file
22+
pytest -v # Verbose
23+
24+
# Build
25+
python -m build
26+
```
27+
28+
## Testing Environment Variables
29+
30+
Tests are integration tests that hit the real API:
31+
32+
| Variable | Purpose |
33+
|----------|---------|
34+
| `SOLAPI_API_KEY` | API authentication |
35+
| `SOLAPI_API_SECRET` | API authentication |
36+
| `SOLAPI_SENDER` | Registered sender number |
37+
| `SOLAPI_RECIPIENT` | Test recipient number |
38+
| `SOLAPI_KAKAO_PF_ID` | Kakao business channel |
39+
| `SOLAPI_KAKAO_TEMPLATE_ID` | Kakao template |
40+
41+
## Architecture
42+
43+
### Package Structure
44+
```
45+
solapi/
46+
├── services/ # message_service.py - single SolapiMessageService class
47+
├── model/ # Pydantic models (see solapi/model/AGENTS.md)
48+
│ ├── request/ # Outbound API payloads
49+
│ ├── response/ # Inbound API responses (deliberately separate)
50+
│ ├── kakao/ # Kakao channel models
51+
│ ├── naver/ # Naver channel models
52+
│ ├── rcs/ # RCS channel models
53+
│ └── webhook/ # Delivery reports
54+
├── lib/ # authenticator.py, fetcher.py
55+
└── error/ # MessageNotReceivedError only
56+
```
57+
58+
### Key Design Decisions
59+
60+
**Single Service Class**: All 10 API methods live in `SolapiMessageService` - do not create additional service classes.
61+
62+
**Request/Response Separation**: Request and response models are deliberately separate and should never be shared, even for similar fields.
63+
64+
**Pydantic Everywhere**: All API models use Pydantic BaseModel with field aliases for camelCase API compatibility:
65+
```python
66+
pf_id: str = Field(alias="pfId")
67+
```
68+
69+
**Phone Number Normalization**: Use `@field_validator` to strip dashes from phone numbers.
70+
71+
### Version Sync Required
72+
73+
When releasing, update version in BOTH locations:
74+
- `pyproject.toml``version = "X.Y.Z"`
75+
- `solapi/model/request/__init__.py``VERSION = "python/X.Y.Z"`
76+
77+
## Code Style
78+
79+
- **Linter**: Ruff (line-length: 88, double quotes, isort)
80+
- **Target**: Python 3.9+
81+
- **Files**: `snake_case.py`
82+
- **Classes**: `PascalCase`
83+
- **Request/Response suffixes**: `*Request`, `*Response`
84+
85+
## Tidy First Principles
86+
87+
Follow Kent Beck's "Tidy First?" principles:
88+
89+
### Separate Changes
90+
- Never mix **structural changes** (refactoring) with **behavioral changes** (features/fixes) in the same commit
91+
- Order: tidying commit → feature commit
92+
93+
### Tidy First
94+
Tidy the relevant code area before making behavioral changes:
95+
- Use guard clauses to reduce nesting
96+
- Remove dead code
97+
- Rename for clarity
98+
- Extract complex conditionals
99+
100+
### Small Steps
101+
- Keep tidying changes small and safe
102+
- One tidying per commit
103+
- Maintain passing tests
104+
105+
## Key Locations
106+
107+
| Task | Location |
108+
|------|----------|
109+
| Send messages | `solapi/services/message_service.py` |
110+
| Request models | `solapi/model/request/` |
111+
| Response models | `solapi/model/response/` |
112+
| Kakao/Naver/RCS | `solapi/model/{kakao,naver,rcs}/` |
113+
| Authentication | `solapi/lib/authenticator.py` |
114+
| HTTP client | `solapi/lib/fetcher.py` |
115+
| Test fixtures | `tests/conftest.py` |
116+
| Usage examples | `examples/simple/` |
117+
118+
## Anti-Patterns
119+
120+
- Do not add CLI/console scripts - this is library-only
121+
- Do not create multiple service classes
122+
- Do not mix request/response models
123+
- Do not use dataclasses or TypedDict for API models - Pydantic only
124+
- Do not hardcode credentials

examples/images/example_square.jpg

17.6 KB
Loading

examples/images/example_wide.jpg

25.4 KB
Loading

0 commit comments

Comments
 (0)