Summary
SitesClient has 8 call sites that do Job.parse_obj(response_data) with no guard on the response shape. If the API returns anything that isn't a dict with a job_id (e.g. an empty list []), callers get a cryptic Pydantic ValidationError instead of a meaningful AtomicAPIError. Make the failure mode actionable.
What triggered this
A live client.sites.update_domain(domain=…, new_domain="...", keep_old_domain=True) call surfaced:
ValidationError: 1 validation error for Job
Input should be a valid dictionary or instance of Job
[type=model_type, input_value=[], input_type=list]
i.e. data: [] came back from the API instead of the documented {job_id, wpcom_blog_id, atomic_site_id, domain_name, old_domain_name} shape. Root cause (API regression, undocumented no-op shape, or error body on a 200) is unknown — because the SDK discarded the HTTP response before it could be inspected. That's the bug to fix, regardless of the API's behaviour.
Call sites affected
All in atomic_sdk/api/sites.py:
- line 125 (
create_site)
- line 144 (
delete_site)
- line 167 (
update_domain)
- line 264, 284, 306, 331, 404 (various software/meta/options ops)
All 8 have the pattern:
response_data = self._post(endpoint)
job = Job.parse_obj(response_data)
job._client = self._client
return job
Scope
1. Add a helper on SitesClient
def _parse_job(self, response_data: Any) -> Job:
"""Parse a job-style response, raising AtomicAPIError (not ValidationError) on shape mismatch."""
if not isinstance(response_data, dict) or "job_id" not in response_data:
raise AtomicAPIError(
message=(
f"Expected a job response (dict with 'job_id'), "
f"got {type(response_data).__name__}: {response_data!r}"
),
status_code=200,
)
job = Job.parse_obj(response_data)
job._client = self._client
return job
2. Apply across all 8 call sites
Replace each job = Job.parse_obj(response_data); job._client = self._client with job = self._parse_job(response_data).
Acceptance criteria
Out of scope
- Investigating why the API returned
[] on that one call (separate question, not an SDK concern).
- Extending the same guard to other resources (
BackupJob parsing, etc.) — do those as separate follow-ups if they surface similar issues.
- Retry on "looks like an error body on a 200" — unrelated; if it's really a 500/429 it belongs in the retry/backoff story.
References
- Observed failure trace:
atomic_sdk/api/sites.py:167 → pydantic/main.py:1222.
- Job model:
atomic_sdk/models.py:42.
Summary
SitesClienthas 8 call sites that doJob.parse_obj(response_data)with no guard on the response shape. If the API returns anything that isn't a dict with ajob_id(e.g. an empty list[]), callers get a cryptic PydanticValidationErrorinstead of a meaningfulAtomicAPIError. Make the failure mode actionable.What triggered this
A live
client.sites.update_domain(domain=…, new_domain="...", keep_old_domain=True)call surfaced:i.e.
data: []came back from the API instead of the documented{job_id, wpcom_blog_id, atomic_site_id, domain_name, old_domain_name}shape. Root cause (API regression, undocumented no-op shape, or error body on a 200) is unknown — because the SDK discarded the HTTP response before it could be inspected. That's the bug to fix, regardless of the API's behaviour.Call sites affected
All in
atomic_sdk/api/sites.py:create_site)delete_site)update_domain)All 8 have the pattern:
Scope
1. Add a helper on
SitesClient2. Apply across all 8 call sites
Replace each
job = Job.parse_obj(response_data); job._client = self._clientwithjob = self._parse_job(response_data).Acceptance criteria
client.sites.update_domain(...)against a response wheredata == []raisesAtomicAPIErrorwith a message containing the actual payload — notpydantic.ValidationError.data: {job_id: 1234, ...}response still returns a validJobinstance with_clientattached.SitesClient— not duplicated inline, not copied across files.Out of scope
[]on that one call (separate question, not an SDK concern).BackupJobparsing, etc.) — do those as separate follow-ups if they surface similar issues.References
atomic_sdk/api/sites.py:167→pydantic/main.py:1222.atomic_sdk/models.py:42.