Skip to content

Commit f3660d4

Browse files
committed
Merge remote-tracking branch 'origin/main' into trio-support
2 parents 4fe56f0 + 6fbdecb commit f3660d4

File tree

17 files changed

+845
-99
lines changed

17 files changed

+845
-99
lines changed

.buildkite/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ARG PYTHON_VERSION=3.13
1+
ARG PYTHON_VERSION=3.14
22
FROM python:${PYTHON_VERSION}
33

44
# Default UID/GID to 1000

.buildkite/pipeline.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,19 @@ steps:
1616
- "3.11"
1717
- "3.12"
1818
- "3.13"
19+
- "3.14"
1920
connection:
2021
- "urllib3"
2122
- "requests"
2223
nox_session:
2324
- "test"
2425
adjustments:
2526
- with:
26-
python: "3.9"
27+
python: "3.10"
2728
connection: "urllib3"
2829
nox_session: "test_otel"
2930
- with:
30-
python: "3.13"
31+
python: "3.14"
3132
connection: "urllib3"
3233
nox_session: "test_otel"
3334
command: ./.buildkite/run-tests

.buildkite/run-tests

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
# Default environment variables
88
export STACK_VERSION="${STACK_VERSION:=8.0.0-SNAPSHOT}"
99
export TEST_SUITE="${TEST_SUITE:=platinum}"
10-
export PYTHON_VERSION="${PYTHON_VERSION:=3.13}"
10+
export PYTHON_VERSION="${PYTHON_VERSION:=3.14}"
1111
export PYTHON_CONNECTION_CLASS="${PYTHON_CONNECTION_CLASS:=urllib3}"
1212

1313
script_path=$(dirname $(realpath $0))

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
runs-on: ubuntu-latest
99
steps:
1010
- name: Checkout Repository
11-
uses: actions/checkout@v4
11+
uses: actions/checkout@v5
1212
- name: Set up Python 3.x
1313
uses: actions/setup-python@v5
1414
with:
@@ -23,7 +23,7 @@ jobs:
2323
runs-on: ubuntu-latest
2424
steps:
2525
- name: Checkout Repository
26-
uses: actions/checkout@v4
26+
uses: actions/checkout@v5
2727
- name: Set up Python 3.x
2828
uses: actions/setup-python@v5
2929
with:
@@ -38,7 +38,7 @@ jobs:
3838
strategy:
3939
fail-fast: false
4040
matrix:
41-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
41+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
4242
nox-session: [""]
4343
runs-on: ["ubuntu-latest"]
4444

@@ -47,7 +47,7 @@ jobs:
4747
continue-on-error: false
4848
steps:
4949
- name: Checkout Repository
50-
uses: actions/checkout@v4
50+
uses: actions/checkout@v5
5151
- name: Set Up Python - ${{ matrix.python-version }}
5252
uses: actions/setup-python@v5
5353
with:

elasticsearch/_async/helpers.py

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,16 @@
3535

3636
import sniffio
3737

38+
from ..compat import safe_task
3839
from ..exceptions import ApiError, NotFoundError, TransportError
3940
from ..helpers.actions import (
4041
_TYPE_BULK_ACTION,
4142
_TYPE_BULK_ACTION_BODY,
4243
_TYPE_BULK_ACTION_HEADER,
4344
_TYPE_BULK_ACTION_HEADER_AND_BODY,
45+
_TYPE_BULK_ACTION_HEADER_WITH_META_AND_BODY,
46+
_TYPE_BULK_ACTION_WITH_META,
47+
BulkMeta,
4448
_ActionChunker,
4549
_process_bulk_chunk_error,
4650
_process_bulk_chunk_success,
@@ -65,9 +69,10 @@ async def _sleep(seconds: float) -> None:
6569

6670

6771
async def _chunk_actions(
68-
actions: AsyncIterable[_TYPE_BULK_ACTION_HEADER_AND_BODY],
72+
actions: AsyncIterable[_TYPE_BULK_ACTION_HEADER_WITH_META_AND_BODY],
6973
chunk_size: int,
7074
max_chunk_bytes: int,
75+
flush_after_seconds: Optional[float],
7176
serializer: Serializer,
7277
) -> AsyncIterable[
7378
Tuple[
@@ -87,10 +92,42 @@ async def _chunk_actions(
8792
chunker = _ActionChunker(
8893
chunk_size=chunk_size, max_chunk_bytes=max_chunk_bytes, serializer=serializer
8994
)
90-
async for action, data in actions:
91-
ret = chunker.feed(action, data)
92-
if ret:
93-
yield ret
95+
96+
if not flush_after_seconds:
97+
async for action, data in actions:
98+
ret = chunker.feed(action, data)
99+
if ret:
100+
yield ret
101+
else:
102+
item_queue: asyncio.Queue[_TYPE_BULK_ACTION_HEADER_WITH_META_AND_BODY] = (
103+
asyncio.Queue()
104+
)
105+
106+
async def get_items() -> None:
107+
try:
108+
async for item in actions:
109+
await item_queue.put(item)
110+
finally:
111+
await item_queue.put((BulkMeta.done, None))
112+
113+
async with safe_task(get_items()):
114+
timeout: Optional[float] = flush_after_seconds
115+
while True:
116+
try:
117+
action, data = await asyncio.wait_for(
118+
item_queue.get(), timeout=timeout
119+
)
120+
timeout = flush_after_seconds
121+
except asyncio.TimeoutError:
122+
action, data = BulkMeta.flush, None
123+
timeout = None
124+
125+
if action is BulkMeta.done:
126+
break
127+
ret = chunker.feed(action, data)
128+
if ret:
129+
yield ret
130+
94131
ret = chunker.flush()
95132
if ret:
96133
yield ret
@@ -170,9 +207,13 @@ async def azip(
170207

171208
async def async_streaming_bulk(
172209
client: AsyncElasticsearch,
173-
actions: Union[Iterable[_TYPE_BULK_ACTION], AsyncIterable[_TYPE_BULK_ACTION]],
210+
actions: Union[
211+
Iterable[_TYPE_BULK_ACTION_WITH_META],
212+
AsyncIterable[_TYPE_BULK_ACTION_WITH_META],
213+
],
174214
chunk_size: int = 500,
175215
max_chunk_bytes: int = 100 * 1024 * 1024,
216+
flush_after_seconds: Optional[float] = None,
176217
raise_on_error: bool = True,
177218
expand_action_callback: Callable[
178219
[_TYPE_BULK_ACTION], _TYPE_BULK_ACTION_HEADER_AND_BODY
@@ -205,6 +246,9 @@ async def async_streaming_bulk(
205246
:arg actions: iterable or async iterable containing the actions to be executed
206247
:arg chunk_size: number of docs in one chunk sent to es (default: 500)
207248
:arg max_chunk_bytes: the maximum size of the request in bytes (default: 100MB)
249+
:arg flush_after_seconds: time in seconds after which a chunk is written even
250+
if hasn't reached `chunk_size` or `max_chunk_bytes`. Set to 0 to not use a
251+
timeout-based flush. (default: 0)
208252
:arg raise_on_error: raise ``BulkIndexError`` containing errors (as `.errors`)
209253
from the execution of the last chunk when some occur. By default we raise.
210254
:arg raise_on_exception: if ``False`` then don't propagate exceptions from
@@ -231,9 +275,14 @@ async def async_streaming_bulk(
231275
if isinstance(retry_on_status, int):
232276
retry_on_status = (retry_on_status,)
233277

234-
async def map_actions() -> AsyncIterable[_TYPE_BULK_ACTION_HEADER_AND_BODY]:
278+
async def map_actions() -> (
279+
AsyncIterable[_TYPE_BULK_ACTION_HEADER_WITH_META_AND_BODY]
280+
):
235281
async for item in aiter(actions):
236-
yield expand_action_callback(item)
282+
if isinstance(item, BulkMeta):
283+
yield item, None
284+
else:
285+
yield expand_action_callback(item)
237286

238287
serializer = client.transport.serializers.get_serializer("application/json")
239288

@@ -245,7 +294,7 @@ async def map_actions() -> AsyncIterable[_TYPE_BULK_ACTION_HEADER_AND_BODY]:
245294
]
246295
bulk_actions: List[bytes]
247296
async for bulk_data, bulk_actions in _chunk_actions(
248-
map_actions(), chunk_size, max_chunk_bytes, serializer
297+
map_actions(), chunk_size, max_chunk_bytes, flush_after_seconds, serializer
249298
):
250299
for attempt in range(max_retries + 1):
251300
to_retry: List[bytes] = []

elasticsearch/compat.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18+
import asyncio
1819
import inspect
1920
import os
2021
import sys
22+
from contextlib import asynccontextmanager, contextmanager
2123
from pathlib import Path
22-
from typing import Tuple, Type, Union
24+
from threading import Thread
25+
from typing import Any, AsyncIterator, Callable, Coroutine, Iterator, Tuple, Type, Union
2326

2427
string_types: Tuple[Type[str], Type[bytes]] = (str, bytes)
2528

@@ -76,9 +79,48 @@ def warn_stacklevel() -> int:
7679
return 0
7780

7881

82+
@contextmanager
83+
def safe_thread(
84+
target: Callable[..., Any], *args: Any, **kwargs: Any
85+
) -> Iterator[Thread]:
86+
"""Run a thread within a context manager block.
87+
88+
The thread is automatically joined when the block ends. If the thread raised
89+
an exception, it is raised in the caller's context.
90+
"""
91+
captured_exception = None
92+
93+
def run() -> None:
94+
try:
95+
target(*args, **kwargs)
96+
except BaseException as exc:
97+
nonlocal captured_exception
98+
captured_exception = exc
99+
100+
thread = Thread(target=run)
101+
thread.start()
102+
yield thread
103+
thread.join()
104+
if captured_exception:
105+
raise captured_exception
106+
107+
108+
@asynccontextmanager
109+
async def safe_task(coro: Coroutine[Any, Any, Any]) -> AsyncIterator[asyncio.Task[Any]]:
110+
"""Run a background task within a context manager block.
111+
112+
The task is awaited when the block ends.
113+
"""
114+
task = asyncio.create_task(coro)
115+
yield task
116+
await task
117+
118+
79119
__all__ = [
80120
"string_types",
81121
"to_str",
82122
"to_bytes",
83123
"warn_stacklevel",
124+
"safe_thread",
125+
"safe_task",
84126
]

elasticsearch/dsl/document_base.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
overload,
3535
)
3636

37+
try:
38+
import annotationlib
39+
except ImportError:
40+
annotationlib = None
41+
3742
try:
3843
from types import UnionType
3944
except ImportError:
@@ -332,6 +337,16 @@ def __init__(self, name: str, bases: Tuple[type, ...], attrs: Dict[str, Any]):
332337
# # ignore attributes
333338
# field10: ClassVar[string] = "a regular class variable"
334339
annotations = attrs.get("__annotations__", {})
340+
if not annotations and annotationlib:
341+
# Python 3.14+ uses annotationlib
342+
annotate = annotationlib.get_annotate_from_class_namespace(attrs)
343+
if annotate:
344+
annotations = (
345+
annotationlib.call_annotate_function(
346+
annotate, format=annotationlib.Format.VALUE
347+
)
348+
or {}
349+
)
335350
fields = {n for n in attrs if isinstance(attrs[n], Field)}
336351
fields.update(annotations.keys())
337352
field_defaults = {}

0 commit comments

Comments
 (0)