你的第一次修改,从头到尾
本页是一段单一的线性脚本:从刚获取的访问令牌开始,做出一个你能在 Prevu3D Platform 中看到的修改,然后再撤销它。全程使用 Client Credentials 流程,脚本中需要的每个 ID 都来自同一脚本中更早的 API 响应,无需手动填写。
你将要做什么
Section titled “你将要做什么”- 获取访问令牌:使用 Client Credentials 进行身份验证
- 发现你的组织:调用
/oauth/api-info获取你的 API URL 和组织 ID - 浏览到某个分部:你的组织的第一个子节点
- 浏览到某个站点:该分部的第一个子节点
- 重命名该站点:一个小的、明显的、可撤销的修改
- 在 Prevu3D Platform 中确认:终点
- 撤销重命名:不留下任何痕迹
- 先完成 Client Credentials 流程指南。你的 OAuth 应用需要
read:hierarchy和write:hierarchy作用域,你的服务用户需要对至少一个站点拥有内容访问权限和编辑级别的角色(参见安全模型)。 - 你的组织需要至少一个包含至少一个站点的分部。本实践会浏览到找到的第一个。
步骤 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,不影响其他任何东西,并且只需一次调用即可撤销。
端点: 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 和组织 IDapi_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、在运行过程中过期的访问令牌,或者需要重试的单次调用(而批次的其余部分仍在继续)。与其在本站的每个示例中都重复添加同样的防御性代码,本节只引入一次这个小型可复用客户端;需要它的示例会链接回这里,而不是重复实现。
RCAPIClient 用三项能力封装了上面的原始 requests 调用:它缓存访问令牌并自动刷新:在令牌过期前主动刷新,并在收到 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)用它代替直接调用 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() 之上构建了一个带退避和放弃条件的轮询循环;处理与监控就使用了它。
下一步是什么?
Section titled “下一步是什么?”你已经从一个刚获取的访问令牌,完成了一次在 Prevu3D Platform 中可见的修改,并成功撤销。