-
Notifications
You must be signed in to change notification settings - Fork 1
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
Changes from all commits
fb231a0
6e6a28a
c4c9740
89fd030
4c6bdc8
6367411
a94f181
29a5504
a5cc3b4
26f5522
e67dbef
696da1b
a75c8ee
a029d06
090edff
ec039c1
201ab95
1d65a51
c71f0f0
48ad60e
87c4adb
79eb6a1
91855da
dbcc009
79d73e0
ce33dd4
a63024a
c92d4c2
abb05cc
23b3fa6
45c076a
810d0cb
4371d45
3548e04
d604a66
8fd0f06
d86d67e
38e5a91
e48eb12
d443b29
8c06955
e93c4c0
1b6fd90
7280381
a063676
ef55dad
b632074
1df5c7e
cefb8fd
311eda4
0bd864b
ca7af9a
e4652de
5b659d9
449f28a
bbd6bff
1e4d8fe
996da25
7255c49
05fc6fe
ba86bdd
96e1618
af99478
3aaa53a
f5a1cae
043f4af
95f7874
8c3be18
721f3d1
770ffbc
5a88b85
a60cb3a
964a99a
69c501d
b6e792c
a938cfd
ef3aeb9
41f1008
92a4a14
cb5774b
4bfd25d
68fcac8
4a2d19a
ad9791f
be8fba2
f4a2f42
833a4c6
549b81f
5f24e15
e2cf7fe
cc43cdd
3b6500b
e514cdf
2d90830
8d875cc
657602e
1b3f7d0
f98a884
60c2c9e
b3bb2d3
eaf5913
8b1b1a9
4f22822
1d63015
7d466ac
7f3c6e0
e1f4405
4f24094
6753fc5
69a88e0
02e6010
b8fe8f6
fcad5b8
68f1d0d
8776f9d
97f22e0
d5a94b6
a607809
f474c03
62193e6
5bec150
4b475d3
42c0f75
914f507
3c6f318
bf51ca1
3e736ee
b977ba5
f74d2ab
5ace993
fb2eb4a
56306b1
5b255ef
2675366
2d1ff81
62ebe96
8384e5d
25af294
e83999b
ec0d892
903dcfe
8838a25
4bffd37
a364208
f2e4ede
68ee1db
aa35a07
75dd11c
61b62d8
4ff9844
812eb59
829e43a
95d821e
375c1f4
717bdbe
c439b88
c9629c4
84388d1
a48958c
ba528f0
963d9c3
065faee
5430973
3ea58ff
5fa431a
56b0c6a
72daeab
6f6e01a
79e08a1
e6e2b95
901ac9b
d2db821
112c242
84b288a
00b7afa
37e8630
406e0a9
d5411e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
name: CI | ||
|
||
on: | ||
pull_request: | ||
push: | ||
branches: [ main ] | ||
|
||
jobs: | ||
lint-test-docs: | ||
runs-on: ${{ matrix.os }} | ||
strategy: | ||
matrix: | ||
python-version: ['3.9', '3.13', '3.14'] | ||
os: [ubuntu-latest, macos-latest, windows-latest] | ||
|
||
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 and type check | ||
run: uv run poe lint | ||
|
||
- 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.os }}-${{ matrix.python-version }} | ||
path: coverage_html_report/ | ||
|
||
deploy-docs: | ||
runs-on: ubuntu-latest | ||
needs: lint-test-docs | ||
# TODO(preview): deploy on releases only | ||
permissions: | ||
contents: read | ||
pages: write | ||
id-token: write | ||
|
||
steps: | ||
- name: Checkout repository | ||
uses: actions/checkout@v4 | ||
|
||
- name: Install uv | ||
uses: astral-sh/setup-uv@v6 | ||
with: | ||
python-version: '3.9' | ||
|
||
- name: Install dependencies | ||
run: uv sync | ||
|
||
- name: Build API docs | ||
run: uv run poe docs | ||
|
||
- name: Upload docs to GitHub Pages | ||
uses: actions/upload-pages-artifact@v3 | ||
with: | ||
path: apidocs | ||
|
||
- name: Deploy to GitHub Pages | ||
uses: actions/deploy-pages@v4 |
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 |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
We will welcome contributions once the SDK has reached a stable release. | ||
|
||
### Type-check and lint | ||
|
||
```sh | ||
uv run poe lint | ||
``` | ||
|
||
### Format | ||
``` | ||
uv run poe format | ||
``` | ||
|
||
### Test | ||
``` | ||
uv run pytest | ||
``` | ||
|
||
### API docs | ||
``` | ||
uv run poe docs | ||
``` |
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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
# Nexus Python SDK | ||
|
||
⚠️ **This SDK is currently at an experimental release stage. Backwards-incompatible changes are anticipated until a stable release is announced.** ⚠️ | ||
|
||
## 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 | ||
|
||
|
||
@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}!") | ||
``` | ||
|
||
|
||
## Note regarding version number | ||
The nexus-rpc name in PyPi was originally held by an unrelated project. Despite the | ||
version being at `v1.x` it is currently at an experimental release stage. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,67 @@ | ||
[project] | ||
name = "nexus-rpc" | ||
version = "0.1.0" | ||
# The nexus-rpc name in PyPi was originally held by an unrelated project that reached | ||
# 1.0.1 and was abandoned in 2018. The name was inherited by the Python Nexus SDK in | ||
# 2025 and version numbering started from 1.1.0. Despite the version number, this is an | ||
# experimental release and backwards-incompatible changes are anticipated until a GA | ||
# release is announced. | ||
version = "1.1.0" | ||
description = "Nexus Python SDK" | ||
readme = "README.md" | ||
authors = [ | ||
{ name = "Dan Davison", email = "dandavison7@gmail.com" } | ||
{ name = "Temporal Technologies", email = "sdk@temporal.io" } | ||
] | ||
requires-python = ">=3.9" | ||
dependencies = [ | ||
"typing-extensions>=4.12.2", | ||
] | ||
|
||
[dependency-groups] | ||
dev = [ | ||
"mypy>=1.15.0", | ||
"poethepoet>=0.35.0", | ||
"pydoctor>=25.4.0", | ||
"pyright>=1.1.402", | ||
"pytest>=8.3.5", | ||
"pytest-asyncio>=0.26.0", | ||
"pytest-cov>=6.1.1", | ||
"pytest-pretty>=1.3.0", | ||
"ruff>=0.12.0", | ||
] | ||
requires-python = ">=3.13" | ||
dependencies = [] | ||
|
||
[build-system] | ||
requires = ["hatchling"] | ||
build-backend = "hatchling.build" | ||
|
||
[tool.hatch.build.targets.wheel] | ||
packages = ["src/nexusrpc"] | ||
|
||
[tool.poe.tasks] | ||
lint = [ | ||
{cmd = "uv run pyright src"}, | ||
{cmd = "uv run mypy --check-untyped-defs src"}, | ||
{cmd = "uv run ruff check --select I"}, | ||
{cmd = "uv run ruff format --check"}, | ||
] | ||
format = [ | ||
{cmd = "uv run ruff check --select I --fix"}, | ||
{cmd = "uv run ruff format"}, | ||
] | ||
docs = [ | ||
{cmd = "uv run pydoctor src/nexusrpc"}, | ||
] | ||
|
||
[tool.pyright] | ||
include = ["src", "tests"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Python packages don't typically put code in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the structure created by I'm not expert on the pros and cons, but I guess it leaves open the possibility of supporting multiple roots more cleanly. Less recently created ones such as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I've seen it both ways. I like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have a strong opinion either, so won't change now. It should be a backwards compatible change, of course. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
[tool.mypy] | ||
disable_error_code = ["empty-body"] | ||
|
||
[tool.ruff] | ||
target-version = "py39" | ||
|
||
[tool.ruff.lint.isort] | ||
combine-as-imports = true | ||
|
||
[tool.pydoctor] | ||
docformat = "google" |
This file was deleted.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wonder if we should be exporting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking the same. I've done it. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
""" | ||
nexusrpc is a library for building Nexus handlers. | ||
|
||
See https://github.com/nexus-rpc and https://github.com/nexus-rpc/api/blob/main/SPEC.md. | ||
|
||
Nexus 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. | ||
""" | ||
|
||
from __future__ import annotations | ||
|
||
from . import handler | ||
from ._common import ( | ||
HandlerError, | ||
HandlerErrorType, | ||
InputT, | ||
Link, | ||
OperationError, | ||
OperationErrorState, | ||
OperationInfo, | ||
OperationState, | ||
OutputT, | ||
) | ||
from ._serializer import Content, LazyValue | ||
from ._service import Operation, ServiceDefinition, service | ||
from ._util import ( | ||
get_operation_definition, | ||
get_service_definition, | ||
set_operation_definition, | ||
) | ||
|
||
__all__ = [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do users need to be able to access the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we're aware of a need for them to: they are just protocols, so structural typing. |
||
"Content", | ||
"get_operation_definition", | ||
"get_service_definition", | ||
"handler", | ||
"HandlerError", | ||
"HandlerErrorType", | ||
"InputT", | ||
"LazyValue", | ||
"Link", | ||
"Operation", | ||
"OperationError", | ||
"OperationErrorState", | ||
"OperationInfo", | ||
"OperationState", | ||
"OutputT", | ||
"service", | ||
"ServiceDefinition", | ||
"set_operation_definition", | ||
] |
There was a problem hiding this comment.
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
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hesitate to recommend
from nexusrpc import service
since it's a common variable name.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my suggestion above to export
handler
fromnexusrpc
, then you can usenexusrpc.handler.service_handler
here without requiring another import statement and this mixed pattern.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we want all our docs and samples to use long names such as
@nexusrpc.handler.service_handler
andctx: nexusrpc.handler.StartOperationContext
I have imported
handler
innexusrpc
for occasions when people want that.