Bentley ProjectWise
Client for a single Bentley ProjectWise datasource exposed through WSG.
One :class:ProjectWiseClient instance maps to one WSG instance — a single
repository (datasource) on a single WSG host. Construct it with the WSG base URL,
the repository id, and a :class:~workbench.clients.projectwise_token.WsgTokenProvider.
The client owns transport (bearer header, error-body logging, instances
envelope parsing) and exposes:
- discovery —
list_repositories(gateway-level; also an auth/health probe) andlist_nav_children(Navigation tree walk), used by the Hoppa API's file-selection UI; - enrichment —
get_documents/get_document(by stable GUID), used by the connector on re-bind; - content —
file_url(the$fileendpoint) plusauth_headers(the bearer header the streaming fetch must send, since WSG offers no SAS/self- authenticating URL on this datasource — see specs/spec-projectwise-client.md §8.4); - writes —
update_environment_attributesand the environment-instance reads it depends on, used by the ProjectWise writeback connector (specs/spec-projectwise-writeback.md §4).
See specs/spec-projectwise-client.md for the full API field notes and gotchas. Key ones
honoured here: the repository id keeps its ~3A encoding literal (never
re-encoded); document reads are scoped (never an unscoped class sweep, §6 #1); and
4xx/5xx bodies (which carry errorId/errorMessage) are logged before the
exception is raised (§6 #5).
ProjectWiseClient
Read and attribute-write access to one ProjectWise datasource via WSG.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_url
|
str
|
WSG API root up to and including the version, e.g.
|
required |
repository_id
|
str
|
The WSG repository id, e.g.
|
required |
token_provider
|
WsgTokenProvider
|
Supplies the |
required |
timeout
|
int
|
Per-request timeout in seconds. |
60
|
auth_headers
auth_headers()
Authorization header for an authenticated WSG request.
The provider owns the full header value (Bearer … or Basic …), so
the client is auth-scheme agnostic. Also passed (as a callable) to
AzureBlobDocumentVersion so the content-streaming fetch can
authenticate $file — WSG requires the Authorization header and
offers no self-authenticating URL on this datasource. Recomputed on every
call so a refreshed token / rotated credential is picked up transparently.
build_web_url
staticmethod
build_web_url(repository_id, instance_id)
Build the ProjectWise Web permalink from just a repository id + GUID.
Static (no client/credentials needed) so a metadata-only bind — where no authenticated client was stood up — can still produce the permalink.
ds is the repository id minus the Bentley.PW-- plugin prefix
(~3A kept literal); doc is the document GUID with hyphens removed.
file_url
file_url(instance_id)
Return the $file content URL for a document.
The bytes stream through the gateway and the request must carry the bearer
header (see :meth:auth_headers); there is no SAS/self-authenticating URL
on this datasource (§8.4).
get_document
get_document(instance_id)
Fetch a single Document instance by GUID, or None if not found.
get_document_attributes
get_document_attributes(instance_id, doc=None, environment_id=None)
Return a document's custom/environment attributes as a flat dict.
Generic — nothing about the datasource is hardcoded. Resolves the
document's EnvironmentId to its Env_<id>_* dynamic class at
runtime, then reaches the per-document attribute instance by traversing
the DocumentEnvironment relationship from the document. This is keyed
by the document GUID (fast, and version-correct — the env instance's own
id carries an unguessable _<n> suffix), and names the relationship
explicitly to disambiguate it from DocumentDefaultEnvironment (WSG
errors on the ambiguous form). Returns the related instance's
properties, or an empty dict when the document has no environment.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instance_id
|
str
|
The document GUID. |
required |
doc
|
dict | None
|
An already-fetched Document instance, to read |
None
|
environment_id
|
Any
|
The document's |
None
|
get_documents
get_documents(instance_ids)
Fetch Document instances by GUID, batched into one query.
Uses the collection + $id in [...] filter form the ProjectWise Web UI
uses (confirmed working on Example), rather than the per-instance path or the
relationship-filter form (which 500s on this plugin — §6 #2). !poly
includes derived classes.
get_dynamic_classes
get_dynamic_classes()
Cached list of PW_WSG_Dynamic class names (Env_*, PrType_*).
The Schema property on ECClassDef is the versioned name
(e.g. PW_WSG_Dynamic.01.02), so it is matched by prefix, not eq.
get_environment_instance
get_environment_instance(instance_id, env_class)
One document's environment instance: instanceId, class and properties.
Reached by traversing DocumentEnvironment from the document rather
than by addressing the instance directly, because the instance's own id
carries an unguessable _<n> suffix that cannot be derived from the
document GUID. That id is what
:meth:update_environment_attributes writes to, so a write always
begins with this read.
Returns None when the document has no instance of env_class — which
is how a writeback run detects that a document's environment has changed
since it was selected, and declines to write against a schema the
document no longer has.
get_environment_instances
get_environment_instances(instance_ids, env_class)
Batched :meth:get_environment_instance, keyed by document GUID.
Chunks at :data:BATCH_SIZE GUIDs per query, which turns a 200-document
writeback preview into ~8 calls rather than 200 — worth doing because
per-call latency, not the rate ceiling, is what bounds a run. Every
document in a writeback target shares one environment class by
construction, so a target batches cleanly.
Documents with no instance of env_class are simply absent from the
result; the caller distinguishes them from a read failure, which raises.
get_instance
get_instance(schema, class_name, instance_id)
Fetch any single class instance by exact instance id, or None.
identity
identity()
The logical account this client authenticates as, if the provider knows.
Never the credential itself — a username or equivalent handle. Feeds
:attr:WritebackReport.written_as so an audit row can record which
service account a writeback run wrote as, which matters because
ProjectWise attributes every change to that account rather than to the
Hoppa user who triggered it.
Returns None when the provider cannot say (a bearer token carries no username) or has not yet resolved its credential — deliberately, so an accessor never triggers a Key Vault fetch as a side effect.
list_nav_children
list_nav_children(node_id=None)
List child nodes of a Navigation node (or the roots when None).
Each node's properties carry Key_ClassName (Project = folder,
Document = doc) and Key_InstanceId (the real GUID for metadata /
$file). HasChildren is unreliable (§6 #4) — don't gate recursion
on it; attempt to descend any Project node within a depth bound.
list_repositories
list_repositories()
List the datasources (repositories) this token can see on the host.
Gateway-level call (no datasource session required), so it doubles as an
auth/health probe and as the source for instance discovery. Each instance
carries instanceId (the repository id) and a DisplayLabel.
query
query(schema, class_name, *, filter_expr=None, select=None, top=None, poly=False)
Run a scoped query against any schema/class; return instances.
Schema-agnostic primitive. filter_expr / select are raw OData
$filter / $select values (e.g. "$id+in+['abc']" and
"DocumentEnvironment-forward-PW_WSG_Dynamic.Env_104_EXAMPLE.*") —
built into the URL so requests doesn't reshape the operators. Always
scope queries (a folder, an id, …); never sweep a class unscoped (§6 #1).
update_environment_attributes
update_environment_attributes(env_instance_id, env_class, properties)
Write environment attributes for one document; return the STORED values.
POSTs a changed instance and returns
changedInstance.instanceAfterChange.properties — what ProjectWise
actually stored, which is not necessarily what was sent. Values
longer than an attribute's configured limit are silently truncated on an
HTTP 200, and that limit is not exposed anywhere in MetaSchema, so
comparing this return against the argument is the only way to detect it.
Callers must compare.
Only the properties passed are written; unsent attributes are left untouched (WSG merges rather than replaces — confirmed empirically), so there is no read-modify-write of the full property set and the payload can carry just the fields that changed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
env_instance_id
|
str
|
The environment instance id from
:meth: |
required |
env_class
|
str
|
The |
required |
properties
|
dict[str, Any]
|
Attribute name → value. Values are sent as strings. |
required |
Raises:
| Type | Description |
|---|---|
WsgError
|
On any non-2xx. Notably HTTP 400 with "document having final status", which is a business-rule rejection, not a transport failure, and nothing is written. |
web_url
web_url(instance_id)
Stable ProjectWise Web permalink that opens the document in a browser.
The pwlink app=webview form opens the document viewer directly and
needs only the datasource + document GUID — no work-area/project ids — so
it is built purely from data already held, with no network call. It is
GUID/URN-based, so it survives the document being moved or renamed.
WsgError
Bases: RuntimeError
A non-2xx response from WSG, carrying the gateway's own diagnostic.
WSG returns {errorId, errorMessage, errorDescription} on failure, and
callers need to tell an auth failure from a throttle from a business-rule
rejection. Note that error_id may be None — the final-status write
rejection returns a null id with a perfectly good message — so never key
classification on it alone; use status_code first and treat message
as the user-facing text.
Attributes:
| Name | Type | Description |
|---|---|---|
status_code |
The HTTP status. |
|
error_id |
WSG's |
|
message |
WSG's |
|
url |
The URL that failed, for logs. |
from_response
classmethod
from_response(response, url)
Build a :class:WsgError from a failed requests response.
Parses the body defensively: the BOM WSG prepends is stripped, and a body that is not JSON (an HTML gateway error page, say) degrades to its first 500 characters rather than masking the real failure with a decode error.
make_projectwise_client
make_projectwise_client(*, organization, wsg_base_url, repository_id)
Build an authenticated client for one ProjectWise datasource.
The one place the Key Vault naming convention for ProjectWise service accounts is applied, so a caller never has to know it.
organization is the org slug — identical in meaning to the value
AzureBlobSession and :class:KeyVaultBasicAuthProvider take. It is not
the Auth0 org_id: passing that builds a different secret name, which
resolves to nothing and fails at the first authenticated call rather than
here.
Deliberately not session-based. The writeback /columns and /validate
paths rebuild their target from the echoed scope and have no initialized
session to draw a client from, so requiring one would force a document load
those paths were designed to skip.