Skip to content

Conversation

@momchil-flex
Copy link
Collaborator

@momchil-flex momchil-flex commented Jan 19, 2026

This is an attempt simplify the webapi components that were introduced in the first component modeler iteration. Specifically, I have removed BatchDetail and BatchTask in favor of everything still being TaskInfo and SimulationTask, respectively. One advantage is that now we just store which task types correspond to a "batch" and the task._is_batch_type(): can be evaluated simply based on the stored task.taskType, rather than calling the detail API every time (previously, this call was cached when the TaskFactory was used, but it was still awkward and would create unnecessary API call repetitions in some cases).

I think I can completely remove BatchTask and TaskFactory (no need to keep backward compatibility of internally used components I would say, especially those that were added very recently for the CM) - but before doing more I wanted to get your opinion @daquintero (maybe also fyi @yaugenst )

I would say this is still not in an ideal state, but looks better to me. Generally, I am not opposed to having the different subclasses, but this was a bit of a mess. For example, WebTask.create was always creating a SimulationTask instead of a BatchTask, and it was a bit hard to figure out what was needed where and why.

I think the main downside of this change is that the TaskInfo now has fields relevant to both regular simulations and to component modelers, so in some cases one part will be None while in other cases another part will be None. But seems OK to me.

Note

Unifies task handling by folding batch/modeler APIs into existing simulation abstractions.

  • Removes BatchDetail and BatchTask; introduces BatchTask = SimulationTask alias for backward compatibility
  • Extends SimulationTask to detect batch types (_is_batch_type) and route to rf/task/... endpoints for detail, submit, abort, and check
  • Simplifies TaskFactory to always return SimulationTask; get() now probes simulation API then falls back to batch API with proper 404 handling
  • Reworks TaskInfo into a unified schema including batch-specific fields; get_info now returns only TaskInfo
  • Updates webapi functions to use _is_batch_type() checks instead of isinstance(BatchTask), adjust monitoring, status, download, load, and cost/real_cost flows
  • Improves delete flow to prefer group/version deletion when available; tests adjusted to mock new endpoints and fields (e.g., groupId, version) and remove deprecated paths

Written by Cursor Bugbot for commit c3b1eb4. This will update automatically on new commits. Configure here.

Copilot AI review requested due to automatic review settings January 19, 2026 09:04
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +479 to +486
# Use suppress_404 and catch all exceptions to handle unmocked endpoints in tests
try:
resp = http.get(f"tidy3d/tasks/{task_id}/detail")
except WebNotFoundError as e:
td.log.error(f"The requested task ID '{task_id}' does not exist.")
raise e
resp = http.get(f"rf/task/{task_id}/statistics", suppress_404=True)
if resp:
task_type = resp.get("taskType") if isinstance(resp, dict) else None
return SimulationTask(taskId=task_id, taskType=task_type)
except Exception:
pass
Copy link

Choose a reason for hiding this comment

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

style: Catching all exceptions with bare except Exception: could hide important errors during debugging

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: tidy3d/web/core/task_core.py
Line: 479:486

Comment:
**style:** Catching all exceptions with bare `except Exception:` could hide important errors during debugging

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Copy link

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.

Pull request overview

This pull request consolidates the batch task and simulation task APIs by unifying BatchTask and BatchDetail into the existing SimulationTask and TaskInfo classes. The change simplifies the codebase by removing duplicate classes while maintaining backward compatibility through deprecated aliases.

Changes:

  • Unified TaskInfo to represent both simulation and batch tasks with optional fields for each type
  • Removed BatchTask class and replaced it with a deprecated alias to SimulationTask
  • Removed BatchDetail class, with its fields merged into TaskInfo
  • Added _is_batch_type() method to SimulationTask for conditional API routing based on task type (RF, TERMINAL_CM, MODAL_CM)
  • Updated all API calls to use the unified classes with conditional endpoint routing
  • Simplified TaskFactory to just delegate to SimulationTask.get()

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 11 comments.

File Description
tidy3d/web/core/task_info.py Unified TaskInfo class to handle both simulation and batch tasks; removed BatchDetail class
tidy3d/web/core/task_core.py Removed BatchTask class and added batch support to SimulationTask; simplified TaskFactory; added check() method
tidy3d/web/api/webapi.py Updated to use unified API with _is_batch_type() checks; changed return type signatures
tests/test_web/test_webapi.py Added mock for batch API endpoint to support unified task retrieval

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +485 to +486
except Exception:
pass
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Using a bare 'except Exception' clause here is too broad. This will catch all exceptions including system exceptions (KeyboardInterrupt, SystemExit, etc.) that should generally not be caught. Consider catching specific exceptions like WebNotFoundError or HTTPError instead, or at minimum use 'except Exception:' with proper logging.

Copilot uses AI. Check for mistakes.
# ─────────────────────────────────────────────────────────────────────────
# Common fields (used by both simulation tasks and batches)
# ─────────────────────────────────────────────────────────────────────────
taskId: str = None
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The field 'taskId' has a default value of None, which means TaskInfo can be instantiated without a task ID. This could be problematic since a task ID should be required to uniquely identify a task. Consider making this field required by removing the default value or using Optional[str] with validation to ensure it's provided when needed.

Suggested change
taskId: str = None
taskId: Optional[str]

Copilot uses AI. Check for mistakes.
message: str = None
"""Status message for the batch."""

tasks: list[BatchMember] = None
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The 'tasks' field defaults to None for simulation tasks but is expected to be a list for batch tasks. The code at line 525 coerces None to an empty list, but this field definition should explicitly handle this. Consider using a default value of None with explicit Optional typing, or use pydantic's Field with a default_factory to provide [] for batch tasks.

Suggested change
tasks: list[BatchMember] = None
tasks: Optional[list[BatchMember]] = None

Copilot uses AI. Check for mistakes.
Comment on lines +1146 to +1147
# Check if task is a batch type (handle mocked objects that may not have the method)
is_batch = task is not None and getattr(task, "_is_batch_type", lambda: False)()
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Using getattr with a lambda default is defensive but could hide underlying issues. If 'task' is not None but doesn't have '_is_batch_type', this indicates a type inconsistency that should be addressed at the source. Consider adding a type check or assertion here to ensure 'task' is actually a SimulationTask when not None, rather than silently handling the case where the method doesn't exist.

Suggested change
# Check if task is a batch type (handle mocked objects that may not have the method)
is_batch = task is not None and getattr(task, "_is_batch_type", lambda: False)()
if task is not None and not isinstance(task, SimulationTask):
raise TypeError(
f"Expected 'SimulationTask' from TaskFactory.get for task_id={task_id!r}, "
f"got {type(task).__name__!r} instead."
)
is_batch = bool(task is not None and task._is_batch_type())

Copilot uses AI. Check for mistakes.
@@ -792,10 +853,49 @@ def abort(self) -> requests.Response:
"""Abort the current task on the server."""
if not self.task_id:
raise ValueError("Task id not found.")
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The error message refers to "batch ID" but the parameter and method signature use "task_id". For consistency with the unified API design where batch tasks are now handled as SimulationTasks, the error message should refer to "task ID" instead of "batch ID".

Suggested change
raise ValueError("Task id not found.")
raise ValueError("Task ID not found.")

Copilot uses AI. Check for mistakes.
Comment on lines +885 to +887
"""
if not self._is_batch_type():
raise NotImplementedError("The 'check' method is only available for batch tasks.")
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The check() method now raises NotImplementedError for non-batch tasks, but this changes the API contract. This could break existing code that might attempt to call check() on simulation tasks. Consider whether this should log a warning or be a no-op instead of raising an exception, or ensure this is properly documented as a breaking change.

Suggested change
"""
if not self._is_batch_type():
raise NotImplementedError("The 'check' method is only available for batch tasks.")
Notes
-----
For non-batch tasks this method is a no-op for backward compatibility
and will log a warning instead of raising an exception.
"""
if not self._is_batch_type():
# For non-batch tasks, keep this as a no-op to avoid breaking existing code.
logger = get_logger_console(__name__)
logger.warning(
"WebTask.check() was called on a non-batch task (task_type=%r); "
"this call is ignored. The 'check' method is only available for "
"batch/modeler tasks.",
getattr(self, "task_type", None),
)
return None # type: ignore[return-value]

Copilot uses AI. Check for mistakes.
from tidy3d.web.core.task_core import (
BatchDetail,
BatchTask,
BatchTask, # noqa: F401 - Deprecated alias, kept for backward compatibility
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The BatchTask alias is deprecated and imported with noqa: F401 to suppress unused import warnings. However, this import should be explicitly documented in a docstring or comment explaining the deprecation timeline and migration path for users. Consider adding a deprecation warning when BatchTask is used, or at minimum document when this alias will be removed.

Suggested change
BatchTask, # noqa: F401 - Deprecated alias, kept for backward compatibility
BatchTask, # noqa: F401 - Deprecated alias.
# NOTE:
# `BatchTask` is imported here solely to preserve backward compatibility for
# external code that previously imported `BatchTask` from
# `tidy3d.web.api.webapi`. This alias is deprecated and will be removed in a
# future major release. Users should instead import `BatchTask` directly from
# `tidy3d.web.core.task_core`, or follow the migration guidance in the
# tidy3d web API documentation to use the newer task abstractions.

Copilot uses AI. Check for mistakes.
Comment on lines +669 to +673
"The 'pay_type' argument is not yet supported for batch tasks."
)
if priority is not None:
raise NotImplementedError(
"The 'priority' argument is not yet supported for batch tasks."
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The NotImplementedError messages for 'pay_type' and 'priority' arguments state they are "not yet supported for batch tasks", implying they may be supported in the future. Consider adding TODO comments with tracking issue numbers so this functionality isn't forgotten, or update the error messages if these features won't be supported.

Suggested change
"The 'pay_type' argument is not yet supported for batch tasks."
)
if priority is not None:
raise NotImplementedError(
"The 'priority' argument is not yet supported for batch tasks."
"The 'pay_type' argument is not supported for batch tasks."
)
if priority is not None:
raise NotImplementedError(
"The 'priority' argument is not supported for batch tasks."

Copilot uses AI. Check for mistakes.
Comment on lines +439 to +442
def _is_batch_type(self) -> bool:
"""Check if this task uses the batch/modeler API."""
return self.task_type in self.BATCH_TASK_TYPES

Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The unified SimulationTask now handles both simulation and batch/modeler tasks with conditional API routing, but there don't appear to be dedicated tests for batch task scenarios (RF, TERMINAL_CM, MODAL_CM task types). Consider adding tests that verify the correct API endpoints are called for batch tasks, including tests for the check() method, submit() with batch tasks, detail() transformation, and abort() routing.

Copilot uses AI. Check for mistakes.
Comment on lines +485 to +486
except Exception:
pass
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except Exception:
pass
except Exception as exc:
td.log.debug(
"Fallback request to 'rf/task/%s/statistics' failed and will be ignored: %s",
task_id,
exc,
)

Copilot uses AI. Check for mistakes.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

task = BatchTask.get(task_id=task_id)
task = SimulationTask.get(task_id=task_id)
detail = _get_batch_detail_handle_error_status(task)
name = detail.name or "modeler_batch"
Copy link

Choose a reason for hiding this comment

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

Calling len() on potentially None tasks field

Medium Severity

The tasks field in TaskInfo defaults to None (changed from [] in the removed BatchDetail class), but _monitor_modeler_batch calls len(detail.tasks) at line 1237 without checking for None first. If the batch API response isn't a dict or the transformation in detail() is bypassed, tasks would be None, causing a TypeError: object of type 'NoneType' has no len().

Additional Locations (1)

Fix in Cursor Fix in Web

@github-actions
Copy link
Contributor

Diff Coverage

Diff: origin/develop...HEAD, staged and unstaged changes

  • tidy3d/web/api/webapi.py (61.9%): Missing lines 118-119,722,725,738,1200,1202,1345
  • tidy3d/web/core/task_core.py (55.8%): Missing lines 162,482-484,515,517,519-520,522,524-526,667-668,671-672,675,857,886-890
  • tidy3d/web/core/task_info.py (100%)

Summary

  • Total: 115 lines
  • Missing: 31 lines
  • Coverage: 73%

tidy3d/web/api/webapi.py

Lines 114-123

  114     """
  115 
  116     # TODO: test properly
  117     try:
! 118         task = SimulationTask.get(resource_id)
! 119         batch_detail = task.detail()
  120         status = batch_detail.status.lower()
  121     except Exception as e:
  122         log.error(f"Could not retrieve batch details for '{resource_id}': {e}")
  123         raise WebError(f"Failed to retrieve status for batch '{resource_id}'.") from e

Lines 718-729

  718 
  719 
  720 def _get_batch_detail_handle_error_status(task: SimulationTask) -> TaskInfo:
  721     """Get batch detail and raise error if status is in ERROR_STATES."""
! 722     detail = task.detail()
  723     status = detail.status.lower()
  724     if status in ERROR_STATES:
! 725         _batch_detail_error(task.task_id)
  726     return detail
  727 
  728 
  729 def get_status(task_id: TaskId) -> str:

Lines 734-742

  734     task_id : str
  735         Unique identifier of task on server.  Returned by :meth:`upload`.
  736     """
  737     task = TaskFactory.get(task_id)
! 738     if task._is_batch_type():
  739         return _get_batch_detail_handle_error_status(task).status
  740     else:
  741         task_info = get_info(task_id)
  742         status = task_info.status

Lines 1196-1206

  1196     max_detail_tasks: int = 20,
  1197 ) -> None:
  1198     """Monitor modeler batch progress with aggregate and per-task views."""
  1199     console = get_logging_console() if verbose else None
! 1200     task = SimulationTask.get(task_id=task_id)
  1201     detail = _get_batch_detail_handle_error_status(task)
! 1202     name = detail.taskName or "modeler_batch"
  1203     group_id = detail.groupId
  1204     status = detail.status.lower()
  1205 
  1206     # Non-verbose path: poll without progress bars then return

Lines 1341-1349

  1341         Optional callback function called when downloading file with ``bytes_in_chunk`` as argument.
  1342 
  1343     """
  1344     task = TaskFactory.get(task_id, verbose=False)
! 1345     if task._is_batch_type():
  1346         raise NotImplementedError("Operation not implemented for modeler batches.")
  1347     info = get_info(task_id, verbose=False)
  1348     remote_sim_file = SIM_FILE_HDF5_GZ
  1349     if info.taskType == "MODE_SOLVER":

tidy3d/web/core/task_core.py

Lines 158-166

  158         """Check if this task uses the batch/modeler API.
  159 
  160         Default implementation returns False. Overridden in SimulationTask.
  161         """
! 162         return False
  163 
  164     @classmethod
  165     def create(
  166         cls,

Lines 478-488

  478         # Fall back to batch/modeler API
  479         # Use suppress_404 and catch all exceptions to handle unmocked endpoints in tests
  480         try:
  481             resp = http.get(f"rf/task/{task_id}/statistics", suppress_404=True)
! 482             if resp:
! 483                 task_type = resp.get("taskType") if isinstance(resp, dict) else None
! 484                 return SimulationTask(taskId=task_id, taskType=task_type)
  485         except Exception:
  486             pass
  487 
  488         td.log.error(f"The requested task ID '{task_id}' does not exist.")

Lines 511-530

  511         TaskInfo
  512             An object containing the task's latest data.
  513         """
  514         if self._is_batch_type():
! 515             resp = http.get(f"rf/task/{self.task_id}/statistics")
  516             # Transform batch response to unified TaskInfo format
! 517             if isinstance(resp, dict):
  518                 # Map batch field names to unified TaskInfo field names
! 519                 if "name" in resp:
! 520                     resp["taskName"] = resp.pop("name")
  521                 # Add taskId from the object itself (not in batch API response)
! 522                 resp["taskId"] = self.task_id
  523                 # Coerce null collection fields to sensible defaults
! 524                 if resp.get("tasks") is None:
! 525                     resp["tasks"] = []
! 526             return TaskInfo(**(resp or {}))
  527         else:
  528             resp = http.get(f"tidy3d/tasks/{self.task_id}/detail")
  529             return TaskInfo(**{"taskId": self.task_id, "taskType": self.task_type, **resp})

Lines 663-679

  663             protocol_version = http_util.get_version()
  664 
  665         if self._is_batch_type():
  666             # TODO: add support for pay_type and priority arguments for batch tasks
! 667             if pay_type != PayType.AUTO:
! 668                 raise NotImplementedError(
  669                     "The 'pay_type' argument is not yet supported for batch tasks."
  670                 )
! 671             if priority is not None:
! 672                 raise NotImplementedError(
  673                     "The 'priority' argument is not yet supported for batch tasks."
  674                 )
! 675             http.post(
  676                 f"rf/task/{self.task_id}/submit",
  677                 {
  678                     "solverVersion": solver_version,
  679                     "protocolVersion": protocol_version,

Lines 853-861

  853         """Abort the current task on the server."""
  854         if not self.task_id:
  855             raise ValueError("Task id not found.")
  856         if self._is_batch_type():
! 857             return http.put(f"rf/task/{self.task_id}/abort", {})
  858         return http.put(
  859             "tidy3d/tasks/abort", json={"taskType": self.task_type, "taskId": self.task_id}
  860         )

Lines 882-894

  882         -------
  883         requests.Response
  884             The server's response to the check request.
  885         """
! 886         if not self._is_batch_type():
! 887             raise NotImplementedError("The 'check' method is only available for batch tasks.")
! 888         if protocol_version is None:
! 889             protocol_version = _get_protocol_version()
! 890         return http.post(
  891             f"rf/task/{self.task_id}/check",
  892             {
  893                 "solverVersion": solver_version,
  894                 "protocolVersion": protocol_version,

Copy link
Contributor

@marcorudolphflex marcorudolphflex left a comment

Choose a reason for hiding this comment

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

As you already pointed out, it may be not be the most elegant solution - but still not bad to burn code. But don't have a strong opinion on that.

except WebNotFoundError as e:
td.log.error(f"The requested task ID '{task_id}' does not exist.")
raise e
resp = http.get(f"rf/task/{task_id}/statistics", suppress_404=True)
Copy link
Contributor

Choose a reason for hiding this comment

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

why is that in a try block at all when we suppress 404? Do we want to let other exceptions pass?

task = SimulationTask(**resp) if resp else None
return task
td.log.error(f"The requested task ID '{task_id}' does not exist.")
raise WebNotFoundError("Resource not found (HTTP 404).")
Copy link
Contributor

Choose a reason for hiding this comment

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

404 is not guaranteed here, is it?

"""
# Try simulation API first (most common case)
# Use suppress_404 to avoid error logging when falling back to batch API
resp = http.get(f"tidy3d/tasks/{task_id}/detail", suppress_404=True)
Copy link
Contributor

Choose a reason for hiding this comment

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

can't we infer the endpoint from the task type?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants