Skip to content

Commit

Permalink
Support list of primitives
Browse files Browse the repository at this point in the history
- Updated documentation
- Added tests
  • Loading branch information
wallneradam committed Aug 2, 2024
1 parent 2716039 commit 765e294
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 12 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,21 @@ class Post(ESModel):
writer: User # User is a nested document
```

<a id="list-primitive-fields"></a>
#### List primitive fields

You can use list of primitive fields:

```python
from typing import List
from esorm import ESModel


class User(ESModel):
emails: List[str]
favorite_ids: List[int]
...
```

<a id="esbasemodel"></a>
#### ESBaseModel
Expand Down
43 changes: 42 additions & 1 deletion docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,46 @@ async def get_user_by_region(region: str = 'europe') -> List[User]:
return await User.search_by_fields(region=region, routing=f"{region}_routing")
```

<a id="retreive-selected-fields-only"></a>
## Retreive Selected Fields Only

When you query documents from Elasticsearch, you can specify which fields you want to get back.
For this you can use teh `_source` argument in `search` methods and `get` method.
This will only work if you specify default values for all the fields you may skip:

```python
import esorm

class Model(esorm.ESModel):
f_int: int = 0
f_str: str = 'a'


async def test_source():
doc = Model(f_int=1, f_str='b')
doc_id = await doc.save()

doc = await Model.get(doc_id)
assert doc.f_str == 'b'
assert doc.f_int == 1

doc = await Model.search_one_by_fields(dict(_id=doc_id), _source=['f_str'])
assert doc.f_str == 'b'
assert doc.f_int == 0

doc = await Model.search_one_by_fields(dict(_id=doc_id), _source=['f_int'])
assert doc.f_str == 'a'
assert doc.f_int == 1

doc = await Model.get(doc_id, _source=['f_str'])
assert doc.f_str == 'b'
assert doc.f_int == 0

doc = await Model.get(doc_id, _source=['f_int'])
assert doc.f_str == 'a'
assert doc.f_int == 1
```

<a id="watchers"></a>
## Watchers

Expand All @@ -218,9 +258,10 @@ a combination of the two.
More info: https://www.elastic.co/guide/en/elasticsearch/reference/current/how-watcher-works.html

<small>
The watcher feature is not free in Elasticsearch, you need to have a license for it. Or if your are
The watcher feature is not free in Elasticsearch, you need to have a license for it. Or if you are
an experienced developer, you can compile Elasticsearch from source and disable the license check.
You can do it for your own use, because the source code is available, though it is not free.
(hint: you need to recompile the `x-pack` plugin only, and you can replace it in the compiled version)
</small>

The following example shows how to create a watcher which deletes all draft documents older than 1 hour:
Expand Down
40 changes: 32 additions & 8 deletions esorm/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,18 +553,20 @@ async def save(self, *, wait_for=False, pipeline: Optional[str] = None, routing:
# noinspection PyShadowingBuiltins
@classmethod
async def get(cls: Type[TModel], id: Union[str, int, float], *, routing: Optional[str] = None,
index: Optional[str] = None) -> TModel:
index: Optional[str] = None, _source: Optional[Union[str, List[str]]] = None, **kwargs) -> TModel:
"""
Fetches document and returns ESModel instance populated with properties.
:param id: Document id
:param routing: Shard routing value
:param index: Index name, if not set, it will use the index from ESConfig3663
:param _source: Fields to return, if not set, it will return all fields
:param kwargs: Other search API params
:raises esorm.error.NotFoundError: Returned if document not found
:return: ESModel object
"""
try:
es_res = await cls.call('get', routing=routing, id=id, index=index)
es_res = await cls.call('get', routing=routing, id=id, index=index, _source=_source, **kwargs)
return await _lazy_process_results(cls.from_es(es_res))
except ElasticNotFoundError:
raise NotFoundError(f"Document with id {id} not found")
Expand Down Expand Up @@ -655,6 +657,7 @@ async def search(cls: Type[TModel], query: ESQuery, *,
routing: Optional[str] = None,
res_dict: bool = False,
index: Optional[str] = None,
_source: Optional[Union[str, List[str]]] = None,
**kwargs) -> Union[List[TModel], Dict[str, TModel]]:
"""
Search Model with query dict
Expand All @@ -667,11 +670,12 @@ async def search(cls: Type[TModel], query: ESQuery, *,
:param routing: Shard routing value
:param res_dict: If the result should be a dict with id as key and model as value instead of a list of models
:param index: Index name, if not set, it will use the index from ESConfig
:param _source: Fields to return, if not set, it will return all fields
:param kwargs: Other search API params
:return: The result list
"""
res = await cls._search(query, page_size=page_size, page=page, sort=sort, routing=routing,
index=index, **kwargs)
index=index, _source=_source, **kwargs)
try:
if res_dict:
res = {hit['_id']: cls.from_es(hit) for hit in res['hits']['hits']}
Expand All @@ -683,17 +687,20 @@ async def search(cls: Type[TModel], query: ESQuery, *,

@classmethod
async def search_one(cls: Type[TModel], query: ESQuery, *, routing: Optional[str] = None,
index: Optional[str] = None, **kwargs) -> Optional[TModel]:
index: Optional[str] = None,
_source: Optional[Union[str, List[str]]] = None,
**kwargs) -> Optional[TModel]:
"""
Search Model and return the first result
:param query: ElasticSearch query dict
:param routing: Shard routing value
:param index: Index name, if not set, it will use the index from ESConfig
:param _source: Fields to return, if not set, it will return all fields
:param kwargs: Other search API params
:return: The first result or None if no result
"""
res = await cls.search(query, page_size=1, routing=routing, index=index, **kwargs)
res = await cls.search(query, page_size=1, routing=routing, _source=_source, index=index, **kwargs)
if len(res) > 0:
return res[0]
else:
Expand Down Expand Up @@ -728,6 +735,7 @@ async def search_by_fields(cls: Type[TModel],
aggs: Optional[ESAggs] = None,
res_dict: bool = False,
index: Optional[str] = None,
_source: Optional[Union[str, List[str]]] = None,
**kwargs) -> List[TModel]:
"""
Search Model by fields as key-value pairs
Expand All @@ -741,19 +749,21 @@ async def search_by_fields(cls: Type[TModel],
:param aggs: Aggregations
:param res_dict: If the result should be a dict with id as key and model as value instead of a list of models
:param index: Index name, if not set, it will use the index from ESConfig
:param _source: Fields to return, if not set, it will return all fields
:param kwargs: Other search API params
:return: The result list
"""
query = cls.create_query_from_dict(fields)
return await cls.search(query, page_size=page_size, page=page, sort=sort, routing=routing,
aggs=aggs, res_dict=res_dict, index=index, **kwargs)
aggs=aggs, res_dict=res_dict, index=index, _source=_source, **kwargs)

@classmethod
async def search_one_by_fields(cls: Type[TModel],
fields: Dict[str, Union[str, int, float]],
*, routing: Optional[str] = None,
aggs: Optional[ESAggs] = None,
index: Optional[str] = None,
_source: Optional[Union[str, List[str]]] = None,
**kwargs) -> Optional[TModel]:
"""
Search Model by fields as key-value pairs and return the first result
Expand All @@ -762,11 +772,12 @@ async def search_one_by_fields(cls: Type[TModel],
:param routing: Shard routing value
:param aggs: Aggregations
:param index: Index name, if not set, it will use the index from ESConfig
:param _source: Fields to return, if not set, it will return all fields
:param kwargs: Other search API params
:return: The first result or None if no result
"""
query = cls.create_query_from_dict(fields)
return await cls.search_one(query, routing=routing, aggs=aggs, index=index, **kwargs)
return await cls.search_one(query, routing=routing, aggs=aggs, index=index, _source=_source, **kwargs)

@classmethod
async def all(cls: Type[TModel], index: Optional[str] = None, **kwargs) -> List[TModel]:
Expand Down Expand Up @@ -1157,8 +1168,21 @@ def get_field_data(pydantic_type: type) -> dict:

# List types
if origin is list:
arg = args[0]

# Python type
try:
return {'type': _pydantic_type_map[arg]}
except KeyError:
pass

# ESORM type
if hasattr(arg, '__es_type__'):
return {'type': arg.__es_type__}

# Nested class
properties = {}
create_mapping(args[0], properties)
create_mapping(arg, properties)
return {
'type': 'nested',
'properties': properties
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = pyesorm
version = 0.5.0
version = 0.5.1
author = Adam Wallner
author_email = [email protected]
description = Python ElasticSearch ORM based on Pydantic
Expand Down
77 changes: 75 additions & 2 deletions tests/test_esorm.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,6 @@ async def test_python_types(self, es, esorm, model_python):
from datetime import datetime, date, time
from uuid import uuid4, UUID

await esorm.setup_mappings()

doc = model_python(
f_str='test', f_int=123, f_float=0.123, f_bool=True,
f_datetime=datetime.now(),
Expand Down Expand Up @@ -912,3 +910,78 @@ async def test_base_model_parent(self, es, esorm, model_base_model_parent):
assert doc.f_float == 0.123

assert doc._id == 'test'

async def test_source(self, es, esorm):
"""
Test _source argument
"""

class TestSourceModel(esorm.ESModel):
f_int: int = 0
f_str: str = 'a'

await esorm.setup_mappings()

doc = TestSourceModel(f_int=1, f_str='b')
doc_id = await doc.save()
assert doc_id is not None

doc = await TestSourceModel.get(doc_id)
assert doc.f_str == 'b'
assert doc.f_int == 1

doc = await TestSourceModel.search_one_by_fields(dict(_id=doc_id), _source=['f_str'])
assert doc.f_str == 'b'
assert doc.f_int == 0

doc = await TestSourceModel.search_one_by_fields(dict(_id=doc_id), _source=['f_int'])
assert doc.f_str == 'a'
assert doc.f_int == 1

doc = await TestSourceModel.get(doc_id, _source=['f_str'])
assert doc.f_str == 'b'
assert doc.f_int == 0

doc = await TestSourceModel.get(doc_id, _source=['f_int'])
assert doc.f_str == 'a'
assert doc.f_int == 1

async def test_primitive_list(self, es, esorm):
"""
Test primitive list
"""
from typing import List

class PrimitiveListModel(esorm.ESModel):
f_int_list: List[int] = []
f_str_list: List[str] = []

await esorm.setup_mappings()

doc = PrimitiveListModel(f_int_list=[1, 2, 3], f_str_list=['a', 'b', 'c'])
doc_id = await doc.save()
assert doc_id is not None

doc = await PrimitiveListModel.get(doc_id)
assert doc.f_int_list == [1, 2, 3]
assert doc.f_str_list == ['a', 'b', 'c']

@pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires Python 3.10 or higher")
async def test_primitive_list_new_syntax(self, es, esorm):
"""
Test primitive list new syntax
"""

class PrimitiveListModel(esorm.ESModel):
f_int_list: list[int] = []
f_str_list: list[str] = []

await esorm.setup_mappings()

doc = PrimitiveListModel(f_int_list=[1, 2, 3], f_str_list=['a', 'b', 'c'])
doc_id = await doc.save()
assert doc_id is not None

doc = await PrimitiveListModel.get(doc_id)
assert doc.f_int_list == [1, 2, 3]
assert doc.f_str_list == ['a', 'b', 'c']

0 comments on commit 765e294

Please sign in to comment.