Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Initial implementation #1

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 175 commits into
base: main
Choose a base branch
from
Open

Initial implementation #1

wants to merge 175 commits into from

Conversation

dandavison
Copy link
Contributor

@dandavison dandavison commented Apr 2, 2025

Nexus Python SDK initial content.

For an overview of the user experience of implementing and calling Nexus handlers, see the Temporal sample, and tests in this PR.

@dandavison dandavison force-pushed the v0 branch 2 times, most recently from a0db8c4 to 2733044 Compare April 22, 2025 20:52
@dandavison dandavison changed the title Initial prototyping Initial implementation May 8, 2025
@dandavison dandavison force-pushed the v0 branch 3 times, most recently from 4c284df to 9bdf24d Compare May 27, 2025 20:13
@dandavison dandavison force-pushed the v0 branch 4 times, most recently from fbb91b0 to 659557b Compare May 30, 2025 01:42
@dandavison dandavison requested a review from Copilot May 30, 2025 01:44
Copilot

This comment was marked as off-topic.

@nexus-rpc nexus-rpc deleted a comment from Copilot AI May 30, 2025
@dandavison dandavison force-pushed the v0 branch 6 times, most recently from 96f9be3 to 9711999 Compare June 9, 2025 20:58
@dandavison dandavison marked this pull request as ready for review June 9, 2025 21:00
@dandavison dandavison force-pushed the v0 branch 6 times, most recently from fa76eac to 706b354 Compare June 9, 2025 22:27
packages = ["src/nexusrpc"]

[tool.pyright]
include = ["src", "tests"]

Choose a reason for hiding this comment

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

Hmm... maybe this pattern started being adopted more, I have typically seen Python project not have an src directory.

Choose a reason for hiding this comment

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

Clean this up?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like this comment was made on a deleted file.

Choose a reason for hiding this comment

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

Wonder if we should be exporting handler here too so users can use nexusrpc.handler.service_handler directly without having to import handler separately.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking the same. I've done it.

Comment on lines 22 to 27
from ._serializer import Content as Content, LazyValue as LazyValue
from ._service import (
Operation as Operation,
ServiceDefinition as ServiceDefinition,
service as service,
)

Choose a reason for hiding this comment

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

Why do you need import as if you're not renaming the items?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK so

(a) Type checkers complain if the imported name is unused. So this style is just how modern Python does it, and how the de-factor formatter ruff formats it.

(b) But you're right, I recently added __all__ collections of exported names to all __init__.pys, because the API docs needed it, and with that we no longer need the as stuff.


Includes information from the request."""

request_id: str

Choose a reason for hiding this comment

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

FTR, an empty request ID is not rejected by the server. This string may be empty.

from nexusrpc.handler._core import BaseServiceCollectionHandler
from nexusrpc.handler._util import get_start_method_input_and_output_type_annotations

from ...handler._operation_handler import OperationHandler

Choose a reason for hiding this comment

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

Given that the OperationHandler works for both sync and async, this separate syncio module seems like overkill.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What arrangement do you prefer? We have the following components that at the moment have sync and asynv versions:

  • Handler
  • LazyValue
  • sync_operation

In any case, I think that what we should do is delete all of the above sync support, until we're sure how we want to present it. I'll do that in a follow-up PR.

To be clear for anyone else reading: when we "delete sync support", the SDK will continue to support user operation handlers with def methods (as long as an executor has been supplied).

Copy link

@bergundy bergundy left a comment

Choose a reason for hiding this comment

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

Added a few comments but AFAIC, there's nothing major.

...


class LazyValueT(Protocol):
Copy link

Choose a reason for hiding this comment

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

But I thought we were removing sync form? At which point is there value here?

"""Request or response data."""


class Serializer(Protocol):
Copy link

Choose a reason for hiding this comment

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

It's not incorrect, it's just too limiting and not common for these use cases. I think we should revisit this since it makes sense for serializer to be a base class one extends instead of duck-typed.

...


class LazyValueT(Protocol):
Copy link

Choose a reason for hiding this comment

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

Yeah, I think my main problem was that this didn't make sense as a protocol

)
return defn

def _validation_errors(self) -> list[str]:
Copy link

Choose a reason for hiding this comment

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

Can we get a general decision on operation-less Nexus service contracts and whether we generally believe they are valid? A dataclass with no fields may be valid, but a service with no calls may not be.

from nexusrpc.handler._operation_handler import OperationHandler


def get_service_definition(
Copy link

Choose a reason for hiding this comment

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

Concern about the name is different than concern about the placement. I think there's not a need to clutter the top-level API with a top-level function here. Whether we call this from_class or get_from_class or whatever I think is a secondary question (I don't see a from_whatever to be a constructor, but can be documented away).



@dataclass(frozen=True)
class OperationContext:
Copy link

Choose a reason for hiding this comment

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

I think it's also a signal of intent as well (i.e. "this is always used as a base class")

...


class BaseServiceCollectionHandler(AbstractHandler):
Copy link

Choose a reason for hiding this comment

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

👍 Would definitely like to reduce class/abstraction count if we can

)
self.service_handlers[sh.service.name] = sh

async def start_operation(
Copy link

Choose a reason for hiding this comment

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

👍 I trust the removal of the unused syncio stuff is being tracked

def service_handler(cls: Type[ServiceHandlerT]) -> Type[ServiceHandlerT]: ...


# TODO(preview): allow service to be provided as positional argument?
Copy link

Choose a reason for hiding this comment

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

I guess what I mean is why is service before * in the impl but after the * here?

self.headers = headers
self.stream = stream

def consume(self, as_type: Optional[Type[Any]] = None) -> Any:
Copy link

Choose a reason for hiding this comment

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

A thought while we're here - arguably a sync implementation should allow async operations and provide an event loop to run them on via run_coroutine_threadsafe similar to how an executor is provided to go from async to sync.

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.

5 participants