Skip to content
Permalink
Browse files

Make fields importable from metrics module and add Date

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 20, 2018
1 parent 4a93c40 commit add83ee3b0a25c17bf42d08e9e143573f78be20f
@@ -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,16 +102,16 @@ 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"
```

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
@@ -182,9 +180,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
@@ -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)
@@ -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
from elasticsearch_metrics.field import Date

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


@@ -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-*"
@@ -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)
@@ -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"

@@ -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
@@ -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,14 +58,14 @@ class Meta:


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

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()

0 comments on commit add83ee

Please sign in to comment.
You can’t perform that action at this time.