Skip to content

feat: Python runtime interface - #651

Open
johnchildren wants to merge 6 commits into
mainfrom
python-runtime-interface
Open

johnchildren wants to merge 6 commits into
mainfrom
python-runtime-interface

Conversation

@johnchildren

Copy link
Copy Markdown
Collaborator

Adds a pythonic interface to the runtime that can be used when a user wants to use the runtime in their python interpreter. The API should be flexible enough to be usable with runtime daemons running on the users system or remotely in future.

@johnchildren

Copy link
Copy Markdown
Collaborator Author

Things to include:

  1. Type stubs for the new interface
  2. A test for running multiple workflows on the same runtime simultaneously
  3. Default file location creation (maybe another issue)

with runtime:
workflow_id = await runtime.save_workflow("will_fail", graph)
run_id = await runtime.start_new_run(workflow_id, {})
with pytest.raises(ValueError) as raises:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I guess it's probably worth adding a custom exception here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think the point here was that will_fail just raises a ValueError there is no specific I chose this

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

makes sense, I'm experimenting with trying to make some richer exceptions which the locations that failed so it might make sense for them to be a custom type.

@johnchildren
johnchildren force-pushed the python-runtime-interface branch 5 times, most recently from 4f0f854 to 639d646 Compare September 15, 2026 15:35
@johnchildren
johnchildren marked this pull request as draft September 15, 2026 16:02
Comment thread tierkreis/tests/controller/test_resume.py Outdated
@johnchildren
johnchildren force-pushed the python-runtime-interface branch from 639d646 to 469821d Compare September 16, 2026 10:10
if "defaults" in name:
pytest.skip("default arguments not supported")

run_outputs = run_workflow(name, g, inputs)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

possibly we can just remove this function now? Or move the implementation into python

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I orginally wrote it to have a more user friendly way (hiding the storage/executor) I think with the new interface this can now be deprecated

@johnchildren
johnchildren marked this pull request as ready for review September 16, 2026 12:17
Comment thread tierkreis/src/runtime.rs
///
/// # Errors
///
/// Will

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🗡

Comment thread tierkreis/src/lib.rs Outdated
Comment thread tierkreis/src/lib.rs
}

#[pyo3(signature = (run_id, attempt=0, /, timeout=604_800))]
async fn wait_for(&self, run_id: Uuid, attempt: u32, timeout: u64) -> PyResult<()> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we return UUID from start_new_run but expect UUID,attempt with wait_for is this intended?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Sort of, I figured as starting a new run always gives attempt 0 it wasn't necessary to return it from start_new_run (though this is a difference to the rust interface). Notice that the pyo3 signature defines a default value for attempt

Co-authored-by: Philipp Seitz <philipp.seitz@quantinuum.com>
Comment thread tierkreis/src/lib.rs
}

// TODO: I'm not sure how debuggable this is
#[pyclass(name = "RuntimeConfig")]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It's not really possible to create one of these objects directly right now as they only exist as arguments to new_from_config. Possibly we could start returning them from the runtime though if users want to debug it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

>>> runtime = await new_from_config({})
Traceback (most recent call last):
  File "/nix/store/d20zkzz2dwl57ncmpi89rmfbvb2cs5c9-python3-3.13.15-env/lib/python3.13/concurrent/futures/_base.py", line 460, in result
    return self.__get_result()
           ~~~~~~~~~~~~~~~~~^^
  File "/nix/store/d20zkzz2dwl57ncmpi89rmfbvb2cs5c9-python3-3.13.15-env/lib/python3.13/concurrent/futures/_base.py", line 402, in __get_result
    raise self._exception
  File "<python-input-2>", line 1, in <module>
    runtime = await new_from_config({})
              ^^^^^^^^^^^^^^^^^^^^^^^^^
Exception: missing field `asset_storage`
while processing 'config'

example error

Comment thread tierkreis/src/lib.rs
}
}

// TODO: I'm not sure how debuggable this is

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

>>> with runtime:
...     id = await runtime.save_workflow(None, {})
...
Traceback (most recent call last):
  File "/nix/store/d20zkzz2dwl57ncmpi89rmfbvb2cs5c9-python3-3.13.15-env/lib/python3.13/concurrent/futures/_base.py", line 460, in result
    return self.__get_result()
           ~~~~~~~~~~~~~~~~~^^
  File "/nix/store/d20zkzz2dwl57ncmpi89rmfbvb2cs5c9-python3-3.13.15-env/lib/python3.13/concurrent/futures/_base.py", line 402, in __get_result
    raise self._exception
  File "<python-input-7>", line 2, in <module>
    id = await runtime.save_workflow(None, {})
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: AttributeError: 'dict' object has no attribute 'model_dump_json'
while processing 'workflow'

Comment thread tierkreis/src/lib.rs
}
}

fn __enter__(&mut self) {

@johnchildren johnchildren Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This isn't working quite as intended:

with runtime:
    ...

with runtime:
    ...

Doesn't work as we consume the event listener. Possibly a fixable bug but a kind of annoying design flaw otherwise.

Comment thread tierkreis/src/lib.rs
}
}

// TODO: I'm not sure how debuggable this is

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

>>> with runtime:
...     wf_id = await runtime.save_workflow(None, compile_simulate())
...     run_id = await runtime.start_new_run(wf_id, {"a": max})
...
Traceback (most recent call last):
  File "/nix/store/d20zkzz2dwl57ncmpi89rmfbvb2cs5c9-python3-3.13.15-env/lib/python3.13/concurrent/futures/_base.py", line 460, in result
    return self.__get_result()
           ~~~~~~~~~~~~~~~~~^^
  File "/nix/store/d20zkzz2dwl57ncmpi89rmfbvb2cs5c9-python3-3.13.15-env/lib/python3.13/concurrent/futures/_base.py", line 402, in __get_result
    raise self._exception
  File "<python-input-14>", line 3, in <module>
    run_id = await runtime.start_new_run(wf_id, {"a": max})
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: failed to extract enum ValueOrMappingOrBytes ('dict[str, bytes] | ValueOrBytes')
- variant BytesMapping (dict[str, bytes]): TypeError: failed to extract field ValueOrMappingOrBytes::BytesMapping.0, caused by TypeError: 'builtin_function_or_method' object is not an instance of 'bytes'
- variant ValueOrBytes (ValueOrBytes): TypeError: failed to extract field ValueOrMappingOrBytes::ValueOrBytes.0, caused by TypeError: failed to extract enum ValueOrMapping ('Mapping | Value')
- variant Mapping (Mapping): TypeError: failed to extract field ValueOrMapping::Mapping.0, caused by TypeError: failed to extract enum Value ('bool | int | float | str | complex | Sequence | Mapping')
- variant Bool (bool): TypeError: failed to extract field Value::Bool.0, caused by TypeError: 'builtin_function_or_method' object is not an instance of 'bool'
- variant Int (int): TypeError: failed to extract field Value::Int.0, caused by TypeError: 'builtin_function_or_method' object cannot be interpreted as an integer
- variant Float (float): TypeError: failed to extract field Value::Float.0, caused by TypeError: must be real number, not builtin_function_or_method
- variant String (str): TypeError: failed to extract field Value::String.0, caused by TypeError: 'builtin_function_or_method' object is not an instance of 'str'
- variant Complex (complex): TypeError: failed to extract field Value::Complex.0, caused by TypeError: must be real number, not builtin_function_or_method
- variant List (Sequence): TypeError: failed to extract field Value::List.0, caused by TypeError: 'builtin_function_or_method' object is not an instance of 'Sequence'
- variant Dict (Mapping): TypeError: failed to extract field Value::Dict.0, caused by TypeError: 'builtin_function_or_method' object is not an instance of 'dict'
- variant Value (Value): TypeError: failed to extract field ValueOrMapping::Value.0, caused by TypeError: failed to extract enum Value ('bool | int | float | str | complex | Sequence | Mapping')
- variant Bool (bool): TypeError: failed to extract field Value::Bool.0, caused by TypeError: 'dict' object is not an instance of 'bool'
- variant Int (int): TypeError: failed to extract field Value::Int.0, caused by TypeError: 'dict' object cannot be interpreted as an integer
- variant Float (float): TypeError: failed to extract field Value::Float.0, caused by TypeError: must be real number, not dict
- variant String (str): TypeError: failed to extract field Value::String.0, caused by TypeError: 'dict' object is not an instance of 'str'
- variant Complex (complex): TypeError: failed to extract field Value::Complex.0, caused by TypeError: must be real number, not dict
- variant List (Sequence): TypeError: failed to extract field Value::List.0, caused by TypeError: 'dict' object is not an instance of 'Sequence'
- variant Dict (Mapping): TypeError: failed to extract field Value::Dict.0, caused by TypeError: failed to extract enum Value ('bool | int | float | str | complex | Sequence | Mapping')
- variant Bool (bool): TypeError: failed to extract field Value::Bool.0, caused by TypeError: 'builtin_function_or_method' object is not an instance of 'bool'
- variant Int (int): TypeError: failed to extract field Value::Int.0, caused by TypeError: 'builtin_function_or_method' object cannot be interpreted as an integer
- variant Float (float): TypeError: failed to extract field Value::Float.0, caused by TypeError: must be real number, not builtin_function_or_method
- variant String (str): TypeError: failed to extract field Value::String.0, caused by TypeError: 'builtin_function_or_method' object is not an instance of 'str'
- variant Complex (complex): TypeError: failed to extract field Value::Complex.0, caused by TypeError: must be real number, not builtin_function_or_method
- variant List (Sequence): TypeError: failed to extract field Value::List.0, caused by TypeError: 'builtin_function_or_method' object is not an instance of 'Sequence'
- variant Dict (Mapping): TypeError: failed to extract field Value::Dict.0, caused by TypeError: 'builtin_function_or_method' object is not an instance of 'dict'
while processing 'inputs'

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.

2 participants