# Python SDK


`ActivationClient`로 Python 앱에 라이센스 활성화를 연결해요. SDK를 설치하고 고객 포털에서 기기를 승인한 뒤, 앱에서 기능을 사용할 수 있는지 확인하는 과정을 알아보세요.

활성화 요청은 승인할 앱 설치를 식별하는 정보예요. 고객이 해당 앱의 이름과 로고가 표시된 포털에서 소유한 라이센스를 선택하면, 그 요청과 기기에 묶인 서명된 활성화 응답을 받아요. 라이센스 하나에는 활성 기기 하나만 등록돼요.

## 1. SDK를 설치하세요

```bash
python3 -m venv .venv
.venv/bin/python -m pip install signox-sdk==0.3.1
```

개발 환경에 설정된 패키지 저장소를 사용하세요. SDK는 앱의 의존성으로 설치해요. 샘플 ZIP에는 실행 예제 소스가 들어 있으며, SDK 설치를 대신하지 않아요.

실행 환경: Python 3.9+, cryptography 의존성은 pip가 설치.

방금 만든 가상환경에 설치된 SDK 버전을 확인하세요. 출력의 Version이 0.3.1이어야 해요. 이후 앱도 같은 가상환경의 Python으로 실행하세요. 다른 Python 실행기에는 구버전 SDK가 설치되어 있을 수 있어요.

```bash
.venv/bin/python -m pip show signox-sdk
```

## 2. 제품과 테스트 라이센스를 준비하세요

벤더 대시보드에서 제품을 선택하고 설정을 여세요. 제품 상세 주소의 UUID를 확인하고 그 제품의 공개키를 내려받으세요. 정책에 demo_export 불리언 기능을 추가해 켠 뒤 테스트 라이센스를 발급하고, 고객 포털에서 테스트 고객에게 구매 키를 등록하세요.

| 입력 | 얻는 위치와 사용 방법 |
|---|---|
| `SIGNOX_API_URL` | 테스트할 환경의 API 주소를 사용해요. 포털 주소와는 달라요. |
| `SIGNOX_PRODUCT_ID` | 벤더의 제품 상세 주소에 있는 UUID예요. |
| `SIGNOX_PUBLIC_KEY_FILE` | 내려받은 제품 공개키 파일의 경로예요. PEM 내용 자체를 변수에 넣지 않아요. |
| `.signox-state` | 예제가 만드는 개인 상태 디렉터리예요. 재시작 후에도 같은 경로를 유지하세요. |

예제를 실행할 셸에 위 세 환경변수를 설정하세요. 제품 공개키는 신뢰하는 앱 배포물에 포함해 사용하세요. 고객 비밀번호는 고객 포털에만 입력하고, 라이센스 키는 앱에 하드코딩하지 않아요.

## 3. 활성화 예제를 작성하세요

아래 전체 코드를 `app.py` 파일로 저장하세요. 모든 명령은 같은 프로젝트 디렉터리에서 실행하세요.

```python
import json
import os
from pathlib import Path
import sys
from signox import ActivationClient
required = ('SIGNOX_API_URL', 'SIGNOX_PRODUCT_ID', 'SIGNOX_PUBLIC_KEY_FILE')
if not all(os.getenv(k) for k in required):
    raise ValueError('Set SIGNOX_API_URL, SIGNOX_PRODUCT_ID and SIGNOX_PUBLIC_KEY_FILE')
client = ActivationClient(os.environ['SIGNOX_PRODUCT_ID'],
    Path(os.environ['SIGNOX_PUBLIC_KEY_FILE']).read_text(),
    base_url=os.environ['SIGNOX_API_URL'], state_dir='.signox-state')
command = sys.argv[1] if len(sys.argv) > 1 else 'start'
if command == 'start':
    print(json.dumps(client.start()))
elif command == 'request':
    print(client.create_request())
elif command == 'poll':
    progress = client.poll()
    print(progress['status'])
    if progress.get('result') and not progress['result'].valid:
        sys.exit(2)
else:
    result = client.apply_response(Path(sys.argv[2]).read_text()) if command == 'apply' else client.validate()
    allowed = result.valid and result.features.get('demo_export') is True
    print(json.dumps(dict(valid=result.valid, code=str(result.code), exportAllowed=allowed)))
    sys.exit(0 if result.valid else 2)
```

브라우저 승인과 파일 전달 모두 같은 클라이언트를 사용해요. SDK가 실행 환경의 장치 모듈을 선택하므로 앱에서 장치 공급자 종류를 고르지 않아요.

## 4. 고객 포털에서 기기를 승인하세요

```bash
.venv/bin/python app.py start
```

출력된 portalUrl을 시스템 브라우저로 여세요. 앱 이름과 기기 이름을 확인한 뒤 고객 계정으로 로그인하고, 소유한 라이센스를 골라 이 기기 활성화를 누르세요. 다른 기기가 등록되어 있다면 사유를 입력해 벤더에게 이전 검토를 요청할 수 있어요.

```bash
.venv/bin/python app.py poll
```

승인 전에는 status가 pending이에요. 승인 후 다시 실행하면 응답을 받아 적용해요. 3초보다 짧은 간격으로 반복 호출하지 마세요. pending은 승인 대기이며 라이센스 검증 성공이 아니에요.

## 5. 기능 실행 전에 권한을 확인하세요

```bash
.venv/bin/python app.py status
```

성공하면 VALID 또는 IN_GRACE_PERIOD가 나와요. 라이센스가 유효하고 demo_export가 true일 때만 예제의 exportAllowed가 true가 돼요. 기능이 없는 라이센스는 유효하더라도 CSV를 만들면 안 돼요. 이 판정을 시작 화면뿐 아니라 앱의 실제 내보내기 함수에도 넣으세요.

[실제 CSV 파일을 만드는 샘플 실행하기](/samples/python/)

## 다른 기기에서 활성화하기

원래 기기에 연결이 없다면 요청을 내보내고, 연결된 PC의 고객 포털 /activate에서 가져오세요. 승인 후 받은 activation.signox를 원래 기기로 옮겨요. SDK의 applyResponse에는 파일 경로가 아닌 파일 내용을 전달해요. 위 예제는 대신 파일을 읽어 줘요.

```bash
.venv/bin/python app.py request
.venv/bin/python app.py apply activation.signox
```

파일로 전달할 때는 언어별 샘플의 activation-request request.signox 명령을 사용하세요. 특히 Maven의 빌드 로그를 요청 파일로 리다이렉션하지 마세요.

연결 없이 응답을 적용하려면 지원되고 신뢰 등록된 기기 키가 필요해요. 보호 기능이 없으면 응답 적용 시 연결해야 해요. 앱을 배포하기 전에 실행 환경과 기기 요구 사항을 확인하세요.

[오프라인 실행 환경 확인하기](/guides/activation-offline/)

## 생성자와 옵션

| 이름 | 타입 | 필수 여부·기본값 | 설명 |
|---|---|---|---|
| `product_id` | `str` | 필수 | 제품 상세 주소에 있는 UUID |
| `product_public_key` | `str` | 필수 | 앱에 포함한 SPKI PEM 공개키 내용 |
| `state_dir` | `str` | 필수 | 이 설치에서만 쓰는 영속 디렉터리 |
| `base_url` | `str` | https://api.signox.kr | 대상 환경의 API 주소 |
| `timeout_ms` | `int` | 10000 ms | 네트워크 요청 제한 시간 |
| `ts_tolerance_sec` | `int` | 300 초 | 서버 시각과의 허용 차이 |
| `offline_grace_days` | `int` | 365 일 | 서버 통신 유예를 줄이는 로컬 상한; 0이면 캐시 유예 해제 |

로컬 유예 상한으로 서버가 서명한 기간을 늘릴 수는 없어요. 만료 유예와 통신 장애 유예는 별개이며, 빈 캐시로 최초 활성화를 허용하지 않아요.

### 신뢰하는 실행 모듈로 시험하기

기본값은 OS에 맞는 모듈을 자동 선택해요. 통제된 기기 시험이나 앱이 직접 제공하는 모듈을 사용할 때는 `bridge_command: str`, `bridge_args: Sequence[str]`을 지정할 수 있어요. 신뢰하는 앱 배포물이나 시험 환경에서 제공한 실행 파일만 사용하세요. 고객 요청 JSON이나 활성화 응답의 경로를 실행하면 안 돼요. 샘플은 제공된 시험용 래퍼를 SIGNOX_OFFLINE_RUNTIME·SIGNOX_OFFLINE_EXECUTABLE로 지정할 수 있어요. 이 옵션으로 기기 신뢰와 서명 검증을 우회하지는 않아요.

## 메서드와 반환값

| 시그니처 | 반환 타입 | 동작 |
|---|---|---|
| `start(name=None)` | `dict` | 요청을 등록하고 브라우저 주소와 요청 번호를 반환해요. |
| `create_request(name=None, renew=False)` | `str` | 공개 요청 JSON을 반환해요. name은 기기 표시명, renew는 설치 자격증명을 유지한 새 요청 생성 여부예요. |
| `poll()` | `dict` | 승인 전에는 pending, 승인 후에는 응답을 적용한 검증 결과를 반환해요. |
| `apply_response(content)` | `LicenseResult` | 응답 문자열을 읽고 요청·기기 일치를 검증한 뒤 적용해요. |
| `validate()` | `LicenseResult` | 라이센스 상태와 현재 기능 권한을 확인해요. |
| `deactivate()` | `LicenseResult` | 연결 기반 등록을 해제해요. 보호된 이용권은 이전 검토로 안내해요. |

Node는 비동기 메서드예요. Java·C#·Python은 동기 메서드이므로 네트워크·장치 작업을 UI 스레드 밖에서 실행하세요. LicenseResult에는 유효 여부·코드·기능값이 들어 있어요. 요청·상태·가져오기 오류의 예외를 검증 성공으로 바꾸면 안 돼요.

## 실패했을 때 확인하기

| 현상·코드 | 원인 | 조치와 재확인 결과 |
|---|---|---|
| `STATE_INVALID` | 저장한 설치 상태가 없거나 손상됨 | 보존한 상태를 복원하세요. 복구할 수 없으면 새 설치에서 요청을 만들어 이전을 신청하세요. |
| `DEVICE_TRUST_REQUIRED` | 검증되지 않은 장비 키에는 포털이 발급할 수 없음 | 운영자가 실제 기기의 키를 별도 신뢰 경로로 대조하고 등록한 뒤 다시 승인하세요. |
| `TRANSFER_REQUIRED` | 다른 기기가 등록되어 있거나 보호된 반납을 확인할 수 없음 | 새 기기의 요청으로 이전을 신청하세요. 벤더는 그 요청을 검토해요. |
| `ACTIVATION_RESPONSE_INVALID` | 다른 파일·서명·요청·기기 | 원래 기기에서 이 요청에 발급된 응답을 적용하세요. 응답 내용을 수정하지 마세요. |
| `REQUEST_EXPIRED` | 요청의 7일 유효기간 경과 | 새 요청을 만들어 고객 승인을 다시 진행하세요. |
| `NETWORK_ERROR` | 일시적인 연결·서비스 장애 | 연결을 복구하세요. 기존의 유효한 서명 이용권·캐시만 연결 없이 사용할 수 있어요. |
| `REQUEST_ERROR` | 잘못된 주소·입력·접근 권한 | 요청을 수정하세요. 영구 HTTP 오류를 무한 재시도하지 마세요. |

서명된 정지·폐기·만료 거절을 받은 뒤 연결이 끊겨도 거절을 유지해야 해요. 이전 오프라인 이용권으로 덮지 마세요. 계속 연결되지 않은 기기는 즉시 원격 회수 신호를 받을 수 없으며, 포맷이나 파일 삭제만으로 옛 백업이 중단됐다고 증명할 수 없어요.

[통합 검증 실행하기](/guides/integration-checks/) · [기기 복구·이전 알아보기](/guides/hwid/)

응답은 받았지만 로컬 검증에 실패하면 polling의 status는 application_failed이고 result는 무효예요. applied와 유효한 result를 함께 확인해야 적용 성공이에요.

요청 생성과 적용에 같은 실행 모듈 설정을 사용하세요. 브라우저·파일 전달 방식은 기기 보호 유무를 선택하는 옵션이 아니고, 요청 이름을 바꾸어도 공급자가 바뀌지 않아요. 검증된 응답에서 OFFLINE_NOT_SUPPORTED가 나오면 필요한 로컬 실행 모듈을 사용할 수 없는 상태이므로 기존 환경을 복원하세요. 무조건 새 신뢰 등록을 요청하는 오류는 아니에요.
