콘텐츠로 이동

처음부터 끝까지, 첫 번째 변경

이 페이지는 새로 발급받은 액세스 토큰에서 시작해 Prevu3D Platform에서 확인할 수 있는 변경을 만들고 다시 되돌리기까지, 하나로 이어지는 스크립트입니다. 처음부터 끝까지 Client Credentials 플로를 사용하며, 필요한 모든 ID는 같은 스크립트의 이전 API 응답에서 얻습니다. 직접 채워 넣어야 할 값은 없습니다.


  1. 액세스 토큰 획득: Client Credentials로 인증하기
  2. 조직 정보 확인: /oauth/api-info를 호출해 API URL과 조직 ID 얻기
  3. 디비전으로 이동: 조직의 첫 번째 하위 노드
  4. 사이트로 이동: 해당 디비전의 첫 번째 하위 노드
  5. 사이트 이름 변경: 작고, 명확하고, 되돌릴 수 있는 변경
  6. Prevu3D Platform에서 확인: 목표 지점
  7. 이름 변경 취소: 흔적을 남기지 않기
  • 먼저 Client Credentials 플로 가이드를 완료하세요. OAuth 애플리케이션에는 read:hierarchy와 write:hierarchy 스코프가 필요하며, 서비스 사용자는 최소 하나의 사이트에 대한 콘텐츠 접근 권한과 편집 수준의 역할이 필요합니다(보안 모델 참조).
  • 조직에는 최소 하나의 사이트를 포함하는 최소 하나의 디비전이 있어야 합니다. 이 실습은 처음 찾은 항목으로 이동합니다.

엔드포인트: POST https://cloud-api.prevu3d.com/oauth/token

POST https://cloud-api.prevu3d.com/oauth/token HTTP/1.1
Host: cloud-api.prevu3d.com
Authorization: Basic eW91ci1jbGllbnQtaWQ6eW91ci1jbGllbnQtc2VjcmV0
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
{
"hasError": false,
"access_token": "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9...",
"expires_in": 3599,
"token_type": "bearer"
}

access_token을 저장하세요. 실패하면 Client Credentials 플로 가이드를 참조하세요.

엔드포인트: GET https://cloud-api.prevu3d.com/oauth/api-info

헤더: Authorization: Bearer <access_token>

{
"organization": {
"id": "217ebd23-ec54-4af0-a6d6-4a441a6d1966",
"name": "Test Organization"
},
"scopes": ["read:hierarchy", "write:hierarchy"],
"apiUrl": "https://api-ue1.prevu3d.com/realityconnect-api"
}

apiUrl과 organization.id를 저장하세요. 아래의 모든 요청은 {apiUrl}을 사용합니다.

엔드포인트: GET {apiUrl}/v1/nodes/{organization_id}/browse

{
"node": { "id": "217ebd23-ec54-4af0-a6d6-4a441a6d1966", "type": "Organization", "...": "..." },
"items": [
{ "id": "3d3b6f2a-9e0a-4b0a-8b0a-1c2d3e4f5a6b", "name": "North America Operations", "type": "Division", "...": "..." }
],
"total": 1
}

items[0].id를 division_id로 사용합니다.

엔드포인트: GET {apiUrl}/v1/nodes/{division_id}/browse

{
"node": { "id": "3d3b6f2a-9e0a-4b0a-8b0a-1c2d3e4f5a6b", "type": "Division", "...": "..." },
"items": [
{ "id": "8a1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e", "name": "Chicago Plant", "type": "Site", "...": "..." }
],
"total": 1
}

items[0].id를 site_id로 사용하고, items[0].name(여기서는 "Chicago Plant")을 기억해 두세요. 7단계에서 필요합니다.

노드 이름 변경은 작고, 명확하고, 되돌릴 수 있는 변경입니다. write:hierarchy가 필요하고, 다른 어떤 것에도 영향을 주지 않으며, 한 번의 호출로 되돌릴 수 있습니다.

엔드포인트: PATCH {apiUrl}/v1/nodes/{site_id}

PATCH https://api-ue1.prevu3d.com/realityconnect-api/v1/nodes/8a1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e HTTP/1.1
Authorization: Bearer <access_token>
Content-Type: application/json
{ "name": "Chicago Plant (test)" }
{ "id": "8a1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e", "name": "Chicago Plant (test)", "type": "Site", "...": "..." }

Prevu3D Platform을 열고, 3단계의 디비전으로 이동한 뒤, 4단계의 사이트를 엽니다.

이제 사이트 페이지 상단과 디비전의 사이트 목록에서 새 이름(이 예시에서는 Chicago Plant (test))이 보일 것입니다. 이것이 목표 지점입니다. 전적으로 API를 통해 이루어진 변경이 제품에서 눈으로 확인됩니다.

엔드포인트: PATCH {apiUrl}/v1/nodes/{site_id}

{ "name": "Chicago Plant" }

4단계에서 기억해 둔 사이트의 원래 이름을 전송하세요. 응답과 Prevu3D Platform 모두 5단계 이전 상태로 돌아간 사이트를 보여줍니다.

이 스크립트는 1단계부터 7단계까지 순서대로 실행됩니다. 이름 변경 후 일시 정지하므로, 변경을 되돌리기 전에 Prevu3D Platform에서 확인할 수 있습니다.

import requests
import base64
client_id = "your-client-id"
client_secret = "your-client-secret"
base_url = "https://cloud-api.prevu3d.com"
# 1단계: 토큰 획득
token_response = requests.post(
f"{base_url}/oauth/token",
data={"grant_type": "client_credentials"},
headers={
"Authorization": f'Basic {base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()}',
"Content-Type": "application/x-www-form-urlencoded",
},
)
token_response.raise_for_status()
access_token = token_response.json()["access_token"]
headers = {"Authorization": f"Bearer {access_token}"}
# 2단계: API URL과 조직 ID 확인
api_info = requests.get(f"{base_url}/oauth/api-info", headers=headers).json()
api_url = api_info["apiUrl"]
organization_id = api_info["organization"]["id"]
# 3단계: 첫 번째 디비전으로 이동
divisions = requests.get(f"{api_url}/v1/nodes/{organization_id}/browse", headers=headers).json()
division_id = divisions["items"][0]["id"]
# 4단계: 해당 디비전의 첫 번째 사이트로 이동
sites = requests.get(f"{api_url}/v1/nodes/{division_id}/browse", headers=headers).json()
site = sites["items"][0]
site_id = site["id"]
original_name = site["name"]
# 5단계: 사이트 이름 변경
new_name = f"{original_name} (test)"
requests.patch(f"{api_url}/v1/nodes/{site_id}", json={"name": new_name}, headers=headers).raise_for_status()
print(f"사이트 {site_id}의 이름을 '{new_name}'(으)로 변경했습니다.")
print("지금 Prevu3D Platform에서 사이트를 열어보세요. 새 이름이 보일 것입니다.")
input("확인했으면 Enter를 눌러 변경 사항을 되돌리세요...")
# 7단계: 이름 변경 취소
requests.patch(f"{api_url}/v1/nodes/{site_id}", json={"name": original_name}, headers=headers).raise_for_status()
print(f"사이트 {site_id}를 '{original_name}'(으)로 복원했습니다.")

위 실습은 해피 패스입니다. 새 토큰, 조용한 네트워크, 재시도할 것이 전혀 없죠. 하지만 대량 임포트, 폴링 루프, 대용량 다운로드처럼 계속 실행되는 스크립트는 결국 429를 만나거나, 실행 도중 액세스 토큰이 만료되거나, 배치의 나머지가 진행되는 동안 재시도가 필요한 단일 호출을 만나게 됩니다. 이 사이트의 모든 예제에 똑같은 방어 코드를 추가하는 대신, 이 섹션에서는 작은 재사용 가능한 클라이언트를 단 한 번 소개합니다. 이를 필요로 하는 예제들은 반복하는 대신 여기로 다시 연결됩니다.

RCAPIClient는 위의 원시 requests 호출을 세 가지 기능으로 감쌉니다. 액세스 토큰을 캐시하고 자동으로 갱신하며(만료 전에는 선제적으로, 401을 받으면 대응적으로), 지연 시간을 추측하는 대신 Retry-After 헤더를 사용해 429를 재시도하고(속도 제한 참고), 무한정 재시도하는 대신 최대 시도 횟수 또는 최대 경과 시간이 지나면 포기합니다.

import time
import base64
import requests
class RCAPIClient:
"""Token cache + refresh, retry with backoff, and a give-up condition.
Wrap the raw `requests` calls above with this once your integration runs
long enough to hit a rate limit or outlive an access token -- most bulk
imports, polling loops, and long-running downloads do.
"""
def __init__(self, client_id, client_secret, cloud_api_base="https://cloud-api.prevu3d.com"):
self.client_id = client_id
self.client_secret = client_secret
self.cloud_api_base = cloud_api_base
self._access_token = None
self._expires_at = 0 # epoch seconds
def _fetch_token(self):
credentials = base64.b64encode(f"{self.client_id}:{self.client_secret}".encode()).decode()
response = requests.post(
f"{self.cloud_api_base}/oauth/token",
data={"grant_type": "client_credentials"},
headers={"Authorization": f"Basic {credentials}"},
)
response.raise_for_status()
body = response.json()
self._access_token = body["access_token"]
# Refresh a little before the real expiry, not exactly at it.
self._expires_at = time.time() + body["expires_in"] - 60
def _token(self):
if self._access_token is None or time.time() >= self._expires_at:
self._fetch_token()
return self._access_token
def request(self, method, url, max_attempts=6, max_elapsed=300, **kwargs):
"""`requests.request()` plus token refresh, 429 backoff, and a give-up condition."""
start = time.time()
headers = kwargs.pop("headers", {})
for attempt in range(1, max_attempts + 1):
headers["Authorization"] = f"Bearer {self._token()}"
response = requests.request(method, url, headers=headers, **kwargs)
if response.status_code == 401 and attempt == 1:
self._access_token = None # token was rejected outright; force one refresh
continue
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 1))
if attempt == max_attempts or time.time() - start + retry_after > max_elapsed:
response.raise_for_status()
time.sleep(retry_after)
continue
return response
return response
def poll(self, url, is_done, interval=15, max_elapsed=3600):
"""GET `url` through this client until `is_done(body)` or `max_elapsed` seconds pass."""
start = time.time()
while True:
response = self.request("GET", url)
response.raise_for_status()
body = response.json()
if is_done(body):
return body
if time.time() - start >= max_elapsed:
raise TimeoutError(f"Gave up polling {url} after {max_elapsed}s")
time.sleep(interval)

위 1~2단계의 토큰 발급과 조직 조회를 예로 들면, requests를 직접 쓰는 대신 이렇게 사용합니다.

client = RCAPIClient(client_id="your-client-id", client_secret="your-client-secret")
api_info = client.request("GET", "https://cloud-api.prevu3d.com/oauth/api-info").json()

request()는 429를 만났을 때만 백오프합니다. 워크로드가 429를 유발하지 않으면서도 버킷 용량에 근접하는 경우(예: 지속적인 대용량 트래픽), 모든 응답에서 X-RateLimit-Remaining과 X-RateLimit-Reset을 읽어 버킷이 비기 전에 속도를 늦추세요 — 속도 제한을 참고하세요. poll()은 request() 위에 백오프와 포기 조건이 있는 폴링 루프를 구성합니다. 처리 및 모니터링에서 이를 사용합니다.

새로 발급받은 액세스 토큰에서 시작해 Prevu3D Platform에서 확인할 수 있는 변경을 만들고 다시 되돌리는 과정을 마쳤습니다.

  • ID 찾기: 이동 외에도 조직, 디비전, 사이트 및 기타 노드의 ID를 확인하는 더 많은 방법.
  • 노드 유형 및 계층 구조: 방금 살펴본 전체 노드 트리와 각 노드 유형이 담을 수 있는 것.
  • 소개: RealityConnect API로 할 수 있는 나머지 모든 작업을 위한 작업 기반 시작점.