跳转到内容

你的第一次修改,从头到尾

本页是一段单一的线性脚本:从刚获取的访问令牌开始,做出一个你能在 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)

用它代替直接调用 requests,例如替换上面步骤 1-2 中获取令牌和查找组织的部分:

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 能做的其他一切。