Skip to content

Commit dc373de

Browse files
committed
Update supported Python versions, ensure httpx responses closed and use
kw_only for request dataclasses Changes ------- * PYCO-95: Updated supported Python versions to be 3.10 - 3.14 * PYCO-96: Use kw_only=True for request dataclasses * PYCO-97: Close httpx response when raising WrappedError from HTTP status code * Update dependencies * Update pyproject.toml and requirement.txt files
1 parent ced9e20 commit dc373de

21 files changed

Lines changed: 1340 additions & 1153 deletions

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ on:
3636

3737
env:
3838
CBCI_PROJECT_TYPE: "ANALYTICS"
39-
CBCI_DEFAULT_PYTHON: "3.9"
40-
CBCI_SUPPORTED_PYTHON_VERSIONS: "3.9 3.10 3.11 3.12 3.13"
39+
CBCI_DEFAULT_PYTHON: "3.10"
40+
CBCI_SUPPORTED_PYTHON_VERSIONS: "3.10 3.11 3.12 3.13 3.14"
4141
CBCI_SUPPORTED_X86_64_PLATFORMS: "linux alpine macos windows"
4242
CBCI_SUPPORTED_ARM64_PLATFORMS: "linux macos"
4343
CBCI_DEFAULT_LINUX_X86_64_PLATFORM: "ubuntu-22.04"

.github/workflows/tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ on:
5858

5959
env:
6060
CBCI_PROJECT_TYPE: "ANALYTICS"
61-
CBCI_DEFAULT_PYTHON: "3.9"
62-
CBCI_SUPPORTED_PYTHON_VERSIONS: "3.9 3.10 3.11 3.12 3.13"
61+
CBCI_DEFAULT_PYTHON: "3.10"
62+
CBCI_SUPPORTED_PYTHON_VERSIONS: "3.10 3.11 3.12 3.13 3.14"
6363
CBCI_SUPPORTED_X86_64_PLATFORMS: "linux alpine macos windows"
6464
CBCI_SUPPORTED_ARM64_PLATFORMS: "linux macos"
6565
CBCI_DEFAULT_LINUX_X86_64_PLATFORM: "ubuntu-22.04"

.github/workflows/verify_release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ on:
5151

5252
env:
5353
CBCI_PROJECT_TYPE: "ANALYTICS"
54-
CBCI_DEFAULT_PYTHON: "3.9"
55-
CBCI_SUPPORTED_PYTHON_VERSIONS: "3.9 3.10 3.11 3.12 3.13"
54+
CBCI_DEFAULT_PYTHON: "3.10"
55+
CBCI_SUPPORTED_PYTHON_VERSIONS: "3.10 3.11 3.12 3.13 3.14"
5656
CBCI_SUPPORTED_X86_64_PLATFORMS: "linux alpine macos windows"
5757
CBCI_SUPPORTED_ARM64_PLATFORMS: "linux macos"
5858
CBCI_DEFAULT_LINUX_X86_64_PLATFORM: "ubuntu-22.04"

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,12 @@ repos:
5858
# Compile requirements
5959
- id: pip-compile
6060
name: pip-compile requirements.in
61-
args: [requirements.in, --python-version, '3.9', --universal, -o, requirements.txt]
61+
args: [requirements.in, --python-version, '3.10', --universal, -o, requirements.txt]
6262
- id: pip-compile
6363
name: pip-compile requirements-dev.in
64-
args: [requirements-dev.in, --python-version, '3.9', --universal, -o, requirements-dev.txt]
64+
args: [requirements-dev.in, --python-version, '3.10', --universal, -o, requirements-dev.txt]
6565
files: ^requirements-dev\.(in|txt)$
6666
- id: pip-compile
6767
name: pip-compile requirements-sphinx.in
68-
args: [requirements-sphinx.in, --python-version, '3.9', --universal, -o, requirements-sphinx.txt]
68+
args: [requirements-sphinx.in, --python-version, '3.10', --universal, -o, requirements-sphinx.txt]
6969
files: ^requirements-sphinx\.(in|txt)$

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Couchbase Python Analytics Client
22
Python client for [Couchbase](https://couchbase.com) Analytics.
33

4-
Currently Python 3.9 - Python 3.13 is supported.
4+
Currently Python 3.10 - Python 3.14 is supported.
55

66
The Analytics SDK supports static typing. Currently only [mypy](https://github.com/python/mypy) is supported. You mileage may vary (YMMV) with the use of other static type checkers (e.g. [pyright](https://github.com/microsoft/pyright)).
77

acouchbase_analytics/protocol/_core/client_adapter.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from couchbase_analytics.common.credential import Credential
2626
from couchbase_analytics.common.deserializer import Deserializer
2727
from couchbase_analytics.common.logging import LogLevel, log_message
28-
from couchbase_analytics.protocol._core.request import CancelRequest, HttpRequest, QueryRequest, StartQueryRequest
28+
from couchbase_analytics.protocol._core.request import HttpRequest
2929
from couchbase_analytics.protocol.connection import _ConnectionDetails
3030
from couchbase_analytics.protocol.options import OptionsBuilder
3131

@@ -169,19 +169,15 @@ async def send_request(self, request: HttpRequest, stream: Optional[bool] = True
169169
if not hasattr(self, '_client'):
170170
raise RuntimeError('Client not created yet')
171171

172-
url = URL(
173-
scheme=request.url.scheme,
174-
host=request.url.ip,
175-
port=request.url.port,
176-
path=request.url.path,
172+
url = URL(scheme=request.url.scheme, host=request.url.ip, port=request.url.port, path=request.url.path)
173+
req = self._client.build_request(
174+
request.method,
175+
url,
176+
data=request.data,
177+
json=request.body,
178+
headers=request.headers,
179+
extensions=request.extensions,
177180
)
178-
if isinstance(request, (QueryRequest, StartQueryRequest)):
179-
req = self._client.build_request(request.method, url, json=request.body, extensions=request.extensions)
180-
else:
181-
data = request.data if isinstance(request, CancelRequest) else None
182-
req = self._client.build_request(
183-
request.method, url, data=data, headers=request.headers, extensions=request.extensions
184-
)
185181

186182
if stream is None:
187183
stream = True

acouchbase_analytics/protocol/_core/request_context.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,22 @@ def timed_out(self) -> bool:
104104
def calculate_backoff(self) -> float:
105105
return self._backoff_calc.calculate_backoff(self._error_context.num_attempts) / 1000
106106

107+
async def check_for_http_status_error(
108+
self,
109+
status_code: int,
110+
ignore_not_found_status: Optional[bool] = False,
111+
close_handler: Optional[Callable[[], Awaitable[None]]] = None,
112+
) -> None:
113+
ctx = str(self._error_context)
114+
err = ErrorMapper.maybe_get_error_from_status_code(
115+
status_code, ctx, ignore_not_found_status=ignore_not_found_status
116+
)
117+
if err is None:
118+
return
119+
if close_handler is not None:
120+
await close_handler()
121+
raise err
122+
107123
def create_response_task(self, fn: Callable[..., Coroutine[Any, Any, Any]], *args: object) -> Task[Any]:
108124
if self._backend is None or self._backend.backend_lib != 'asyncio':
109125
raise RuntimeError('Must use the asyncio backend to create a response task.')
@@ -188,10 +204,13 @@ async def process_response(
188204
core_response: HttpCoreResponse,
189205
close_handler: Callable[[], Awaitable[None]],
190206
handle_context_shutdown: Optional[bool] = False,
207+
ignore_not_found_status: Optional[bool] = False,
191208
) -> Any:
192209
# we have all the data, close the core response/stream
193210
await close_handler()
194-
211+
await self.check_for_http_status_error(
212+
core_response.status_code, ignore_not_found_status=ignore_not_found_status
213+
)
195214
try:
196215
json_response = core_response.json()
197216
except json.JSONDecodeError:
@@ -243,7 +262,6 @@ async def send_request(
243262
'request_deadline': f'{self._request_deadline}',
244263
}
245264
self.log_message('HTTP response', LogLevel.DEBUG, message_data=message_data)
246-
self._check_for_http_status_error(response.status_code, ignore_not_found_status=ignore_not_found_status)
247265
return response
248266

249267
async def shutdown(
@@ -265,12 +283,6 @@ async def shutdown(
265283
self._shutdown = True
266284
self.log_message('Request context shutdown complete', LogLevel.INFO)
267285

268-
def _check_for_http_status_error(self, status_code: int, ignore_not_found_status: Optional[bool] = False) -> None:
269-
ctx = str(self._error_context)
270-
ErrorMapper.maybe_raise_error_from_status_code(
271-
status_code, ctx, ignore_not_found_status=ignore_not_found_status
272-
)
273-
274286
def _check_timed_out(self) -> None:
275287
if self._request_state in (RequestState.Timeout, RequestState.Error):
276288
return

acouchbase_analytics/protocol/_core/response.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ async def _process_no_body_response(self) -> None:
123123
if 200 <= status_code < 300 or status_code == 404:
124124
await self._request_context.shutdown()
125125
return
126+
await self._request_context.check_for_http_status_error(status_code, ignore_not_found_status=True)
126127
ctx = str(self._request_context.error_context)
127128
raise WrappedError(AnalyticsError(context=ctx, message=f'Request failed with status {status_code}.'))
128129

@@ -131,6 +132,9 @@ async def _process_response(self) -> None:
131132
**INTERNAL**
132133
"""
133134
self._json_response = await self._request_context.process_response(
134-
self._core_response, self.close, handle_context_shutdown=True
135+
self._core_response,
136+
self.close,
137+
handle_context_shutdown=True,
138+
ignore_not_found_status=self._has_no_body_response,
135139
)
136140
await self.set_metadata(json_data=self._json_response)

acouchbase_analytics/protocol/streaming.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ async def send_request(self) -> None:
172172
self._request_context.start_stream(self._core_response)
173173
# block until we either know we have rows or we have an error
174174
await self._request_context.wait_for_results_or_errors()
175+
176+
# it is possible we have "rows" but the HTTP status code is indicative of an error (e.g. 404, 503, etc.),
177+
# so we need to check for HTTP status errors before allowing iteration to continue
178+
await self._request_context.check_for_http_status_error(
179+
self._core_response.status_code, close_handler=self.close
180+
)
175181
if not self._request_context.okay_to_iterate:
176182
await self._request_context.finish_processing_stream()
177183
await self._process_response()

couchbase_analytics/protocol/_core/client_adapter.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from couchbase_analytics.common.credential import Credential
2626
from couchbase_analytics.common.deserializer import Deserializer
2727
from couchbase_analytics.common.logging import LogLevel, log_message
28-
from couchbase_analytics.protocol._core.request import CancelRequest, HttpRequest, QueryRequest, StartQueryRequest
28+
from couchbase_analytics.protocol._core.request import HttpRequest
2929
from couchbase_analytics.protocol.connection import _ConnectionDetails
3030
from couchbase_analytics.protocol.options import OptionsBuilder
3131

@@ -168,13 +168,14 @@ def send_request(self, request: HttpRequest, stream: Optional[bool] = True) -> R
168168
raise RuntimeError('Client not created yet')
169169

170170
url = URL(scheme=request.url.scheme, host=request.url.ip, port=request.url.port, path=request.url.path)
171-
if isinstance(request, (QueryRequest, StartQueryRequest)):
172-
req = self._client.build_request(request.method, url, json=request.body, extensions=request.extensions)
173-
else:
174-
data = request.data if isinstance(request, CancelRequest) else None
175-
req = self._client.build_request(
176-
request.method, url, data=data, headers=request.headers, extensions=request.extensions
177-
)
171+
req = self._client.build_request(
172+
request.method,
173+
url,
174+
data=request.data,
175+
json=request.body,
176+
headers=request.headers,
177+
extensions=request.extensions,
178+
)
178179

179180
if stream is None:
180181
stream = True

0 commit comments

Comments
 (0)