Skip to content
Merged
Show file tree
Hide file tree
Changes from 72 commits
Commits
Show all changes
178 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
41f1008
Make sync_operation_handler support sync start methods
dandavison Jun 26, 2025
92a4a14
Fix callable instances
dandavison Jun 26, 2025
cb5774b
rename: @sync_operation
dandavison Jun 26, 2025
4bfd25d
Clean up imports in tests
dandavison Jun 26, 2025
68fcac8
Remove @operation_handler from public API for now
dandavison Jun 26, 2025
4a2d19a
CI test on 3.9, 3.13, 3.14
dandavison Jun 26, 2025
ad9791f
Activate vercel publishing of apidocs
dandavison Jun 26, 2025
be8fba2
Start adding README content
dandavison Jun 26, 2025
f4a2f42
uv remove --group dev httpx
dandavison Jun 26, 2025
833a4c6
Make Content.data required
dandavison Jun 26, 2025
549b81f
Rename method: from_user_class -> from_class
dandavison Jun 26, 2025
5f24e15
Relocate OperationError
dandavison Jun 26, 2025
e2cf7fe
Make dataclasses `frozen`
dandavison Jun 26, 2025
cc43cdd
Make OperationContext non-instantiatable
dandavison Jun 26, 2025
3b6500b
Make dataclasses frozen=True
dandavison Jun 26, 2025
e514cdf
Make request ID required
dandavison Jun 26, 2025
2d90830
Eliminate types.module
dandavison Jun 26, 2025
8d875cc
Add FetchOperationResultContext timeout with stub implementation
dandavison Jun 27, 2025
657602e
Cleanup
dandavison Jun 28, 2025
1b3f7d0
Cleanup
dandavison Jun 28, 2025
f98a884
Move DummySerializer
dandavison Jun 28, 2025
60c2c9e
Work around lack of proper subtype compatibility check for parameteri…
dandavison Jun 28, 2025
b3bb2d3
get_service_definition cleanup
dandavison Jun 28, 2025
eaf5913
Make get_service_definition public
dandavison Jun 28, 2025
8b1b1a9
Don't raise in get_service_definition
dandavison Jun 28, 2025
4f22822
Move get_operation_factory
dandavison Jun 28, 2025
1d63015
Fixup operation getters/setters
dandavison Jun 28, 2025
7d466ac
test_request_routing
dandavison Jun 28, 2025
7f3c6e0
Refactor
dandavison Jun 28, 2025
e1f4405
Cleanup
dandavison Jun 28, 2025
4f24094
Failing test
dandavison Jun 28, 2025
6753fc5
Documentation
dandavison Jun 28, 2025
69a88e0
Bug fix: take op name from Operation in service definition
dandavison Jun 28, 2025
02e6010
Bug fix 2
dandavison Jun 28, 2025
b8fe8f6
Test request routing without service definition
dandavison Jun 28, 2025
fcad5b8
Fix tests
dandavison Jun 28, 2025
68f1d0d
Cleanup: collect_operation_handler_factories_by_method_name
dandavison Jun 29, 2025
8776f9d
Cleanups from code review
dandavison Jun 29, 2025
97f22e0
Move HandlerError to root module
dandavison Jun 29, 2025
d5a94b6
Create syncio version of @sync_operation, in syncio module
dandavison Jun 29, 2025
a607809
Cleanup
dandavison Jun 29, 2025
f474c03
Service definition: only inherit operations from decorated classes
dandavison Jun 29, 2025
62193e6
pydoctor
dandavison Jun 29, 2025
5bec150
make pyright pass
dandavison Jun 29, 2025
4b475d3
Make mypy pass
dandavison Jun 29, 2025
42c0f75
Add poe tasks
dandavison Jun 29, 2025
914f507
CI os/python matrix
dandavison Jun 29, 2025
3c6f318
Cleanup
dandavison Jun 29, 2025
bf51ca1
AbstractHandler base class
dandavison Jun 29, 2025
3e736ee
Don't leak operation names on NOT_FOUND
dandavison Jun 29, 2025
b977ba5
Tweak error message
dandavison Jun 29, 2025
f74d2ab
RuntimeError -> ValueError
dandavison Jun 29, 2025
5ace993
Test invalid usage
dandavison Jun 29, 2025
fb2eb4a
Cleanup
dandavison Jun 29, 2025
56306b1
Bug fix: get error when operation definition lacks type params
dandavison Jun 29, 2025
5b255ef
Create parallel syncio tree
dandavison Jun 29, 2025
2675366
HandlerError docstrings and properties
dandavison Jun 29, 2025
2d1ff81
Delete cause
dandavison Jun 29, 2025
62ebe96
poe commands
dandavison Jun 29, 2025
8384e5d
Deploy API docs to GH pages
dandavison Jun 29, 2025
25af294
Docstrings
dandavison Jun 29, 2025
e83999b
TEMP: reduce GHA matrix
dandavison Jun 29, 2025
ec0d892
Don't skip deploy docs
dandavison Jun 29, 2025
903dcfe
Move utils
dandavison Jun 29, 2025
8838a25
Import-time validation of def/async def methods used with sync_operation
dandavison Jun 29, 2025
4bffd37
Remove TODOs
dandavison Jun 30, 2025
a364208
Differentiate syncio/asyncio handlers
dandavison Jun 29, 2025
f2e4ede
Test type checking
dandavison Jun 30, 2025
68ee1db
Docstrings
dandavison Jun 30, 2025
aa35a07
Reorder
dandavison Jun 30, 2025
75dd11c
Add __all__s
dandavison Jun 30, 2025
61b62d8
Examples
dandavison Jun 30, 2025
4ff9844
Add examples
dandavison Jun 30, 2025
812eb59
Only expose intended syncio.handler components
dandavison Jun 30, 2025
829e43a
Update CONTRIBUTING.md
dandavison Jun 30, 2025
95d821e
Revert "TEMP: reduce GHA matrix"
dandavison Jun 30, 2025
375c1f4
ServiceDefinitionT -> ServiceT
dandavison Jun 30, 2025
717bdbe
Docstrings
dandavison Jun 30, 2025
c439b88
Do not expose get_operation_factory
dandavison Jun 30, 2025
c9629c4
Stope doing `as` imports
dandavison Jun 30, 2025
84388d1
Docstring
dandavison Jun 30, 2025
a48958c
Make Content.data non-nullable
dandavison Jun 30, 2025
ba528f0
Move content out of __init__.py
dandavison Jun 30, 2025
963d9c3
Import handler in nexusrpc
dandavison Jun 30, 2025
065faee
Delete spurious check
dandavison Jun 30, 2025
5430973
Failing test of enforcement of method name uniqueness
dandavison Jun 30, 2025
3ea58ff
Enforce method name uniqueness and 1:1 between op_handler and op_defn
dandavison Jun 30, 2025
5fa431a
Remove accidentally exposed utility function
dandavison Jun 30, 2025
56b0c6a
Cleanup
dandavison Jun 30, 2025
72daeab
Add is_callable utility
dandavison Jun 30, 2025
6f6e01a
Support callable instances as operation methods
dandavison Jun 30, 2025
79e08a1
Cleanup
dandavison Jun 30, 2025
e6e2b95
Fix test assertion
dandavison Jun 30, 2025
901ac9b
Skip test
dandavison Jun 30, 2025
d2db821
Add no type annotations invalid usage test
dandavison Jun 30, 2025
112c242
Fix CI test matrix
dandavison Jul 1, 2025
84b288a
Make OperationContext an ABC
dandavison Jul 1, 2025
00b7afa
Fix position of * in signature
dandavison Jul 1, 2025
37e8630
Bump version with explanatory note
dandavison Jul 2, 2025
406e0a9
Capture and assert on warnings in tests
dandavison Jul 2, 2025
d5411e3
Add README note regarding versions
dandavison Jul 2, 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 ]
Copy link
Copy Markdown

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed

pull_request:
branches: [ main ]

jobs:
test:
# TODO(preview): other platforms
Copy link
Copy Markdown

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added os/python matrix

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I had to reduce the matrix for now because GH was not running all the jobs (stuck in queuing state; does nexus-rpc need a different status account?)

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

Choose a reason for hiding this comment

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

Is it really pertinent to test on all of those releases? Looks like for Temporal's Python SDK, we're only testing min and max supported.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bump here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I've changed to ['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
# TODO(prerelease)
# - name: Deploy prod API docs
# if: ${{ github.ref == 'refs/heads/main' }}
# 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
Copy link
Copy Markdown

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, CONTRIBUTING.md contents are now

image

```

### 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.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Nexus Python SDK

## What is Nexus?

Nexus is a synchronous RPC protocol. Arbitrary duration operations are modelled 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.

TODO(prerelease): README content
Comment thread
dandavison marked this conversation as resolved.
Outdated
31 changes: 29 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,36 @@ readme = "README.md"
authors = [
{ name = "Dan Davison", email = "dandavison7@gmail.com" }
Copy link
Copy Markdown

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

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

[dependency-groups]
dev = [
"httpx>=0.28.1",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Where is this dependency used?

Copy link
Copy Markdown
Contributor Author

@dandavison dandavison Jun 9, 2025

Choose a reason for hiding this comment

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

It' used by the basic Nexus HTTP client that's in src/nexusrpc/testing/client.py, that the Temporal SDK uses for its testing.

Copy link
Copy Markdown

@cretz cretz Jun 10, 2025

Choose a reason for hiding this comment

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

I see, missed that. I think we shouldn't expose an HTTP client (even for testing) in the primary SDK at this time (but by all means move this to tests/). I expect there is a future where there is a real Nexus HTTP client. Do we need Nexus HTTP tests in Temporal SDK yet? Can we wait until we have Nexus HTTP in Nexus SDK first? It is a bit strange to see a dev dependency for this code depended on outside the repo as opposed to an extra or something.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nexus HTTP tests in Temporal SDK are the primary way Nexus is being tested. The workflow caller adds a lot of other machinery on top of Nexus request-response basics, so I'm testing as much as possible by HTTP, and then adding additional workflow caller testing on top of that. This is the same approach as Typescript. I can move the HTTP client to the Temporal repo tests/helpers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I don't think HTTP tests should be how Nexus is tested in the Temporal SDK (any more than using the HTTP interface would be how we'd test activities). There is only one official way Nexus handlers are called, that should be the way we test IMO. I suspect we'd encourage users to test their Nexus handlers similarly (the only way that works in all Temporal environments). If we must test differently than a user might test/invoke Nexus and use HTTP, yes definitely would prefer this not be part of the Nexus SDK and just part of the Temporal test helpers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since src/nexusrpc/testing/client.py, can we remove this dep?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, removed httpx dev dependency

"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"]
Copy link
Copy Markdown

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the structure created by uv init --lib and seen in recently created libraries such as https://github.com/anthropics/anthropic-sdk-python
https://github.com/openai/openai-agents-python
https://github.com/openai/openai-python

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 pydantic do as you describe.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yeah I've seen it both ways. I like nexusrpc top-level myself but no strong opinion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown

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.


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

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

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

This file was deleted.

48 changes: 48 additions & 0 deletions src/nexusrpc/__init__.py
Copy link
Copy Markdown

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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,
)
Copy link
Copy Markdown

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



@dataclass
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
Copy link
Copy Markdown

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right I see that VSCode / LSP picks up the strings-below-dataclass field convention that one sees, even though I don't think it's an official part of the language; added.



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

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


@dataclass
class OperationInfo:
"""
Information about an operation.
"""

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

# The operation's state
state: OperationState
125 changes: 125 additions & 0 deletions src/nexusrpc/_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import (
Any,
AsyncIterable,
Awaitable,
Iterable,
Mapping,
Optional,
Protocol,
Type,
Union,
)


@dataclass
class Content:
"""
A container for a map of headers and a byte array of data.

It is used by the SDK's Serializer interface implementations.
"""

headers: Mapping[str, str]
"""
Header that should include information on how to deserialize this content.
Headers constructed by the framework always have lower case keys.
User provided keys are treated case-insensitively.
"""

data: Optional[bytes] = None
Copy link
Copy Markdown

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

"""Request or response data. May be undefined for empty data."""


class Serializer(Protocol):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pedantic, but I think an ABC is clearer here, especially if we choose to add new methods with default implementations on this as we do with payload converters in SDK)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Unless we can point to incorrect behavior I'm OK leaving this as Protocol for now. Added to Preview list to revisit.

Copy link
Copy Markdown

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.

"""
Serializer is used by the framework to serialize/deserialize input and output.
"""

def serialize(self, value: Any) -> Union[Content, Awaitable[Content]]:
"""Serialize encodes a value into a Content."""
...

# TODO(prerelease): does None work as the sentinel type here, meaning do not attempt
# type conversion, despite the fact that Python treats None as a valid type?
def deserialize(
self, content: Content, as_type: Optional[Type[Any]] = None
) -> Union[Any, Awaitable[Any]]:
"""Deserialize decodes a Content into a value.

Args:
content: The content to deserialize.
as_type: The type to convert the result of deserialization into.
Do not attempt type conversion if this is None.
"""
...


class LazyValue:
"""
A container for a value encoded in an underlying stream.

It is used to stream inputs and outputs in the various client and server APIs.
"""

def __init__(
self,
serializer: Serializer,
headers: Mapping[str, str],
stream: Optional[Union[AsyncIterable[bytes], Iterable[bytes]]] = None,
) -> None:
"""
Args:
serializer: The serializer to use for consuming the value.
headers: Headers that include information on how to process the stream's content.
Headers constructed by the framework always have lower case keys.
User provided keys are treated case-insensitively.
stream: Iterable that contains request or response data. None means empty data.
"""
self.serializer = serializer
self.headers = headers
self.stream = stream

async def consume(self, as_type: Optional[Type[Any]] = None) -> Any:
"""
Consume the underlying reader stream, deserializing via the embedded serializer.
"""
# TODO(prerelease): HandlerError(BAD_REQUEST) on error while deserializing?
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Let's do that in the server implementation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right.

if self.stream is None:
return await self.serializer.deserialize(
Content(headers=self.headers), as_type=as_type
)
elif not isinstance(self.stream, AsyncIterable):
raise ValueError("When using consume, stream must be an AsyncIterable")

return await self.serializer.deserialize(
Content(
headers=self.headers,
data=b"".join([c async for c in self.stream]),
),
as_type=as_type,
)

# TODO(prerelease): we have a syncio module now for the syncio version of SyncOperationHandler
# SHould this go in a syncio module?
def consume_sync(self, as_type: Optional[Type[Any]] = None) -> Any:
"""
Consume the underlying reader stream, deserializing via the embedded serializer.
"""
# TODO(prerelease): HandlerError(BAD_REQUEST) on error while deserializing?
if self.stream is None:
return self.serializer.deserialize(
Content(headers=self.headers), as_type=as_type
)
elif not isinstance(self.stream, Iterable):
raise ValueError("When using consume_sync, stream must be an Iterable")

return self.serializer.deserialize(
Content(
headers=self.headers,
data=b"".join([c for c in self.stream]),
),
as_type=as_type,
)
Loading
Loading