fix(api): guard SitesClient job response parsing#20
Conversation
|
Verified the Changes on SDK locally: create: malformed [] -> AtomicAPIError ok
delete: malformed [] -> AtomicAPIError ok
update_domain: malformed [] -> AtomicAPIError ok
manage_software: malformed [] -> AtomicAPIError ok
set_wordpress_version: malformed [] -> AtomicAPIError ok
update_options: malformed [] -> AtomicAPIError ok
update_persistent_data: malformed [] -> AtomicAPIError ok
reset_db_password: malformed [] -> AtomicAPIError ok
success payload -> Job with _client ok
missing job_id dict -> AtomicAPIError ok |
There was a problem hiding this comment.
Pull request overview
Adds a centralized guard in SitesClient for parsing “job-style” responses so unexpected payload shapes raise an actionable AtomicAPIError rather than bubbling up a Pydantic ValidationError.
Changes:
- Introduced
SitesClient._parse_job()to validate response shape (dict +job_id) before parsing intoJob. - Updated all job-returning Sites endpoints to use
_parse_job()instead of duplicatingJob.parse_obj(...)+_clientwiring.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "Expected a job response (dict with 'job_id'), " | ||
| f"got {type(response_data).__name__}: {response_data!r}" | ||
| ), | ||
| status_code=200, |
There was a problem hiding this comment.
Leaving this as-is because issue #11 explicitly requested status_code=200 for malformed job payloads returned from a successful _post() call. Non-2xx responses are still handled earlier by ResourceClient._request; this helper only covers the “HTTP 200 but unexpected data shape” case.
|
Verified in bench console via EasyDash that the patched Input: from easydash.wp_cloud import atomic
from atomic_sdk.exceptions import AtomicAPIError
from atomic_sdk.models import Job
domain = "ab-prod-test.stagingview.net"
site = frappe.get_doc("WPC Site", domain)
client = atomic.get_client(site.organization)
print(client.sites.__class__)
print(client.sites.__class__._parse_job)
orig_post = client.sites._post
methods = [
("create", lambda client=client: client.sites.create(admin_user="admin", admin_email="admin@example.com", domain_name="example.com")),
("delete", lambda client=client, domain=domain: client.sites.delete(domain=domain)),
("update_domain", lambda client=client, domain=domain: client.sites.update_domain(new_domain="new.example.com", domain=domain, keep_old_domain=True)),
("manage_software", lambda client=client, domain=domain: client.sites.manage_software({"plugins/akismet/latest": "activate"}, domain=domain)),
("set_wordpress_version", lambda client=client, domain=domain: client.sites.set_wordpress_version("latest", domain=domain)),
("update_options", lambda client=client, domain=domain: client.sites.update_options({"set": {"blog_public": "0"}}, domain=domain)),
("update_persistent_data", lambda client=client, domain=domain: client.sites.update_persistent_data({"foo": {"value": "bar"}}, domain=domain)),
("reset_db_password", lambda client=client, domain=domain: client.sites.reset_db_password(domain=domain)),
]
def fake_post_empty(endpoint, data=None, json=None):
print("fake _post:", endpoint)
return []
client.sites._post = fake_post_empty
for name, call in methods:
try:
call()
except AtomicAPIError as exc:
print(name, "OK", exc)
except Exception as exc:
print(name, "WRONG", type(exc), exc)
else:
print(name, "WRONG no error")
def fake_post_success(endpoint, data=None, json=None, domain=domain):
print("fake _post success:", endpoint)
return {"job_id": 1234, "atomic_site_id": 5678, "domain_name": domain}
client.sites._post = fake_post_success
job = client.sites.update_domain(new_domain="new.example.com", domain=domain)
print(type(job), job.job_id, job.atomic_site_id, job.domain_name, job._client is client)
client.sites._post = orig_postOutput: |
Closes : #11