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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
fb231a0
Initial content
dandavison Jan 30, 2025
6e6a28a
Generate API docs
dandavison Jun 9, 2025
c4c9740
Changes based on review comments
dandavison Jun 9, 2025
89fd030
Move types
dandavison Jun 9, 2025
4c6bdc8
Eliminate Operation.key
dandavison Jun 10, 2025
6367411
Use Mapping for headers
dandavison Jun 10, 2025
a94f181
s/status/state/
dandavison Jun 10, 2025
29a5504
Delete ServiceHandlerDefinition
dandavison Jun 10, 2025
a5cc3b4
Add failing test
dandavison Jun 10, 2025
26f5522
Bug fix
dandavison Jun 10, 2025
e67dbef
Remove MISSING_TYPE
dandavison Jun 11, 2025
696da1b
Use None as the sentinel value
dandavison Jun 11, 2025
a75c8ee
Test output covariance
dandavison Jun 11, 2025
a029d06
Make output covariance tests pass
dandavison Jun 11, 2025
090edff
Test input contravariance
dandavison Jun 11, 2025
ec039c1
Make input contravariance tests pass
dandavison Jun 11, 2025
201ab95
Fix bugs found by Cursor BugBot
dandavison Jun 11, 2025
1d65a51
Minor fixups
dandavison Jun 12, 2025
c71f0f0
Move http client to Temporal SDK
dandavison Jun 13, 2025
48ad60e
Do not require cause for HandlerError
dandavison Jun 14, 2025
87c4adb
Clean-up AI-authored test
dandavison Jun 15, 2025
79eb6a1
Failg test for service definition inheritance
dandavison Jun 15, 2025
91855da
Add tests of service defn inheritance
dandavison Jun 15, 2025
dbcc009
Clean up test
dandavison Jun 15, 2025
79d73e0
Clean up test
dandavison Jun 15, 2025
ce33dd4
service decorator: refactor
dandavison Jun 15, 2025
a63024a
service decorator: refactor
dandavison Jun 15, 2025
c92d4c2
Cleanup
dandavison Jun 16, 2025
abb05cc
Use default Operation constructor
dandavison Jun 15, 2025
23b3fa6
Use get_annotations shim
dandavison Jun 15, 2025
45c076a
Refactor
dandavison Jun 15, 2025
810d0cb
Refactor
dandavison Jun 15, 2025
4371d45
Refactor: ServiceDefinition.from_user_clas
dandavison Jun 15, 2025
3548e04
Use ServiceDefinition if already computed
dandavison Jun 15, 2025
d604a66
Use first operation encountered in mro
dandavison Jun 15, 2025
8fd0f06
Refactor: recursion
dandavison Jun 16, 2025
d86d67e
Test: use different input and output types
dandavison Jun 16, 2025
38e5a91
Validate service definition on creation
dandavison Jun 16, 2025
e48eb12
Support sync and async users
dandavison Jun 16, 2025
d443b29
Evolve collection of operations
dandavison Jun 17, 2025
8c06955
Collect operations from attributes as well as annotations
dandavison Jun 17, 2025
e93c4c0
Refactor
dandavison Jun 17, 2025
1b6fd90
Fixups
dandavison Jun 17, 2025
7280381
Use keys throughout; map to name overrides at end
dandavison Jun 17, 2025
a063676
Fix operation merge edge case
dandavison Jun 17, 2025
ef55dad
Move Link and OperationInfo to top-level
dandavison Jun 17, 2025
b632074
Reorganize directory structure
dandavison Jun 17, 2025
1df5c7e
Revert handler rename
dandavison Jun 18, 2025
cefb8fd
Import order
dandavison Jun 19, 2025
311eda4
Rename: SyncExecutor -> Executor
dandavison Jun 19, 2025
0bd864b
TODO: Executor
dandavison Jun 19, 2025
ca7af9a
Do not require Executor wrapper
dandavison Jun 19, 2025
e4652de
Rename with Sync/Async suffixes
dandavison Jun 19, 2025
5b659d9
Reorganize
dandavison Jun 19, 2025
449f28a
Upgrade ruff
dandavison Jun 19, 2025
bbd6bff
combine-as-imports formatter config
dandavison Jun 20, 2025
1e4d8fe
Backport inspect.get_annotations from 3.13.5
dandavison Jun 21, 2025
996da25
Unskip test
dandavison Jun 21, 2025
7255c49
Docstring
dandavison Jun 22, 2025
05fc6fe
Cleanup
dandavison Jun 22, 2025
ba86bdd
Default to async
dandavison Jun 23, 2025
96e1618
Refactor types
dandavison Jun 23, 2025
af99478
Switch to SyncOperationHandler
dandavison Jun 23, 2025
3aaa53a
s/start_method/start/
dandavison Jun 23, 2025
f5a1cae
Implement fetch_operation_info and fetch_operation_result on Handler
dandavison Jun 24, 2025
043f4af
Update get_start_method_input_and_output_types_annotations
dandavison Jun 24, 2025
95f7874
Delete unused get_start_method types utility
dandavison Jun 24, 2025
8c3be18
Switch to SyncOperationHandler.from_callable
dandavison Jun 25, 2025
721f3d1
Rename test
dandavison Jun 25, 2025
770ffbc
Move OperationInfo to top level
dandavison Jun 25, 2025
5a88b85
Cleanup
dandavison Jun 25, 2025
a60cb3a
Test all operation methods
dandavison Jun 25, 2025
964a99a
Cleanup
dandavison Jun 25, 2025
69c501d
Cleanup
dandavison Jun 25, 2025
b6e792c
Revert "Delete unused get_start_method types utility"
dandavison Jun 25, 2025
a938cfd
New version of sync_operation decorator
dandavison Jun 25, 2025
ef3aeb9
Support name override, add overloads
dandavison Jun 25, 2025
56f86fe
Make sync_operation_handler support sync start methods
dandavison Jun 26, 2025
a6a2e97
Fix callable instances
dandavison Jun 26, 2025
e4a8762
rename: @sync_operation
dandavison Jun 26, 2025
cf79d71
Clean up imports in tests
dandavison Jun 26, 2025
0e2a28b
Remove @operation_handler from public API for now
dandavison Jun 26, 2025
32f9e55
CI test on 3.9, 3.13, 3.14
dandavison Jun 26, 2025
f87ee04
Activate vercel publishing of apidocs
dandavison Jun 26, 2025
0508c56
Start adding README content
dandavison Jun 26, 2025
ed75834
uv remove --group dev httpx
dandavison Jun 26, 2025
4494e2a
Make Content.data required
dandavison Jun 26, 2025
5b2fc91
Rename method: from_user_class -> from_class
dandavison Jun 26, 2025
280b8ab
Relocate OperationError
dandavison Jun 26, 2025
59f3f1b
Make dataclasses `frozen`
dandavison Jun 26, 2025
7eb03d5
Make OperationContext non-instantiatable
dandavison Jun 26, 2025
937511f
Make dataclasses frozen=True
dandavison Jun 26, 2025
42a1e34
Make request ID required
dandavison Jun 26, 2025
11d7d4e
Eliminate types.module
dandavison Jun 26, 2025
56f8c23
Add FetchOperationResultContext timeout with stub implementation
dandavison Jun 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: CI

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?

pull_request:
branches: [ main ]

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?

runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.13', '3.14']

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
uv sync

- name: Lint
run: |
uv run ruff format --check
uv run ruff check

- name: Type check
# TODO(preview): Get both passing
run: |
uv run pyright . || true
uv run mypy --check-untyped-defs . || true

- name: Run tests
run: |
uv run pytest --cov=src --cov-report=html:coverage_html_report

- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-html-report-${{ matrix.python-version }}
path: coverage_html_report/

- 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.

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.

env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
run: npx vercel deploy build/apidocs -t ${{ secrets.VERCEL_TOKEN }} --prod --yes
13 changes: 4 additions & 9 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

# Virtual environments
__pycache__
.venv
apidocs
dist
docs
1 change: 0 additions & 1 deletion .python-version

This file was deleted.

19 changes: 19 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
### Type-checking, Linting, and Formatting

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

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?

```

### Formatting
```
uv run ruff check --select I --fix
uv run ruff format
```

### API docs
```
uv run pydoctor src/nexusrpc
```
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Temporal Technologies Inc. All Rights Reserved

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Nexus Python SDK

## What is Nexus?

[Nexus](https://github.com/nexus-rpc/) is a synchronous RPC protocol. Arbitrary duration operations are modeled on top of
a set of pre-defined synchronous RPCs.

A Nexus caller calls a handler. The handler may respond inline (synchronous response) or
return a token referencing the ongoing operation (asynchronous response). The caller can
cancel an asynchronous operation, check for its outcome, or fetch its current state. The
caller can also specify a callback URL, which the handler uses to deliver the result of
an asynchronous operation when it is ready.

## Installation

```
uv add nexus-rpc
```
or
```
pip install nexus-rpc
```

## Usage

The SDK currently supports two use cases:

1. As an end user, defining Nexus services and operations.

2. Implementing a Nexus handler that can accept and respond to incoming Nexus requests, dispatching to the corresponding user-defined Nexus operation.

The handler in (2) would form part of a server or worker that processes Nexus requests; the SDK does not yet provide reference implementations of these, or of a Nexus client.

### Defining Nexus services and operations

```python
from dataclasses import dataclass

import nexusrpc
from nexusrpc.handler import StartOperationContext, service_handler, sync_operation
Comment on lines +39 to +40

Choose a reason for hiding this comment

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

Why do you only sometimes import from?



@dataclass
class MyInput:
name: str


@dataclass
class MyOutput:
message: str


@nexusrpc.service
class MyNexusService:
my_sync_operation: nexusrpc.Operation[MyInput, MyOutput]


@service_handler(service=MyNexusService)
class MyNexusServiceHandler:
# You can create an __init__ method accepting what is needed by your operation
# handlers to handle requests. You will typically instantiate your service handler class
# when starting your Nexus server/worker.

# This is a Nexus operation that responds synchronously to all requests. That means
# that the `start` method returns the final operation result.
#
# Sync operations are free to make arbitrary network calls.
@sync_operation
async def my_sync_operation(
self, ctx: StartOperationContext, input: MyInput
) -> MyOutput:
return MyOutput(message=f"Hello {input.name}!")
```

30 changes: 28 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,35 @@ readme = "README.md"
authors = [
{ name = "Dan Davison", email = "[email protected]" }

Choose a reason for hiding this comment

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

Authors should be the Temporal SDK team.

]
requires-python = ">=3.13"
dependencies = []
requires-python = ">=3.9"
dependencies = [
"typing-extensions>=4.12.2",
]

[dependency-groups]
dev = [
"mypy>=1.15.0",
"pydoctor>=25.4.0",
"pyright>=1.1.400",
"pytest>=8.3.5",
"pytest-asyncio>=0.26.0",
"pytest-cov>=6.1.1",
"pytest-pretty>=1.3.0",
"ruff>=0.12.0",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
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.

Python packages don't typically put code in src, instead they use the name of the package directly (nexusrpc should be in the root folder).


[tool.ruff]
target-version = "py39"

[tool.ruff.lint.isort]
combine-as-imports = true
2 changes: 0 additions & 2 deletions src/nexus_rpc/__init__.py

This file was deleted.

68 changes: 68 additions & 0 deletions src/nexusrpc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from dataclasses import dataclass
from enum import Enum

from ._serializer import Content as Content, LazyValue as LazyValue
from ._service import (
Operation as Operation,
ServiceDefinition as ServiceDefinition,
service as service,
)
from ._types import InputT as InputT, OutputT as OutputT


@dataclass(frozen=True)
class Link:
"""
Link contains a URL and a Type that can be used to decode the URL.
Links can contain any arbitrary information as a percent-encoded URL.
It can be used to pass information about the caller to the handler, or vice versa.
"""

# The URL must be percent-encoded.
url: str
# Can describe an actual data type for decoding the URL. Valid chars: alphanumeric, '_', '.',
# '/'
type: str
Comment on lines +21 to +25

Choose a reason for hiding this comment

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

These comments don't count as docstrings AFAIU, They should (not just here, throughout the entire codebase).



class OperationState(Enum):
"""
Describes the current state of an operation.
"""

SUCCEEDED = "succeeded"
FAILED = "failed"
CANCELED = "canceled"
RUNNING = "running"


@dataclass(frozen=True)
class OperationInfo:
"""
Information about an operation.
"""

# Token identifying the operation (returned on operation start).
token: str

# The operation's state
state: OperationState


class OperationErrorState(Enum):
"""
The state of an operation as described by an OperationError.
"""

FAILED = "failed"
CANCELED = "canceled"


class OperationError(Exception):
"""
An error that represents "failed" and "canceled" operation results.
"""

def __init__(self, message: str, *, state: OperationErrorState):
super().__init__(message)
self.state = state
Loading
Loading