PYCO-94: Support Server Side Async Requests#11
Conversation
4bb3dcd to
1cd3532
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new “server-side async” query execution mode (start_query) to both the sync and async APIs, introducing query-handle abstractions for polling status, fetching results, discarding results, and canceling requests.
Changes:
- Introduces
start_query()on Cluster/Scope (sync + async) plusBlockingQueryHandle/AsyncQueryHandleimplementations. - Refactors request/response plumbing to support both streaming and non-streaming HTTP flows (new request types, contexts, and response wrappers).
- Adds unit + integration test coverage for start-query options and server-side async query lifecycle.
Reviewed changes
Copilot reviewed 66 out of 67 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/utils/_client_adapter.py | Expands test adapter request typing to cover new request types used by server-side async flows. |
| tests/utils/_async_client_adapter.py | Expands async test adapter request typing for new request types used by server-side async flows. |
| tests/environments/base_environment.py | Adds polling helpers for query-handle workflows in sync/async test environments. |
| couchbase_analytics_version.py | Suppresses type-check noise for optional tomli dependency. |
| couchbase_analytics/tests/start_query_options_t.py | New tests validating StartQueryOptions transformation/building behavior. |
| couchbase_analytics/tests/start_query_integration_t.py | New sync integration tests for server-side async query lifecycle (status/results/cancel/discard). |
| couchbase_analytics/tests/query_options_t.py | Updates tests to call renamed request builder method (build_query_request). |
| couchbase_analytics/tests/connection_t.py | Updates connection-string tests to use build_query_request. |
| couchbase_analytics/scope.pyi | Adds start_query overloads and handle return type for public Scope API. |
| couchbase_analytics/scope.py | Implements Scope.start_query() delegating to protocol layer. |
| couchbase_analytics/result.py | Minor cleanup of export module formatting. |
| couchbase_analytics/query_handle.py | New public re-export module for blocking query-handle types. |
| couchbase_analytics/protocol/streaming.py | Updates streaming response to use StreamingRequestContext and inject request_id into metadata building. |
| couchbase_analytics/protocol/scope.pyi | Adds protocol Scope start_query overloads returning blocking handle. |
| couchbase_analytics/protocol/scope.py | Implements protocol Scope.start_query() and refactors streaming execution to use StreamingRequestContext. |
| couchbase_analytics/protocol/query_handle.py | New sync protocol query-handle implementations (status/results/cancel/discard). |
| couchbase_analytics/protocol/options.py | Extends options transforms to include StartQuery + FetchResults options building. |
| couchbase_analytics/protocol/errors.py | Maps HTTP 404 to QueryNotFoundError for handle/status/result fetch operations. |
| couchbase_analytics/protocol/connection.py | Updates cluster options building API usage after options-builder refactor. |
| couchbase_analytics/protocol/cluster.pyi | Adds protocol Cluster start_query overloads returning blocking handle. |
| couchbase_analytics/protocol/cluster.py | Implements protocol Cluster.start_query() and refactors streaming execution to use StreamingRequestContext. |
| couchbase_analytics/protocol/_core/retries.py | Generalizes retry handler typing to support both streaming and non-streaming response types. |
| couchbase_analytics/protocol/_core/response.py | New non-streaming HttpResponse wrapper for handle/status/cancel/discard requests. |
| couchbase_analytics/protocol/_core/request_context.py | Splits base vs streaming contexts; adds support for non-streaming requests and handle-style requests. |
| couchbase_analytics/protocol/_core/request.py | Introduces HttpRequest base + StartQueryRequest/CancelRequest/FetchResultsRequest, and renames build_base_query_request → build_query_request. |
| couchbase_analytics/protocol/_core/json_stream.py | Tightens typing to StreamingRequestContext for streaming-only operations. |
| couchbase_analytics/protocol/_core/client_adapter.py | Extends adapter to send non-JSON requests (cancel/discard/handle GET/DELETE) and pass streaming flag. |
| couchbase_analytics/options.py | Exposes StartQueryOptions and FetchResultsOptions from common options. |
| couchbase_analytics/errors.py | Exposes QueryNotFoundError from common errors. |
| couchbase_analytics/common/query_handle.py | New common abstract interfaces for blocking/async query handles, statuses, and result handles. |
| couchbase_analytics/common/options_base.py | Adds StartQueryOptions* and FetchResultsOptions* option-key definitions/bases. |
| couchbase_analytics/common/options.py | Adds StartQueryOptions/FetchResultsOptions and expands QueryOptions docs. |
| couchbase_analytics/common/logging.py | Prevents logging attempts when handler streams are closed. |
| couchbase_analytics/common/errors.py | Adds QueryNotFoundError. |
| couchbase_analytics/common/_core/result.py | Refines typing from Coroutine to Awaitable in result interfaces. |
| couchbase_analytics/common/_core/query_handle.py | New shared core query-handle data model (QueryHandleStatusResponse). |
| couchbase_analytics/common/_core/query.py | Allows metadata builder to accept injected request_id (needed for handle fetch-results responses). |
| couchbase_analytics/common/_core/error_context.py | Generalizes request context capture to support non-query HTTP requests and explicit path. |
| couchbase_analytics/cluster.pyi | Adds public Cluster start_query overloads returning blocking handle. |
| couchbase_analytics/cluster.py | Implements Cluster.start_query() delegating to protocol layer. |
| conftest.py | Registers new test suites for options + integration start-query tests. |
| acouchbase_analytics/tests/start_query_integration_t.py | New async integration tests for server-side async query lifecycle. |
| acouchbase_analytics/tests/query_options_t.py | Updates tests to call renamed request builder method (build_query_request). |
| acouchbase_analytics/tests/connection_t.py | Updates connection-string tests to use build_query_request. |
| acouchbase_analytics/scope.pyi | Adds async Scope start_query overloads returning awaitable handle; updates execute_query arg typing. |
| acouchbase_analytics/scope.py | Adds start_query() to async public Scope API and tweaks execute_query typing/docs. |
| acouchbase_analytics/query_handle.py | Exposes async query-handle types (re-export). |
| acouchbase_analytics/protocol/streaming.py | Updates streaming response to use async streaming context and inject request_id into metadata building. |
| acouchbase_analytics/protocol/scope.pyi | Adds protocol async Scope start_query overloads. |
| acouchbase_analytics/protocol/scope.py | Implements async protocol start_query() and refactors streaming execution to use async streaming context. |
| acouchbase_analytics/protocol/query_handle.py | New async protocol query-handle implementations (status/results/cancel/discard). |
| acouchbase_analytics/protocol/cluster.pyi | Adds protocol async Cluster start_query overloads. |
| acouchbase_analytics/protocol/cluster.py | Implements async protocol start_query() and refactors streaming execution to use async streaming context. |
| acouchbase_analytics/protocol/_core/retries.py | Generalizes async retry handler typing to support streaming and non-streaming response types. |
| acouchbase_analytics/protocol/_core/response.py | New async non-streaming AsyncHttpResponse wrapper for handle/status/cancel/discard requests. |
| acouchbase_analytics/protocol/_core/request_context.py | Splits base vs streaming async contexts; adds support for non-streaming requests and handle-style requests. |
| acouchbase_analytics/protocol/_core/client_adapter.py | Extends async adapter to send non-JSON requests and pass streaming flag. |
| acouchbase_analytics/protocol/_core/anyio_utils.py | Fixes fallback behavior when sniffio is unavailable; adds anyio helpers. |
| acouchbase_analytics/options.py | Exposes StartQueryOptions and FetchResultsOptions from common options. |
| acouchbase_analytics/errors.py | Exposes QueryNotFoundError from common errors. |
| acouchbase_analytics/cluster.pyi | Adds public async Cluster start_query overloads returning awaitable handle. |
| acouchbase_analytics/cluster.py | Implements async Cluster.start_query() delegating to protocol layer. |
| .pre-commit-config.yaml | Adds sniffio and anyio to pre-commit test env dependencies. |
| .gitignore | Fixes ruff comment and ignores .DS_Store. |
| .github/workflows/verify_release.yml | Updates default macOS runner labels. |
| .github/workflows/tests.yml | Updates default macOS runner labels. |
| .github/workflows/publish.yml | Updates default macOS runner labels. |
Comments suppressed due to low confidence (2)
tests/utils/_client_adapter.py:53
- send_request_override now accepts HttpRequest/CancelRequest, but it still unconditionally reads request.body and always builds the outgoing request as JSON. HttpRequest (status/fetch/discard) and CancelRequest don’t have a .body attribute (and may need form data + headers), so this override will raise AttributeError during server-side async flows (e.g., QueryHandle.fetch_status()). Update the override to branch on request type/attributes (JSON body vs form data vs no body) similar to the production adapter.
tests/utils/_async_client_adapter.py:52 - Async send_request_override still assumes request.body exists and always builds the request with json=..., but start_query async handles use HttpRequest/CancelRequest for status/cancel/discard which don’t have a .body. This will raise at runtime. Update the override to handle CancelRequest (form data + headers), Query/StartQuery (json body), and generic HttpRequest (no json), matching the production adapter’s behavior.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 65 out of 66 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 65 out of 66 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 67 out of 68 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if isinstance(request, (QueryRequest, StartQueryRequest)): | ||
| req = self._client.build_request(request.method, url, json=request.body, extensions=request.extensions) | ||
| else: | ||
| headers = request.headers if request.headers is not None else None | ||
| data = request.data if isinstance(request, CancelRequest) else None | ||
| req = self._client.build_request( | ||
| request.method, url, data=data, headers=headers, extensions=request.extensions | ||
| ) | ||
|
|
There was a problem hiding this comment.
I think we could simplify this part, and avoid the isinstance checks:
- It would probably be a good idea to pass on the headers even in the case of
(Start)QueryRequest. If there's no need for headers, I imagine they will just be missing from the request. - The
dataandbodyfields could be on the baseHttpRequestinstead, since we could have other types of requests using form data potentially.
That would let us reduce the above to:
req = self._client.build_request(request.method, url, headers=request.headers, json=request.body, data = request.data, extensions=request.extensions)
There was a problem hiding this comment.
So, as we still currently support Python 3.9, the dataclass inheritance for the various requests is why I'm in this "mess". If I include body/data in HttpRequest as optional (and they would need to be optional) then child classes could not specify mandatory fields.
I created PYCO-95 to drop Python 3.9 and add Python 3.14. As part of that ticket I will move to using kw_only=True and iron this out too. 👍
|
|
||
| @dataclass | ||
| class QueryRequest: | ||
| class HttpRequest: |
There was a problem hiding this comment.
This is somewhat related to my other comment about send_request in the client adapter.
I think it would make sense for certain HTTP-related fields to be in this base class instead of the subclasses, and be default-initialized to None. That lets us pass on the fields to the HTTP client without having to do type checks about the specific type of the request.
Though it appears that it's not possible for the child dataclass to have default-initialized fields, if the parent class also has default-initialized fields. Something that resolves this is declaring them with @dataclass(kw_only=True), which means the fields can only be initialized with keyword arguments (It might be a good idea to disallow initialization via positional parameters regardless given the large number of fields anyway).
There was a problem hiding this comment.
I created PYCO-95 to drop Python 3.9 and add Python 3.14. As part of that ticket I will move to using kw_only=True and iron this out too. 👍
| method: str | ||
| headers: Mapping[str, str] | ||
| max_retries: int | ||
| method: str = 'POST' |
There was a problem hiding this comment.
I think we can still re-declare some fields (such as the method) in the child classes with a sensible default, even if the parent class has no default.
There was a problem hiding this comment.
Yes, that is possible. I'll look into that when I address PYCO-95. 👍
7e202f6 to
9d75d47
Compare
Changes ------- * Added base RequestContext for handling simple HTTP requests. * Added StreamingRequestContext for streaming HTTP requests. * Added QueryHandle, QueryStatus and QueryResultHandle objects to adhere to RFC (and provide appriopriate "flavors" for sync and async APIs). * Added start_query() to cluster & scope objects for both sync and async APIs. * Expanded core _RequestBuilder to include builder methods for more request types. * Added QueryNotFoundError. * Added handle_request_timeout to TimeoutOptions. * Added unit tests for StartQueryOptions. * Added integration tests to validate start_query() behavior. * Update API docs to include new server async requests API.
Changes