elasticsearch 9.1.2__py3-none-any.whl → 9.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. elasticsearch/_async/client/__init__.py +94 -44
  2. elasticsearch/_async/client/async_search.py +7 -0
  3. elasticsearch/_async/client/cat.py +8 -1
  4. elasticsearch/_async/client/cluster.py +9 -8
  5. elasticsearch/_async/client/eql.py +7 -0
  6. elasticsearch/_async/client/esql.py +26 -3
  7. elasticsearch/_async/client/fleet.py +1 -5
  8. elasticsearch/_async/client/graph.py +1 -5
  9. elasticsearch/_async/client/ilm.py +2 -10
  10. elasticsearch/_async/client/indices.py +158 -28
  11. elasticsearch/_async/client/inference.py +280 -123
  12. elasticsearch/_async/client/ingest.py +8 -0
  13. elasticsearch/_async/client/license.py +4 -2
  14. elasticsearch/_async/client/ml.py +2 -2
  15. elasticsearch/_async/client/nodes.py +1 -3
  16. elasticsearch/_async/client/project.py +67 -0
  17. elasticsearch/_async/client/security.py +39 -0
  18. elasticsearch/_async/client/simulate.py +8 -0
  19. elasticsearch/_async/client/slm.py +1 -5
  20. elasticsearch/_async/client/snapshot.py +20 -10
  21. elasticsearch/_async/client/sql.py +7 -0
  22. elasticsearch/_async/client/streams.py +2 -3
  23. elasticsearch/_async/helpers.py +28 -15
  24. elasticsearch/_sync/client/__init__.py +94 -44
  25. elasticsearch/_sync/client/async_search.py +7 -0
  26. elasticsearch/_sync/client/cat.py +8 -1
  27. elasticsearch/_sync/client/cluster.py +9 -8
  28. elasticsearch/_sync/client/eql.py +7 -0
  29. elasticsearch/_sync/client/esql.py +26 -3
  30. elasticsearch/_sync/client/fleet.py +1 -5
  31. elasticsearch/_sync/client/graph.py +1 -5
  32. elasticsearch/_sync/client/ilm.py +2 -10
  33. elasticsearch/_sync/client/indices.py +158 -28
  34. elasticsearch/_sync/client/inference.py +280 -123
  35. elasticsearch/_sync/client/ingest.py +8 -0
  36. elasticsearch/_sync/client/license.py +4 -2
  37. elasticsearch/_sync/client/ml.py +2 -2
  38. elasticsearch/_sync/client/nodes.py +1 -3
  39. elasticsearch/_sync/client/project.py +67 -0
  40. elasticsearch/_sync/client/security.py +39 -0
  41. elasticsearch/_sync/client/simulate.py +8 -0
  42. elasticsearch/_sync/client/slm.py +1 -5
  43. elasticsearch/_sync/client/snapshot.py +20 -10
  44. elasticsearch/_sync/client/sql.py +7 -0
  45. elasticsearch/_sync/client/streams.py +2 -3
  46. elasticsearch/_version.py +2 -2
  47. elasticsearch/client.py +2 -0
  48. elasticsearch/compat.py +2 -15
  49. elasticsearch/dsl/_async/document.py +2 -1
  50. elasticsearch/dsl/_sync/document.py +2 -1
  51. elasticsearch/dsl/document_base.py +38 -13
  52. elasticsearch/dsl/pydantic.py +152 -0
  53. elasticsearch/dsl/search_base.py +5 -1
  54. elasticsearch/esql/esql.py +331 -41
  55. elasticsearch/esql/functions.py +88 -0
  56. elasticsearch/helpers/actions.py +1 -1
  57. {elasticsearch-9.1.2.dist-info → elasticsearch-9.2.0.dist-info}/METADATA +26 -4
  58. {elasticsearch-9.1.2.dist-info → elasticsearch-9.2.0.dist-info}/RECORD +61 -58
  59. {elasticsearch-9.1.2.dist-info → elasticsearch-9.2.0.dist-info}/WHEEL +0 -0
  60. {elasticsearch-9.1.2.dist-info → elasticsearch-9.2.0.dist-info}/licenses/LICENSE +0 -0
  61. {elasticsearch-9.1.2.dist-info → elasticsearch-9.2.0.dist-info}/licenses/NOTICE +0 -0
@@ -2390,7 +2390,7 @@ class MlClient(NamespacedClient):
2390
2390
  exclude_interim: t.Optional[bool] = None,
2391
2391
  filter_path: t.Optional[t.Union[str, t.Sequence[str]]] = None,
2392
2392
  human: t.Optional[bool] = None,
2393
- overall_score: t.Optional[t.Union[float, str]] = None,
2393
+ overall_score: t.Optional[float] = None,
2394
2394
  pretty: t.Optional[bool] = None,
2395
2395
  start: t.Optional[t.Union[str, t.Any]] = None,
2396
2396
  top_n: t.Optional[int] = None,
@@ -5716,7 +5716,7 @@ class MlClient(NamespacedClient):
5716
5716
  <p>Validate an anomaly detection job.</p>
5717
5717
 
5718
5718
 
5719
- `<https://www.elastic.co/guide/en/machine-learning/9.1/ml-jobs.html>`_
5719
+ `<https://www.elastic.co/guide/en/machine-learning/9.2/ml-jobs.html>`_
5720
5720
 
5721
5721
  :param analysis_config:
5722
5722
  :param analysis_limits:
@@ -368,9 +368,7 @@ class NodesClient(NamespacedClient):
368
368
  human: t.Optional[bool] = None,
369
369
  include_segment_file_sizes: t.Optional[bool] = None,
370
370
  include_unloaded_segments: t.Optional[bool] = None,
371
- level: t.Optional[
372
- t.Union[str, t.Literal["cluster", "indices", "shards"]]
373
- ] = None,
371
+ level: t.Optional[t.Union[str, t.Literal["indices", "node", "shards"]]] = None,
374
372
  pretty: t.Optional[bool] = None,
375
373
  timeout: t.Optional[t.Union[str, t.Literal[-1], t.Literal[0]]] = None,
376
374
  types: t.Optional[t.Sequence[str]] = None,
@@ -0,0 +1,67 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ import typing as t
19
+
20
+ from elastic_transport import ObjectApiResponse
21
+
22
+ from ._base import NamespacedClient
23
+ from .utils import (
24
+ Stability,
25
+ _rewrite_parameters,
26
+ _stability_warning,
27
+ )
28
+
29
+
30
+ class ProjectClient(NamespacedClient):
31
+
32
+ @_rewrite_parameters()
33
+ @_stability_warning(Stability.EXPERIMENTAL)
34
+ def tags(
35
+ self,
36
+ *,
37
+ error_trace: t.Optional[bool] = None,
38
+ filter_path: t.Optional[t.Union[str, t.Sequence[str]]] = None,
39
+ human: t.Optional[bool] = None,
40
+ pretty: t.Optional[bool] = None,
41
+ ) -> ObjectApiResponse[t.Any]:
42
+ """
43
+ .. raw:: html
44
+
45
+ <p>Return tags defined for the project</p>
46
+
47
+ """
48
+ __path_parts: t.Dict[str, str] = {}
49
+ __path = "/_project/tags"
50
+ __query: t.Dict[str, t.Any] = {}
51
+ if error_trace is not None:
52
+ __query["error_trace"] = error_trace
53
+ if filter_path is not None:
54
+ __query["filter_path"] = filter_path
55
+ if human is not None:
56
+ __query["human"] = human
57
+ if pretty is not None:
58
+ __query["pretty"] = pretty
59
+ __headers = {"accept": "application/json"}
60
+ return self.perform_request( # type: ignore[return-value]
61
+ "GET",
62
+ __path,
63
+ params=__query,
64
+ headers=__headers,
65
+ endpoint_id="project.tags",
66
+ path_parts=__path_parts,
67
+ )
@@ -2052,6 +2052,45 @@ class SecurityClient(NamespacedClient):
2052
2052
  path_parts=__path_parts,
2053
2053
  )
2054
2054
 
2055
+ @_rewrite_parameters()
2056
+ def get_stats(
2057
+ self,
2058
+ *,
2059
+ error_trace: t.Optional[bool] = None,
2060
+ filter_path: t.Optional[t.Union[str, t.Sequence[str]]] = None,
2061
+ human: t.Optional[bool] = None,
2062
+ pretty: t.Optional[bool] = None,
2063
+ ) -> ObjectApiResponse[t.Any]:
2064
+ """
2065
+ .. raw:: html
2066
+
2067
+ <p>Get security stats.</p>
2068
+ <p>Gather security usage statistics from all node(s) within the cluster.</p>
2069
+
2070
+
2071
+ `<https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-security-get-stats>`_
2072
+ """
2073
+ __path_parts: t.Dict[str, str] = {}
2074
+ __path = "/_security/stats"
2075
+ __query: t.Dict[str, t.Any] = {}
2076
+ if error_trace is not None:
2077
+ __query["error_trace"] = error_trace
2078
+ if filter_path is not None:
2079
+ __query["filter_path"] = filter_path
2080
+ if human is not None:
2081
+ __query["human"] = human
2082
+ if pretty is not None:
2083
+ __query["pretty"] = pretty
2084
+ __headers = {"accept": "application/json"}
2085
+ return self.perform_request( # type: ignore[return-value]
2086
+ "GET",
2087
+ __path,
2088
+ params=__query,
2089
+ headers=__headers,
2090
+ endpoint_id="security.get_stats",
2091
+ path_parts=__path_parts,
2092
+ )
2093
+
2055
2094
  @_rewrite_parameters(
2056
2095
  body_fields=(
2057
2096
  "grant_type",
@@ -56,6 +56,7 @@ class SimulateClient(NamespacedClient):
56
56
  t.Mapping[str, t.Mapping[str, t.Any]]
57
57
  ] = None,
58
58
  mapping_addition: t.Optional[t.Mapping[str, t.Any]] = None,
59
+ merge_type: t.Optional[t.Union[str, t.Literal["index", "template"]]] = None,
59
60
  pipeline: t.Optional[str] = None,
60
61
  pipeline_substitutions: t.Optional[
61
62
  t.Mapping[str, t.Mapping[str, t.Any]]
@@ -93,6 +94,11 @@ class SimulateClient(NamespacedClient):
93
94
  :param index_template_substitutions: A map of index template names to substitute
94
95
  index template definition objects.
95
96
  :param mapping_addition:
97
+ :param merge_type: The mapping merge type if mapping overrides are being provided
98
+ in mapping_addition. The allowed values are one of index or template. The
99
+ index option merges mappings the way they would be merged into an existing
100
+ index. The template option merges mappings the way they would be merged into
101
+ a template.
96
102
  :param pipeline: The pipeline to use as the default pipeline. This value can
97
103
  be used to override the default pipeline of the index.
98
104
  :param pipeline_substitutions: Pipelines to test. If you don’t specify the `pipeline`
@@ -116,6 +122,8 @@ class SimulateClient(NamespacedClient):
116
122
  __query["filter_path"] = filter_path
117
123
  if human is not None:
118
124
  __query["human"] = human
125
+ if merge_type is not None:
126
+ __query["merge_type"] = merge_type
119
127
  if pipeline is not None:
120
128
  __query["pipeline"] = pipeline
121
129
  if pretty is not None:
@@ -431,11 +431,7 @@ class SlmClient(NamespacedClient):
431
431
  __body["retention"] = retention
432
432
  if schedule is not None:
433
433
  __body["schedule"] = schedule
434
- if not __body:
435
- __body = None # type: ignore[assignment]
436
- __headers = {"accept": "application/json"}
437
- if __body is not None:
438
- __headers["content-type"] = "application/json"
434
+ __headers = {"accept": "application/json", "content-type": "application/json"}
439
435
  return self.perform_request( # type: ignore[return-value]
440
436
  "PUT",
441
437
  __path,
@@ -872,35 +872,40 @@ class SnapshotClient(NamespacedClient):
872
872
 
873
873
  :param name: The name of the repository.
874
874
  :param blob_count: The total number of blobs to write to the repository during
875
- the test. For realistic experiments, you should set it to at least `2000`.
875
+ the test. For realistic experiments, set this parameter to at least `2000`.
876
876
  :param concurrency: The number of operations to run concurrently during the test.
877
+ For realistic experiments, leave this parameter unset.
877
878
  :param detailed: Indicates whether to return detailed results, including timing
878
879
  information for every operation performed during the analysis. If false,
879
880
  it returns only a summary of the analysis.
880
881
  :param early_read_node_count: The number of nodes on which to perform an early
881
882
  read operation while writing each blob. Early read operations are only rarely
882
- performed.
883
+ performed. For realistic experiments, leave this parameter unset.
883
884
  :param max_blob_size: The maximum size of a blob to be written during the test.
884
- For realistic experiments, you should set it to at least `2gb`.
885
+ For realistic experiments, set this parameter to at least `2gb`.
885
886
  :param max_total_data_size: An upper limit on the total size of all the blobs
886
- written during the test. For realistic experiments, you should set it to
887
+ written during the test. For realistic experiments, set this parameter to
887
888
  at least `1tb`.
888
889
  :param rare_action_probability: The probability of performing a rare action such
889
- as an early read, an overwrite, or an aborted write on each blob.
890
+ as an early read, an overwrite, or an aborted write on each blob. For realistic
891
+ experiments, leave this parameter unset.
890
892
  :param rarely_abort_writes: Indicates whether to rarely cancel writes before
891
- they complete.
893
+ they complete. For realistic experiments, leave this parameter unset.
892
894
  :param read_node_count: The number of nodes on which to read a blob after writing.
895
+ For realistic experiments, leave this parameter unset.
893
896
  :param register_operation_count: The minimum number of linearizable register
894
- operations to perform in total. For realistic experiments, you should set
895
- it to at least `100`.
897
+ operations to perform in total. For realistic experiments, set this parameter
898
+ to at least `100`.
896
899
  :param seed: The seed for the pseudo-random number generator used to generate
897
900
  the list of operations performed during the test. To repeat the same set
898
901
  of operations in multiple experiments, use the same seed in each experiment.
899
902
  Note that the operations are performed concurrently so might not always happen
900
- in the same order on each run.
903
+ in the same order on each run. For realistic experiments, leave this parameter
904
+ unset.
901
905
  :param timeout: The period of time to wait for the test to complete. If no response
902
906
  is received before the timeout expires, the test is cancelled and returns
903
- an error.
907
+ an error. For realistic experiments, set this parameter sufficiently long
908
+ to allow the test to complete.
904
909
  """
905
910
  if name in SKIP_IN_PATH:
906
911
  raise ValueError("Empty value passed for parameter 'name'")
@@ -1266,6 +1271,11 @@ class SnapshotClient(NamespacedClient):
1266
1271
  <p>If you omit the <code>&lt;snapshot&gt;</code> request path parameter, the request retrieves information only for currently running snapshots.
1267
1272
  This usage is preferred.
1268
1273
  If needed, you can specify <code>&lt;repository&gt;</code> and <code>&lt;snapshot&gt;</code> to retrieve information for specific snapshots, even if they're not currently running.</p>
1274
+ <p>Note that the stats will not be available for any shard snapshots in an ongoing snapshot completed by a node that (even momentarily) left the cluster.
1275
+ Loading the stats from the repository is an expensive operation (see the WARNING below).
1276
+ Therefore the stats values for such shards will be -1 even though the &quot;stage&quot; value will be &quot;DONE&quot;, in order to minimize latency.
1277
+ A &quot;description&quot; field will be present for a shard snapshot completed by a departed node explaining why the shard snapshot's stats results are invalid.
1278
+ Consequently, the total stats for the index will be less than expected due to the missing values from these shards.</p>
1269
1279
  <p>WARNING: Using the API to return the status of any snapshots other than currently running snapshots can be expensive.
1270
1280
  The API requires a read from the repository for each shard in each snapshot.
1271
1281
  For example, if you have 100 snapshots with 1,000 shards each, an API request that includes all snapshots will require 100,000 reads (100 snapshots x 1,000 shards).</p>
@@ -285,6 +285,7 @@ class SqlClient(NamespacedClient):
285
285
  page_timeout: t.Optional[t.Union[str, t.Literal[-1], t.Literal[0]]] = None,
286
286
  params: t.Optional[t.Sequence[t.Any]] = None,
287
287
  pretty: t.Optional[bool] = None,
288
+ project_routing: t.Optional[str] = None,
288
289
  query: t.Optional[str] = None,
289
290
  request_timeout: t.Optional[t.Union[str, t.Literal[-1], t.Literal[0]]] = None,
290
291
  runtime_mappings: t.Optional[t.Mapping[str, t.Mapping[str, t.Any]]] = None,
@@ -332,6 +333,10 @@ class SqlClient(NamespacedClient):
332
333
  is no longer available. Subsequent scroll requests prolong the lifetime of
333
334
  the scroll cursor by the duration of `page_timeout` in the scroll request.
334
335
  :param params: The values for parameters in the query.
336
+ :param project_routing: Specifies a subset of projects to target for the search
337
+ using project metadata tags in a subset of Lucene query syntax. Allowed Lucene
338
+ queries: the _alias tag and a single value (possibly wildcarded). Examples:
339
+ _alias:my-project _alias:_origin _alias:*pr* Supported in serverless only.
335
340
  :param query: The SQL query to run.
336
341
  :param request_timeout: The timeout before the request fails.
337
342
  :param runtime_mappings: One or more runtime fields for the search request. These
@@ -357,6 +362,8 @@ class SqlClient(NamespacedClient):
357
362
  __query["human"] = human
358
363
  if pretty is not None:
359
364
  __query["pretty"] = pretty
365
+ if project_routing is not None:
366
+ __query["project_routing"] = project_routing
360
367
  if not __body:
361
368
  if allow_partial_search_results is not None:
362
369
  __body["allow_partial_search_results"] = allow_partial_search_results
@@ -15,6 +15,7 @@
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
17
 
18
+
18
19
  import typing as t
19
20
 
20
21
  from elastic_transport import ObjectApiResponse, TextApiResponse
@@ -144,9 +145,7 @@ class StreamsClient(NamespacedClient):
144
145
  error_trace: t.Optional[bool] = None,
145
146
  filter_path: t.Optional[t.Union[str, t.Sequence[str]]] = None,
146
147
  human: t.Optional[bool] = None,
147
- master_timeout: t.Optional[
148
- t.Union[str, t.Literal["d", "h", "m", "micros", "ms", "nanos", "s"]]
149
- ] = None,
148
+ master_timeout: t.Optional[t.Union[str, t.Literal[-1], t.Literal[0]]] = None,
150
149
  pretty: t.Optional[bool] = None,
151
150
  ) -> ObjectApiResponse[t.Any]:
152
151
  """
elasticsearch/_version.py CHANGED
@@ -15,5 +15,5 @@
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
17
 
18
- __versionstr__ = "9.1.2"
19
- __es_specification_commit__ = "cc623e3b52dd3dfd85848ee992713d37da020bfb"
18
+ __versionstr__ = "9.2.0"
19
+ __es_specification_commit__ = "2f74c26e0a1d66c42232ce2830652c01e8717f00"
elasticsearch/client.py CHANGED
@@ -47,6 +47,7 @@ from ._sync.client.migration import MigrationClient as MigrationClient # noqa:
47
47
  from ._sync.client.ml import MlClient as MlClient # noqa: F401
48
48
  from ._sync.client.monitoring import MonitoringClient as MonitoringClient # noqa: F401
49
49
  from ._sync.client.nodes import NodesClient as NodesClient # noqa: F401
50
+ from ._sync.client.project import ProjectClient as ProjectClient # noqa: F401
50
51
  from ._sync.client.query_rules import QueryRulesClient as QueryRulesClient # noqa: F401
51
52
  from ._sync.client.rollup import RollupClient as RollupClient # noqa: F401
52
53
  from ._sync.client.search_application import ( # noqa: F401
@@ -106,6 +107,7 @@ __all__ = [
106
107
  "MlClient",
107
108
  "MonitoringClient",
108
109
  "NodesClient",
110
+ "ProjectClient",
109
111
  "RollupClient",
110
112
  "SearchApplicationClient",
111
113
  "SearchableSnapshotsClient",
elasticsearch/compat.py CHANGED
@@ -15,14 +15,13 @@
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
17
 
18
- import asyncio
19
18
  import inspect
20
19
  import os
21
20
  import sys
22
- from contextlib import asynccontextmanager, contextmanager
21
+ from contextlib import contextmanager
23
22
  from pathlib import Path
24
23
  from threading import Thread
25
- from typing import Any, AsyncIterator, Callable, Coroutine, Iterator, Tuple, Type, Union
24
+ from typing import Any, Callable, Iterator, Tuple, Type, Union
26
25
 
27
26
  string_types: Tuple[Type[str], Type[bytes]] = (str, bytes)
28
27
 
@@ -105,22 +104,10 @@ def safe_thread(
105
104
  raise captured_exception
106
105
 
107
106
 
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
-
119
107
  __all__ = [
120
108
  "string_types",
121
109
  "to_str",
122
110
  "to_bytes",
123
111
  "warn_stacklevel",
124
112
  "safe_thread",
125
- "safe_task",
126
113
  ]
@@ -126,9 +126,10 @@ class AsyncDocument(DocumentBase, metaclass=AsyncIndexMeta):
126
126
  Create an :class:`~elasticsearch.dsl.Search` instance that will search
127
127
  over this ``Document``.
128
128
  """
129
- return AsyncSearch(
129
+ s = AsyncSearch[Self](
130
130
  using=cls._get_using(using), index=cls._default_index(index), doc_type=[cls]
131
131
  )
132
+ return s.source(exclude_vectors=False)
132
133
 
133
134
  @classmethod
134
135
  async def get(
@@ -120,9 +120,10 @@ class Document(DocumentBase, metaclass=IndexMeta):
120
120
  Create an :class:`~elasticsearch.dsl.Search` instance that will search
121
121
  over this ``Document``.
122
122
  """
123
- return Search(
123
+ s = Search[Self](
124
124
  using=cls._get_using(using), index=cls._default_index(index), doc_type=[cls]
125
125
  )
126
+ return s.source(exclude_vectors=False)
126
127
 
127
128
  @classmethod
128
129
  def get(
@@ -34,6 +34,8 @@ from typing import (
34
34
  overload,
35
35
  )
36
36
 
37
+ from typing_extensions import _AnnotatedAlias
38
+
37
39
  try:
38
40
  import annotationlib
39
41
  except ImportError:
@@ -358,6 +360,10 @@ class DocumentOptions:
358
360
  # the field has a type annotation, so next we try to figure out
359
361
  # what field type we can use
360
362
  type_ = annotations[name]
363
+ type_metadata = []
364
+ if isinstance(type_, _AnnotatedAlias):
365
+ type_metadata = type_.__metadata__
366
+ type_ = type_.__origin__
361
367
  skip = False
362
368
  required = True
363
369
  multi = False
@@ -404,6 +410,12 @@ class DocumentOptions:
404
410
  # use best field type for the type hint provided
405
411
  field, field_kwargs = self.type_annotation_map[type_] # type: ignore[assignment]
406
412
 
413
+ # if this field does not have a right-hand value, we look in the metadata
414
+ # of the annotation to see if we find it there
415
+ for md in type_metadata:
416
+ if isinstance(md, (_FieldMetadataDict, Field)):
417
+ attrs[name] = md
418
+
407
419
  if field:
408
420
  field_kwargs = {
409
421
  "multi": multi,
@@ -416,17 +428,20 @@ class DocumentOptions:
416
428
  # this field has a right-side value, which can be field
417
429
  # instance on its own or wrapped with mapped_field()
418
430
  attr_value = attrs[name]
419
- if isinstance(attr_value, dict):
431
+ if isinstance(attr_value, _FieldMetadataDict):
420
432
  # the mapped_field() wrapper function was used so we need
421
433
  # to look for the field instance and also record any
422
434
  # dataclass-style defaults
435
+ if attr_value.get("exclude"):
436
+ # skip this field
437
+ continue
423
438
  attr_value = attrs[name].get("_field")
424
439
  default_value = attrs[name].get("default") or attrs[name].get(
425
440
  "default_factory"
426
441
  )
427
442
  if default_value:
428
443
  field_defaults[name] = default_value
429
- if attr_value:
444
+ if isinstance(attr_value, Field):
430
445
  value = attr_value
431
446
  if required is not None:
432
447
  value._required = required
@@ -505,12 +520,19 @@ class Mapped(Generic[_FieldType]):
505
520
  M = Mapped
506
521
 
507
522
 
523
+ class _FieldMetadataDict(dict[str, Any]):
524
+ """This class is used to identify metadata returned by the `mapped_field()` function."""
525
+
526
+ pass
527
+
528
+
508
529
  def mapped_field(
509
530
  field: Optional[Field] = None,
510
531
  *,
511
532
  init: bool = True,
512
533
  default: Any = None,
513
534
  default_factory: Optional[Callable[[], Any]] = None,
535
+ exclude: bool = False,
514
536
  **kwargs: Any,
515
537
  ) -> Any:
516
538
  """Construct a field using dataclass behaviors
@@ -520,22 +542,25 @@ def mapped_field(
520
542
  options.
521
543
 
522
544
  :param field: The instance of ``Field`` to use for this field. If not provided,
523
- an instance that is appropriate for the type given to the field is used.
545
+ an instance that is appropriate for the type given to the field is used.
524
546
  :param init: a value of ``True`` adds this field to the constructor, and a
525
- value of ``False`` omits it from it. The default is ``True``.
547
+ value of ``False`` omits it from it. The default is ``True``.
526
548
  :param default: a default value to use for this field when one is not provided
527
- explicitly.
549
+ explicitly.
528
550
  :param default_factory: a callable that returns a default value for the field,
529
- when one isn't provided explicitly. Only one of ``factory`` and
530
- ``default_factory`` can be used.
551
+ when one isn't provided explicitly. Only one of ``factory`` and
552
+ ``default_factory`` can be used.
553
+ :param exclude: Set to ``True`` to exclude this field from the Elasticsearch
554
+ index.
531
555
  """
532
- return {
533
- "_field": field,
534
- "init": init,
535
- "default": default,
536
- "default_factory": default_factory,
556
+ return _FieldMetadataDict(
557
+ _field=field,
558
+ init=init,
559
+ default=default,
560
+ default_factory=default_factory,
561
+ exclude=exclude,
537
562
  **kwargs,
538
- }
563
+ )
539
564
 
540
565
 
541
566
  @dataclass_transform(field_specifiers=(mapped_field,))
@@ -0,0 +1,152 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type
19
+
20
+ from pydantic import BaseModel, Field, PrivateAttr
21
+ from typing_extensions import Annotated, Self, dataclass_transform
22
+
23
+ from elasticsearch import dsl
24
+
25
+
26
+ class ESMeta(BaseModel):
27
+ """Metadata items associated with Elasticsearch documents."""
28
+
29
+ id: str = ""
30
+ index: str = ""
31
+ primary_term: int = 0
32
+ seq_no: int = 0
33
+ version: int = 0
34
+ score: float = 0
35
+
36
+
37
+ class _BaseModel(BaseModel):
38
+ meta: Annotated[ESMeta, dsl.mapped_field(exclude=True)] = Field(
39
+ default=ESMeta(),
40
+ init=False,
41
+ )
42
+
43
+
44
+ class _BaseESModelMetaclass(type(BaseModel)): # type: ignore[misc]
45
+ """Generic metaclass methods for BaseEsModel and AsyncBaseESModel."""
46
+
47
+ @staticmethod
48
+ def process_annotations(
49
+ metacls: Type["_BaseESModelMetaclass"], annotations: Dict[str, Any]
50
+ ) -> Dict[str, Any]:
51
+ """Process Pydantic typing annotations and adapt them so that they can
52
+ be used to create the Elasticsearch document.
53
+ """
54
+ updated_annotations = {}
55
+ for var, ann in annotations.items():
56
+ if isinstance(ann, type(BaseModel)):
57
+ # an inner Pydantic model is transformed into an Object field
58
+ updated_annotations[var] = metacls.make_dsl_class(
59
+ metacls, dsl.InnerDoc, ann
60
+ )
61
+ elif (
62
+ hasattr(ann, "__origin__")
63
+ and ann.__origin__ in [list, List]
64
+ and isinstance(ann.__args__[0], type(BaseModel))
65
+ ):
66
+ # an inner list of Pydantic models is transformed into a Nested field
67
+ updated_annotations[var] = List[ # type: ignore[assignment,misc]
68
+ metacls.make_dsl_class(metacls, dsl.InnerDoc, ann.__args__[0])
69
+ ]
70
+ else:
71
+ updated_annotations[var] = ann
72
+ return updated_annotations
73
+
74
+ @staticmethod
75
+ def make_dsl_class(
76
+ metacls: Type["_BaseESModelMetaclass"],
77
+ dsl_class: type,
78
+ pydantic_model: type,
79
+ pydantic_attrs: Optional[Dict[str, Any]] = None,
80
+ ) -> type:
81
+ """Create a DSL document class dynamically, using the structure of a
82
+ Pydantic model."""
83
+ dsl_attrs = {
84
+ attr: value
85
+ for attr, value in dsl_class.__dict__.items()
86
+ if not attr.startswith("__")
87
+ }
88
+ pydantic_attrs = {
89
+ **(pydantic_attrs or {}),
90
+ "__annotations__": metacls.process_annotations(
91
+ metacls, pydantic_model.__annotations__
92
+ ),
93
+ }
94
+ return type(dsl_class)(
95
+ f"_ES{pydantic_model.__name__}",
96
+ (dsl_class,),
97
+ {
98
+ **pydantic_attrs,
99
+ **dsl_attrs,
100
+ "__qualname__": f"_ES{pydantic_model.__name__}",
101
+ },
102
+ )
103
+
104
+
105
+ class BaseESModelMetaclass(_BaseESModelMetaclass):
106
+ """Metaclass for the BaseESModel class."""
107
+
108
+ def __new__(cls, name: str, bases: Tuple[type, ...], attrs: Dict[str, Any]) -> Any:
109
+ model = super().__new__(cls, name, bases, attrs)
110
+ model._doc = cls.make_dsl_class(cls, dsl.Document, model, attrs)
111
+ return model
112
+
113
+
114
+ class AsyncBaseESModelMetaclass(_BaseESModelMetaclass):
115
+ """Metaclass for the AsyncBaseESModel class."""
116
+
117
+ def __new__(cls, name: str, bases: Tuple[type, ...], attrs: Dict[str, Any]) -> Any:
118
+ model = super().__new__(cls, name, bases, attrs)
119
+ model._doc = cls.make_dsl_class(cls, dsl.AsyncDocument, model, attrs)
120
+ return model
121
+
122
+
123
+ @dataclass_transform(kw_only_default=True, field_specifiers=(Field, PrivateAttr))
124
+ class BaseESModel(_BaseModel, metaclass=BaseESModelMetaclass):
125
+ _doc: ClassVar[Type[dsl.Document]]
126
+
127
+ def to_doc(self) -> dsl.Document:
128
+ """Convert this model to an Elasticsearch document."""
129
+ data = self.model_dump()
130
+ meta = {f"_{k}": v for k, v in data.pop("meta", {}).items() if v}
131
+ return self._doc(**meta, **data)
132
+
133
+ @classmethod
134
+ def from_doc(cls, dsl_obj: dsl.Document) -> Self:
135
+ """Create a model from the given Elasticsearch document."""
136
+ return cls(meta=ESMeta(**dsl_obj.meta.to_dict()), **dsl_obj.to_dict())
137
+
138
+
139
+ @dataclass_transform(kw_only_default=True, field_specifiers=(Field, PrivateAttr))
140
+ class AsyncBaseESModel(_BaseModel, metaclass=AsyncBaseESModelMetaclass):
141
+ _doc: ClassVar[Type[dsl.AsyncDocument]]
142
+
143
+ def to_doc(self) -> dsl.AsyncDocument:
144
+ """Convert this model to an Elasticsearch document."""
145
+ data = self.model_dump()
146
+ meta = {f"_{k}": v for k, v in data.pop("meta", {}).items() if v}
147
+ return self._doc(**meta, **data)
148
+
149
+ @classmethod
150
+ def from_doc(cls, dsl_obj: dsl.AsyncDocument) -> Self:
151
+ """Create a model from the given Elasticsearch document."""
152
+ return cls(meta=ESMeta(**dsl_obj.meta.to_dict()), **dsl_obj.to_dict())