diff --git a/.github/workflows/test_djelme.yml b/.github/workflows/test_djelme.yml index 2a12a1b..d345a93 100644 --- a/.github/workflows/test_djelme.yml +++ b/.github/workflows/test_djelme.yml @@ -28,10 +28,6 @@ jobs: python: ['3.10', '3.11', '3.12', '3.13', '3.14'] django: ['4.2', '5.1', '5.2'] services: - elasticsearch6: - image: elasticsearch:6.8.23 - ports: - - 9206:9200 elasticsearch8: image: elasticsearch:8.19.11 ports: @@ -51,9 +47,8 @@ jobs: run: alias python${{ steps.setup-py.outputs.python-version }}=${{ steps.setup-py.outputs.python-path }} - name: install despondencies - run: pip install -U poetry && poetry install --with=dev --extras=elastic6 --extras=elastic8 + run: pip install -U poetry && poetry install --with=dev --extras=elastic8 - run: TOXENV=`echo 'py${{ matrix.python }}-django${{matrix.django}}' | sed 's/\.//g'` poetry run tox env: - ELASTICSEARCH6_URL: http://localhost:9206 ELASTICSEARCH8_URL: http://localhost:9208 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0118e89..4821acd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,14 +36,14 @@ install [poetry](https://python-poetry.org): pip install poetry ``` -install the current project (in editable mode) and dependencies (with the `dev` dependency group and the extras `elastic6`, `elastic8`, `anydjango`): +install the current project (in editable mode) and dependencies (with the `dev` dependency group and the extras `elastic8`, `anydjango`): ``` -poetry install --with=dev --extras=elastic6 --extras=elastic8 --extras=anydjango +poetry install --with=dev --extras=elastic8 --extras=anydjango ``` ### run tests and checks -these expect elasticsearches to be running and configured in `elasticsearch_metrics/tests/settings.py` (or set environment variables `ELASTICSEARCH6_URL` and `ELASTICSEARCH8_URL`) -- see `using docker-style container tools`, below, for one way to do that +these expect elasticsearch to be running and configured in `elasticsearch_metrics/tests/settings.py` (or set environment variable and `ELASTICSEARCH8_URL`) -- see `using docker-style container tools`, below, for one way to do that running the python module `elasticsearch_metrics.tests` will run tests and linting checks -- any code merged to `main` should pass for all supported python and django combinations @@ -93,9 +93,9 @@ see `testbox.Containerfile` and `docker-compose.yml` for a local setup -- runnin (note: examples use `pc` aliased to a `docker-compose` equivalent) -start elasticsearches in the background +start elasticsearch in the background ``` -pc up -d elasticsearch6 elasticsearch8 +pc up -d elasticsearch8 ``` run tests and lint diff --git a/README.md b/README.md index 49091c3..bc0bb67 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,13 @@ Django app for storing time-series metrics in Elasticsearch. python importables: - `elasticsearch_metrics` - `elasticsearch_metrics.imps.elastic8` (an implementation with elasticsearch 8) -- `elasticsearch_metrics.imps.elastic6` (an implementation with elasticsearch 6; deprecated) - ... ## Pre-requisites * Python >=3.10 * Django 4.2, 5.1, or 5.2 -* Elasticsearch 8 (or 6, for deprecated back-compat) +* Elasticsearch 8 ## Install diff --git a/docker-compose.yml b/docker-compose.yml index 8561448..be84aba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,11 @@ version: '3' services: - elasticsearch6: - image: docker.elastic.co/elasticsearch/elasticsearch:6.3.1 - healthcheck: - interval: 30s - retries: 10 - test: curl -s http://localhost:9200/_cluster/health | grep -vq '"status":"red"' - networks: - - djelme elasticsearch8: image: docker.elastic.co/elasticsearch/elasticsearch:8.19.11 environment: - xpack.security.enabled=false - discovery.type=single-node + - ES_JAVA_OPTS=-Xms256m -Xmx256m # reduce memory usage healthcheck: interval: 30s retries: 10 @@ -24,13 +17,10 @@ services: context: . dockerfile: testbox.Containerfile depends_on: - elasticsearch6: - condition: service_healthy elasticsearch8: condition: service_healthy environment: # TOXENV: py310-django42 - ELASTICSEARCH6_URL: elasticsearch6:9200 ELASTICSEARCH8_URL: http://elasticsearch8:9200 networks: - djelme diff --git a/elasticsearch_metrics/imps/__init__.py b/elasticsearch_metrics/imps/__init__.py index a449ca3..328ea32 100644 --- a/elasticsearch_metrics/imps/__init__.py +++ b/elasticsearch_metrics/imps/__init__.py @@ -1,4 +1 @@ -__all__ = ( - "elastic6", - "elastic8", -) +__all__ = ("elastic8",) diff --git a/elasticsearch_metrics/imps/elastic6.py b/elasticsearch_metrics/imps/elastic6.py deleted file mode 100644 index 5933fc0..0000000 --- a/elasticsearch_metrics/imps/elastic6.py +++ /dev/null @@ -1,400 +0,0 @@ -"""elasticsearch_metrics.imps.elastic6: store events and reports in elasticsearch 6 - -consider this code frozen/deprecated -- will be removed once no longer needed -""" - -from __future__ import annotations -import collections -from collections.abc import Iterator -import dataclasses -import datetime -import functools -import logging - -from django.apps import apps -from django.conf import settings -from django.utils import timezone -from elasticsearch6.exceptions import NotFoundError -from elasticsearch6_dsl import Document, connections, Date -from elasticsearch6_dsl.document import IndexMeta, MetaField -from elasticsearch6_dsl.index import Index - -# re-export all fields, for back-compat convenience -from elasticsearch6_dsl.field import * # noqa: F40 - -from elasticsearch_metrics import signals -from elasticsearch_metrics import exceptions -from elasticsearch_metrics.protocols import ProtoDjelmeBackend -from elasticsearch_metrics.registry import djelme_registry - -DEFAULT_DATE_FORMAT = "%Y.%m.%d" - -logger = logging.getLogger(__name__) - - -class ReadonlyAttrMap: - def __init__(self, inner_obj): - self.__inner_obj = inner_obj - - def __getitem__(self, key): - try: - return getattr(self.__inner_obj, key) - except AttributeError as e: - raise KeyError(key) from e - - def __contains__(self, key): - return hasattr(self.__inner_obj, key) - - def __setitem__(self, key, value): - """implement __setitem__ to appease MutableMapping type on ChainMap -- does nothing""" - - -def _get_default_using(): - """get the elasticsearch-dsl connection name to use""" - _available_backends = djelme_registry.each_backend_name(imp_module_name=__name__) - try: - (_backend_name,) = _available_backends - except ValueError: - logger.warning(f"no djelme backends configured using imp module {__name__!r}!") - return None - return _backend_name - - -class MetricMeta(IndexMeta): - """Metaclass for the base `Metric` class.""" - - def __new__(mcls, name, bases, attrs): # noqa: B902 - - meta = attrs.get("Meta", None) - module = attrs.get("__module__") - - new_cls = super(MetricMeta, mcls).__new__(mcls, name, bases, attrs) - # Also ensure initialization is only performed for subclasses of Metric - # (excluding Metric class itself). - if not any( - b for b in bases if isinstance(b, MetricMeta) and b is not BaseMetric - ): - return new_cls - - template_name = getattr(meta, "template_name", None) - abstract = getattr(meta, "abstract", False) - - app_label = getattr(meta, "app_label", None) - # Look for an application configuration to attach the model to. - app_config = apps.get_containing_app_config(module) - if app_label is None: - if app_config is None: - if not abstract: - raise RuntimeError( - "Metric class %s.%s doesn't declare an explicit " - "app_label and isn't in an application in " - "INSTALLED_APPS." % (module, name) - ) - else: - app_label = app_config.label - assert isinstance(app_label, str) - - if not template_name: - metric_name = new_cls.__name__.lower() - # If template_name not specified in class Meta, - # compute it as _ - template_name = f"{app_label}_{metric_name}" - - new_cls.__template_name = template_name - new_cls.__template_pattern = f"{template_name}_*" # template pattern - # Abstract base metrics can't be instantiated and don't appear in - # the list of metrics for an app. - if not abstract: - djelme_registry.register_recordtype( - new_cls, - imp_module_name=__name__, - app_label=app_label, - default_backend=new_cls._index._using or "", - ) - return new_cls - - # Override IndexMeta.construct_index so that - # a new Index is created for every metric class - # and Index attrs are inherited - @classmethod - def construct_index(cls, opts, bases): - parent_configs = [ - base._index.to_dict() for base in bases if hasattr(base, "_index") - ] - if opts: - index_config = collections.ChainMap(ReadonlyAttrMap(opts), *parent_configs) # type: ignore[arg-type] - else: - index_config = collections.ChainMap(*parent_configs) - - i = Index( - index_config.get("name", "*"), - using=index_config.get("using", _get_default_using()), - ) - i.settings(**index_config.get("settings", {})) - i.aliases(**index_config.get("aliases", {})) - for a in index_config.get("analyzers", ()): - i.analyzer(a) - return i - - @property - def _template_name(self): - _prefix = self.get_index_name_prefix() - return f"{_prefix}{self.__template_name}" - - @property - def _template_pattern(self): - _prefix = self.get_index_name_prefix() - return f"{_prefix}{self.__template_pattern}" - - -# We need this intermediate BaseMetric class so that -# we can run MetricMeta ahead of IndexMeta -class BaseMetric(metaclass=MetricMeta): - """Base metric class with which to define custom metric classes. - - Example usage: - - .. code-block:: python - - from elasticsearch_metrics import metrics - - - class PageView(metrics.Metric): - page_id = metrics.Integer(index=True, doc_values=True) - - class Index: - settings = { - "number_of_shards": 2, - "refresh_interval": "5s", - } - """ - - timestamp = Date(doc_values=True, required=True) - - class Meta: - source = MetaField(enabled=False) - - @classmethod - def sync_index_template(cls, using=None): - """Sync the index template for this metric in Elasticsearch.""" - index_template = cls.get_timeseries_index_template() - index_template.document(cls) - signals.pre_index_template_create.send( - cls, index_template=index_template, using=using - ) - index_template.save(using=using) - signals.post_index_template_create.send( - cls, index_template=index_template, using=using - ) - return index_template - - @classmethod - def check_index_template(cls, using: str | None = None) -> None: - """Check if class is in sync with index template in Elasticsearch. - - :raise: IndexTemplateNotFoundError if index template does not exist. - :raise: IndexTemplateOutOfSyncError if mappings, settings, or index patterns - are out of sync. - :return: True if index template exsits and mappings, settings, and index patterns - are in sync. - """ - client = cls._get_connection(using=using) - try: - template = client.indices.get_template(cls._template_name) - except NotFoundError as client_error: - template_name = cls._template_name - metric_name = cls.__name__ - raise exceptions.IndexTemplateNotFoundError( - f"Index template {template_name!r} does not exist for {metric_name}", - client_error=client_error, - ) from client_error - else: - current_data = list(template.values())[0] - template_data = cls.get_timeseries_index_template().to_dict() - - mappings_in_sync = current_data["mappings"] == template_data["mappings"] - if "settings" in current_data and "index" in current_data["settings"]: - current_settings = current_data["settings"]["index"] - template_settings = template_data.get("settings", {}) - # ES automatically casts number_of_shards and number_of_replicas to a string - # so we need to cast before we compare - # TODO: Are there other settings that need to be handled? - number_settings = {"number_of_shards", "number_of_replicas"} - for setting in number_settings: - if setting in template_settings: - template_settings[setting] = str(template_settings[setting]) - settings_in_sync = current_settings == template_settings - else: - settings_in_sync = True - patterns_in_sync = ( - current_data["index_patterns"] == template_data["index_patterns"] - ) - - if not all([mappings_in_sync, settings_in_sync, patterns_in_sync]): - template_name = cls._template_name - metric_name = cls.__name__ - word_map = { - "mappings": mappings_in_sync, - "patterns": patterns_in_sync, - "settings": settings_in_sync, - } - out_of_sync = ", ".join( - [key for key, value in word_map.items() if not value] - ) - raise exceptions.IndexTemplateOutOfSyncError( - f"Index template {template_name!r} is out of sync with {metric_name} ({out_of_sync})", - mappings_in_sync=mappings_in_sync, - patterns_in_sync=patterns_in_sync, - settings_in_sync=settings_in_sync, - ) - - @classmethod - def check_djelme_setup(cls, using: str | None = None) -> None: - cls.check_index_template(using) - - @classmethod - @functools.cache - def require_been_setup(cls, using: str | None = None) -> None: - """check setup once -- raise on failure, remember success""" - cls.check_djelme_setup(using) - - @classmethod - def get_timeseries_index_template(cls): - """Return an `IndexTemplate ` for this metric.""" - return cls._index.as_template( - template_name=cls._template_name, pattern=cls._template_pattern - ) - - @classmethod - def get_index_name_prefix(cls) -> str: - return "" - - @classmethod - def get_index_name(cls, date: datetime.date | None = None) -> str: - date = date or timezone.now().date() - dateformat = getattr( - settings, "ELASTICSEARCH_METRICS_DATE_FORMAT", DEFAULT_DATE_FORMAT - ) - date_formatted = date.strftime(dateformat) - return f"{cls._template_name}_{date_formatted}" - - -class Metric(Document, BaseMetric): - __doc__ = BaseMetric.__doc__ - - @classmethod - def record(cls, timestamp=None, **kwargs): - """Persist a metric in Elasticsearch. - - :param datetime timestamp: Timestamp for the metric. - """ - instance = cls(timestamp=timestamp, **kwargs) - index = cls.get_index_name(timestamp) - instance.save(index=index) - return instance - - @classmethod - def init(cls, index=None, using=None): - """Create the index and populate the mappings in elasticsearch. - - override Document.init to choose an index name - """ - cls.sync_index_template(using=using) - return super().init(index=index or cls.get_index_name(), using=using) - - def save(self, using=None, index=None, validate=True, **kwargs): - """Same as `Document.save`, except will save into the index determined - by the metric's timestamp field. - """ - self.require_been_setup(using=using) # prevent automapped indexes - self.timestamp = self.timestamp or timezone.now() - if not index: - index = self.get_index_name(date=self.timestamp) - - cls = self.__class__ - signals.pre_save.send(cls, instance=self, using=using, index=index) - ret = super(Metric, self).save( - using=using, index=index, validate=validate, **kwargs - ) - signals.post_save.send(cls, instance=self, using=using, index=index) - return ret - - def djelme_index_name(self) -> str: # for ProtoDjelmeRecord - assert self.timestamp is not None - return self.get_index_name(self.timestamp) - - @classmethod - def _default_index(cls, index=None): - """Overrides Document._default_index so that .search, .get, etc. - use the metric's template pattern as the default index - """ - return index or cls._template_pattern - - @classmethod - def _get_using(cls, using=None): - """get the elasticsearch6_dsl connection name to use - - overrides elasticsearch6.Document._get_using to allow - getting connection name from a djelme imp and default - to the first configured imp that uses this imp module - """ - _backend = None - if using in (None, "default"): - return _get_default_using() - elif isinstance(using, str) and (using in djelme_registry.all_backends): - _backend = djelme_registry.get_backend(using) - assert isinstance(_backend, DjelmeElastic6Backend) - return _backend.backend_name - return super()._get_using(using) - - -@dataclasses.dataclass -class DjelmeElastic6Backend: - """DjelmeElastic6Backend: the elastic6 implementation of djelme (for use by generic djelme code)""" - - backend_name: str - imp_kwargs: dict[str, str] - - @property - def elastic_client(self): - # assumes `connections.configure` was already called - return connections.get_connection(self.backend_name) - - def djelme_backend_name(self) -> str: # for ProtoDjelmeBackend - return self.backend_name - - def djelme_imp_kwargs(self) -> dict[str, str]: # for ProtoDjelmeBackend - return self.imp_kwargs - - def djelme_setup(self, recordtypes: collections.abc.Iterable[type]) -> None: - for _metric_type in recordtypes: - assert issubclass(_metric_type, Metric) - logger.info("setting up %r", _metric_type) - _metric_type.init(using=self.backend_name) - - def djelme_teardown(self, recordtypes: collections.abc.Iterable[type]) -> None: - for _metric_type in recordtypes: - assert issubclass(_metric_type, Metric) - logger.info("tearing down %r", _metric_type) - _indexname_wildcard = _metric_type._template_pattern - _templatename = _metric_type._template_name - _client = self.elastic_client - _client.indices.delete(index=_indexname_wildcard) - try: - _client.indices.delete_template(_templatename) - except NotFoundError: - pass - - -djelme_backend = DjelmeElastic6Backend # for ProtoDjelmeImp - - -def djelme_when_ready( # for ProtoDjelmeImp - backends: Iterator[ProtoDjelmeBackend], -) -> None: - connections.configure( - **{ - _backend.djelme_backend_name(): _backend.djelme_imp_kwargs() - for _backend in backends - } - ) diff --git a/elasticsearch_metrics/tests/dummy6app/__init__.py b/elasticsearch_metrics/tests/dummy6app/__init__.py deleted file mode 100644 index 9a720f4..0000000 --- a/elasticsearch_metrics/tests/dummy6app/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = ("apps", "models", "metrics") diff --git a/elasticsearch_metrics/tests/dummy6app/apps.py b/elasticsearch_metrics/tests/dummy6app/apps.py deleted file mode 100644 index 844569f..0000000 --- a/elasticsearch_metrics/tests/dummy6app/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class Dummy6appConfig(AppConfig): - name = "elasticsearch_metrics.tests.dummy6app" diff --git a/elasticsearch_metrics/tests/dummy6app/metrics.py b/elasticsearch_metrics/tests/dummy6app/metrics.py deleted file mode 100644 index f1f8f71..0000000 --- a/elasticsearch_metrics/tests/dummy6app/metrics.py +++ /dev/null @@ -1,12 +0,0 @@ -from elasticsearch_metrics.imps import elastic6 - - -class Dummy6Metric(elastic6.Metric): - my_int = elastic6.Integer() - - -class Dummy6MetricWithExplicitTemplateName(elastic6.Metric): - my_keyword = elastic6.Keyword() - - class Meta: - template_name = "dummy6metric" diff --git a/elasticsearch_metrics/tests/dummy6app/models.py b/elasticsearch_metrics/tests/dummy6app/models.py deleted file mode 100644 index ea9b783..0000000 --- a/elasticsearch_metrics/tests/dummy6app/models.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = () diff --git a/elasticsearch_metrics/tests/settings.py b/elasticsearch_metrics/tests/settings.py index 8de980e..19812fa 100644 --- a/elasticsearch_metrics/tests/settings.py +++ b/elasticsearch_metrics/tests/settings.py @@ -7,18 +7,12 @@ ALLOWED_HOSTS = [] INSTALLED_APPS = [ "elasticsearch_metrics.apps.ElasticsearchMetricsConfig", - "elasticsearch_metrics.tests.dummy6app", "elasticsearch_metrics.tests.dummy8app", ] MIDDLEWARE_CLASSES = [] DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "test_djelme"}} DJELME_BACKENDS = { - "my_elastic6": { - "elasticsearch_metrics.imps.elastic6": { - "hosts": os.environ.get("ELASTICSEARCH6_URL", ""), - }, - }, "my_elastic8": { "elasticsearch_metrics.imps.elastic8": { "hosts": os.environ.get("ELASTICSEARCH8_URL", ""), diff --git a/elasticsearch_metrics/tests/test_autodiscovery.py b/elasticsearch_metrics/tests/test_autodiscovery.py index b5aa6de..cad23d9 100644 --- a/elasticsearch_metrics/tests/test_autodiscovery.py +++ b/elasticsearch_metrics/tests/test_autodiscovery.py @@ -4,8 +4,6 @@ class TestAutodiscovery(SimpleDjelmeTestCase): def test_app_metrics_are_automatically_registered(self): - # from dummy6app/metrics.py: - self.assertTrue(djelme_registry.get_recordtype("dummy6app.Dummy6Metric")) # from dummy8app/metrics.py: self.assertTrue(djelme_registry.get_recordtype("dummy8app.Dummy8Event")) self.assertTrue(djelme_registry.get_recordtype("dummy8app.Dummy8Event")) diff --git a/elasticsearch_metrics/tests/test_imps_elastic6.py b/elasticsearch_metrics/tests/test_imps_elastic6.py deleted file mode 100644 index f7de7aa..0000000 --- a/elasticsearch_metrics/tests/test_imps_elastic6.py +++ /dev/null @@ -1,317 +0,0 @@ -import unittest -from unittest import mock -import datetime as dt - -from django.utils import timezone - -from elasticsearch_metrics.imps import elastic6 -from elasticsearch6_dsl import ( - IndexTemplate, - analyzer, - tokenizer, - connections, -) -from elasticsearch6_dsl.document import MetaField - -from elasticsearch_metrics import signals -from elasticsearch_metrics.exceptions import ( - IndexTemplateNotFoundError, - IndexTemplateOutOfSyncError, -) -from elasticsearch_metrics.tests.util import ( - SimpleDjelmeTestCase, - MockConnectionTestCase, - RealElasticTestCase, - NoSetupRealElasticTestCase, -) -from elasticsearch_metrics.tests.dummy6app.metrics import ( - Dummy6Metric, - Dummy6MetricWithExplicitTemplateName, -) - -route_prefix_analyzer = analyzer( - "route_prefix_analyzer", - tokenizer=tokenizer("route_prefix_tokenizer", "path_hierarchy", delimiter="."), -) - - -class PreprintView(elastic6.Metric): - provider_id = elastic6.Keyword(index=True) - page_id = elastic6.Keyword(index=True) - preprint_id = elastic6.Keyword(index=True) - route_name = elastic6.Text(analyzer=route_prefix_analyzer) - - class Index: - settings = {"refresh_interval": "-1"} - - class Meta: - app_label = "dummy6app" - template_name = "osf_metrics_preprintviews" - - -class TestGetIndexName(SimpleDjelmeTestCase): - def test_get_index_name(self): - date = dt.date(2020, 2, 14) - assert ( - PreprintView.get_index_name(date=date) - == "osf_metrics_preprintviews_2020.02.14" - ) - - def test_get_index_name_respects_date_format_setting(self): - with self.settings(ELASTICSEARCH_METRICS_DATE_FORMAT="%Y-%m-%d"): - date = dt.date(2020, 2, 14) - assert ( - PreprintView.get_index_name(date=date) - == "osf_metrics_preprintviews_2020-02-14" - ) - - def test_get_index_name_gets_index_for_today_by_default(self): - today = timezone.now().date() - today_formatted = today.strftime("%Y.%m.%d") - assert PreprintView.get_index_name() == "osf_metrics_preprintviews_{}".format( - today_formatted - ) - - -class TestGetIndexTemplate(SimpleDjelmeTestCase): - def test_get_index_template_returns_template_with_correct_name_and_pattern(self): - template = PreprintView.get_timeseries_index_template() - assert isinstance(template, IndexTemplate) - assert template._template_name == "osf_metrics_preprintviews" - assert "osf_metrics_preprintviews_*" in template.to_dict()["index_patterns"] - - def test_get_index_template_respects_index_settings(self): - template = PreprintView.get_timeseries_index_template() - assert template._index.to_dict()["settings"] == { - "refresh_interval": "-1", - "analysis": { - "analyzer": { - "route_prefix_analyzer": { - "tokenizer": "route_prefix_tokenizer", - "type": "custom", - }, - }, - "tokenizer": { - "route_prefix_tokenizer": { - "delimiter": ".", - "type": "path_hierarchy", - }, - }, - }, - } - - def test_get_index_template_creates_template_with_mapping(self): - template = PreprintView.get_timeseries_index_template() - mappings = template.to_dict()["mappings"] - assert mappings["doc"]["_source"]["enabled"] is False - properties = mappings["doc"]["properties"] - assert "timestamp" in properties - assert properties["timestamp"] == {"doc_values": True, "type": "date"} - assert properties["provider_id"] == {"type": "keyword", "index": True} - assert properties["page_id"] == {"type": "keyword", "index": True} - assert properties["preprint_id"] == {"type": "keyword", "index": True} - - # regression test - def test_mappings_are_not_shared(self): - template1 = Dummy6Metric.get_timeseries_index_template() - template2 = Dummy6MetricWithExplicitTemplateName.get_timeseries_index_template() - assert "my_int" in template1.to_dict()["mappings"]["doc"]["properties"] - assert "my_keyword" not in template1.to_dict()["mappings"]["doc"]["properties"] - assert "my_int" not in template2.to_dict()["mappings"]["doc"]["properties"] - assert "my_keyword" in template2.to_dict()["mappings"]["doc"]["properties"] - - @unittest.skip( - "TODO: detects containing app, now tests under elasticsearch_metrics" - ) - def test_declaring_metric_with_no_app_label_or_template_name_errors(self): - with self.assertRaises(RuntimeError): - - class BadMetric(elastic6.Metric): - pass - - with self.assertRaises(RuntimeError): - - class MyMetric(elastic6.Metric): - class Meta: - template_name = "osf_metrics_preprintviews" - - def test_get_index_template_default_template_name(self): - template = Dummy6Metric.get_timeseries_index_template() - assert isinstance(template, IndexTemplate) - assert template._template_name == "dummy6app_dummy6metric" - assert "dummy6app_dummy6metric_*" in template.to_dict()["index_patterns"] - - def test_get_index_template_uses_app_label_in_class_meta(self): - class MyMetric(elastic6.Metric): - class Meta: - app_label = "myapp" - - template = MyMetric.get_timeseries_index_template() - assert template._template_name == "myapp_mymetric" - - def test_template_name_defined_with_no_template_falls_back_to_default_template( - self, - ): - template = Dummy6MetricWithExplicitTemplateName.get_timeseries_index_template() - # template name specified in class Meta - assert template._template_name == "dummy6metric" - # template pattern generated using template name - assert "dummy6metric_*" in template.to_dict()["index_patterns"] - - def test_inheritance(self): - class MyBaseMetric(elastic6.Metric): - page_id = elastic6.Keyword(index=True) - - class Index: - settings = {"number_of_shards": 2} - - class Meta: - abstract = True - - class ConcreteMetric(MyBaseMetric): - class Meta: - app_label = "dummy6app" - - template = ConcreteMetric.get_timeseries_index_template() - assert template._template_name == "dummy6app_concretemetric" - assert template._index.to_dict()["settings"] == {"number_of_shards": 2} - - def test_source_may_be_enabled(self): - class MyMetric(elastic6.Metric): - class Meta: - app_label = "dummy6app" - template_name = "mymetric" - source = MetaField(enabled=True) - - template = MyMetric.get_timeseries_index_template() - - template_dict = template.to_dict() - doc = template_dict["mappings"]["doc"] - assert doc["_source"]["enabled"] is True - - -class TestRecord(MockConnectionTestCase): - def test_calls_index(self): - timestamp = dt.datetime(2017, 8, 21) - p = PreprintView.record(timestamp=timestamp, provider_id="abc12") - assert self.mock_es6_connection.index.call_count == 1 - assert p.timestamp == timestamp - assert p.provider_id == "abc12" - - @mock.patch.object(timezone, "now") - def test_defaults_timestamp_to_now(self, mock_now): - fake_now = dt.datetime(2016, 8, 21) - mock_now.return_value = fake_now - - p = PreprintView.record(provider_id="abc12") - assert self.mock_es6_connection.index.call_count == 1 - assert p.timestamp == fake_now - - -class TestSignals(MockConnectionTestCase): - @mock.patch.object(PreprintView, "get_timeseries_index_template") - def test_create_metric_sends_signals(self, mock_get_index_template): - mock_pre_index_template_listener = mock.Mock() - mock_post_index_template_listener = mock.Mock() - signals.pre_index_template_create.connect(mock_pre_index_template_listener) - signals.post_index_template_create.connect(mock_post_index_template_listener) - PreprintView.sync_index_template() - assert mock_pre_index_template_listener.call_count == 1 - assert mock_post_index_template_listener.call_count == 1 - pre_call_kwargs = mock_pre_index_template_listener.call_args[1] - assert "index_template" in pre_call_kwargs - assert "using" in pre_call_kwargs - - post_call_kwargs = mock_pre_index_template_listener.call_args[1] - assert "index_template" in post_call_kwargs - assert "using" in post_call_kwargs - - def test_save_sends_signals(self): - mock_pre_save_listener = mock.Mock() - mock_post_save_listener = mock.Mock() - signals.pre_save.connect(mock_pre_save_listener, sender=PreprintView) - signals.post_save.connect(mock_post_save_listener, sender=PreprintView) - - provider_id = "12345" - page_id = "abcde" - preprint_id = "zyxwv" - doc = PreprintView( - provider_id=provider_id, page_id=page_id, preprint_id=preprint_id - ) - doc.save() - - assert mock_pre_save_listener.call_count == 1 - pre_save_kwargs = mock_pre_save_listener.call_args[1] - assert isinstance(pre_save_kwargs["instance"], PreprintView) - assert "index" in pre_save_kwargs - assert "using" in pre_save_kwargs - assert pre_save_kwargs["sender"] is PreprintView - - assert mock_post_save_listener.call_count == 1 - post_save_kwargs = mock_pre_save_listener.call_args[1] - assert isinstance(post_save_kwargs["instance"], PreprintView) - assert "index" in post_save_kwargs - assert "using" in post_save_kwargs - assert post_save_kwargs["sender"] is PreprintView - - -class TestIntegration(RealElasticTestCase): - @property - def es6_client(self): - return connections.get_connection("my_elastic6") - - def test_create_document(self): - provider_id = "12345" - page_id = "abcde" - preprint_id = "zyxwv" - doc = PreprintView( - provider_id=provider_id, page_id=page_id, preprint_id=preprint_id - ) - doc.save() - document = PreprintView.get(id=doc.meta.id, index=PreprintView.get_index_name()) - assert document is not None # TODO: more thoroughly - - # index mappings should match the index template - name = PreprintView.get_index_name() - mapping = self.es6_client.indices.get_mapping(index=name) - properties = mapping[name]["mappings"]["doc"]["properties"] - assert properties["timestamp"] == {"type": "date"} - assert properties["provider_id"] == {"type": "keyword"} - assert properties["page_id"] == {"type": "keyword"} - assert properties["preprint_id"] == {"type": "keyword"} - - -class TestIntegrationSetup(NoSetupRealElasticTestCase): - @property - def es6_client(self): - return connections.get_connection("my_elastic6") - - def test_init(self): - PreprintView.init() - name = PreprintView.get_index_name() - mapping = self.es6_client.indices.get_mapping(index=name) - properties = mapping[name]["mappings"]["doc"]["properties"] - assert properties["timestamp"] == {"type": "date"} - assert properties["provider_id"] == {"type": "keyword"} - assert properties["page_id"] == {"type": "keyword"} - assert properties["preprint_id"] == {"type": "keyword"} - - def test_check_djelme_setup(self): - with self.assertRaises(IndexTemplateNotFoundError): - PreprintView.check_djelme_setup() - PreprintView.sync_index_template() - assert PreprintView.check_djelme_setup() is None - - # When settings change, template is out of sync - PreprintView._index.settings( - **{"refresh_interval": "1s", "number_of_shards": 1, "number_of_replicas": 2} - ) - with self.assertRaises(IndexTemplateOutOfSyncError) as excinfo: - PreprintView.check_djelme_setup() - error = excinfo.exception - assert error.settings_in_sync is False - assert error.mappings_in_sync is True - assert error.patterns_in_sync is True - - PreprintView.sync_index_template() - assert PreprintView.check_djelme_setup() is None diff --git a/elasticsearch_metrics/tests/test_management_commands/test_djelme_check.py b/elasticsearch_metrics/tests/test_management_commands/test_djelme_check.py index 06c93be..aeb9706 100644 --- a/elasticsearch_metrics/tests/test_management_commands/test_djelme_check.py +++ b/elasticsearch_metrics/tests/test_management_commands/test_djelme_check.py @@ -10,9 +10,6 @@ class TestCheckRecordtypes(SimpleDjelmeTestCase): def setUp(self): - self.mock6_check_djelme_setup = self.enterContext( - mock.patch("elasticsearch_metrics.imps.elastic6.Metric.check_djelme_setup"), - ) self.mock8_timeseries_check_djelme_setup = self.enterContext( mock.patch( "elasticsearch_metrics.imps.elastic8.TimeseriesRecord.check_djelme_setup" @@ -24,15 +21,6 @@ def setUp(self): ), ) - def test_exits_with_error_if_out_of_sync_6(self): - self.mock6_check_djelme_setup.side_effect = ( - exceptions.IndexTemplateNotFoundError( - "Index template does not exist", client_error=None - ) - ) - with self.assertRaises(CommandError): - self.run_mgmt_command(djelme_backend_check) - def test_exits_with_error_if_out_of_sync_8(self): self.mock8_timeseries_check_djelme_setup.side_effect = ( exceptions.IndexTemplateNotFoundError( @@ -52,8 +40,7 @@ def test_exits_with_error_if_out_of_sync_8_simplerec(self): def test_exits_with_success(self): self.run_mgmt_command(djelme_backend_check) _call_count = ( - self.mock6_check_djelme_setup.call_count - + self.mock8_timeseries_check_djelme_setup.call_count + self.mock8_timeseries_check_djelme_setup.call_count + self.mock8_simple_check_djelme_setup.call_count ) assert _call_count == len(list(registry.each_recordtype())) diff --git a/elasticsearch_metrics/tests/test_management_commands/test_djelme_setup.py b/elasticsearch_metrics/tests/test_management_commands/test_djelme_setup.py index dcd1729..1b7cf0e 100644 --- a/elasticsearch_metrics/tests/test_management_commands/test_djelme_setup.py +++ b/elasticsearch_metrics/tests/test_management_commands/test_djelme_setup.py @@ -2,8 +2,8 @@ from django.core.management import CommandError +from elasticsearch_metrics.imps import elastic8 from elasticsearch_metrics.management.commands import djelme_backend_setup -from elasticsearch_metrics.imps import elastic6 from elasticsearch_metrics.registry import djelme_registry from elasticsearch_metrics.tests.util import SimpleDjelmeTestCase @@ -13,9 +13,6 @@ class TestDjelmeSetup(SimpleDjelmeTestCase): def setUp(self): self.mock_inits = [ - self.enterContext( - unittest.mock.patch("elasticsearch_metrics.imps.elastic6.Metric.init"), - ), self.enterContext( unittest.mock.patch( "elasticsearch_metrics.imps.elastic8.TimeseriesRecord.init" @@ -40,7 +37,7 @@ def test_with_invalid_app(self): assert "No recordtypes found for app 'notanapp'" in str(_raises.exception) def test_with_app_label(self): - class DummyMetric2(elastic6.Metric): + class DummyMetric2(elastic8.SimpleRecord): class Meta: app_label = "dummyapp2" diff --git a/elasticsearch_metrics/tests/test_management_commands/test_djelme_types.py b/elasticsearch_metrics/tests/test_management_commands/test_djelme_types.py index e1ab535..9272d18 100644 --- a/elasticsearch_metrics/tests/test_management_commands/test_djelme_types.py +++ b/elasticsearch_metrics/tests/test_management_commands/test_djelme_types.py @@ -5,10 +5,16 @@ class TestDjelmeTypesCommand(SimpleDjelmeTestCase): def test_without_args_by_name(self): out, err = self.run_mgmt_command(djelme_backend_types) - assert "Dummy6Metric" in out assert "Dummy8Event" in out + assert "Monthly8Event" in out + assert "ThingHappened" in out + assert "ThingHappeningsReport" in out + assert "SimpleKV" in out def test_without_args_by_command(self): out, err = self.run_mgmt_command("djelme_backend_types") - assert "Dummy6Metric" in out assert "Dummy8Event" in out + assert "Monthly8Event" in out + assert "ThingHappened" in out + assert "ThingHappeningsReport" in out + assert "SimpleKV" in out diff --git a/elasticsearch_metrics/tests/test_registry.py b/elasticsearch_metrics/tests/test_registry.py index b8d30b1..0a6bb37 100644 --- a/elasticsearch_metrics/tests/test_registry.py +++ b/elasticsearch_metrics/tests/test_registry.py @@ -1,56 +1,51 @@ from django.test import SimpleTestCase -from elasticsearch_metrics.imps import elastic6 +from elasticsearch_metrics.imps import elastic8 from elasticsearch_metrics.registry import djelme_registry -from elasticsearch_metrics.tests.dummy6app.metrics import Dummy6Metric from elasticsearch_metrics.tests.dummy8app.metrics import Dummy8Event -class MetricWithAppLabel(elastic6.Metric): +class RecordWithAppLabel(elastic8.TimeseriesRecord): class Meta: - app_label = "dummy6app" + app_label = "dummy8app" class TestTimeseriesTypeRegistry(SimpleTestCase): def test_conflicting_recordtype(self): with self.assertRaises(RuntimeError): - class MetricWithAppLabel(elastic6.Metric): + class RecordWithAppLabel(elastic8.TimeseriesRecord): class Meta: - app_label = "dummy6app" + app_label = "dummy8app" def test_get_recordtype(self): - assert ( - djelme_registry.get_recordtype("dummy6app", "Dummy6Metric") is Dummy6Metric - ) - assert djelme_registry.get_recordtype("dummy6app.Dummy6Metric") is Dummy6Metric assert djelme_registry.get_recordtype("dummy8app", "Dummy8Event") is Dummy8Event assert djelme_registry.get_recordtype("dummy8app.Dummy8Event") is Dummy8Event with self.assertRaises(LookupError) as excinfo: - djelme_registry.get_recordtype("dummy6app", "DoesNotExist") + djelme_registry.get_recordtype("dummy8app", "DoesNotExist") assert ( - "App 'dummy6app' doesn't have a 'DoesNotExist' metric." + "App 'dummy8app' doesn't have a 'DoesNotExist' metric." in excinfo.exception.args[0] ) with self.assertRaises(LookupError) as excinfo: - djelme_registry.get_recordtype("notanapp", "Dummy6Metric") + djelme_registry.get_recordtype("notanapp", "Dummy8Event") assert ( "No recordtypes found in app with label 'notanapp'." in excinfo.exception.args[0] ) def test_get_recordtypes(self): - class AnotherMetric(elastic6.Metric): + class AnotherRecord(elastic8.TimeseriesRecord): class Meta: app_label = "anotherapp" - assert Dummy6Metric in djelme_registry.each_recordtype() - assert AnotherMetric in djelme_registry.each_recordtype() - assert Dummy6Metric in djelme_registry.each_recordtype(app_label="dummy6app") - assert AnotherMetric not in djelme_registry.each_recordtype( - app_label="dummy6app" + assert Dummy8Event in djelme_registry.each_recordtype() + assert AnotherRecord in djelme_registry.each_recordtype() + assert Dummy8Event in djelme_registry.each_recordtype(app_label="dummy8app") + assert AnotherRecord not in djelme_registry.each_recordtype( + app_label="dummy8app" ) with self.assertRaises(LookupError) as excinfo: @@ -61,14 +56,17 @@ class Meta: ) def test_get_recordtypes_excludes_abstract_recordtypes(self): - class AbstractMetric(elastic6.Metric): + class AbstractRecord(elastic8.TimeseriesRecord): class Meta: abstract = True - class ConcreteMetric(AbstractMetric): + class ConcreteRecord(AbstractRecord): class Meta: app_label = "anotherapp" - assert elastic6.Metric not in djelme_registry.each_recordtype() - assert AbstractMetric not in djelme_registry.each_recordtype() - assert ConcreteMetric in djelme_registry.each_recordtype() + assert elastic8.BaseDjelmeRecord not in djelme_registry.each_recordtype() + assert elastic8.TimeseriesRecord not in djelme_registry.each_recordtype() + assert elastic8.EventRecord not in djelme_registry.each_recordtype() + assert elastic8.CyclicRecord not in djelme_registry.each_recordtype() + assert AbstractRecord not in djelme_registry.each_recordtype() + assert ConcreteRecord in djelme_registry.each_recordtype() diff --git a/elasticsearch_metrics/tests/util.py b/elasticsearch_metrics/tests/util.py index 4845e2e..a16a580 100644 --- a/elasticsearch_metrics/tests/util.py +++ b/elasticsearch_metrics/tests/util.py @@ -82,25 +82,14 @@ class MockConnectionTestCase(SimpleDjelmeTestCase): def setUp(self): super().setUp() clear_setup_check_caches() - self.mock_es6_connection = mock.Mock() self.mock_es8_connection = mock.Mock() - self.mock_es6_connection.index.return_value = {"result": "created"} self.mock_es8_connection.index.return_value = {"result": "created"} - self.enterContext( - mock.patch( - "elasticsearch_metrics.imps.elastic6.Document._get_connection", - return_value=self.mock_es6_connection, - ), - ) self.enterContext( mock.patch( "elasticsearch_metrics.imps.elastic8.esdsl.Document._get_connection", return_value=self.mock_es8_connection, ), ) - self.mock_es6_require_been_setup = self.enterContext( - mock.patch("elasticsearch_metrics.imps.elastic6.Metric.require_been_setup"), - ) self.mock_es8_require_been_setup = self.enterContext( mock.patch( "elasticsearch_metrics.imps.elastic8.BaseDjelmeRecord.require_been_setup" @@ -116,18 +105,13 @@ def prefixed_index_names(prefix: str = ""): "elasticsearch_metrics.imps.elastic8.BaseDjelmeRecord.get_index_name_prefix", return_value=_name_prefix, ), - mock.patch( - "elasticsearch_metrics.imps.elastic6.BaseMetric.get_index_name_prefix", - return_value=_name_prefix, - ), ): yield def clear_setup_check_caches(): - from elasticsearch_metrics.imps import elastic6, elastic8 + from elasticsearch_metrics.imps import elastic8 - elastic6.Metric.require_been_setup.cache_clear() elastic8.BaseDjelmeRecord.require_been_setup.cache_clear() diff --git a/mypy.ini b/mypy.ini index 02e4445..d066039 100644 --- a/mypy.ini +++ b/mypy.ini @@ -38,9 +38,3 @@ disable_error_code = import-untyped,import-not-found [mypy-elasticsearch_metrics.tests.*] disable_error_code = var-annotated - -[mypy-elasticsearch_metrics.tests.dummy6app.*,elasticsearch_metrics.tests.test_imps_elastic6] -disable_error_code = attr-defined - -[mypy-elasticsearch_metrics.imps.elastic6] -strict = False diff --git a/poetry.lock b/poetry.lock index 2161599..b576169 100644 --- a/poetry.lock +++ b/poetry.lock @@ -788,47 +788,6 @@ urllib3 = ">=1.26.2,<3" [package.extras] develop = ["aiohttp", "furo", "httpx", "opentelemetry-api", "opentelemetry-sdk", "orjson", "pytest", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "pytest-mock", "requests", "respx", "sphinx (>2)", "sphinx-autodoc-typehints", "trustme"] -[[package]] -name = "elasticsearch6" -version = "6.4.2" -description = "Python client for Elasticsearch 6.x" -optional = true -python-versions = "*" -groups = ["main"] -markers = "extra == \"elastic6\"" -files = [ - {file = "elasticsearch6-6.4.2-py2.py3-none-any.whl", hash = "sha256:40c777d193a021a77bd06e8b0c3284d7a9c3d15ac080a9fa616ddfa58058a73d"}, - {file = "elasticsearch6-6.4.2.tar.gz", hash = "sha256:91e304f227cd3cfc17ad3b5ec0518cfcc31af9157fae92aa6f97d20941adc209"}, -] - -[package.dependencies] -urllib3 = ">=1.21.1" - -[package.extras] -develop = ["coverage", "mock", "nose", "nosexcover", "pyaml", "requests (>=2.0.0,<3.0.0)", "sphinx (<1.7)", "sphinx-rtd-theme"] -requests = ["requests (>=2.4.0,<3.0.0)"] - -[[package]] -name = "elasticsearch6-dsl" -version = "6.4.0" -description = "Python client for Elasticsearch" -optional = true -python-versions = "*" -groups = ["main"] -markers = "extra == \"elastic6\"" -files = [ - {file = "elasticsearch6-dsl-6.4.0.tar.gz", hash = "sha256:4bbc60919b73484d028eca31f749f0eea80d8b0bfe0a9a33b54eb0afca1d9b5f"}, - {file = "elasticsearch6_dsl-6.4.0-py2.py3-none-any.whl", hash = "sha256:a5767ef65c50f7c8af7ba6c176bd8df2c1fb501c644bc196cbd675f15c0f2be1"}, -] - -[package.dependencies] -elasticsearch6 = ">=6.0.0,<7.0.0" -python-dateutil = "*" -six = "*" - -[package.extras] -develop = ["coverage (<5.0.0)", "mock", "pytest (>=3.0.0)", "pytest-cov", "pytz", "sphinx", "sphinx-rtd-theme"] - [[package]] name = "elasticsearch8" version = "8.19.3" @@ -1728,7 +1687,7 @@ description = "Extensions to the standard Python datetime module" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main"] -markers = "extra == \"elastic6\" or extra == \"elastic8\"" +markers = "extra == \"elastic8\"" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -1974,7 +1933,7 @@ description = "Python 2 and 3 compatibility utilities" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main"] -markers = "extra == \"elastic6\" or extra == \"elastic8\"" +markers = "extra == \"elastic8\"" files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -2141,7 +2100,7 @@ files = [ {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, ] -markers = {main = "extra == \"elastic6\" or extra == \"elastic8\""} +markers = {main = "extra == \"elastic8\""} [package.extras] brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] @@ -2373,10 +2332,9 @@ cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and pyt [extras] anydjango = ["django"] -elastic6 = ["elasticsearch6-dsl"] elastic8 = ["elasticsearch8"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4" -content-hash = "c242563df56dbfc5ea7980a7daaccf91b948c08d811386eb3dac80fcdda1b770" +content-hash = "c111ea68a38ef36c15e36db09a503bab0fc10e972663c9064997ea6840714bc0" diff --git a/pyproject.toml b/pyproject.toml index b729aaf..05ca88d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "django-elasticsearch-metrics" -version = "2026.0.4" +version = "2026.0.5" description = "Django app for storing time-series metrics in Elasticsearch." authors = [ {name = "CenterForOpenScience", email = "support@cos.io"} @@ -37,7 +37,6 @@ classifiers = [ ] [project.optional-dependencies] # installable extras -elastic6 = [ "elasticsearch6-dsl>=6.3.0,<7.0.0" ] elastic8 = [ "elasticsearch8>=8.0.0,<9.0.0" ] # elastic9 = [ "elasticsearch9>=9.0.0,<10.0.0" ] anydjango = [ "django" ] diff --git a/testbox.Containerfile b/testbox.Containerfile index 0dae5ab..ff41739 100644 --- a/testbox.Containerfile +++ b/testbox.Containerfile @@ -6,7 +6,7 @@ RUN mkdir -p /code WORKDIR /code COPY pyproject.toml poetry.lock ./ -RUN poetry install --compile --no-root --with=dev --extras=elastic6 --extras=elastic8 --extras=anydjango +RUN poetry install --compile --no-root --with=dev --extras=elastic8 --extras=anydjango COPY ./ ./ RUN poetry install --only-root diff --git a/tox.ini b/tox.ini index 6bc1287..a03d51f 100644 --- a/tox.ini +++ b/tox.ini @@ -5,12 +5,10 @@ envlist = [testenv] passenv = - ELASTICSEARCH6_URL ELASTICSEARCH8_URL setenv = DJANGO_SETTINGS_MODULE={env:DJANGO_SETTINGS_MODULE:elasticsearch_metrics.tests.settings} extras = - elastic6 elastic8 dependency_groups = tests