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

Initial implementation #1

wants to merge 95 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
Copy link

@cretz cretz left a comment

Choose a reason for hiding this comment

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

Started adding some more comments here but started seeing a lot of past comments that still apply so unsure if ready for second round. Stopped at about src/nexus_rpc/nexusrpc/handler/_core.py but will pick back up when told ready for re-review.

runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
Copy link

Choose a reason for hiding this comment

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

Bump here

User provided keys are treated case-insensitively.
"""

data: Optional[bytes] = None
Copy link

Choose a reason for hiding this comment

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

Pedantic, but I don't think we should default to None, I think any instantiator of this should explicitly pass None if they have no data, but creating a Content should always required data IMO. Same for not having default stream on LazyValue constructor.

Copy link
Contributor Author

@dandavison dandavison Jun 26, 2025

Choose a reason for hiding this comment

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

[Content] ... Pedantic, but I don't think we should default to None

Agree, done.

Copy link

Choose a reason for hiding this comment

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

IMO there aren't enough types here to deserve its own user-facing module. Just put InputT and OutputT in the nexusrpc module. The other ones don't really have any bounds/constraints, so there's not really any value in even having them be user facing, they can just be TypeVars where needed as needed (or shared across files, but don't need to be user facing).

@@ -0,0 +1,133 @@
# See
# https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
Copy link

Choose a reason for hiding this comment

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

3.9 will be EOL by October. I'd be ok if we went ahead and said Python Nexus is 3.10+ and remove any 3.9 polyfills/workarounds

Copy link
Contributor Author

@dandavison dandavison Jun 26, 2025

Choose a reason for hiding this comment

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

OK let's consider this for Preview; added to list

provided, the default behavior for the error type is used.
"""
super().__init__(message)
self.__cause__ = cause
Copy link

Choose a reason for hiding this comment

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

Hrmm, Python exceptions do not usually accept cause in constructors, they usually rely on the from clause during raise or in the rare case of needing to create the exception with a cause without a raise, users manually set __cause__ themselves after creation. Having it in constructor is confusing, especially in cases where it's unclear it may override.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ack. I haven't resolved this yet.


def __init__(
self,
user_service_handlers: Sequence[Any],
Copy link

Choose a reason for hiding this comment

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

If this is a base handler, why accept these? What if I want to implement my base handler without any user service handlers? I think we should have a true abstract handler with no implementation defaults (i.e. basically an interface). Can we move all of this constructor stuff to the Handler class and get rid of this constructor? If we need some kind of shared utilities between Handler and SyncHandler so they can accept similar constructor arguments, that can make sense (or even a shared base class), but IMO we still need a no-impl/no-constructor interface for handlers.

# TODO(prerelease): we have a syncio module now housing the syncio version of
# SyncOperationHandler. If we're retaining that then this (and an async version of
# LazyValue) should go in there.
class SyncioHandler(BaseHandler):
Copy link

Choose a reason for hiding this comment

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

Is there a use case for this at this time? If so, we should eagerly check at construction that all calls on all operation handlers are not async def based. And we should require the executor on constructor.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a use case for this at this time?

How else will a team using a non-async/await framework such as Django implement a Nexus server/worker?

Comment on lines +189 to +193
if not self.executor:
raise RuntimeError(
"Operation start handler method is not an `async def` but "
"no executor was provided to the Handler constructor. "
)
Copy link

Choose a reason for hiding this comment

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

Can we detect this at construction time? Meaning can we check in the constructor that if an executor is not present, all calls on all operation handlers are async def?

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.

Looks great!


on:
push:
branches: [ main, v0 ]

Choose a reason for hiding this comment

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

Why v0? Is it to run CI in this PR? Will you remove it eventually?


jobs:
test:
# TODO(preview): other platforms

Choose a reason for hiding this comment

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

Do you plan on addressing this or tracking in an issue?

- name: Build API docs
run: uv run pydoctor src/nexusrpc

- name: Deploy prod API docs

Choose a reason for hiding this comment

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

Is this set up? What domain are you planning to push to? I was planning on using GH docs for TS for now.

run: uv run pydoctor src/nexusrpc

- name: Deploy prod API docs
if: ${{ github.ref == 'refs/heads/main' }}

Choose a reason for hiding this comment

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

nit: we should publish docs on a stable release not on push to main. We made this mistake with Temporal SDKs, let's not repeat.

Comment on lines +4 to +7
uv run pyright
uv run mypy --check-untyped-defs .
uv run ruff check --select I
uv run ruff format --check

Choose a reason for hiding this comment

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

Can all of this be packaged in one command?


from typing import TypeVar

# Operation input type

Choose a reason for hiding this comment

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

Can you make sure these comments are considered docstrings?

Comment on lines +22 to +30
BAD_REQUEST = "BAD_REQUEST"
UNAUTHENTICATED = "UNAUTHENTICATED"
UNAUTHORIZED = "UNAUTHORIZED"
NOT_FOUND = "NOT_FOUND"
RESOURCE_EXHAUSTED = "RESOURCE_EXHAUSTED"
INTERNAL = "INTERNAL"
NOT_IMPLEMENTED = "NOT_IMPLEMENTED"
UNAVAILABLE = "UNAVAILABLE"
UPSTREAM_TIMEOUT = "UPSTREAM_TIMEOUT"

Choose a reason for hiding this comment

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

Worth documenting each of these error types.

)
if self.operation_handlers:
msg += f": {', '.join(sorted(self.operation_handlers.keys()))}"
msg += "."

Choose a reason for hiding this comment

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

Is it common to put periods at the end of error messages in Python? Doesn't seem like the standard library does this.

Comment on lines +430 to +431
# TODO(prerelease): Do we want to require users to create this wrapper? Two
# alternatives:

Choose a reason for hiding this comment

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

I think just use the standard concurrent futures executor interface here.

)

method_name = get_callable_name(start)
operation_handler_factory.__nexus_operation__ = nexusrpc.Operation(

Choose a reason for hiding this comment

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

Why does the factory need to have this attribute?

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