-
Notifications
You must be signed in to change notification settings - Fork 8
SOLAPI Python SDK 5.0.3 #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5390500
efc3edf
d5c5dbf
4f5f51c
c6bcc8d
27c8d0c
726e163
bee47b7
c04acf0
2a1e79f
e90e83f
c96af92
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
||
| 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 | ||
Palbahngmiyine marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| os.chdir(project_dir) | ||
|
|
||
| # 1. ruff check --fix | ||
| subprocess.run( | ||
| ["ruff", "check", "--fix", file_path], | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=30, | ||
Palbahngmiyine marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
Palbahngmiyine marked this conversation as resolved.
Show resolved
Hide resolved
Palbahngmiyine marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # 2. ruff format | ||
| subprocess.run( | ||
| ["ruff", "format", file_path], | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=30, | ||
| ) | ||
Palbahngmiyine marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # 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()) | ||
| 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\"" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,3 +37,6 @@ debug | |
|
|
||
| # ruff | ||
| .ruff_cache/ | ||
|
|
||
| # omc | ||
| .omc/ | ||
| 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) |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Much of the information in |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
json.load(sys.stdin)fails, the hook currently returns0(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 return1(failure) in this case.