diff --git a/README.md b/README.md index 7cefd0c..804673b 100644 --- a/README.md +++ b/README.md @@ -40,12 +40,11 @@ A `Metric` is a subclass of [`elasticsearch_dsl.Document`](https://elasticsearch ```python # myapp/metrics.py -from elasticsearch_metrics.metrics import Metric -from elasticsearch_dsl import Integer +from elasticsearch_metrics import metrics -class PageView(Metric): - user_id = Integer() +class PageView(metrics.Metric): + user_id = metrics.Integer() ``` Use the `sync_metrics` management command to ensure that the [index template](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html) @@ -83,8 +82,8 @@ You can configure the index template settings by setting `Metric.Index.settings`. ```python -class PageView(Metric): - user_id = Integer() +class PageView(metrics.Metric): + user_id = metrics.Integer() class Index: settings = {"number_of_shards": 2, "refresh_interval": "5s"} @@ -103,7 +102,7 @@ If you declare a `Metric` outside of an app, you will need to set ```python -class PageView(Metric): +class PageView(metrics.Metric): class Meta: app_label = "myapp" ``` @@ -111,8 +110,8 @@ class PageView(Metric): Alternatively, you can set `template_name` and/or `template` explicitly. ```python -class PageView(Metric): - user_id = Integer() +class PageView(metrics.Metric): + user_id = metrics.Integer() class Meta: template_name = "myapp_pviews" @@ -122,12 +121,11 @@ class PageView(Metric): ## Abstract metrics ```python -from elasticsearch_metrics.metrics import Metric -from elasticsearch_dsl import Integer +from elasticsearch_metrics import metrics -class MyBaseMetric(Metric): - user_id = Integer() +class MyBaseMetric(metrics.Metric): + user_id = metrics.Integer() class Meta: abstract = True @@ -188,9 +186,9 @@ Signals are located in the `elasticsearch_metrics.signals` module. To enable `_source`, you can override it in `class Meta`. ```python -class MyMetric(Metric): +class MyMetric(metrics.Metric): class Meta: - source = MetaField(enabled=True) + source = metrics.MetaField(enabled=True) ``` ## License diff --git a/elasticsearch_metrics/field.py b/elasticsearch_metrics/field.py new file mode 100644 index 0000000..14e25a5 --- /dev/null +++ b/elasticsearch_metrics/field.py @@ -0,0 +1,28 @@ +from django.conf import settings +from elasticsearch_dsl import field as edsl_field + +__all__ = ["Date"] +# Expose all fields from elasticsearch_dsl.field +# We do this instead of 'from elasticsearch_dsl.field import *' because elasticsearch_metrics +# has its own subclass of Date +for each in ( + field_name + for field_name in dir(edsl_field) + if field_name != "Date" and not field_name.startswith("_") +): + field = getattr(edsl_field, each) + is_field_subclass = isinstance(field, type) and issubclass(field, edsl_field.Field) + if field is edsl_field.Field or is_field_subclass: + globals()[each] = field + field.__module__ = __name__ + __all__.append(each) + + +class Date(edsl_field.Date): + """Same as `elasticsearch_dsl.field.Date` except that this respects + the TIMEZONE Django setting. + """ + + def __init__(self, default_timezone=None, *args, **kwargs): + default_timezone = default_timezone or getattr(settings, "TIMEZONE", None) + super(Date, self).__init__(default_timezone=default_timezone, *args, **kwargs) diff --git a/elasticsearch_metrics/metrics.py b/elasticsearch_metrics/metrics.py index 1f88b27..458658e 100644 --- a/elasticsearch_metrics/metrics.py +++ b/elasticsearch_metrics/metrics.py @@ -2,12 +2,16 @@ from django.conf import settings from django.utils import timezone from django.utils.six import add_metaclass -from elasticsearch_dsl import Document, Date +from elasticsearch_dsl import Document from elasticsearch_dsl.document import IndexMeta, MetaField from elasticsearch_metrics.signals import pre_index_template_create, pre_save, post_save from elasticsearch_metrics.registry import registry +# Fields should be imported from this module +from elasticsearch_metrics.field import * # noqa: F40 +from elasticsearch_metrics.field import Date + DEFAULT_DATE_FORMAT = "%Y.%m.%d" @@ -73,10 +77,10 @@ class BaseMetric(object): .. code-block:: python - from elasticsearch_metrics.metrics import Metric + from elasticsearch_metrics import metrics - class PageView(Metric): - user_id = Integer() + class PageView(metrics.Metric): + user_id = metrics.Integer() class Index: settings = { diff --git a/tests/dummyapp/metrics.py b/tests/dummyapp/metrics.py index e2f4ae3..2b084d2 100644 --- a/tests/dummyapp/metrics.py +++ b/tests/dummyapp/metrics.py @@ -1,15 +1,15 @@ -from elasticsearch_metrics.metrics import Metric +from elasticsearch_metrics import metrics -class DummyMetric(Metric): +class DummyMetric(metrics.Metric): pass -class DummyMetricWithExplicitTemplateName(Metric): +class DummyMetricWithExplicitTemplateName(metrics.Metric): class Meta: template_name = "dummymetric" -class DummyMetricWithExplicitTemplatePattern(Metric): +class DummyMetricWithExplicitTemplatePattern(metrics.Metric): class Meta: template = "dummymetric-*" diff --git a/tests/test_field.py b/tests/test_field.py new file mode 100644 index 0000000..678a11d --- /dev/null +++ b/tests/test_field.py @@ -0,0 +1,12 @@ +import pytest +from dateutil import tz + +from elasticsearch_metrics import metrics + + +class TestDate: + @pytest.mark.parametrize("timezone", ["America/Chicago", "UTC"]) + def test_respects_timezone_setting(self, settings, timezone): + settings.TIMEZONE = timezone + field = metrics.Date() + assert field._default_timezone == tz.gettz(timezone) diff --git a/tests/test_management_commands/test_sync_metrics.py b/tests/test_management_commands/test_sync_metrics.py index 12aa252..f137037 100644 --- a/tests/test_management_commands/test_sync_metrics.py +++ b/tests/test_management_commands/test_sync_metrics.py @@ -1,7 +1,8 @@ import pytest import mock + from elasticsearch_metrics.management.commands.sync_metrics import Command -from elasticsearch_metrics.metrics import Metric +from elasticsearch_metrics import metrics from elasticsearch_metrics.registry import registry @@ -25,7 +26,7 @@ def test_with_invalid_app(capsys, run_mgmt_command, mock_create_index_template): def test_with_app_label(run_mgmt_command, mock_create_index_template): - class DummyMetric2(Metric): + class DummyMetric2(metrics.Metric): class Meta: app_label = "dummyapp2" diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 496fa73..0f606db 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -2,8 +2,8 @@ import pytest import datetime as dt from django.utils import timezone -from elasticsearch_metrics.metrics import Metric -from elasticsearch_dsl import IndexTemplate, Keyword, MetaField +from elasticsearch_metrics import metrics +from elasticsearch_dsl import IndexTemplate from elasticsearch_metrics.signals import pre_index_template_create, pre_save, post_save from tests.dummyapp.metrics import ( @@ -13,10 +13,10 @@ ) -class PreprintView(Metric): - provider_id = Keyword(index=True) - user_id = Keyword(index=True) - preprint_id = Keyword(index=True) +class PreprintView(metrics.Metric): + provider_id = metrics.Keyword(index=True) + user_id = metrics.Keyword(index=True) + preprint_id = metrics.Keyword(index=True) class Index: settings = {"refresh_interval": "-1"} @@ -65,12 +65,12 @@ def test_get_index_template_respects_index_settings(self): def test_declaring_metric_with_no_app_label_or_template_name_errors(self): with pytest.raises(RuntimeError): - class BadMetric(Metric): + class BadMetric(metrics.Metric): pass with pytest.raises(RuntimeError): - class MyMetric(Metric): + class MyMetric(metrics.Metric): class Meta: template_name = "osf_metrics_preprintviews" @@ -81,7 +81,7 @@ def test_get_index_template_default_template_name(self): assert "dummyapp_dummymetric-*" in template.to_dict()["index_patterns"] def test_get_index_template_uses_app_label_in_class_meta(self): - class MyMetric(Metric): + class MyMetric(metrics.Metric): class Meta: app_label = "myapp" @@ -110,8 +110,8 @@ def test_template_defined_with_no_template_name_falls_back_to_default_name(self) assert "dummymetric-*" in template.to_dict()["index_patterns"] def test_inheritance(self): - class MyBaseMetric(Metric): - user_id = Keyword(index=True) + class MyBaseMetric(metrics.Metric): + user_id = metrics.Keyword(index=True) class Meta: abstract = True @@ -207,12 +207,12 @@ def test_save_sends_signals(self): # TODO: Can we make this test not use ES? @pytest.mark.es def test_source_may_be_enabled(self, client): - class MyMetric(Metric): + class MyMetric(metrics.Metric): class Meta: app_label = "dummyapp" template_name = "mymetric" template = "mymetric-*" - source = MetaField(enabled=True) + source = metrics.MetaField(enabled=True) MyMetric.create_index_template() template_name = MyMetric._template_name diff --git a/tests/test_registry.py b/tests/test_registry.py index 3dc09d8..e2a24db 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -1,11 +1,11 @@ import pytest -from elasticsearch_metrics.metrics import Metric +from elasticsearch_metrics import metrics from elasticsearch_metrics.registry import registry from tests.dummyapp.metrics import DummyMetric -class MetricWithAppLabel(Metric): +class MetricWithAppLabel(metrics.Metric): class Meta: app_label = "dummyapp" @@ -22,7 +22,7 @@ def test_metric_with_explicit_label_set_is_in_registry(): def test_conflicting_metric(): with pytest.raises(RuntimeError): - class DummyMetric(Metric): + class DummyMetric(metrics.Metric): class Meta: app_label = "dummyapp" @@ -43,7 +43,7 @@ def test_get_metric(): def test_get_metrics(): - class AnotherMetric(Metric): + class AnotherMetric(metrics.Metric): class Meta: app_label = "anotherapp" @@ -58,7 +58,7 @@ class Meta: def test_get_metrics_excludes_abstract_metrics(): - class AbstractMetric(Metric): + class AbstractMetric(metrics.Metric): class Meta: abstract = True @@ -66,6 +66,6 @@ class ConcreteMetric(AbstractMetric): class Meta: app_label = "anotherapp" - assert Metric not in registry.get_metrics() + assert metrics.Metric not in registry.get_metrics() assert AbstractMetric not in registry.get_metrics() assert ConcreteMetric in registry.get_metrics()