Skip to content

Commit 446ef9f

Browse files
committed
Targets use ID not UUIDs
Signed-off-by: Cédric Foellmi <cedric@onekiloparsec.dev>
1 parent 37e3b95 commit 446ef9f

3 files changed

Lines changed: 182 additions & 77 deletions

File tree

arcsecond/api/resources.py

Lines changed: 73 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,53 +6,76 @@
66
class ArcsecondTargetListsResource(ArcsecondAPIEndpoint):
77
"""Target-list specific helpers built on top of the generic endpoint contract."""
88

9-
target_relation_keys = ("targets", "target_uuids", "target_ids")
9+
target_relation_key = "targets"
10+
target_writable_fields = (
11+
"id",
12+
"pk",
13+
"object",
14+
"name",
15+
"identifier",
16+
"target_class",
17+
"mode",
18+
"color",
19+
"notes",
20+
"tags",
21+
"profile",
22+
"organisation",
23+
)
1024

1125
def _ensure_iterable(self, values):
1226
if values is None:
1327
return None
28+
if isinstance(values, dict):
29+
return [values]
1430
if isinstance(values, (str, int)):
1531
return [values]
1632
return list(values)
1733

18-
def _normalise_target_references(self, targets):
34+
def _target_payload_identity(self, target):
35+
if target.get("id") is not None:
36+
return ("id", target["id"])
37+
if target.get("pk") is not None:
38+
return ("pk", target["pk"])
39+
return (
40+
"composite",
41+
target.get("target_class"),
42+
target.get("identifier"),
43+
target.get("name"),
44+
target.get("mode"),
45+
)
46+
47+
def _normalise_target_payloads(self, targets):
1948
values = self._ensure_iterable(targets)
2049
if values is None:
2150
return None
2251

23-
refs = []
52+
payloads = []
2453
for target in values:
25-
if isinstance(target, dict):
26-
ref = (
27-
target.get("uuid")
28-
or target.get("id")
29-
or target.get("pk")
30-
or target.get("name")
54+
if not isinstance(target, dict):
55+
raise ArcsecondError(
56+
"Target list helpers expect target payload dictionaries, not scalar IDs or UUIDs. "
57+
"Pass dictionaries such as `plan_target_payload(...).payload` or target objects returned "
58+
"by `api.targets.read()/list()/upsert()`."
3159
)
32-
if ref is None:
33-
raise ArcsecondError(
34-
"Target dictionaries must include one of: uuid, id, pk or name."
35-
)
36-
refs.append(ref)
37-
else:
38-
refs.append(target)
39-
return refs
40-
41-
def _target_key_from_payload(self, payload, target_key=None):
42-
if target_key:
43-
return target_key
44-
for key in self.target_relation_keys:
45-
if payload and key in payload:
46-
return key
47-
return self.target_relation_keys[0]
60+
61+
payload = {
62+
key: value
63+
for key, value in target.items()
64+
if key in self.target_writable_fields and value is not None
65+
}
66+
if not payload:
67+
raise ArcsecondError(
68+
"Target dictionaries must include at least one writable target field."
69+
)
70+
71+
payloads.append(payload)
72+
return payloads
4873

4974
def _build_payload(self, json=None, targets=None, target_key=None, **fields):
5075
payload = super()._build_payload(json=json, **fields) or {}
51-
normalised_targets = self._normalise_target_references(targets)
76+
normalised_targets = self._normalise_target_payloads(targets)
5277
if normalised_targets is not None:
53-
payload[self._target_key_from_payload(payload, target_key=target_key)] = (
54-
normalised_targets
55-
)
78+
payload[target_key or self.target_relation_key] = normalised_targets
5679
return payload or None
5780

5881
def create(self, json=None, targets=None, target_key=None, **fields):
@@ -74,14 +97,14 @@ def upsert(self, match_field="name", json=None, targets=None, target_key=None, *
7497
return super().upsert(match_field=match_field, json=payload)
7598

7699
def _read_target_refs(self, target_list, target_key=None):
77-
key = self._target_key_from_payload(target_list or {}, target_key=target_key)
100+
key = target_key or self.target_relation_key
78101
raw_targets = (target_list or {}).get(key, [])
79-
refs = self._normalise_target_references(raw_targets) or []
102+
refs = self._normalise_target_payloads(raw_targets) or []
80103
return key, refs
81104

82105
def set_targets(self, id_name_uuid, targets, target_key=None):
83-
target_key = self._target_key_from_payload({}, target_key=target_key)
84-
return self.update(id_name_uuid, **{target_key: self._normalise_target_references(targets)})
106+
target_key = target_key or self.target_relation_key
107+
return self.update(id_name_uuid, **{target_key: self._normalise_target_payloads(targets)})
85108

86109
def clear_targets(self, id_name_uuid, target_key=None):
87110
return self.set_targets(id_name_uuid, [], target_key=target_key)
@@ -92,9 +115,14 @@ def add_targets(self, id_name_uuid, targets, target_key=None):
92115
return None, error
93116

94117
key, current_refs = self._read_target_refs(target_list, target_key=target_key)
95-
for ref in self._normalise_target_references(targets) or []:
96-
if ref not in current_refs:
97-
current_refs.append(ref)
118+
current_identities = {
119+
self._target_payload_identity(target): target for target in current_refs
120+
}
121+
for target in self._normalise_target_payloads(targets) or []:
122+
identity = self._target_payload_identity(target)
123+
if identity not in current_identities:
124+
current_refs.append(target)
125+
current_identities[identity] = target
98126
return self.update(id_name_uuid, **{key: current_refs})
99127

100128
def remove_targets(self, id_name_uuid, targets, target_key=None):
@@ -103,8 +131,15 @@ def remove_targets(self, id_name_uuid, targets, target_key=None):
103131
return None, error
104132

105133
key, current_refs = self._read_target_refs(target_list, target_key=target_key)
106-
refs_to_remove = set(self._normalise_target_references(targets) or [])
107-
remaining_refs = [ref for ref in current_refs if ref not in refs_to_remove]
134+
refs_to_remove = {
135+
self._target_payload_identity(target)
136+
for target in (self._normalise_target_payloads(targets) or [])
137+
}
138+
remaining_refs = [
139+
ref
140+
for ref in current_refs
141+
if self._target_payload_identity(ref) not in refs_to_remove
142+
]
108143
return self.update(id_name_uuid, **{key: remaining_refs})
109144

110145
def add_target(self, id_name_uuid, target, target_key=None):

docs/resources.md

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,9 @@ For many resources, `upsert()` is convenient when your script wants create-or-up
6464
semantics:
6565

6666
```python
67-
target, error = api.targets.upsert(
68-
name="M 42",
69-
ra_deg=83.82208,
70-
dec_deg=-5.39111,
67+
dataset, error = api.datasets.upsert(
68+
name="My dataset",
69+
description="Synced from Python",
7170
)
7271
```
7372

@@ -170,27 +169,26 @@ JSON payloads yourself.
170169

171170
```python
172171
target, error = api.targets.create(
173-
name="47 Tuc",
174-
ra_deg=6.023625,
175-
dec_deg=-72.08128,
172+
name="51 Peg b",
173+
target_class="Exoplanet",
176174
)
177175
```
178176

179177
```python
180178
target, error = api.targets.update(
181-
"6d7f9f0b-2d67-4d3d-9e74-2d6d0e749b91",
182-
description="Globular cluster in Tucana",
179+
42,
180+
notes="Globular cluster in Tucana",
183181
)
184182
```
185183

186184
```python
187-
target, error = api.targets.upsert(
185+
plan = plan_target_payload(
188186
name="M 42",
189-
ra_deg=83.82208,
190-
dec_deg=-5.39111,
191-
source_name="Orion Nebula",
187+
inferred_target_class="AstronomicalObject",
192188
)
193189

190+
target, error = api.targets.upsert(json=plan.payload)
191+
194192
if error:
195193
raise error
196194
```
@@ -222,16 +220,34 @@ if error:
222220
`api.targetlists` supports the generic CRUD methods above, and also a few helpers for
223221
managing the list membership.
224222

223+
Targets are managed through integer target IDs on `api.targets`, but target lists
224+
themselves are managed through UUIDs on `api.targetlists`.
225+
226+
When you create or update a target list, the `targets` field must contain target
227+
payload dictionaries, not target IDs or UUIDs. The easiest inputs are:
228+
229+
- payloads returned by `plan_target_payload(...).payload`
230+
- target objects returned by `api.targets.read()`, `api.targets.list()`, or `api.targets.upsert()`
231+
225232
You can create a target list and attach targets in the same call:
226233

227234
```python
235+
from arcsecond import plan_target_payload
236+
237+
m42 = plan_target_payload(
238+
name="M 42",
239+
inferred_target_class="AstronomicalObject",
240+
).payload
241+
242+
pegb = plan_target_payload(
243+
name="51 Peg b",
244+
inferred_target_class="Exoplanet",
245+
).payload
246+
228247
target_list, error = api.targetlists.create(
229248
name="Tonight candidates",
230249
description="Targets imported for Night Explorer",
231-
targets=[
232-
"6d7f9f0b-2d67-4d3d-9e74-2d6d0e749b91",
233-
"c17cb272-5ed6-4a6f-a219-0ff4727863c2",
234-
],
250+
targets=[m42, pegb],
235251
)
236252

237253
if error:
@@ -241,12 +257,12 @@ if error:
241257
To replace the full content of a list:
242258

243259
```python
260+
target_a, error = api.targets.read(42)
261+
target_b, error = api.targets.read(314)
262+
244263
target_list, error = api.targetlists.set_targets(
245264
"a0e974a6-6f2d-4b7a-b9d2-3a3f7d7ef61a",
246-
[
247-
"6d7f9f0b-2d67-4d3d-9e74-2d6d0e749b91",
248-
"c17cb272-5ed6-4a6f-a219-0ff4727863c2",
249-
],
265+
[target_a, target_b],
250266
)
251267
```
252268

@@ -255,24 +271,31 @@ To incrementally manage membership:
255271
```python
256272
target_list, error = api.targetlists.add_targets(
257273
"a0e974a6-6f2d-4b7a-b9d2-3a3f7d7ef61a",
258-
["c17cb272-5ed6-4a6f-a219-0ff4727863c2"],
274+
[
275+
plan_target_payload(
276+
name="M 31",
277+
inferred_target_class="AstronomicalObject",
278+
).payload
279+
],
259280
)
260281
```
261282

262283
```python
263284
target_list, error = api.targetlists.remove_target(
264285
"a0e974a6-6f2d-4b7a-b9d2-3a3f7d7ef61a",
265-
"c17cb272-5ed6-4a6f-a219-0ff4727863c2",
286+
target_b,
266287
)
267288
```
268289

269290
If you want create-or-update semantics for a list name:
270291

271292
```python
293+
target_a, error = api.targets.read(42)
294+
272295
target_list, error = api.targetlists.upsert(
273296
name="Tonight candidates",
274297
description="Synced from Python",
275-
targets=["6d7f9f0b-2d67-4d3d-9e74-2d6d0e749b91"],
298+
targets=[target_a],
276299
)
277300
```
278301

@@ -287,7 +310,7 @@ deleted, error = api.targetlists.delete("a0e974a6-6f2d-4b7a-b9d2-3a3f7d7ef61a")
287310
## Target Lists With Planned Targets
288311

289312
When you import a target list from an external source, apply the same planning rule to
290-
each individual target first, then create the list from the created target UUIDs.
313+
each individual target first, then create the list from the resulting target payloads.
291314

292315
```python
293316
from arcsecond import ArcsecondAPI, ArcsecondConfig, plan_target_payload
@@ -307,7 +330,7 @@ candidates = [
307330
},
308331
]
309332

310-
target_ids = []
333+
targets = []
311334
for candidate in candidates:
312335
plan = plan_target_payload(**candidate)
313336
if not plan.is_valid:
@@ -317,11 +340,11 @@ for candidate in candidates:
317340
if error:
318341
raise error
319342

320-
target_ids.append(target["uuid"])
343+
targets.append(target)
321344

322345
target_list, error = api.targetlists.create(
323346
name="Imported targets",
324-
targets=target_ids,
347+
targets=targets,
325348
)
326349
```
327350

0 commit comments

Comments
 (0)