From a98a4d647f880f461ca5658da553cefabf49b5de Mon Sep 17 00:00:00 2001 From: RapidPoseidon Date: Fri, 19 Jun 2026 08:50:07 +0000 Subject: [PATCH 1/2] fix(job): read job state field renamed from status The job endpoint response model (GetJobByIdEndpointOutput) was regenerated in 3.14.2 with its `status` field renamed to `state` (now an AudienceJobState enum), but RapidataJob.get_status() still read `.status`, raising AttributeError on every job status check (e.g. display_progress_bar, get_results). Read `.state.value` to restore the str return contract; the enum values (Completed, Failed, ...) already match the strings the callers compare against. Co-Authored-By: Claude Opus 4.8 Co-Authored-By: lino --- src/rapidata/rapidata_client/job/rapidata_job.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/rapidata/rapidata_client/job/rapidata_job.py b/src/rapidata/rapidata_client/job/rapidata_job.py index 358fc4e33..83d1bf71a 100644 --- a/src/rapidata/rapidata_client/job/rapidata_job.py +++ b/src/rapidata/rapidata_client/job/rapidata_job.py @@ -160,7 +160,9 @@ def get_status(self) -> str: The current status of the job as a string. """ with tracer.start_as_current_span("RapidataJob.get_status"): - return self._openapi_service.order.job_api.job_job_id_get(self.id).status + return self._openapi_service.order.job_api.job_job_id_get( + self.id + ).state.value def get_results(self) -> RapidataResults: """ From fb7a4a3c73e82602192e89bc0d5bae9c41b5383a Mon Sep 17 00:00:00 2001 From: RapidPoseidon Date: Fri, 19 Jun 2026 09:16:21 +0000 Subject: [PATCH 2/2] fix(client): restore pyright field-access checks on lazy models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LazyValidatedModel overrode __getattribute__ with a `-> Any` return, which is class-visible to type checkers. pyright then treats every attribute access on any generated model as a valid `Any`, so renamed or removed backend fields (e.g. reading `.status` after it became `.state`) pass the type-check CI and only blow up at runtime. Guard the override behind `if not TYPE_CHECKING:` — the same trick Pydantic uses for its own __getattr__/__setattr__ (see pydantic/main.py and pydantic#8643). Runtime behaviour is unchanged (TYPE_CHECKING is False at runtime, so the lazy access-time guard still runs); pyright now resolves attribute access against each model's declared field annotations and flags unknown fields. Verified: pyright flags `.status` on GetJobByIdEndpointOutput while `.state` stays clean, and lazy construct/access still works at runtime. Co-Authored-By: Claude Opus 4.8 Co-Authored-By: lino --- src/rapidata/api_client/lazy_model.py | 79 +++++++++++++++------------ 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/src/rapidata/api_client/lazy_model.py b/src/rapidata/api_client/lazy_model.py index 89ff89a43..3a9e4987b 100644 --- a/src/rapidata/api_client/lazy_model.py +++ b/src/rapidata/api_client/lazy_model.py @@ -13,7 +13,7 @@ from __future__ import annotations -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, TYPE_CHECKING from pydantic import BaseModel, ConfigDict, ValidationError @@ -122,38 +122,45 @@ def _lazy_construct( # ------------------------------------------------------------------ # Access-time guard – raise only when a bad field is actually read # ------------------------------------------------------------------ - def __getattribute__(self, name: str) -> Any: - # Only intercept known model fields; everything else takes the - # fast super() path (Pydantic internals, dunder attrs, methods). - # Use getattr (MRO-aware) rather than type(self).__dict__.get(): - # in Pydantic v2, fields are exposed via the `model_fields` - # class-property and are NOT present in the class's own __dict__, - # so a __dict__ lookup always misses and the guard never fires. - model_fields = getattr(type(self), "model_fields", None) - if model_fields is not None and name in model_fields: - # model_construct() bypasses __init__, so `_field_validation_errors` - # may not be set on every instance. Treat missing as "no errors". - errors: Optional[Dict[str, dict]] - try: - errors = object.__getattribute__( - self, "_field_validation_errors" - ) - except AttributeError: - errors = None - if errors and name in errors: - err = errors[name] - message = ( - f"Field '{name}' on {type(self).__name__} has an unexpected " - f"type from the backend: {err.get('msg', err)}" - ) - # Imported lazily to avoid pulling the higher SDK layer at - # module import time (this base class is imported by every - # generated model). - from rapidata.rapidata_client.api.rapidata_api_client import ( - format_outdated_sdk_note, - ) - note = format_outdated_sdk_note() - if note: - message = f"{message}\n{note}" - raise TypeError(message) - return super().__getattribute__(name) + # Guarded against TYPE_CHECKING for the same reason Pydantic guards its own + # __getattr__/__setattr__ (pydantic/main.py): a class-visible __getattribute__ + # returning Any makes type checkers treat *every* attribute access as a valid + # Any, which silently hides renamed/removed backend fields (e.g. .status after + # it became .state). Hiding it from pyright restores per-field access checking + # against the declared model annotations while leaving runtime behaviour intact. + if not TYPE_CHECKING: + + def __getattribute__(self, name: str) -> Any: + # Only intercept known model fields; everything else takes the + # fast super() path (Pydantic internals, dunder attrs, methods). + # Use getattr (MRO-aware) rather than type(self).__dict__.get(): + # in Pydantic v2, fields are exposed via the `model_fields` + # class-property and are NOT present in the class's own __dict__, + # so a __dict__ lookup always misses and the guard never fires. + model_fields = getattr(type(self), "model_fields", None) + if model_fields is not None and name in model_fields: + # model_construct() bypasses __init__, so `_field_validation_errors` + # may not be set on every instance. Treat missing as "no errors". + errors: Optional[Dict[str, dict]] + try: + errors = object.__getattribute__(self, "_field_validation_errors") + except AttributeError: + errors = None + if errors and name in errors: + err = errors[name] + message = ( + f"Field '{name}' on {type(self).__name__} has an unexpected " + f"type from the backend: {err.get('msg', err)}" + ) + # Imported lazily to avoid pulling the higher SDK layer at + # module import time (this base class is imported by every + # generated model). + from rapidata.rapidata_client.api.rapidata_api_client import ( + format_outdated_sdk_note, + ) + + note = format_outdated_sdk_note() + if note: + message = f"{message}\n{note}" + raise TypeError(message) + return super().__getattribute__(name)