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.
- {workwire-0.4.0 → workwire-0.6.0}/CHANGELOG.md +14 -0
- {workwire-0.4.0 → workwire-0.6.0}/PKG-INFO +33 -9
- {workwire-0.4.0 → workwire-0.6.0}/README.md +32 -8
- {workwire-0.4.0 → workwire-0.6.0}/pyproject.toml +1 -1
- {workwire-0.4.0 → workwire-0.6.0}/src/workwire/_client.py +1 -5
- {workwire-0.4.0 → workwire-0.6.0}/src/workwire/_coverage.py +2 -2
- {workwire-0.4.0 → workwire-0.6.0}/src/workwire/_models.py +60 -10
- {workwire-0.4.0 → workwire-0.6.0}/src/workwire/resources/jobs.py +2 -2
- {workwire-0.4.0 → workwire-0.6.0}/src/workwire/resources/locations.py +20 -6
- {workwire-0.4.0 → workwire-0.6.0}/src/workwire/resources/partnerships.py +28 -10
- {workwire-0.4.0 → workwire-0.6.0}/src/workwire/resources/projects.py +7 -4
- {workwire-0.4.0 → workwire-0.6.0}/src/workwire/resources/wires.py +2 -5
- {workwire-0.4.0 → workwire-0.6.0}/.gitignore +0 -0
- {workwire-0.4.0 → workwire-0.6.0}/LICENSE +0 -0
- {workwire-0.4.0 → workwire-0.6.0}/SECURITY.md +0 -0
- {workwire-0.4.0 → workwire-0.6.0}/src/workwire/__init__.py +0 -0
- {workwire-0.4.0 → workwire-0.6.0}/src/workwire/_version.py +0 -0
- {workwire-0.4.0 → workwire-0.6.0}/src/workwire/errors.py +0 -0
- {workwire-0.4.0 → workwire-0.6.0}/src/workwire/py.typed +0 -0
- {workwire-0.4.0 → workwire-0.6.0}/src/workwire/resources/__init__.py +0 -0
|
@@ -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.
|
|
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(
|
|
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(
|
|
111
|
-
|
|
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(
|
|
116
|
-
|
|
117
|
-
|
|
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(
|
|
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(
|
|
88
|
-
|
|
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(
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
@@ -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``,
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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``)
|
|
88
|
-
``kind``
|
|
89
|
-
|
|
90
|
-
|
|
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``)
|
|
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 (``
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 ``
|
|
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 (
|
|
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
|
|
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,
|
|
69
|
-
|
|
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``)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|