Skip to content

Commit

Permalink
Add an dataclass for emitting events
Browse files Browse the repository at this point in the history
  • Loading branch information
Zsailer committed Aug 17, 2022
1 parent fa5f3c7 commit 3c5689d
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 73 deletions.
9 changes: 6 additions & 3 deletions docs/user_guide/application.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ To begin using Jupyter Events in your Python application, create an instance of
```python
from jupyter_core.application import JupyterApp
from jupyter_events import EventLogger
from jupyter_events import Event


class MyApplication(JupyterApp):
Expand Down Expand Up @@ -44,9 +45,11 @@ Call `.emit(...)` within the application to emit an instance of the event.
...
# Emit event telling listeners that this event happened.
self.eventlogger.emit(
id="myapplication.org/my-method",
version=1,
data={"msg": "Hello, world!"}
Event(
id="myapplication.org/my-method",
version=1,
data={"msg": "Hello, world!"}
)
)
# Do something else...
...
Expand Down
16 changes: 10 additions & 6 deletions docs/user_guide/first-event.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,19 @@ handler = StreamHandler()
logger.register_handler(handler)
```

The logger knows about the event and where to send it; all that's left is to emit an instance of the event!
The logger knows about the event and where to send it; all that's left is to emit an instance of the event! To to do this, import and create an instance of the `Event` dataclass, setting the `schema_id`, `version`, and `data` attributes and pass it to the `.emit` method.

```python
from jupyter_events import Event

logger.emit(
schema_id="myapplication.org/example-event",
version=1,
data={
"name": "My Event"
}
Event(
schema_id="myapplication.org/example-event",
version=1,
data={
"name": "My Event"
}
)
)
```

Expand Down
2 changes: 1 addition & 1 deletion docs/user_guide/modifiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def my_modifier(data: ModifierData) -> dict:
...
```

`ModifierData` is a dataclass with three attributes: `schema_id` (`str`), `version` (`int`), and `data` (`dict`). The return value is the mutated data dictionary. This dictionary will be validated and emitted _after_ it is modified, so it still must follow the event's schema.
`ModifierData` is a dataclass with three attributes: `schema_id` (`str`), `version` (`int`), and `event_data` (`dict`). The return value is the mutated data dictionary. This dictionary will be validated and emitted _after_ it is modified, so it still must follow the event's schema.

Next, add this modifier to the event logger using the `.add_modifier` method:

Expand Down
6 changes: 3 additions & 3 deletions jupyter_events/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Increment this version when the metadata included with each event
# changes.
EVENTS_METADATA_VERSION = 1
from .dataclasses import Event, ModifierData
from .logger import EVENTS_METADATA_VERSION, EventLogger
from .schema import EventSchema
15 changes: 15 additions & 0 deletions jupyter_events/dataclasses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from dataclasses import dataclass


@dataclass
class ModifierData:
schema_id: str
version: int
event_data: dict


@dataclass
class Event:
schema_id: str
version: int
data: dict

This comment has been minimized.

Copy link
@Zsailer

Zsailer Aug 17, 2022

Author Member

Even though these dataclasses look similar (today), I made them separate dataclasses so that we can evolve their associated APIs separately.

39 changes: 21 additions & 18 deletions jupyter_events/logger.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""
Emit structured, discrete events when various actions happen.
"""
import copy
import inspect
import json
import logging
import warnings
from dataclasses import dataclass
from datetime import datetime
from pathlib import PurePath
from typing import Callable, Union
Expand All @@ -14,10 +14,14 @@
from traitlets import Instance, List, default
from traitlets.config import Config, Configurable

from . import EVENTS_METADATA_VERSION
from .dataclasses import Event, ModifierData
from .schema_registry import SchemaRegistry
from .traits import Handlers

# Increment this version when the metadata included with each event
# changes.
EVENTS_METADATA_VERSION = 1


class SchemaNotRegistered(Warning):
"""A warning to raise when an event is given to the logger
Expand All @@ -31,13 +35,6 @@ class ModifierError(Exception):
"""


@dataclass
class ModifierData:
schema_id: str
version: int
data: dict


# Only show this warning on the first instance
# of each event type that fails to emit.
warnings.simplefilter("once", SchemaNotRegistered)
Expand Down Expand Up @@ -182,7 +179,7 @@ def modifier_signature(data: ModifierData) -> dict:
"and the return value."
)

def emit(self, schema_id: str, version: int, data: dict, timestamp_override=None):
def emit(self, event: Event, timestamp_override=None):
"""
Record given event with schema has occurred.
Expand All @@ -208,21 +205,27 @@ def emit(self, schema_id: str, version: int, data: dict, timestamp_override=None

# If the schema hasn't been registered, raise a warning to make sure
# this was intended.
if (schema_id, version) not in self.schemas:
if (event.schema_id, event.version) not in self.schemas:
warnings.warn(
f"({schema_id}, {version}) has not been registered yet. If "
"this was not intentional, please register the schema using the "
f"({event.schema_id}, {event.version}) has not "
"been registered yet. If this was not intentional, "
"please register the schema using the "
"`register_event_schema` method.",
SchemaNotRegistered,
)
return

# Modify this event in-place.
# Deep copy the data and modify the copy.
data = copy.deepcopy(event.data)
for modifier in self.modifiers:
data = modifier(schema_id, version, data)
data = modifier(
ModifierData(
schema_id=event.schema_id, version=event.version, event_data=data
)
)

# Process this event, i.e. validate and redact (in place)
self.schemas.validate_event(schema_id, version, data)
self.schemas.validate_event(event.schema_id, event.version, data)

# Generate the empty event capsule.
if timestamp_override is None:
Expand All @@ -231,8 +234,8 @@ def emit(self, schema_id: str, version: int, data: dict, timestamp_override=None
timestamp = timestamp_override
capsule = {
"__timestamp__": timestamp.isoformat() + "Z",
"__schema__": schema_id,
"__schema_version__": version,
"__schema__": event.schema_id,
"__schema_version__": event.version,
"__metadata_version__": EVENTS_METADATA_VERSION,
}
capsule.update(data)
Expand Down
41 changes: 21 additions & 20 deletions tests/test_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from traitlets import TraitError
from traitlets.config.loader import PyFileConfigLoader

from jupyter_events import yaml
from jupyter_events import Event, yaml
from jupyter_events.logger import EventLogger
from jupyter_events.schema_registry import SchemaRegistryException

Expand Down Expand Up @@ -114,7 +114,8 @@ def test_timestamp_override():

timestamp_override = datetime.utcnow() - timedelta(days=1)
el.emit(
"test/test", 1, {"something": "blah"}, timestamp_override=timestamp_override
Event(schema_id="test/test", version=1, data={"something": "blah"}),
timestamp_override=timestamp_override,
)
handler.flush()
event_capsule = json.loads(output.getvalue())
Expand All @@ -141,13 +142,7 @@ def test_emit():
el = EventLogger(handlers=[handler])
el.register_event_schema(schema)

el.emit(
"test/test",
1,
{
"something": "blah",
},
)
el.emit(Event(schema_id="test/test", version=1, data={"something": "blah"}))
handler.flush()

event_capsule = json.loads(output.getvalue())
Expand Down Expand Up @@ -235,7 +230,9 @@ def test_emit_badschema():
el.allowed_schemas = ["test/test"]

with pytest.raises(jsonschema.ValidationError):
el.emit("test/test", 1, {"something": "blah", "status": "hi"}) # 'not-in-enum'
el.emit(
Event("test/test", 1, {"something": "blah", "status": "hi"})
) # 'not-in-enum'


def test_unique_logger_instances():
Expand Down Expand Up @@ -277,18 +274,22 @@ def test_unique_logger_instances():
el1.allowed_schemas = ["test/test1"]

el0.emit(
"test/test0",
1,
{
"something": "blah",
},
Event(
"test/test0",
1,
{
"something": "blah",
},
)
)
el1.emit(
"test/test1",
1,
{
"something": "blah",
},
Event(
"test/test1",
1,
{
"something": "blah",
},
)
)
handler0.flush()
handler1.flush()
Expand Down
44 changes: 22 additions & 22 deletions tests/test_modifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import pytest

from jupyter_events import Event
from jupyter_events.dataclasses import ModifierData
from jupyter_events.logger import EventLogger, ModifierError
from jupyter_events.schema import EventSchema

Expand All @@ -22,16 +24,16 @@ def test_modifier_function():
logger.register_handler(handler)
logger.register_event_schema(schema)

def redactor(schema_id: str, version: int, data: dict) -> dict:
if "username" in data:
data["username"] = "<masked>"
return data
def redactor(data: ModifierData) -> dict:
d = data.event_data
if "username" in d:
d["username"] = "<masked>"
return d

# Add the modifier
logger.add_modifier(redactor)

logger.emit(schema.id, schema.version, {"username": "jovyan"})

logger.emit(Event(schema.id, schema.version, {"username": "jovyan"}))
# Flush from the handler
handler.flush()
# Read from the io
Expand All @@ -53,17 +55,18 @@ def test_modifier_method():
logger.register_event_schema(schema)

class Redactor:
def redact(self, schema_id: str, version: int, data: dict) -> dict:
if "username" in data:
data["username"] = "<masked>"
return data
def redact(self, data: ModifierData) -> dict:
d = data.event_data
if "username" in d:
d["username"] = "<masked>"
return d

redactor = Redactor()

# Add the modifier
logger.add_modifier(redactor.redact)

logger.emit(schema.id, schema.version, {"username": "jovyan"})
logger.emit(Event(schema.id, schema.version, {"username": "jovyan"}))

# Flush from the handler
handler.flush()
Expand All @@ -76,10 +79,8 @@ def redact(self, schema_id: str, version: int, data: dict) -> dict:
def test_bad_modifier_functions():
logger = EventLogger()

def modifier_with_extra_args(
schema_id: str, version: int, data: dict, unknown_arg: dict
) -> dict:
return data
def modifier_with_extra_args(data: ModifierData, unknown_arg: dict) -> dict:
return data.event_data

with pytest.raises(ModifierError):
logger.add_modifier(modifier_with_extra_args)
Expand All @@ -101,12 +102,11 @@ def test_bad_modifier_method():
logger = EventLogger()

class Redactor:
def redact(
self, schema_id: str, version: int, data: dict, extra_args: dict
) -> dict:
if "username" in data:
data["username"] = "<masked>"
return data
def redact(self, data: ModifierData, extra_args: dict) -> dict:
d = data.event_data
if "username" in d:
d["username"] = "<masked>"
return d

redactor = Redactor()

Expand All @@ -120,7 +120,7 @@ def redact(
def test_modifier_without_annotations():
logger = EventLogger()

def modifier_with_extra_args(schema_id, version, data, unknown_arg):
def modifier_with_extra_args(data):
return data

with pytest.raises(ModifierError):
Expand Down

0 comments on commit 3c5689d

Please sign in to comment.