1515# specific language governing permissions and limitations
1616# under the License.
1717
18- from typing import Any , ClassVar , Dict , Tuple , Type
18+ from typing import Any , ClassVar , Dict , List , Optional , Tuple , Type
1919
2020from pydantic import BaseModel , Field , PrivateAttr
2121from typing_extensions import Annotated , Self , dataclass_transform
2222
2323from elasticsearch import dsl
2424
2525
26+ class ESMeta (BaseModel ):
27+ id : str = ""
28+ index : str = ""
29+ primary_term : int = 0
30+ seq_no : int = 0
31+ version : int = 0
32+
33+
2634class _BaseModel (BaseModel ):
27- meta : Annotated [Dict [str , Any ], dsl .mapped_field (exclude = True )] = Field (default = {})
35+ meta : Annotated [ESMeta , dsl .mapped_field (exclude = True )] = Field (
36+ default = ESMeta (), init = False
37+ )
2838
2939
30- class BaseESModelMetaclass (type (BaseModel )): # type: ignore[misc]
31- def __new__ (cls , name : str , bases : Tuple [type , ...], attrs : Dict [str , Any ]) -> Any :
32- model = super ().__new__ (cls , name , bases , attrs )
40+ class _BaseESModelMetaclass (type (BaseModel )): # type: ignore[misc]
41+ @staticmethod
42+ def process_annotations (metacls : Type ["_BaseESModelMetaclass" ], annotations : Dict [str , Any ]) -> Dict [str , Any ]:
43+ updated_annotations = {}
44+ for var , ann in annotations .items ():
45+ if isinstance (ann , type (BaseModel )):
46+ # an inner Pydantic model is transformed into an Object field
47+ updated_annotations [var ] = metacls .make_dsl_class (metacls , dsl .InnerDoc , ann )
48+ elif (
49+ hasattr (ann , "__origin__" )
50+ and ann .__origin__ in [list , List ]
51+ and isinstance (ann .__args__ [0 ], type (BaseModel ))
52+ ):
53+ # an inner list of Pydantic models is transformed into a Nested field
54+ updated_annotations [var ] = List [ # type: ignore[assignment,misc]
55+ metacls .make_dsl_class (metacls , dsl .InnerDoc , ann .__args__ [0 ])
56+ ]
57+ else :
58+ updated_annotations [var ] = ann
59+ return updated_annotations
60+
61+ @staticmethod
62+ def make_dsl_class (metacls : Type ["_BaseESModelMetaclass" ], dsl_class : type , pydantic_model : type , pydantic_attrs : Optional [Dict [str , Any ]] = None ) -> type :
3363 dsl_attrs = {
3464 attr : value
35- for attr , value in dsl . AsyncDocument .__dict__ .items ()
65+ for attr , value in dsl_class .__dict__ .items ()
3666 if not attr .startswith ("__" )
3767 }
38- model ._doc = type (dsl .AsyncDocument )( # type: ignore[misc]
39- f"_ES{ name } " ,
40- dsl .AsyncDocument .__bases__ ,
41- {** attrs , ** dsl_attrs , "__qualname__" : f"_ES{ name } " },
68+ pydantic_attrs = {
69+ ** (pydantic_attrs or {}),
70+ "__annotations__" : metacls .process_annotations (
71+ metacls , pydantic_model .__annotations__
72+ ),
73+ }
74+ return type (dsl_class )(
75+ f"_ES{ pydantic_model .__name__ } " ,
76+ (dsl_class ,),
77+ {
78+ ** pydantic_attrs ,
79+ ** dsl_attrs ,
80+ "__qualname__" : f"_ES{ pydantic_model .__name__ } " ,
81+ },
4282 )
83+
84+
85+ class BaseESModelMetaclass (_BaseESModelMetaclass ):
86+ def __new__ (cls , name : str , bases : Tuple [type , ...], attrs : Dict [str , Any ]) -> Any :
87+ model = super ().__new__ (cls , name , bases , attrs )
88+ model ._doc = cls .make_dsl_class (cls , dsl .Document , model , attrs )
4389 return model
4490
4591
4692@dataclass_transform (kw_only_default = True , field_specifiers = (Field , PrivateAttr ))
4793class BaseESModel (_BaseModel , metaclass = BaseESModelMetaclass ):
94+ _doc : ClassVar [Type [dsl .Document ]]
95+
96+ def to_doc (self ) -> dsl .Document :
97+ data = self .model_dump ()
98+ meta = {f"_{ k } " : v for k , v in data .pop ("meta" , {}).items ()}
99+ return self ._doc (** meta , ** data )
100+
101+ @classmethod
102+ def from_doc (cls , dsl_obj : dsl .Document ) -> Self :
103+ return cls (meta = ESMeta (** dsl_obj .meta .to_dict ()), ** dsl_obj .to_dict ())
104+
105+
106+ class AsyncBaseESModelMetaclass (_BaseESModelMetaclass ):
107+ def __new__ (cls , name : str , bases : Tuple [type , ...], attrs : Dict [str , Any ]) -> Any :
108+ model = super ().__new__ (cls , name , bases , attrs )
109+ model ._doc = cls .make_dsl_class (cls , dsl .AsyncDocument , model , attrs )
110+ return model
111+
112+
113+ @dataclass_transform (kw_only_default = True , field_specifiers = (Field , PrivateAttr ))
114+ class AsyncBaseESModel (_BaseModel , metaclass = AsyncBaseESModelMetaclass ):
48115 _doc : ClassVar [Type [dsl .AsyncDocument ]]
49116
50117 def to_doc (self ) -> dsl .AsyncDocument :
@@ -54,4 +121,9 @@ def to_doc(self) -> dsl.AsyncDocument:
54121
55122 @classmethod
56123 def from_doc (cls , dsl_obj : dsl .AsyncDocument ) -> Self :
57- return cls (meta = dsl_obj .meta .to_dict (), ** dsl_obj .to_dict ())
124+ return cls (meta = ESMeta (** dsl_obj .meta .to_dict ()), ** dsl_obj .to_dict ())
125+
126+
127+ # TODO
128+ # - object and nested fields
129+ # - tests
0 commit comments