Skip to content

feat(python): add a resource factory, and pass a resource as data - #331

Closed
moetemp wants to merge 1 commit into
moe/up/field-flattenfrom
moe/up/resource-factory
Closed

moetemp wants to merge 1 commit into
moe/up/field-flattenfrom
moe/up/resource-factory

Conversation

@moetemp

@moetemp moetemp commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

What changed?

This PR gives each Python resource a factory, and passes a resource as data.

  • The client extension gets one factory per resource, named after the resource (client.stream). The Workflow package gets the same factory as a function, and exports it. It takes the definition of a resource with @nexus.definition, or its runtime types, and then the constructor fields by keyword. It makes no call. The client's handle calls through that client, and a Workflow's handle calls the operations of the code that runs it.
  • @nexus.flatten-in-api param="stream" fields="id backend" keep=true on a resource makes the factory take fields of one constructor parameter's record in place of the parameter. With keep=true the factory also takes the whole value, so client.stream(Turns, id="s1", backend="redis") and client.stream(Turns, stream=ref) both work, and a call with neither or both raises TypeError.
  • An optional resource field defaults to None. A resource with a required field after an optional one is a kw_only dataclass.
  • A resource with one required field of a proto-backed record, and no other required field, gets a transfer converter. Its wire form is that record. The parameter type gives the type arguments on decode, as in Stream[Turn], and the decoded handle takes the current operations. A Stream was not data before, since the default converter tried to encode body_type.

nexgen refuses a resource flatten with a fill, an unknown parameter, a keep that isn't true or false, a record that no proto backs, a required field of the record that the factory doesn't promote, and a promoted field with the name of another parameter. It also refuses a factory with the name of another client method, or of an operation function or a module of the Workflow package.

The stream sample's Stream takes the flatten of Brook's next WIT. The other Python samples get their factories and the None defaults.

It sits on #330. The stack is #308, #310, #311, #297, #296, #303, #304, #309, #318, #319, #320, #321, #322, #323, #324, #325, #326, #327, #328, #329, #330, then this PR.

Why?

The Stream API of the proposal has client.stream(definition, *, id=None, stream=None, backend=None), a handle on an existing Stream, as get_workflow_handle is for a Workflow. A handle is also an Activity argument and a query result, so it must be data. The facade has its own data form and a BindQueryResults interceptor for this today.

The design put param= and keep= in the field flatten PR. They are here, since only the factory reads them.

TypeScript and .NET keep their constructors, which take the optional fields already, and their factories come with their client targets. .NET passes a handle in the JSON form of its properties already. Go builds a handle with a struct literal, and Java has no proto-backed models.

The factory also takes the other constructor fields, so client.stream takes created and status too. A factory for a resource such as StartedWorkflow needs all of its fields.

How did you test it?

Python tests build a handle with the client factory from the id and from the ref, check Stream[Turn] with assert_type, check that the handle calls through the client, and check the refusal of neither and both. Another test builds a handle with the Workflow factory. A round trip through the data converter checks the ref on the wire and the body type from Stream[Turn]. A generation test checks the kw_only dataclass and the Workflow factory of a resource with a required field after an optional one. A unit test checks the keys of the resource flatten, and rejection fixtures cover each refusal.
cargo validate passes.

Each resource gets a factory on the client and in the Workflow package, which builds a handle with
no call. A resource with one proto-backed record field passes as that record, so a handle can be an
argument or a result.
@moetemp

moetemp commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Moved to moetemp/nexgen#87 for review first. It comes back here after that.

@moetemp moetemp closed this Oct 28, 2026
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.

1 participant