Skip to content

Commit

Permalink
Make fields importable from metrics module and add Date
Browse files Browse the repository at this point in the history
This adds a custom `Date` field which respects
`settings.TIMEZONE`.

This also exposes elasticsearch_dsl's fields from the metrics
module, so that all fields are imported from the same place:

```python
from elasticsearch_metrics import metrics

class MyMetric(metrics.Metric):
  my_keyword = metrics.Keyword()
  my_date = metrics.Date()
```

This approach was the same used in webargs, which uses
marshmallow fields under the hood but overrides a single
field. Discussion: marshmallow-code/webargs#61 (comment)

close #18
  • Loading branch information
sloria committed Aug 21, 2018
1 parent 4a93c40 commit add83ee
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 41 deletions.
28 changes: 13 additions & 15 deletions README.md
Expand Up @@ -40,12 +40,11 @@ A `Metric` is a subclass of [`elasticsearch_dsl.Document`](https://elasticsearch
```python ```python
# myapp/metrics.py # myapp/metrics.py


from elasticsearch_metrics.metrics import Metric from elasticsearch_metrics import metrics
from elasticsearch_dsl import Integer




class PageView(Metric): class PageView(metrics.Metric):
user_id = Integer() 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) Use the `sync_metrics` management command to ensure that the [index template](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html)
Expand Down Expand Up @@ -83,8 +82,8 @@ You can configure the index template settings by setting
`Metric.Index.settings`. `Metric.Index.settings`.


```python ```python
class PageView(Metric): class PageView(metrics.Metric):
user_id = Integer() user_id = metrics.Integer()


class Index: class Index:
settings = {"number_of_shards": 2, "refresh_interval": "5s"} settings = {"number_of_shards": 2, "refresh_interval": "5s"}
Expand All @@ -103,16 +102,16 @@ If you declare a `Metric` outside of an app, you will need to set




```python ```python
class PageView(Metric): class PageView(metrics.Metric):
class Meta: class Meta:
app_label = "myapp" app_label = "myapp"
``` ```


Alternatively, you can set `template_name` and/or `template` explicitly. Alternatively, you can set `template_name` and/or `template` explicitly.


```python ```python
class PageView(Metric): class PageView(metrics.Metric):
user_id = Integer() user_id = metrics.Integer()


class Meta: class Meta:
template_name = "myapp_pviews" template_name = "myapp_pviews"
Expand All @@ -122,12 +121,11 @@ class PageView(Metric):
## Abstract metrics ## Abstract metrics


```python ```python
from elasticsearch_metrics.metrics import Metric from elasticsearch_metrics import metrics
from elasticsearch_dsl import Integer




class MyBaseMetric(Metric): class MyBaseMetric(metrics.Metric):
user_id = Integer() user_id = metrics.Integer()


class Meta: class Meta:
abstract = True abstract = True
Expand Down Expand Up @@ -182,9 +180,9 @@ Signals are located in the `elasticsearch_metrics.signals` module.
To enable `_source`, you can override it in `class Meta`. To enable `_source`, you can override it in `class Meta`.


```python ```python
class MyMetric(Metric): class MyMetric(metrics.Metric):
class Meta: class Meta:
source = MetaField(enabled=True) source = metrics.MetaField(enabled=True)
``` ```


## License ## License
Expand Down
28 changes: 28 additions & 0 deletions 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)
6 changes: 5 additions & 1 deletion elasticsearch_metrics/metrics.py
Expand Up @@ -2,12 +2,16 @@
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.six import add_metaclass 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_dsl.document import IndexMeta, MetaField


from elasticsearch_metrics.signals import pre_index_template_create, pre_save, post_save from elasticsearch_metrics.signals import pre_index_template_create, pre_save, post_save
from elasticsearch_metrics.registry import registry from elasticsearch_metrics.registry import registry


# Fields should be imported from this module
from elasticsearch_metrics.field import * # noqa
from elasticsearch_metrics.field import Date

DEFAULT_DATE_FORMAT = "%Y.%m.%d" DEFAULT_DATE_FORMAT = "%Y.%m.%d"




Expand Down
8 changes: 4 additions & 4 deletions 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 pass




class DummyMetricWithExplicitTemplateName(Metric): class DummyMetricWithExplicitTemplateName(metrics.Metric):
class Meta: class Meta:
template_name = "dummymetric" template_name = "dummymetric"




class DummyMetricWithExplicitTemplatePattern(Metric): class DummyMetricWithExplicitTemplatePattern(metrics.Metric):
class Meta: class Meta:
template = "dummymetric-*" template = "dummymetric-*"
12 changes: 12 additions & 0 deletions 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)
5 changes: 3 additions & 2 deletions tests/test_management_commands/test_sync_metrics.py
@@ -1,7 +1,8 @@
import pytest import pytest
import mock import mock

from elasticsearch_metrics.management.commands.sync_metrics import Command 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 from elasticsearch_metrics.registry import registry




Expand All @@ -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): def test_with_app_label(run_mgmt_command, mock_create_index_template):
class DummyMetric2(Metric): class DummyMetric2(metrics.Metric):
class Meta: class Meta:
app_label = "dummyapp2" app_label = "dummyapp2"


Expand Down
26 changes: 13 additions & 13 deletions tests/test_metrics.py
Expand Up @@ -2,8 +2,8 @@
import pytest import pytest
import datetime as dt import datetime as dt
from django.utils import timezone from django.utils import timezone
from elasticsearch_metrics.metrics import Metric from elasticsearch_metrics import metrics
from elasticsearch_dsl import IndexTemplate, Keyword, MetaField from elasticsearch_dsl import IndexTemplate


from elasticsearch_metrics.signals import pre_index_template_create, pre_save, post_save from elasticsearch_metrics.signals import pre_index_template_create, pre_save, post_save
from tests.dummyapp.metrics import ( from tests.dummyapp.metrics import (
Expand All @@ -13,10 +13,10 @@
) )




class PreprintView(Metric): class PreprintView(metrics.Metric):
provider_id = Keyword(index=True) provider_id = metrics.Keyword(index=True)
user_id = Keyword(index=True) user_id = metrics.Keyword(index=True)
preprint_id = Keyword(index=True) preprint_id = metrics.Keyword(index=True)


class Index: class Index:
settings = {"refresh_interval": "-1"} settings = {"refresh_interval": "-1"}
Expand Down Expand Up @@ -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): def test_declaring_metric_with_no_app_label_or_template_name_errors(self):
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):


class BadMetric(Metric): class BadMetric(metrics.Metric):
pass pass


with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):


class MyMetric(Metric): class MyMetric(metrics.Metric):
class Meta: class Meta:
template_name = "osf_metrics_preprintviews" template_name = "osf_metrics_preprintviews"


Expand All @@ -81,7 +81,7 @@ def test_get_index_template_default_template_name(self):
assert "dummyapp_dummymetric-*" in template.to_dict()["index_patterns"] assert "dummyapp_dummymetric-*" in template.to_dict()["index_patterns"]


def test_get_index_template_uses_app_label_in_class_meta(self): def test_get_index_template_uses_app_label_in_class_meta(self):
class MyMetric(Metric): class MyMetric(metrics.Metric):
class Meta: class Meta:
app_label = "myapp" app_label = "myapp"


Expand Down Expand Up @@ -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"] assert "dummymetric-*" in template.to_dict()["index_patterns"]


def test_inheritance(self): def test_inheritance(self):
class MyBaseMetric(Metric): class MyBaseMetric(metrics.Metric):
user_id = Keyword(index=True) user_id = metrics.Keyword(index=True)


class Meta: class Meta:
abstract = True abstract = True
Expand Down Expand Up @@ -207,12 +207,12 @@ def test_save_sends_signals(self):
# TODO: Can we make this test not use ES? # TODO: Can we make this test not use ES?
@pytest.mark.es @pytest.mark.es
def test_source_may_be_enabled(self, client): def test_source_may_be_enabled(self, client):
class MyMetric(Metric): class MyMetric(metrics.Metric):
class Meta: class Meta:
app_label = "dummyapp" app_label = "dummyapp"
template_name = "mymetric" template_name = "mymetric"
template = "mymetric-*" template = "mymetric-*"
source = MetaField(enabled=True) source = metrics.MetaField(enabled=True)


MyMetric.create_index_template() MyMetric.create_index_template()
template_name = MyMetric._template_name template_name = MyMetric._template_name
Expand Down
12 changes: 6 additions & 6 deletions tests/test_registry.py
@@ -1,11 +1,11 @@
import pytest import pytest


from elasticsearch_metrics.metrics import Metric from elasticsearch_metrics import metrics
from elasticsearch_metrics.registry import registry from elasticsearch_metrics.registry import registry
from tests.dummyapp.metrics import DummyMetric from tests.dummyapp.metrics import DummyMetric




class MetricWithAppLabel(Metric): class MetricWithAppLabel(metrics.Metric):
class Meta: class Meta:
app_label = "dummyapp" app_label = "dummyapp"


Expand All @@ -22,7 +22,7 @@ def test_metric_with_explicit_label_set_is_in_registry():
def test_conflicting_metric(): def test_conflicting_metric():
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):


class DummyMetric(Metric): class DummyMetric(metrics.Metric):
class Meta: class Meta:
app_label = "dummyapp" app_label = "dummyapp"


Expand All @@ -43,7 +43,7 @@ def test_get_metric():




def test_get_metrics(): def test_get_metrics():
class AnotherMetric(Metric): class AnotherMetric(metrics.Metric):
class Meta: class Meta:
app_label = "anotherapp" app_label = "anotherapp"


Expand All @@ -58,14 +58,14 @@ class Meta:




def test_get_metrics_excludes_abstract_metrics(): def test_get_metrics_excludes_abstract_metrics():
class AbstractMetric(Metric): class AbstractMetric(metrics.Metric):
class Meta: class Meta:
abstract = True abstract = True


class ConcreteMetric(AbstractMetric): class ConcreteMetric(AbstractMetric):
class Meta: class Meta:
app_label = "anotherapp" 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 AbstractMetric not in registry.get_metrics()
assert ConcreteMetric in registry.get_metrics() assert ConcreteMetric in registry.get_metrics()

0 comments on commit add83ee

Please sign in to comment.