Skip to content

Commit

Permalink
support composable templates
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelgrinberg committed Nov 14, 2024
1 parent 5511abe commit a228bb2
Show file tree
Hide file tree
Showing 16 changed files with 191 additions and 14 deletions.
11 changes: 10 additions & 1 deletion elasticsearch_dsl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,14 @@
construct_field,
)
from .function import SF
from .index import AsyncIndex, AsyncIndexTemplate, Index, IndexTemplate
from .index import (
AsyncIndex,
AsyncIndexTemplate,
AsyncNewIndexTemplate,
Index,
IndexTemplate,
NewIndexTemplate,
)
from .mapping import AsyncMapping, Mapping
from .query import Q, Query
from .response import AggResponse, Response, UpdateByQueryResponse
Expand Down Expand Up @@ -109,6 +116,7 @@
"AsyncIndexTemplate",
"AsyncMapping",
"AsyncMultiSearch",
"AsyncNewIndexTemplate",
"AsyncSearch",
"AsyncUpdateByQuery",
"AttrDict",
Expand Down Expand Up @@ -158,6 +166,7 @@
"Murmur3",
"Nested",
"NestedFacet",
"NewIndexTemplate",
"Object",
"Percolator",
"Q",
Expand Down
54 changes: 54 additions & 0 deletions elasticsearch_dsl/_async/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,47 @@ async def save(
)


class AsyncNewIndexTemplate:
def __init__(
self,
name: str,
template: str,
index: Optional["AsyncIndex"] = None,
priority: Optional[int] = None,
**kwargs: Any,
):
if index is None:
self._index = AsyncIndex(template, **kwargs)
else:
if kwargs:
raise ValueError(
"You cannot specify options for Index when"
" passing an Index instance."
)
self._index = index.clone()
self._index._name = template
self._template_name = name
self.priority = priority

def __getattr__(self, attr_name: str) -> Any:
return getattr(self._index, attr_name)

def to_dict(self) -> Dict[str, Any]:
d: Dict[str, Any] = {"template": self._index.to_dict()}
d["index_patterns"] = [self._index._name]
if self.priority is not None:
d["priority"] = self.priority
return d

async def save(
self, using: Optional[AsyncUsingType] = None
) -> "ObjectApiResponse[Any]":
es = get_connection(using or self._index._using)
return await es.indices.put_index_template(
name=self._template_name, **self.to_dict()
)


class AsyncIndex(IndexBase):
_using: AsyncUsingType

Expand Down Expand Up @@ -109,6 +150,19 @@ def as_template(
template_name, pattern or self._name, index=self, order=order
)

def as_new_template(
self,
template_name: str,
pattern: Optional[str] = None,
priority: Optional[int] = None,
) -> AsyncNewIndexTemplate:
# TODO: should we allow pattern to be a top-level arg?
# or maybe have an IndexPattern that allows for it and have
# Document._index be that?
return AsyncNewIndexTemplate(
template_name, pattern or self._name, index=self, priority=priority
)

async def load_mappings(self, using: Optional[AsyncUsingType] = None) -> None:
await self.get_or_create_mapping().update_from_es(
self._name, using=using or self._using
Expand Down
50 changes: 50 additions & 0 deletions elasticsearch_dsl/_sync/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,43 @@ def save(self, using: Optional[UsingType] = None) -> "ObjectApiResponse[Any]":
return es.indices.put_template(name=self._template_name, body=self.to_dict())


class NewIndexTemplate:
def __init__(
self,
name: str,
template: str,
index: Optional["Index"] = None,
priority: Optional[int] = None,
**kwargs: Any,
):
if index is None:
self._index = Index(template, **kwargs)
else:
if kwargs:
raise ValueError(
"You cannot specify options for Index when"
" passing an Index instance."
)
self._index = index.clone()
self._index._name = template
self._template_name = name
self.priority = priority

def __getattr__(self, attr_name: str) -> Any:
return getattr(self._index, attr_name)

def to_dict(self) -> Dict[str, Any]:
d: Dict[str, Any] = {"template": self._index.to_dict()}
d["index_patterns"] = [self._index._name]
if self.priority is not None:
d["priority"] = self.priority
return d

def save(self, using: Optional[UsingType] = None) -> "ObjectApiResponse[Any]":
es = get_connection(using or self._index._using)
return es.indices.put_index_template(name=self._template_name, **self.to_dict())


class Index(IndexBase):
_using: UsingType

Expand Down Expand Up @@ -103,6 +140,19 @@ def as_template(
template_name, pattern or self._name, index=self, order=order
)

def as_new_template(
self,
template_name: str,
pattern: Optional[str] = None,
priority: Optional[int] = None,
) -> NewIndexTemplate:
# TODO: should we allow pattern to be a top-level arg?
# or maybe have an IndexPattern that allows for it and have
# Document._index be that?
return NewIndexTemplate(
template_name, pattern or self._name, index=self, priority=priority
)

def load_mappings(self, using: Optional[UsingType] = None) -> None:
self.get_or_create_mapping().update_from_es(
self._name, using=using or self._using
Expand Down
8 changes: 6 additions & 2 deletions elasticsearch_dsl/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@
# specific language governing permissions and limitations
# under the License.

from ._async.index import AsyncIndex, AsyncIndexTemplate # noqa: F401
from ._sync.index import Index, IndexTemplate # noqa: F401
from ._async.index import ( # noqa: F401
AsyncIndex,
AsyncIndexTemplate,
AsyncNewIndexTemplate,
)
from ._sync.index import Index, IndexTemplate, NewIndexTemplate # noqa: F401
3 changes: 2 additions & 1 deletion examples/alias_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

ALIAS = "test-blog"
PATTERN = ALIAS + "-*"
PRIORITY = 100


class BlogPost(Document):
Expand Down Expand Up @@ -81,7 +82,7 @@ def setup() -> None:
deploy.
"""
# create an index template
index_template = BlogPost._index.as_template(ALIAS, PATTERN)
index_template = BlogPost._index.as_new_template(ALIAS, PATTERN, priority=PRIORITY)
# upload the template into elasticsearch
# potentially overriding the one already there
index_template.save()
Expand Down
3 changes: 2 additions & 1 deletion examples/async/alias_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

ALIAS = "test-blog"
PATTERN = ALIAS + "-*"
PRIORITY = 100


class BlogPost(AsyncDocument):
Expand Down Expand Up @@ -82,7 +83,7 @@ async def setup() -> None:
deploy.
"""
# create an index template
index_template = BlogPost._index.as_template(ALIAS, PATTERN)
index_template = BlogPost._index.as_new_template(ALIAS, PATTERN, priority=PRIORITY)
# upload the template into elasticsearch
# potentially overriding the one already there
await index_template.save()
Expand Down
2 changes: 1 addition & 1 deletion examples/async/parent_child.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ async def save(self, **kwargs: Any) -> None: # type: ignore[override]

async def setup() -> None:
"""Create an IndexTemplate and save it into elasticsearch."""
index_template = Post._index.as_template("base")
index_template = Post._index.as_new_template("base", priority=100)
await index_template.save()


Expand Down
2 changes: 1 addition & 1 deletion examples/parent_child.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ def save(self, **kwargs: Any) -> None: # type: ignore[override]

def setup() -> None:
"""Create an IndexTemplate and save it into elasticsearch."""
index_template = Post._index.as_template("base")
index_template = Post._index.as_new_template("base", priority=100)
index_template.save()


Expand Down
4 changes: 4 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def teardown_method(self, _: Any) -> None:
)
self.client.indices.delete(index="*", expand_wildcards=expand_wildcards)
self.client.indices.delete_template(name="*")
self.client.indices.delete_index_template(name="*")

def es_version(self) -> Tuple[int, ...]:
if not hasattr(self, "_es_version"):
Expand Down Expand Up @@ -172,6 +173,9 @@ def write_client(client: Elasticsearch) -> Generator[Elasticsearch, None, None]:
for index_name in client.indices.get(index="test-*", expand_wildcards="all"):
client.indices.delete(index=index_name)
client.options(ignore_status=404).indices.delete_template(name="test-template")
client.options(ignore_status=404).indices.delete_index_template(
name="test-template"
)


@pytest_asyncio.fixture
Expand Down
25 changes: 24 additions & 1 deletion tests/test_integration/_async/test_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
AsyncDocument,
AsyncIndex,
AsyncIndexTemplate,
AsyncNewIndexTemplate,
Date,
Text,
analysis,
Expand All @@ -35,7 +36,29 @@ class Post(AsyncDocument):

@pytest.mark.asyncio
async def test_index_template_works(async_write_client: AsyncElasticsearch) -> None:
it = AsyncIndexTemplate("test-template", "test-*")
it = AsyncIndexTemplate("test-template", "test-legacy-*")
it.document(Post)
it.settings(number_of_replicas=0, number_of_shards=1)
await it.save()

i = AsyncIndex("test-legacy-blog")
await i.create()

assert {
"test-legacy-blog": {
"mappings": {
"properties": {
"title": {"type": "text", "analyzer": "my_analyzer"},
"published_from": {"type": "date"},
}
}
}
} == await async_write_client.indices.get_mapping(index="test-legacy-blog")


@pytest.mark.asyncio
async def test_new_index_template_works(async_write_client: AsyncElasticsearch) -> None:
it = AsyncNewIndexTemplate("test-template", "test-*")
it.document(Post)
it.settings(number_of_replicas=0, number_of_shards=1)
await it.save()
Expand Down
34 changes: 32 additions & 2 deletions tests/test_integration/_sync/test_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@
import pytest
from elasticsearch import Elasticsearch

from elasticsearch_dsl import Date, Document, Index, IndexTemplate, Text, analysis
from elasticsearch_dsl import (
Date,
Document,
Index,
IndexTemplate,
NewIndexTemplate,
Text,
analysis,
)


class Post(Document):
Expand All @@ -28,7 +36,29 @@ class Post(Document):

@pytest.mark.sync
def test_index_template_works(write_client: Elasticsearch) -> None:
it = IndexTemplate("test-template", "test-*")
it = IndexTemplate("test-template", "test-legacy-*")
it.document(Post)
it.settings(number_of_replicas=0, number_of_shards=1)
it.save()

i = Index("test-legacy-blog")
i.create()

assert {
"test-legacy-blog": {
"mappings": {
"properties": {
"title": {"type": "text", "analyzer": "my_analyzer"},
"published_from": {"type": "date"},
}
}
}
} == write_client.indices.get_mapping(index="test-legacy-blog")


@pytest.mark.sync
def test_new_index_template_works(write_client: Elasticsearch) -> None:
it = NewIndexTemplate("test-template", "test-*")
it.document(Post)
it.settings(number_of_replicas=0, number_of_shards=1)
it.save()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async def test_alias_migration(async_write_client: AsyncElasticsearch) -> None:
await alias_migration.setup()

# verify that template, index, and alias has been set up
assert await async_write_client.indices.exists_template(name=ALIAS)
assert await async_write_client.indices.exists_index_template(name=ALIAS)
assert await async_write_client.indices.exists(index=PATTERN)
assert await async_write_client.indices.exists_alias(name=ALIAS)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
@pytest_asyncio.fixture
async def question(async_write_client: AsyncElasticsearch) -> Question:
await setup()
assert await async_write_client.indices.exists_template(name="base")
assert await async_write_client.indices.exists_index_template(name="base")

# create a question object
q = Question(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_alias_migration(write_client: Elasticsearch) -> None:
alias_migration.setup()

# verify that template, index, and alias has been set up
assert write_client.indices.exists_template(name=ALIAS)
assert write_client.indices.exists_index_template(name=ALIAS)
assert write_client.indices.exists(index=PATTERN)
assert write_client.indices.exists_alias(name=ALIAS)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
@pytest.fixture
def question(write_client: Elasticsearch) -> Question:
setup()
assert write_client.indices.exists_template(name="base")
assert write_client.indices.exists_index_template(name="base")

# create a question object
q = Question(
Expand Down
1 change: 1 addition & 0 deletions utils/run-unasync.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def main(check=False):
"AsyncIndexMeta": "IndexMeta",
"AsyncIndexTemplate": "IndexTemplate",
"AsyncIndex": "Index",
"AsyncNewIndexTemplate": "NewIndexTemplate",
"AsyncUpdateByQuery": "UpdateByQuery",
"AsyncMapping": "Mapping",
"AsyncFacetedSearch": "FacetedSearch",
Expand Down

0 comments on commit a228bb2

Please sign in to comment.