Skip to content

refactor(BA-4911): migrate ScalingGroupOpts from attrs/trafaret to Pydantic BaseModel#9723

Open
HyeockJinKim wants to merge 5 commits intomainfrom
BA-4911
Open

refactor(BA-4911): migrate ScalingGroupOpts from attrs/trafaret to Pydantic BaseModel#9723
HyeockJinKim wants to merge 5 commits intomainfrom
BA-4911

Conversation

@HyeockJinKim
Copy link
Collaborator

Summary

  • Replace @attr.define + JSONSerializableMixin + trafaret with Pydantic BaseModel(frozen=True) for ScalingGroupOpts
  • Change column type from StructuredJSONObjectColumn(ScalingGroupOpts) to PydanticColumn(ScalingGroupOpts) in ScalingGroupRow
  • Update legacy GraphQL layer to use model_validate() / model_dump(mode="json") instead of from_json() / to_json()
  • Add field validators/serializers for pending_timeout (timedelta↔float), allowed_session_types (enum↔str), and agent_selection_strategy (enum↔str)

Test plan

  • Unit tests added: tests/unit/manager/models/scaling_group/test_row.py (19 tests)
  • Covers: default values, frozen model, roundtrip serialization, field validators, backward compatibility with legacy JSON data
  • pants fmt/fix/lint all pass

Resolves BA-4911

Copilot AI review requested due to automatic review settings March 5, 2026 17:04
HyeockJinKim added a commit that referenced this pull request Mar 5, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@github-actions github-actions bot added size:L 100~500 LoC comp:manager Related to Manager component labels Mar 5, 2026
Comment on lines +110 to +114
@classmethod
def from_json(cls, obj: Mapping[str, Any]) -> ScalingGroupOpts:
return cls(**cls.as_trafaret().check(obj))
def validate_allowed_session_types(cls, value: Any) -> list[SessionTypes]:
if not isinstance(value, list):
raise ValueError(f"Expected a list, got {type(value)}")
return [SessionTypes(v) if isinstance(v, str) else v for v in value]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review whether this is the required validator.

Comment on lines +120 to +127
@field_validator("pending_timeout", mode="before")
@classmethod
def as_trafaret(cls) -> t.Trafaret:
return t.Dict({
t.Key("allowed_session_types", default=["interactive", "batch"]): t.List(
tx.Enum(SessionTypes), min_length=1
),
t.Key("pending_timeout", default=0): tx.TimeDuration(allow_negative=False),
# Each scheduler impl refers an additional "config" key.
t.Key("config", default={}): t.Mapping(t.String, t.Any),
t.Key("agent_selection_strategy", default=AgentSelectionStrategy.DISPERSED): tx.Enum(
AgentSelectionStrategy
),
t.Key("agent_selector_config", default={}): agent_selector_config_iv,
t.Key("enforce_spreading_endpoint_replica", default=False): t.ToBool,
t.Key("allow_fractional_resource_fragmentation", default=True): t.ToBool,
t.Key("route_cleanup_target_statuses", default=["unhealthy"]): t.List(
t.Enum("healthy", "unhealthy", "degraded")
),
}).allow_extra("*")
def validate_pending_timeout(cls, value: Any) -> timedelta:
if isinstance(value, (int, float)):
return timedelta(seconds=value)
if isinstance(value, timedelta):
return value
raise ValueError(f"Expected a number or timedelta, got {type(value)}")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review whether this is also essential.

HyeockJinKim and others added 5 commits March 6, 2026 11:57
…dantic BaseModel

- Replace @attr.define with Pydantic BaseModel (frozen=True)
- Add field validators for pending_timeout (float→timedelta) and
  allowed_session_types (str list→SessionTypes list)
- Add field serializers for pending_timeout (→float), allowed_session_types
  (→str list), and agent_selection_strategy (→str)
- Remove to_json(), from_json(), as_trafaret() methods
- Change scheduler_opts column type from StructuredJSONObjectColumn to PydanticColumn
- Update default from {} to ScalingGroupOpts (callable factory)
- Replace to_json()/from_json() calls in gql_legacy with model_dump()/model_validate()
- Remove unused imports (attr, trafaret, JSONSerializableMixin, StructuredJSONObjectColumn)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Test roundtrip serialization, default instantiation, field validators
(pending_timeout timedelta, allowed_session_types enum, agent_selection_strategy),
invalid input validation errors, and backward compatibility with legacy JSON data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pydantic v2 natively handles str→enum coercion for string-backed enums
and int/float→timedelta coercion, making the custom `mode="before"`
validators unnecessary. Serializers are retained for DB-compatible
JSON output format (float seconds, string enum values).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp:manager Related to Manager component size:L 100~500 LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants