トークンから目に見える変更まで
このページは、新しく取得したアクセストークンから、Prevu3D Platformで確認できる変更を行い、また元に戻すまでの1本のスクリプトです。最初から最後までClient Credentialsフローを使用し、必要なIDはすべて同じスクリプトの中の前のAPIレスポンスから取得します。手入力で埋める項目はありません。
実施する内容
Section titled “実施する内容”- アクセストークンを取得する: Client Credentialsで認証する
- 組織情報を取得する:
/oauth/api-infoを呼び出してAPI URLと組織IDを取得する - ディビジョンを参照する: 組織の最初の子ノード
- サイトを参照する: そのディビジョンの最初の子ノード
- サイトの名前を変更する: 小さく、明確で、元に戻せる変更
- Prevu3D Platformで確認する: ゴール地点
- 名前変更を元に戻す: 痕跡を残さない
- 先にClient Credentialsフローガイドを完了してください。OAuthアプリケーションには
read:hierarchyスコープとwrite:hierarchyスコープが必要で、サービスユーザーには少なくとも1つのサイトに対するコンテンツアクセスと編集レベルのロールが必要です(セキュリティモデルを参照)。 - 組織には、少なくとも1つのサイトを含む少なくとも1つのディビジョンが必要です。この一連の流れでは、最初に見つかったものを参照します。
ステップ1:アクセストークンを取得する
Section titled “ステップ1:アクセストークンを取得する”エンドポイント: POST https://cloud-api.prevu3d.com/oauth/token
POST https://cloud-api.prevu3d.com/oauth/token HTTP/1.1Host: cloud-api.prevu3d.comAuthorization: Basic eW91ci1jbGllbnQtaWQ6eW91ci1jbGllbnQtc2VjcmV0Content-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フローガイドを参照してください。
ステップ2:組織情報を取得する
Section titled “ステップ2:組織情報を取得する”エンドポイント: 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}を使用します。
ステップ3:ディビジョンを参照する
Section titled “ステップ3:ディビジョンを参照する”エンドポイント: 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として使用します。
ステップ4:サイトを参照する
Section titled “ステップ4:サイトを参照する”エンドポイント: 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で使用します。
ステップ5:サイトの名前を変更する
Section titled “ステップ5:サイトの名前を変更する”ノードの名前変更は、小さく、明確で、元に戻せる変更です。write:hierarchyが必要で、他には何も影響せず、1回の呼び出しで元に戻せます。
エンドポイント: PATCH {apiUrl}/v1/nodes/{site_id}
PATCH https://api-ue1.prevu3d.com/realityconnect-api/v1/nodes/8a1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e HTTP/1.1Authorization: Bearer <access_token>Content-Type: application/json
{ "name": "Chicago Plant (test)" }{ "id": "8a1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e", "name": "Chicago Plant (test)", "type": "Site", "...": "..." }ステップ6:Prevu3D Platformで確認する
Section titled “ステップ6:Prevu3D Platformで確認する”Prevu3D Platformを開き、ステップ3のディビジョンに移動して、ステップ4のサイトを開きます。
この例ではChicago Plant (test)という新しい名前が、サイトのページの上部と、ディビジョンのサイト一覧の両方に表示されるはずです。 これがゴール地点です。API経由だけで行った変更が、製品上で目に見える形になりました。
ステップ7:名前変更を元に戻す
Section titled “ステップ7:名前変更を元に戻す”エンドポイント: PATCH {apiUrl}/v1/nodes/{site_id}
{ "name": "Chicago Plant" }ステップ4で控えたサイトの元の名前を送信します。レスポンスとPrevu3D Platformの両方で、サイトがステップ5より前の状態に戻っていることが確認できます。
Pythonで試す
Section titled “Pythonで試す”このスクリプトはステップ1〜7を順番に実行します。名前変更のあとで一時停止するので、変更を元に戻す前にPrevu3D Platformで確認できます。
import requestsimport 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}'に復元しました。")本番運用向けバージョン
Section titled “本番運用向けバージョン”上記のウォークスルーはハッピーパスです。トークンは新しく、ネットワークは静かで、再試行するものは何もありません。しかし、一括インポートやポーリングループ、大きなダウンロードなど、長く動き続けるスクリプトは、いずれ429や、実行途中で期限切れになるアクセストークン、あるいはバッチの残りが進む一方で再試行が必要になる1件の呼び出しに遭遇します。このサイトのすべての例に同じ防御的なコードを追加していく代わりに、このセクションでは小さな再利用可能なクライアントを一度だけ導入します。必要なページはここへのリンクを貼るだけで、繰り返しません。
RCAPIClientは、上記の生のrequests呼び出しを3つの機能でラップします。アクセストークンをキャッシュして自動的に更新すること(期限切れ前にプロアクティブに、401が返ってきたときにリアクティブに)、遅延を推測する代わりにRetry-Afterヘッダーを使って429を再試行すること(レート制限を参照)、そして無限に再試行し続ける代わりに最大試行回数または最大経過時間の後にあきらめることです。
import timeimport base64import 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()の上に、バックオフとあきらめ条件を備えたポーリングループを構築します。処理とモニタリングがこれを使用しています。
次のステップ
Section titled “次のステップ”新しく取得したアクセストークンから、Prevu3D Platformで確認できる変更を行い、また元に戻すところまで完了しました。