Skip to content

Commit de3a307

Browse files
author
ikemilian-lewis
committed
fix: more merge conflict resolutions
1 parent 6b16e2c commit de3a307

21 files changed

Lines changed: 938 additions & 453 deletions

src/orb/application/commands/request_creation_handlers.py

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -552,9 +552,9 @@ def _persist_return_follow_up_context(
552552
if not isinstance(provider_data, dict) or not provider_data:
553553
return
554554

555-
updated_request = with_request_follow_up_context(request, provider_data)
556-
557555
with self.uow_factory.create_unit_of_work() as uow:
556+
current_request = uow.requests.get_by_id(request.request_id) or request
557+
updated_request = with_request_follow_up_context(current_request, provider_data)
558558
events = uow.requests.save(updated_request)
559559
for event in events or []:
560560
self.event_publisher.publish(event) # type: ignore[union-attr]
@@ -631,18 +631,3 @@ async def _update_request_to_terminating(self, request: Any) -> None:
631631
RequestStatus.IN_PROGRESS,
632632
"Termination accepted: waiting for instances to reach terminated state",
633633
)
634-
635-
async def _update_request_to_in_progress_with_message(self, request: Any, message: str) -> None:
636-
"""Persist an in-progress return message after submit without claiming completion."""
637-
try:
638-
await self._update_request_status(
639-
request,
640-
RequestStatus.IN_PROGRESS,
641-
message,
642-
)
643-
except Exception as update_error:
644-
self.logger.error(
645-
"Failed to update request status to in_progress: %s",
646-
update_error,
647-
exc_info=True,
648-
)

src/orb/application/services/machine_sync_service.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ def _resolve_return_resource_id(
237237
if not isinstance(pending_cleanup, dict):
238238
continue
239239

240-
resource_id = pending_cleanup.get("vmss_name")
240+
resource_id = pending_cleanup.get("resource_id")
241241
if resource_id in (None, ""):
242242
continue
243243

@@ -393,10 +393,8 @@ async def sync_machines_with_provider(
393393
# Stamp a terminal "Terminated" if provider didn't
394394
# supply something more specific (EC2's StateReason).
395395
# - Otherwise pass through whatever provider returned.
396-
from orb.domain.machine.machine_status import MachineStatus as _MS
397-
398396
_new_reason = provider_machine.status_reason
399-
if provider_machine.status == _MS.TERMINATED:
397+
if provider_machine.status == MachineStatus.TERMINATED:
400398
if not _new_reason or "in progress" in (_new_reason or "").lower():
401399
_new_reason = "Terminated"
402400
machine_data["status_reason"] = _new_reason

src/orb/application/services/provider_validation_service.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22

33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING, Optional
5+
from typing import TYPE_CHECKING
66

77
from orb.domain.base.exceptions import ApplicationError
88

99
if TYPE_CHECKING:
1010
from orb.domain.template.template_aggregate import Template
1111

1212
from orb.domain.base.ports import ContainerPort, LoggingPort, ProviderSelectionPort
13-
from orb.domain.base.ports.provider_validation_port import ProviderValidationPort
1413
from orb.domain.base.results import ProviderSelectionResult
1514

1615

@@ -22,12 +21,10 @@ def __init__(
2221
container: ContainerPort,
2322
logger: LoggingPort,
2423
provider_selection_port: ProviderSelectionPort,
25-
validator: Optional[ProviderValidationPort] = None,
2624
) -> None:
2725
self._container = container
2826
self.logger = logger
2927
self._provider_selection_port = provider_selection_port
30-
self._validator = validator
3128

3229
async def select_and_validate_provider(self, template: Template) -> ProviderSelectionResult:
3330
"""Select provider and validate template compatibility."""
@@ -38,13 +35,6 @@ async def select_and_validate_provider(self, template: Template) -> ProviderSele
3835
selection_result.selection_reason,
3936
)
4037

41-
if self._validator is not None:
42-
template_dict = template if isinstance(template, dict) else vars(template)
43-
result = self._validator.validate_template_configuration(template_dict)
44-
if not result.get("valid", True):
45-
errors = result.get("errors", [])
46-
raise ApplicationError("; ".join(errors))
47-
4838
validation_result = self._provider_selection_port.validate_template_requirements(
4939
template,
5040
selection_result.provider_name,

src/orb/application/services/request_status_service.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import dataclasses
2121
from typing import Optional, Tuple
2222

23+
from orb.application.services.request_follow_up_context import get_request_follow_up_context
2324
from orb.domain.base import UnitOfWorkFactory
2425
from orb.domain.base.exceptions import ProviderContractError
2526
from orb.domain.base.ports.logging_port import LoggingPort
@@ -123,18 +124,8 @@ def _determine_return_status(
123124
"""Determine return request status from machine termination states."""
124125
db_machine_count = len(db_machines)
125126
follow_up_pending_message = "Return in progress: awaiting provider follow-up cleanup"
126-
127-
if provider_metadata.get("termination_follow_up_failed"):
128-
details = provider_metadata.get("termination_follow_up_details", [{}])
129-
error = details[0].get("last_delete_error") if details else None
130-
message = "Return request failed: provider follow-up cleanup failed"
131-
if error:
132-
message = f"{message}: {error}"
133-
return RequestStatus.FAILED.value, message
134-
135-
termination_follow_up_pending = bool(
136-
provider_metadata.get("termination_follow_up_pending", False)
137-
)
127+
follow_up_context = get_request_follow_up_context(request)
128+
termination_follow_up_pending = follow_up_context.get("follow_up_kind") == "termination"
138129

139130
# For return requests: empty provider_machines *with* DB records means all
140131
# instances are gone from AWS — genuinely terminated. But if we have

src/orb/application/services/spot_placement_execution.py

Lines changed: 1 addition & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44
from dataclasses import dataclass
5-
from typing import Any, Awaitable, Callable, Mapping
5+
from typing import Any, Callable, Mapping
66

77
from orb.application.services.spot_placement_planner import PlacementPlanEntry
88
from orb.domain.base.exceptions import DomainException
@@ -97,91 +97,6 @@ def build_planned_execution_metadata(
9797
class SpotPlacementExecutionService:
9898
"""Execute a placement plan via provider callbacks."""
9999

100-
async def execute_plan_async(
101-
self,
102-
plan: list[PlacementPlanEntry],
103-
total_count: int,
104-
build_child_template: Callable[[PlacementPlanEntry], Any],
105-
build_child_request: Callable[[int, int], Any],
106-
launch_child: Callable[[Any, Any], Awaitable[Mapping[str, Any]]],
107-
is_capacity_like_failure: Callable[[dict[str, Any]], bool],
108-
) -> SpotPlacementExecutionSummary:
109-
"""Async variant of ``execute_plan`` for native async provider handlers."""
110-
resource_ids: list[str] = []
111-
instances: list[dict[str, Any]] = []
112-
child_results: list[dict[str, Any]] = []
113-
failed_subplans: list[dict[str, Any]] = []
114-
carryover = 0
115-
fulfilled = 0
116-
terminated_early = False
117-
terminal_error_message: str | None = None
118-
119-
for idx, plan_entry in enumerate(plan):
120-
requested_for_entry = plan_entry.planned_count + carryover
121-
if requested_for_entry <= 0:
122-
continue
123-
124-
child_template = build_child_template(plan_entry)
125-
child_request = build_child_request(requested_for_entry, idx)
126-
try:
127-
raw_result = await launch_child(child_request, child_template)
128-
except DomainException as exc:
129-
raw_result = self._domain_exception_child_result(exc)
130-
131-
child_result = self._normalize_child_result(
132-
plan_entry=plan_entry,
133-
requested_count=requested_for_entry,
134-
raw_result=raw_result,
135-
)
136-
child_results.append(child_result)
137-
138-
if child_result["success"]:
139-
resource_ids.extend(child_result["resource_ids"])
140-
instances.extend(child_result["instances"])
141-
fulfilled_for_child = child_result["fulfilled_count"]
142-
fulfilled += fulfilled_for_child
143-
carryover = max(requested_for_entry - fulfilled_for_child, 0)
144-
if carryover == 0:
145-
continue
146-
147-
failed_subplans.append(child_result)
148-
if is_capacity_like_failure(child_result):
149-
continue
150-
151-
terminated_early = True
152-
terminal_error_message = child_result["error_message"]
153-
break
154-
155-
failed_subplans.append(child_result)
156-
if is_capacity_like_failure(child_result):
157-
carryover = requested_for_entry
158-
continue
159-
160-
if resource_ids or instances:
161-
terminated_early = True
162-
terminal_error_message = child_result["error_message"]
163-
break
164-
165-
return SpotPlacementExecutionSummary(
166-
resource_ids=[],
167-
instances=[],
168-
child_results=child_results,
169-
failed_subplans=failed_subplans,
170-
unfulfilled_count=total_count,
171-
terminated_early=True,
172-
terminal_error_message=child_result["error_message"],
173-
)
174-
175-
return SpotPlacementExecutionSummary(
176-
resource_ids=resource_ids,
177-
instances=instances,
178-
child_results=child_results,
179-
failed_subplans=failed_subplans,
180-
unfulfilled_count=max(total_count - fulfilled, carryover),
181-
terminated_early=terminated_early,
182-
terminal_error_message=terminal_error_message,
183-
)
184-
185100
def execute_plan(
186101
self,
187102
plan: list[PlacementPlanEntry],

src/orb/cli/main.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,7 @@ async def main() -> None:
219219
else:
220220
# Rich console output can wrap long structured strings, which may break
221221
# consumers that try to parse stdout as exact JSON.
222-
if not args.quiet:
223-
print(formatted_output)
222+
print(formatted_output)
224223

225224
if exit_code != 0:
226225
sys.exit(exit_code)

src/orb/domain/base/ports/provider_registry_port.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ def generate_provider_name(cls, config: dict[str, Any]) -> str:
5050
"""Generate a provider instance name from provider config."""
5151
...
5252

53+
@classmethod
54+
def get_ui_column_schema(cls) -> list[Any]:
55+
"""Return provider-specific UI column descriptors."""
56+
...
57+
5358

5459
class ProviderRegistryPort(ABC):
5560
"""Port that the application layer uses to interact with the provider registry.

src/orb/infrastructure/scheduler/base/strategy.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from orb.application.services.provider_registry_service import ProviderRegistryService
2323
from orb.domain.base.ports.configuration_port import ConfigurationPort
2424
from orb.domain.base.ports.logging_port import LoggingPort
25+
from orb.domain.template.template_aggregate import Template
2526

2627

2728
class BaseSchedulerStrategy(SchedulerPort, ABC):
@@ -247,23 +248,23 @@ def get_config_directory(self) -> str:
247248
return self._coalesce_directory(
248249
config_override=getattr(self.config_manager.app_config.scheduler, "config_dir", None),
249250
env_var_name="CONFIG_DIR",
250-
default_factory=lambda: self._get_platform_config_dir(),
251+
default_factory=self._get_platform_config_dir,
251252
)
252253

253254
def get_working_directory(self) -> str:
254255
"""Get working directory with coalesce pattern."""
255256
return self._coalesce_directory(
256257
config_override=getattr(self.config_manager.app_config.scheduler, "work_dir", None),
257258
env_var_name="WORK_DIR",
258-
default_factory=lambda: self._get_platform_work_dir(),
259+
default_factory=self._get_platform_work_dir,
259260
)
260261

261262
def get_logs_directory(self) -> str:
262263
"""Get logs directory with coalesce pattern."""
263264
return self._coalesce_directory(
264265
config_override=getattr(self.config_manager.app_config.scheduler, "log_dir", None),
265266
env_var_name="LOG_DIR",
266-
default_factory=lambda: self._get_platform_logs_dir(),
267+
default_factory=self._get_platform_logs_dir,
267268
)
268269

269270
def get_log_level(self) -> str:
@@ -416,7 +417,7 @@ def format_template_for_display(self, template: TemplateDTO) -> dict[str, Any]:
416417
"""Default implementation - clean to_dict without scheduler-specific formatting."""
417418
return template.to_dict()
418419

419-
def format_template_for_provider(self, template: TemplateDTO) -> dict[str, Any]:
420+
def format_template_for_provider(self, template: "TemplateDTO | Template") -> dict[str, Any]:
420421
"""Format either a stored template DTO or a domain template for provider operations."""
421422
if isinstance(template, TemplateDTO):
422423
return template.to_template_config()

src/orb/infrastructure/template/dtos.py

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -143,23 +143,67 @@ def to_template_config(self) -> dict[str, Any]:
143143
@classmethod
144144
def from_domain(cls, template) -> "TemplateDTO":
145145
"""Convert domain template to DTO."""
146-
template_data = (
147-
template.model_dump() if hasattr(template, "model_dump") else vars(template)
148-
)
149-
dto_field_names = cls.model_fields.keys()
150-
dto_data = {
151-
field_name: template_data[field_name]
152-
for field_name in dto_field_names
153-
if field_name in template_data and field_name != "provider_config"
154-
}
155-
156-
provider_type = dto_data.get("provider_type")
146+
provider_type = getattr(template, "provider_type", None)
147+
provider_config: Optional[BaseModel] = None
157148
if provider_type:
158-
dto_data["provider_config"] = TemplateExtensionRegistry.create_extension_config(
149+
template_data = (
150+
template.model_dump() if hasattr(template, "model_dump") else vars(template)
151+
)
152+
provider_config = TemplateExtensionRegistry.create_extension_config(
159153
str(provider_type), template_data
160154
)
161155

162-
return cls.model_validate(dto_data)
156+
return cls(
157+
# Core fields
158+
template_id=template.template_id,
159+
name=getattr(template, "name", None),
160+
description=getattr(template, "description", None),
161+
# Instance configuration
162+
image_id=getattr(template, "image_id", None),
163+
max_instances=getattr(template, "max_instances", 1),
164+
# Machine types configuration
165+
machine_types=getattr(template, "machine_types", {}),
166+
machine_types_ondemand=getattr(template, "machine_types_ondemand", {}),
167+
machine_types_priority=getattr(template, "machine_types_priority", {}),
168+
# Network configuration
169+
subnet_ids=getattr(template, "subnet_ids", []),
170+
security_group_ids=getattr(template, "security_group_ids", []),
171+
network_zones=getattr(template, "network_zones", []),
172+
public_ip_assignment=getattr(template, "public_ip_assignment", None),
173+
# Pricing and allocation
174+
price_type=getattr(template, "price_type", "ondemand"),
175+
allocation_strategy=getattr(template, "allocation_strategy", None),
176+
max_price=getattr(template, "max_price", None),
177+
# Storage configuration
178+
root_device_volume_size=getattr(template, "root_device_volume_size", None),
179+
volume_type=getattr(template, "volume_type", None),
180+
iops=getattr(template, "iops", None),
181+
throughput=getattr(template, "throughput", None),
182+
storage_encryption=getattr(template, "storage_encryption", None),
183+
encryption_key=getattr(template, "encryption_key", None),
184+
# Access and security
185+
key_name=getattr(template, "key_name", None),
186+
user_data=getattr(template, "user_data", None),
187+
instance_profile=getattr(template, "instance_profile", None),
188+
# Advanced configuration
189+
monitoring_enabled=getattr(template, "monitoring_enabled", None),
190+
# Metadata
191+
tags=getattr(template, "tags", {}),
192+
metadata=getattr(template, "metadata", {}),
193+
provider_config=provider_config,
194+
provider_data=getattr(template, "provider_data", {}),
195+
# Provider identification
196+
provider_type=provider_type,
197+
provider_name=getattr(template, "provider_name", None),
198+
provider_api=getattr(template, "provider_api", None),
199+
# Timestamps
200+
created_at=getattr(template, "created_at", None),
201+
updated_at=getattr(template, "updated_at", None),
202+
# Active status
203+
is_active=getattr(template, "is_active", True),
204+
# Legacy fields
205+
version=getattr(template, "version", None),
206+
)
163207

164208

165209
@dataclass

src/orb/providers/aws/configuration/template_extension.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,6 @@ class AWSTemplateExtensionConfig(BaseModel):
7777
None, description="AWS Context field for EC2 Fleet, ASG, and Spot Fleet"
7878
)
7979

80-
# AWS DTO-only fields preserved by TemplateDTO.provider_config round-trips.
81-
launch_template_id: Optional[str] = Field(
82-
None, description="EC2 Launch Template ID for provider-specific template storage"
83-
)
84-
abis_instance_requirements: Optional[dict[str, Any]] = Field(
85-
None, description="Attribute-based instance selection requirements"
86-
)
87-
8880
@field_validator("subnet_ids")
8981
@classmethod
9082
def validate_subnet_ids(cls, v: Optional[list[str]]) -> Optional[list[str]]:

0 commit comments

Comments
 (0)