"""Common utilities for workflow packages."""importfunctoolsimportinspectfromtypingimportDict,Optional
[docs]defcheck_dependencies(packages:Dict[str,str])->None:"""Check that optional packages are importable; raise with install hints. Parameters ---------- packages : dict Mapping of import name to install instruction, e.g. ``{"geopandas": "pip install geopandas"}``. Raises ------ ImportError If any package cannot be imported. """importimportlibmissing=[]formodule_name,install_hintinpackages.items():try:importlib.import_module(module_name)exceptImportError:missing.append(f" {module_name} → {install_hint}")ifmissing:msg="Missing optional dependencies:\n"+"\n".join(missing)raiseImportError(msg)
[docs]defparse_quantity(s:str):"""Parse a string like ``"1000 km"`` into a ``uw.quantity``. Parameters ---------- s : str Value and unit separated by whitespace, e.g. ``"1e21 Pa*s"``. Returns ------- UWQuantity The parsed quantity. """importunderworld3asuwparts=s.strip().split(None,1)iflen(parts)==2:value,unit=partsreturnuw.quantity(float(value),unit)# Dimensionless numberreturnuw.quantity(float(parts[0]),"")
[docs]defshow_source(fn)->None:"""Display the source code of *fn* with syntax highlighting. In Jupyter the source is rendered as a Python code block via ``IPython.display.Markdown``. In a terminal it falls back to plain ``print``. Works on any function or method, not just workflow steps. Parameters ---------- fn : callable The function whose source you want to inspect. """source=inspect.getsource(fn)try:fromIPython.displayimportMarkdown,displaydisplay(Markdown(f"```python\n{source}\n```"))exceptImportError:print(source)
[docs]defworkflow_step(fn=None,*,description=None,produces=None,requires=None):"""Mark a function as a workflow helper step. Attaches a ``.view()`` method that displays the function source (syntax-highlighted in Jupyter). The decorator is transparent — the wrapped function behaves identically to the original. Can be used with or without arguments:: @workflow_step def create_mesh(config): ... @workflow_step(description="Build the simulation mesh") def create_mesh(config): ... @workflow_step( description="Adapt mesh near fault surfaces", produces=["adapted_mesh"], requires=["mesh", "fault_surfaces"], ) def adapt_mesh(mesh, faults, config): ... The *description* is stored as ``fn.workflow_description`` and the *produces*/*requires* lists document the DAG of product dependencies (used by ``view()`` and ``WorkflowProducts``). Parameters ---------- fn : callable, optional The function to decorate (when used without parentheses). description : str, optional Short human-readable description of this step. produces : list of str, optional Product names this step creates. requires : list of str, optional Product names this step depends on. """def_decorate(func):@functools.wraps(func)defwrapper(*args,**kwargs):returnfunc(*args,**kwargs)# Attach metadatawrapper._is_workflow_step=Truewrapper.workflow_description=descriptionorfunc.__doc__or""wrapper.workflow_produces=list(produces)ifproduceselse[]wrapper.workflow_requires=list(requires)ifrequireselse[]# Attach view() — shows source in Jupyter or terminaldef_view():show_source(func)wrapper.view=_viewreturnwrapper# Support both @workflow_step and @workflow_step(description=...)iffnisnotNone:return_decorate(fn)return_decorate