workwire 0.4.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.0](https://github.com/workwire/workwire-python/compare/v0.5.0...v0.6.0) (2026-10-30)
4
+
5
+
6
+ ### Features
7
+
8
+ * **WOR-547:** backend v1.71.1's spec — partnerships name the counterparty, the wire list carries viewer_party_id ([#26](https://github.com/workwire/workwire-python/issues/26)) ([e5199ba](https://github.com/workwire/workwire-python/commit/e5199ba1e6c2bf95136815e475698fc0a93da02c))
9
+
10
+ ## [0.5.0](https://github.com/workwire/workwire-python/compare/v0.4.0...v0.5.0) (2026-10-24)
11
+
12
+
13
+ ### Features
14
+
15
+ * **WOR-511:** backend v1.62.0's spec — unit_type on locations and jobs, the kind_rank reason ([#24](https://github.com/workwire/workwire-python/issues/24)) ([c97d486](https://github.com/workwire/workwire-python/commit/c97d4860acfe7a73598c6c3407ccc9dc5d666be2))
16
+
3
17
  ## [0.4.0](https://github.com/workwire/workwire-python/compare/v0.3.0...v0.4.0) (2026-10-18)
4
18
 
5
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: workwire
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Official Python SDK for the Workwire API
5
5
  Project-URL: Homepage, https://github.com/workwire/workwire-python
6
6
  Project-URL: Changelog, https://github.com/workwire/workwire-python/blob/main/CHANGELOG.md
@@ -54,7 +54,7 @@ project = client.projects.create(name="Elm St. Townhomes")
54
54
 
55
55
  # The partnership with your sub — create one (see below) or pick an
56
56
  # established one from client.partnerships.list().
57
- partnership = client.partnerships.create(sub_org_id="...")
57
+ partnership = client.partnerships.create(org_id="...")
58
58
 
59
59
  wire = client.wires.create(
60
60
  project_id=project.id,
@@ -107,15 +107,28 @@ shown to the other side.
107
107
  ```python
108
108
  from workwire import LocationNode, Party
109
109
 
110
- project = client.projects.create(name="Elm St. Townhomes", template="development", locations=[
111
- LocationNode("building", "Bldg A", children=[LocationNode("floor", "Level 1")]),
112
- ])
110
+ project = client.projects.create(
111
+ name="Elm St. Townhomes",
112
+ template="development",
113
+ locations=[
114
+ LocationNode("building", "Bldg A", children=[LocationNode("floor", "Level 1")]),
115
+ ],
116
+ )
113
117
  tree = client.projects.locations.list(project.id).data # the site root first
114
118
  level_1 = next(loc for loc in tree if loc.name == "Level 1")
115
- client.wires.create(project_id=project.id, title="Drywall — Level 1", parties=[Party.partnered(
116
- partnership_id=partnership.id, trade="Drywall", scope_text="Level 1.",
117
- contract_value="40000.00", scope_location_ids=[level_1.id], # one Job per scope location
118
- )])
119
+ client.wires.create(
120
+ project_id=project.id,
121
+ title="Drywall — Level 1",
122
+ parties=[
123
+ Party.partnered(
124
+ partnership_id=partnership.id,
125
+ trade="Drywall",
126
+ scope_text="Level 1.",
127
+ contract_value="40000.00",
128
+ scope_location_ids=[level_1.id], # one Job per scope location
129
+ )
130
+ ],
131
+ )
119
132
  page = client.jobs.list(project=project.id) # page.quantity_unit is your org's unit
120
133
  client.jobs.update(page.data[0].id, your_reference="PO-4471-A", quantity="12.5", checked=True)
121
134
  ```
@@ -125,6 +138,17 @@ one `client.jobs.update` returns — carries `location_name`, `location_kind` an
125
138
  `location_path` (the ancestors, root first), so a sub reads its scope without
126
139
  the GC's tree; `client.jobs.list` rows had them already.
127
140
 
141
+ A tree only descends: `site` › `building`/`house` › `floor` ›
142
+ `unit`/`common_area` › `room`, levels skippable but never repeated or
143
+ reversed. A node that breaks the rank raises `InvalidRequestError` with code
144
+ `invalid_parent` and `details` `{"reason": "kind_rank", "child_kind": ...,
145
+ "parent_kind": ...}`.
146
+
147
+ `LocationNode(..., unit_type="C1.1 (2 Bedroom)")` (or
148
+ `locations.create(..., unit_type=...)`) labels a unit's plan; every
149
+ `Location` reads it back as `unit_type`, and a Job as `location_unit_type` —
150
+ `None` when unset.
151
+
128
152
  Leave `scope_location_ids` out to scope a party to the whole project.
129
153
  `client.jobs.update` sends only the fields you pass: leave one out to keep
130
154
  it, pass `None` to clear it. Add to a tree later with
@@ -31,7 +31,7 @@ project = client.projects.create(name="Elm St. Townhomes")
31
31
 
32
32
  # The partnership with your sub — create one (see below) or pick an
33
33
  # established one from client.partnerships.list().
34
- partnership = client.partnerships.create(sub_org_id="...")
34
+ partnership = client.partnerships.create(org_id="...")
35
35
 
36
36
  wire = client.wires.create(
37
37
  project_id=project.id,
@@ -84,15 +84,28 @@ shown to the other side.
84
84
  ```python
85
85
  from workwire import LocationNode, Party
86
86
 
87
- project = client.projects.create(name="Elm St. Townhomes", template="development", locations=[
88
- LocationNode("building", "Bldg A", children=[LocationNode("floor", "Level 1")]),
89
- ])
87
+ project = client.projects.create(
88
+ name="Elm St. Townhomes",
89
+ template="development",
90
+ locations=[
91
+ LocationNode("building", "Bldg A", children=[LocationNode("floor", "Level 1")]),
92
+ ],
93
+ )
90
94
  tree = client.projects.locations.list(project.id).data # the site root first
91
95
  level_1 = next(loc for loc in tree if loc.name == "Level 1")
92
- client.wires.create(project_id=project.id, title="Drywall — Level 1", parties=[Party.partnered(
93
- partnership_id=partnership.id, trade="Drywall", scope_text="Level 1.",
94
- contract_value="40000.00", scope_location_ids=[level_1.id], # one Job per scope location
95
- )])
96
+ client.wires.create(
97
+ project_id=project.id,
98
+ title="Drywall — Level 1",
99
+ parties=[
100
+ Party.partnered(
101
+ partnership_id=partnership.id,
102
+ trade="Drywall",
103
+ scope_text="Level 1.",
104
+ contract_value="40000.00",
105
+ scope_location_ids=[level_1.id], # one Job per scope location
106
+ )
107
+ ],
108
+ )
96
109
  page = client.jobs.list(project=project.id) # page.quantity_unit is your org's unit
97
110
  client.jobs.update(page.data[0].id, your_reference="PO-4471-A", quantity="12.5", checked=True)
98
111
  ```
@@ -102,6 +115,17 @@ one `client.jobs.update` returns — carries `location_name`, `location_kind` an
102
115
  `location_path` (the ancestors, root first), so a sub reads its scope without
103
116
  the GC's tree; `client.jobs.list` rows had them already.
104
117
 
118
+ A tree only descends: `site` › `building`/`house` › `floor` ›
119
+ `unit`/`common_area` › `room`, levels skippable but never repeated or
120
+ reversed. A node that breaks the rank raises `InvalidRequestError` with code
121
+ `invalid_parent` and `details` `{"reason": "kind_rank", "child_kind": ...,
122
+ "parent_kind": ...}`.
123
+
124
+ `LocationNode(..., unit_type="C1.1 (2 Bedroom)")` (or
125
+ `locations.create(..., unit_type=...)`) labels a unit's plan; every
126
+ `Location` reads it back as `unit_type`, and a Job as `location_unit_type` —
127
+ `None` when unset.
128
+
105
129
  Leave `scope_location_ids` out to scope a party to the whole project.
106
130
  `client.jobs.update` sends only the fields you pass: leave one out to keep
107
131
  it, pass `None` to clear it. Add to a tree later with
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "workwire"
7
- version = "0.4.0"
7
+ version = "0.6.0"
8
8
  description = "Official Python SDK for the Workwire API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -96,9 +96,7 @@ class BaseClient:
96
96
  return {
97
97
  "Authorization": f"Bearer {self._api_key}",
98
98
  "Accept": "application/json",
99
- "User-Agent": (
100
- f"workwire-python/{__version__} python/{platform.python_version()}"
101
- ),
99
+ "User-Agent": (f"workwire-python/{__version__} python/{platform.python_version()}"),
102
100
  "X-Workwire-Client-User-Agent": telemetry,
103
101
  }
104
102
 
@@ -248,5 +246,3 @@ class BaseClient:
248
246
  headers={k.lower(): v for k, v in response.headers.items()},
249
247
  body=body,
250
248
  )
251
-
252
-
@@ -66,7 +66,7 @@ OPERATIONS: dict[str, dict[str, object]] = {
66
66
  "createProjectLocation": {
67
67
  "method": "POST",
68
68
  "path": "/v1/projects/{id}/locations",
69
- "request_fields": {"parent_id", "kind", "name"},
69
+ "request_fields": {"parent_id", "kind", "name", "unit_type"},
70
70
  "query_params": set(),
71
71
  "response_model": _models.Location,
72
72
  },
@@ -116,7 +116,7 @@ OPERATIONS: dict[str, dict[str, object]] = {
116
116
  "createPartnership": {
117
117
  "method": "POST",
118
118
  "path": "/v1/partnerships",
119
- "request_fields": {"sub_org_id", "cold_invite"},
119
+ "request_fields": {"org_id", "sub_org_id", "cold_invite"},
120
120
  "query_params": set(),
121
121
  "response_model": _models.Partnership,
122
122
  },
@@ -19,7 +19,7 @@ from __future__ import annotations
19
19
 
20
20
  from collections.abc import Iterator, Mapping, Sequence
21
21
  from collections.abc import Set as AbstractSet
22
- from dataclasses import dataclass
22
+ from dataclasses import dataclass, field
23
23
  from typing import Any, Literal
24
24
 
25
25
  __all__ = [
@@ -186,7 +186,8 @@ class PartyJob(WorkwireObject):
186
186
  (the ancestors, root first; empty at the site root) are the server's
187
187
  description of where the Job sits in the GC's tree; ``your_reference``,
188
188
  ``quantity`` and ``checked_at`` are **your org's own** fields, never the
189
- other side's."""
189
+ other side's. ``location_unit_type`` is the location's ``unit_type`` — the
190
+ label a builder gives a unit's plan, ``None`` when unset."""
190
191
 
191
192
  _nested = {"location_path": JobPathLocation}
192
193
 
@@ -195,6 +196,7 @@ class PartyJob(WorkwireObject):
195
196
  location_id: str
196
197
  location_name: str
197
198
  location_kind: str
199
+ location_unit_type: str | None
198
200
  location_path: list[JobPathLocation]
199
201
  your_reference: str | None
200
202
  quantity: str | None
@@ -264,21 +266,36 @@ class Wire(WorkwireObject):
264
266
 
265
267
 
266
268
  class WireListItem(WorkwireObject):
269
+ """One row of :meth:`Wires.list`. ``viewer_party_id`` is your org's one
270
+ party on the wire when your org is its sub — ``None`` when your org is
271
+ the wire's origin or holds more than one party on it."""
272
+
267
273
  id: str
268
274
  display_id: str
269
275
  project_id: str
270
276
  title: str
271
277
  state: str
272
278
  party_count: int
279
+ viewer_party_id: str | None
273
280
  created_at: str
274
281
  sent_at: str | None
275
282
  closed_at: str | None
276
283
 
277
284
 
278
285
  class Partnership(WorkwireObject):
286
+ """A partnership as created, read from your org's side:
287
+ ``counterparty_org_id`` is the other org and ``initiated_by_org_id`` is
288
+ the org that asked for the partnership — equal to ``counterparty_org_id``
289
+ when the other org asked, your org's id otherwise. ``gc_org_id`` and
290
+ ``sub_org_id`` are deprecated — still served, replaced by
291
+ ``counterparty_org_id`` and ``initiated_by_org_id``. ``invitation_id`` is
292
+ set on the cold-invite path only."""
293
+
279
294
  id: str
280
295
  gc_org_id: str
281
296
  sub_org_id: str
297
+ counterparty_org_id: str
298
+ initiated_by_org_id: str
282
299
  status: str
283
300
  initiation_path: str
284
301
  created_at: str
@@ -288,9 +305,21 @@ class Partnership(WorkwireObject):
288
305
 
289
306
 
290
307
  class PartnershipDetail(WorkwireObject):
308
+ """One partnership read from your org's side: ``counterparty_org_id`` and
309
+ ``counterparty_name`` (``None`` while the other org has no name yet) are
310
+ the other org; ``initiated_by_org_id`` is the org that asked for the
311
+ partnership — equal to ``counterparty_org_id`` when the other org asked,
312
+ your org's id otherwise. Deprecated — still served: ``gc_org_id`` and
313
+ ``sub_org_id`` (replaced by ``counterparty_org_id`` and
314
+ ``initiated_by_org_id``), ``gc_org_name`` and ``sub_org_name`` (replaced
315
+ by ``counterparty_name``)."""
316
+
291
317
  id: str
292
318
  gc_org_id: str
293
319
  sub_org_id: str
320
+ counterparty_org_id: str
321
+ counterparty_name: str | None
322
+ initiated_by_org_id: str
294
323
  status: str
295
324
  initiation_path: str
296
325
  created_at: str
@@ -301,10 +330,18 @@ class PartnershipDetail(WorkwireObject):
301
330
 
302
331
 
303
332
  class PartnershipListItem(WorkwireObject):
333
+ """One row of :meth:`Partnerships.list`, read from your org's side:
334
+ ``counterparty_org_id`` and ``counterparty_name`` are the other org;
335
+ ``initiated_by_org_id`` is the org that asked for the partnership — equal
336
+ to ``counterparty_org_id`` when the other org asked, your org's id
337
+ otherwise. ``role`` is deprecated — still served, replaced by
338
+ ``initiated_by_org_id``."""
339
+
304
340
  id: str
305
341
  role: str
306
342
  counterparty_org_id: str
307
343
  counterparty_name: str | None
344
+ initiated_by_org_id: str
308
345
  status: str
309
346
  initiation_path: str
310
347
  created_at: str
@@ -313,13 +350,16 @@ class PartnershipListItem(WorkwireObject):
313
350
 
314
351
  class Location(WorkwireObject):
315
352
  """One location of a project's tree. ``path`` is the ancestor ids, root
316
- first (empty on the root, whose ``parent_id`` is ``None``)."""
353
+ first (empty on the root, whose ``parent_id`` is ``None``). ``unit_type``
354
+ is the label a builder gives a unit's plan — ``None`` when unset; always
355
+ ``None`` on a split."""
317
356
 
318
357
  id: str
319
358
  project_id: str
320
359
  parent_id: str | None
321
360
  kind: str
322
361
  name: str
362
+ unit_type: str | None
323
363
  path: list[str]
324
364
  archived_at: str | None
325
365
  created_at: str
@@ -361,7 +401,9 @@ class JobMilestones(WorkwireObject):
361
401
  class JobRow(WorkwireObject):
362
402
  """One row of :meth:`Jobs.list`: the Job with its party, wire, project,
363
403
  location and counterparty context. ``your_reference``, ``quantity`` and
364
- ``checked_at`` are your org's own fields, never the other side's."""
404
+ ``checked_at`` are your org's own fields, never the other side's.
405
+ ``location_unit_type`` is the location's ``unit_type`` — the label a
406
+ builder gives a unit's plan, ``None`` when unset."""
365
407
 
366
408
  _nested = {"location_path": JobPathLocation, "milestones": JobMilestones}
367
409
 
@@ -380,6 +422,7 @@ class JobRow(WorkwireObject):
380
422
  location_id: str
381
423
  location_name: str
382
424
  location_kind: str
425
+ location_unit_type: str | None
383
426
  location_path: list[JobPathLocation]
384
427
  counterparty_org_id: str | None
385
428
  counterparty_name: str | None
@@ -453,13 +496,20 @@ class ColdInvite:
453
496
 
454
497
  @dataclass(frozen=True)
455
498
  class LocationNode:
456
- """One location of a tree being created — its ``kind``, its ``name``, and
457
- the ``children`` nested beneath it. Any nesting is allowed (a building
458
- may hold floors or units directly). At most 500 locations per request,
499
+ """One location of a tree being created — its ``kind``, its ``name``, its
500
+ optional ``unit_type`` (the label a builder gives a unit's plan, "C1.1
501
+ (2 Bedroom)"), and the ``children`` nested beneath it. A tree only
502
+ descends — each kind ranks strictly below its parent's, ``site`` ›
503
+ ``building``/``house`` › ``floor`` › ``unit``/``common_area`` › ``room``,
504
+ levels skippable but never repeated or reversed, else
505
+ ``InvalidRequestError`` code ``invalid_parent`` with
506
+ ``details["reason"] == "kind_rank"``. At most 500 locations per request,
459
507
  counted over every level."""
460
508
 
461
509
  kind: LocationKind
462
510
  name: str
511
+ # Keyword-only, so a positional third argument is still ``children``.
512
+ unit_type: str | None = field(default=None, kw_only=True)
463
513
  children: Sequence[LocationNode] | None = None
464
514
 
465
515
  def __post_init__(self) -> None:
@@ -475,6 +525,8 @@ class LocationNode:
475
525
 
476
526
  def to_payload(self) -> dict[str, Any]:
477
527
  payload: dict[str, Any] = {"kind": self.kind, "name": self.name}
528
+ if self.unit_type is not None: # absent, never null: the bytes stay as before
529
+ payload["unit_type"] = self.unit_type
478
530
  if self.children is not None:
479
531
  payload["children"] = [child.to_payload() for child in self.children]
480
532
  return payload
@@ -524,9 +576,7 @@ class Party:
524
576
 
525
577
  def __init__(self, payload: dict[str, Any], *, _guard: object = None) -> None:
526
578
  if _guard is not _PARTY_GUARD:
527
- raise TypeError(
528
- "Construct with Party.partnered(...) or Party.cold_invite(...)"
529
- )
579
+ raise TypeError("Construct with Party.partnered(...) or Party.cold_invite(...)")
530
580
  self._payload = payload
531
581
 
532
582
  @classmethod
@@ -43,11 +43,11 @@ def _quantity(value: str | Decimal | None) -> str | None:
43
43
  return format(value, "f")
44
44
  if isinstance(value, float):
45
45
  raise TypeError(
46
- "quantity must be a decimal string (\"12.5\") or a decimal.Decimal, "
46
+ 'quantity must be a decimal string ("12.5") or a decimal.Decimal, '
47
47
  "never a float: a binary float is not an exact quantity"
48
48
  )
49
49
  raise TypeError(
50
- "quantity must be a decimal string (\"12.5\") or a decimal.Decimal, "
50
+ 'quantity must be a decimal string ("12.5") or a decimal.Decimal, '
51
51
  f"not {type(value).__name__} — pass str(n) or Decimal(n)"
52
52
  )
53
53
 
@@ -3,7 +3,8 @@
3
3
 
4
4
  Every project has one ``site`` root, created with the project
5
5
  (``Project.root_location_id``), and beneath it houses, buildings, floors,
6
- units, rooms and common areas, nested any way. Locations have stable ids —
6
+ units, rooms and common areas, each kind ranked below its parent's (see
7
+ :class:`~workwire.LocationNode`). Locations have stable ids —
7
8
  nothing references one by name or path. Seed a tree when you create the
8
9
  project (``projects.create(locations=...)``) or add to it here; renames,
9
10
  moves and archives happen in the Workwire app.
@@ -76,6 +77,7 @@ class ProjectLocations:
76
77
  *,
77
78
  kind: LocationKind,
78
79
  name: str,
80
+ unit_type: str | None = None,
79
81
  parent_id: str | None = None,
80
82
  idempotency_key: str | None = None,
81
83
  timeout: float | None = None,
@@ -83,13 +85,22 @@ class ProjectLocations:
83
85
  """Add one location beneath ``parent_id`` — absent, the project's
84
86
  root. Requires ``projects:write``.
85
87
 
88
+ ``unit_type`` is the label a builder gives a unit's plan ("C1.1
89
+ (2 Bedroom)"); left ``None``, it is not sent.
90
+
86
91
  A parent that is not an unarchived location of this project's tree
87
- raises ``InvalidRequestError`` (code ``invalid_parent``); an unknown
88
- ``kind`` is checked with the body here — ``InvalidRequestError``
89
- code ``validation_error``, ``fields["kind"]``. An archived project's
90
- tree is read-only (``ConflictError``, code ``project_archived``).
92
+ raises ``InvalidRequestError`` (code ``invalid_parent``), and so does
93
+ a ``kind`` that does not rank below the parent's (``site`` ›
94
+ ``building``/``house`` › ``floor`` › ``unit``/``common_area`` ›
95
+ ``room``) — that refusal's ``details`` are ``{"reason": "kind_rank",
96
+ "child_kind": ..., "parent_kind": ...}``. An unknown ``kind`` is
97
+ checked with the body here — ``InvalidRequestError`` code
98
+ ``validation_error``, ``fields["kind"]``. An archived project's tree
99
+ is read-only (``ConflictError``, code ``project_archived``).
91
100
  """
92
101
  body: dict[str, Any] = {"kind": kind, "name": name}
102
+ if unit_type is not None:
103
+ body["unit_type"] = unit_type
93
104
  if parent_id is not None:
94
105
  body["parent_id"] = parent_id
95
106
  data, request_id = self._client._request(
@@ -117,7 +128,10 @@ class ProjectLocations:
117
128
  At most 500 locations, counted over every level: more raises
118
129
  ``InvalidRequestError`` (code ``too_many_locations``, with
119
130
  ``details["limit"]``), as does an unknown ``kind``
120
- (``invalid_location_kind``); a refused node creates nothing. Returns
131
+ (``invalid_location_kind``) and a node whose kind does not rank below
132
+ its own parent's (``invalid_parent``, with ``details`` ``{"reason":
133
+ "kind_rank", "child_kind": ..., "parent_kind": ...}``); a refused
134
+ node creates nothing. Returns
121
135
  a :class:`LocationBatch` — a plain list of the created locations,
122
136
  flat, every parent before its children, so you have every id; each
123
137
  carries the response's ``request_id``.
@@ -1,9 +1,15 @@
1
1
  """Partnerships — the GC↔sub relationship Wires require.
2
2
 
3
- Create one against a known Workwire org (``sub_org_id=``) or cold-invite a
4
- sub by email (``cold_invite=ColdInvite(...)`` — the partnership, the
5
- invitation, and the email are created atomically). Exactly one of the two;
6
- the invitation email greets the recipient **as your organization**.
3
+ Create one against a known Workwire org (``org_id=``; the deprecated
4
+ ``sub_org_id=`` is still accepted in its place) or cold-invite a sub by email
5
+ (``cold_invite=ColdInvite(...)`` — the partnership, the invitation, and the
6
+ email are created atomically). Exactly one of them; the invitation email
7
+ greets the recipient **as your organization**.
8
+
9
+ A partnership reads from your organization's side: ``counterparty_org_id``
10
+ is the other organization and ``initiated_by_org_id`` is the organization
11
+ that asked for the partnership — equal to ``counterparty_org_id`` when the
12
+ other organization asked, yours otherwise.
7
13
  """
8
14
 
9
15
  from __future__ import annotations
@@ -29,13 +35,20 @@ class Partnerships:
29
35
  def create(
30
36
  self,
31
37
  *,
38
+ org_id: str | None = None,
32
39
  sub_org_id: str | None = None,
33
40
  cold_invite: ColdInvite | None = None,
34
41
  idempotency_key: str | None = None,
35
42
  timeout: float | None = None,
36
43
  ) -> Partnership:
37
44
  """Create a partnership. Requires ``partnerships:write``. Pass
38
- exactly one of ``sub_org_id`` or ``cold_invite``.
45
+ exactly one of ``org_id`` (an organization already on Workwire) or
46
+ ``cold_invite``. ``sub_org_id`` is deprecated — still accepted in
47
+ place of ``org_id`` and sent as ``sub_org_id``.
48
+
49
+ The partnership comes back read from your side:
50
+ ``initiated_by_org_id`` is the organization that asked — yours, on a
51
+ create.
39
52
 
40
53
  The cold-invite path **emails a real person**, counts against the
41
54
  100/org/rolling-24h machine invitation quota (breach raises
@@ -43,10 +56,12 @@ class Partnerships:
43
56
  ``partnership_invites_per_org_per_day``), and returns the
44
57
  partnership with ``invitation_id`` set.
45
58
  """
46
- if (sub_org_id is None) == (cold_invite is None):
47
- raise ValueError("pass exactly one of sub_org_id or cold_invite")
59
+ if sum(arg is not None for arg in (org_id, sub_org_id, cold_invite)) != 1:
60
+ raise ValueError("pass exactly one of org_id, sub_org_id or cold_invite")
48
61
  body: dict[str, Any]
49
- if sub_org_id is not None:
62
+ if org_id is not None:
63
+ body = {"org_id": org_id}
64
+ elif sub_org_id is not None:
50
65
  body = {"sub_org_id": sub_org_id}
51
66
  else:
52
67
  assert cold_invite is not None
@@ -65,8 +80,11 @@ class Partnerships:
65
80
  def retrieve(
66
81
  self, partnership_id: str, *, timeout: float | None = None
67
82
  ) -> PartnershipDetail:
68
- """Fetch one partnership your org is a side of, with both org
69
- names. Requires ``partnerships:read``."""
83
+ """Fetch one partnership your org is a side of, read from your
84
+ side: ``counterparty_org_id`` and ``counterparty_name`` are the other
85
+ organization, and ``initiated_by_org_id`` is the organization that
86
+ asked. The deprecated ``gc_org_id`` / ``sub_org_id`` and both org
87
+ names are still served. Requires ``partnerships:read``."""
70
88
  data, request_id = self._client._request(
71
89
  "GET", f"/v1/partnerships/{partnership_id}", timeout=timeout
72
90
  )
@@ -45,10 +45,13 @@ class Projects:
45
45
  (``root_location_id``). ``locations`` seeds a tree beneath the root
46
46
  in the same transaction — at most 500 locations counted over every
47
47
  level (``too_many_locations``), each of a known kind
48
- (``invalid_location_kind``); a refused node creates nothing, the
49
- project included. ``template`` (``development``, ``single_home``,
50
- ``renovation``) records the Project Type your tree follows in the
51
- project's history only — the server seeds nothing from it.
48
+ (``invalid_location_kind``) ranked below its parent's
49
+ (``invalid_parent``, with ``details`` ``{"reason": "kind_rank",
50
+ "child_kind": ..., "parent_kind": ...}`` — see :class:`LocationNode`);
51
+ a refused node creates nothing, the project included. ``template``
52
+ (``development``, ``single_home``, ``renovation``) records the Project
53
+ Type your tree follows in the project's history only — the server
54
+ seeds nothing from it.
52
55
  """
53
56
  body: dict[str, Any] = {"name": name}
54
57
  if address is not None:
@@ -46,8 +46,7 @@ class Wires:
46
46
  for party in parties:
47
47
  if not isinstance(party, Party):
48
48
  raise TypeError(
49
- "parties must be built with Party.partnered(...) or "
50
- "Party.cold_invite(...)"
49
+ "parties must be built with Party.partnered(...) or Party.cold_invite(...)"
51
50
  )
52
51
  body: dict[str, Any] = {
53
52
  "project_id": project_id,
@@ -70,9 +69,7 @@ class Wires:
70
69
  def retrieve(self, wire_id: str, *, timeout: float | None = None) -> Wire:
71
70
  """Fetch one wire with embedded parties, milestones, and POs.
72
71
  Requires ``wires:read``."""
73
- data, request_id = self._client._request(
74
- "GET", f"/v1/wires/{wire_id}", timeout=timeout
75
- )
72
+ data, request_id = self._client._request("GET", f"/v1/wires/{wire_id}", timeout=timeout)
76
73
  return Wire(data, request_id=request_id)
77
74
 
78
75
  def go_live(
File without changes
File without changes
File without changes
File without changes