Skip to content

Temporal Nexus Python proposal #97

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

dandavison
Copy link

@dandavison dandavison commented Apr 15, 2025

This is an early-stage design document for how Nexus functionality will be exposed in the Nexus Python SDK and in the Temporal Python SDK. The design is still evolving, and feedback is welcome.

The design proposed here is being prototyped in this draft PR.

Rendered markdown

@workflow.defn
class EchoCallerWorkflow:
def __init__(self):
self.nexus_client = NexusClient(
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
self.nexus_client = NexusClient(
self.nexus_client = workflow.NexusClient(

self.nexus_client = NexusClient(
MyNexusService, # or string name "my-nexus-service",
"my-nexus-endpoint-name",
schedule_to_close_timeout=timedelta(seconds=30),
Copy link
Member

Choose a reason for hiding this comment

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

I think this needs to be per-operation kwarg. Java uniquely does some of these at the service/stub level because of how it proxies, but all other SDKs can do operation level.

```python
import nexusrpc.handler

@nexusrpc.handler.service(interface=interface.MyNexusService, name="my-service") # import-time check that interface was implemented
Copy link
Member

Choose a reason for hiding this comment

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

Can we use a different name for the decorator besides service? I know being in handler resolves the ambiguity, but I still think we should avoid ambiguous names across the two modules of the nexusrpc project. Also, similar to operation below, when we talk about "service" we are talking about the contract, not ambiguously talking about the contract or implementation. Maybe service_impl or service_handler or something.

```python
import nexusrpc.handler

@nexusrpc.handler.service(interface=interface.MyNexusService, name="my-service") # import-time check that interface was implemented
Copy link
Member

Choose a reason for hiding this comment

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

Not sure interface is a good name here. Maybe it doesn't need to be a kwarg at all?

```python
import nexusrpc.handler

@nexusrpc.handler.service(interface=interface.MyNexusService, name="my-service") # import-time check that interface was implemented
Copy link
Member

Choose a reason for hiding this comment

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

The name parameter here seems like something that should be on the service side not the impl side. It is needed by callers too so it is part of the contract.

- `StartOperationOptions`

```python
@dataclass
Copy link
Member

Choose a reason for hiding this comment

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

Pedantic, but recommend frozen here

class MyNexusService: # instantiated when instantiating worker
# User is free to define custom constructor

@nexusrpc.handler.operation(name="my-invalid-python-identifier")
Copy link
Member

Choose a reason for hiding this comment

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

Like services, names of operations are part of the contract and therefore need to be on the contract side not the impl side

- In the shorthand form, the decorator takes in the `start` handler method and returns a factory method `(Service) -> Operation` (that factory method takes care of creating an operation with the specified start method). Thus at run-time, the method on their service has a signature that differs from what they wrote under the decorator. Decorators in Python are used to effect fairly radical transformations: for example `@property`, or `@contextlib.contextmanager` in the standard library. But the main point in defense of the radicalness of this decorator transformation is that the user is not expected to call this method: it is called by the framework.
- The decorator propagates type annotations on the start method defined on the service to become run-time type annotation objects on the methods defined on the operation [[nexus-sdk PR](https://github.com/nexus-rpc/sdk-python/blob/e318efeb5b615a7dab916af1c5f01fe97ffff841/src/nexusrpc/handler.py#L294), [sdk-python PR](https://github.com/temporalio/sdk-python/blob/08430098aac4d92c3744b1245c5585819abadad3/temporalio/nexus/handler.py#L155-L169)]. This allows, e.g. a Nexus worker to instantiate the correct Python type from payloads received over the wire.

**[ALTERNATIVES CONSIDERED]**
Copy link
Member

Choose a reason for hiding this comment

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

Not sure this is an alternative. Isn't what is below effectively what @nexusrpc.handler.sync_operation is sugar for?

@temporalio.nexus.handler.workflow_operation
async def hello(
self, input: HelloInput, options: nexusrpc.handler.StartOperationOptions
) -> nexusrpc.handler.StartOperationResult[HelloOutput]:
Copy link
Member

Choose a reason for hiding this comment

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

Could have a more specific, clearer subtype to return here, e.g. temporalio.nexus.handler.WorkflowOperationHandler[HelloOutput]

```python
@nexusrpc.handler.service(interface=interface.MyNexusService)
class MyNexusService:
@temporalio.nexus.handler.workflow_operation
Copy link
Member

Choose a reason for hiding this comment

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

Safe to assume this is just shorthand for something like:

def hello(self) -> WorkflowOperationHandler[HelloInput, HelloOutput]:
    return temporalio.nexus.handler.WorkflowOperationHandler(HelloWorkflow.run)

And I could do that myself if I didn't want the decorator (e.g. maybe doing something a bit more dynamic)?

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