Skip to content

feat(python): add @nexus.poll - #325

Merged
moetemp merged 16 commits into
mainfrom
moe/up/python-poll
Nov 4, 2026
Merged

moetemp merged 16 commits into
mainfrom
moe/up/python-poll

Conversation

@moetemp

@moetemp moetemp commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

What changed?

This PR adds @nexus.poll to the Python client target. An operation with @nexus.poll python="<helper>" targets="client" gets an iter_<name> async iterator. It yields each response and asks the support package's <helper>(request, response) for the next request, which returns None to stop.

  • A client resource method that binds the operation gets iter_<name>, and so does the client extension for an operation that no resource method binds. The stream sample gets Stream.iter_read. Its helper ends a read that doesn't wait at the newest Record, and starts each later read at the last page's next_offset.
  • Each page is its own operation, with its own random operation ID. The extension iterator uses one Nexus client for all pages.
  • nexgen refuses the directive with no targets, with targets that include workflow, on a resource method, and on an operation it can't poll. The Workflow package doesn't change.

Why?

A WIT stream can't cross a Nexus call, so the Stream read is a request and a response. The iterator reads every page, and the support package decides the next request.

Not in this PR: an iterator that yields one Record at a time and gives the offset to resume from. It can be a generated transform on top of this page iterator.

How did you test it?

Link to a test plan if any -

  • Unit Tests
  • Staging
  • End to End Tests

test_sdk_client.py reads every page through Stream.iter_read, ends a read that doesn't wait at the newest Record, and starts the read after start=END at the first page's offset. Rejection fixtures cover the client-only rule, an operation that returns a resource, and an iterator name clash.

This was referenced Oct 28, 2026
@tconley1428
tconley1428 force-pushed the refactor/api-generation-targets branch from cafb166 to 70ac708 Compare November 2, 2026 17:06
@moetemp
moetemp force-pushed the moe/up/python-poll branch from 0bf4004 to 8fde033 Compare November 2, 2026 19:49
Base automatically changed from refactor/api-generation-targets to main November 3, 2026 23:36
@moetemp
moetemp force-pushed the moe/up/python-poll branch from 8fde033 to f260389 Compare November 4, 2026 00:51
Comment thread src/generator/python.rs Outdated

/// Refuses a client poll that can't get an iterator, and an iterator with the name of another
/// operation, method or field, since one of the two would hide the other.
fn validate_client_polls(services: &[RenderedService<'_>], source_path: &Path) -> Result<()> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Seems like this should happen a lot earlier, outside of python. That would also mean you don't need to thread source path 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.

Agreed. I moved the checks into a new compiler pass, PollValidationPass, in 6a421d5. It runs on the planned spec, right after reachability and before any generator.

@moetemp
moetemp requested a review from tconley1428 November 4, 2026 01:22
Comment thread src/lib.rs Outdated
let planned_tree = type_planning.apply(operation_lowered_tree)?;
let planned_tree = planning::ReachabilityPass::new().apply(planned_tree)?;
let planned_tree =
planning::PollValidationPass::new(nexgen_config::current().targets).apply(planned_tree)?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No real reason to pass in the targets, since it can also check the config.

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.

Agreed. The pass reads the targets from the config now, in 07f142b.

@moetemp
moetemp requested a review from tconley1428 November 4, 2026 01:28

@moetemp moetemp left a comment

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.

Agreed. The pass reads the targets from the config now, in 07f142b.

Comment thread src/lib.rs Outdated
let planned_tree = type_planning.apply(operation_lowered_tree)?;
let planned_tree = planning::ReachabilityPass::new().apply(planned_tree)?;
let planned_tree =
planning::PollValidationPass::new(nexgen_config::current().targets).apply(planned_tree)?;

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.

Agreed. The pass reads the targets from the config now, in 07f142b.

moetemp and others added 16 commits November 3, 2026 18:37
The poll helper names the support function that builds the next request. Only
the client target gets an iterator, so the parser refuses a poll without
`target="client"` and a poll on a resource method.
A WIT stream can't cross a Nexus call, so a read is a request and a response.
The iterator reads every page. The extension creates one Nexus client for the
iteration, and a handle uses its own client.

Co-authored-by: Tim Conley <timothy.conley@temporal.io>
The Stream service sets `next_offset` on an empty page too, so the helper
reads it as the Stream reader does. Each answer's next offset sets the later
reads, also after `start` or `last`.
A caller that changes a yielded page then can't change the next read.
`AsyncGenerator` lets a caller close the iterator with `aclose()` or
`contextlib.aclosing`. A handle's iterator also gets the method doc and `Args:`.
The poll directive then reads its targets as the other directives do. A
refused poll gives the service, the operation and the input path.
Also rewrite poll comments that read two ways or use `name` as a verb.
A generic directive check is planned for syntax and placement. The kept fixtures cover the client-only rule, an operation shape the iterator can't take, and an iterator name clash.
The full stream helper is in the sample, so the guide keeps a short version and links to it.
Co-authored-by: Tim Conley <timothy.conley@temporal.io>
@moetemp
moetemp force-pushed the moe/up/python-poll branch from 07f142b to 10f9a37 Compare November 4, 2026 01:47
@moetemp
moetemp merged commit e5f5dd5 into main Nov 4, 2026
5 checks passed
@moetemp
moetemp deleted the moe/up/python-poll branch November 4, 2026 04:10
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.

2 participants