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 8b6a365 commit a61eae6
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 44 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
# 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)
Expand Down Expand Up @@ -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"}
Expand All @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
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)
12 changes: 8 additions & 4 deletions elasticsearch_metrics/metrics.py
Expand Up @@ -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"


Expand Down Expand Up @@ -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 = {
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


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


class DummyMetricWithExplicitTemplatePattern(Metric):
class DummyMetricWithExplicitTemplatePattern(metrics.Metric):
class Meta:
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 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


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):
class DummyMetric2(Metric):
class DummyMetric2(metrics.Metric):
class Meta:
app_label = "dummyapp2"

Expand Down
26 changes: 13 additions & 13 deletions tests/test_metrics.py
Expand Up @@ -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 (
Expand All @@ -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"}
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):
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"

Expand All @@ -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"

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"]

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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions 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"

Expand All @@ -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"

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


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

Expand All @@ -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 a61eae6

Please sign in to comment.