Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add arg skip_schema_validation to @on() #56

Merged
merged 2 commits into from
Dec 3, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions ocpp/charge_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,11 @@ async def route_message(self, raw_msg):
return

if msg.message_type_id == MessageType.Call:
# Call's can be validated right away because the 'action' is know.
# Call's can be validated right away because the 'action' is known.
# The 'action' is required to get the correct schema.
#
# CallResult's don't have an action field. The action must be
# deducted from corresponding Call.
validate_payload(msg, self._ocpp_version)
await self._handle_call(msg)
elif msg.message_type_id in \
[MessageType.CallResult, MessageType.CallError]:
Expand All @@ -168,6 +167,9 @@ async def _handle_call(self, msg):
raise NotImplementedError(f"No handler for '{msg.action}' "
"registered.")

if not handlers.get('_skip_schema_validation', False):
validate_payload(msg, self._ocpp_version)

# OCPP uses camelCase for the keys in the payload. It's more pythonic
# to use snake_case for keyword arguments. Therefore the keys must be
# 'translated'. Some examples:
Expand Down Expand Up @@ -205,7 +207,9 @@ async def _handle_call(self, msg):
camel_case_payload = snake_to_camel_case(response_payload)

response = msg.create_call_result(camel_case_payload)
validate_payload(response, self._ocpp_version)

if not handlers.get('_skip_schema_validation', False):
validate_payload(response, self._ocpp_version)

await self._send(response.to_json())

Expand Down
22 changes: 18 additions & 4 deletions ocpp/routing.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import functools


def on(action):
def on(action, *, skip_schema_validation=False):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does the asterisk come into play?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All arguments after the aterisk can only used with as keyword arguments.

So the following call would raise a TypeError:

def foo(*, a):
    pass

foo(1)  # will raise a TypeError

Instead it is required to use keyword arguments:

foo(a=1)  # works fine

Without aterisk the @on() decorator could be used like this:

@on("MeterValues", True)
def on_meter_values(*args, **kwargs):
   pass

Without the keyword it is hard to guess what the meaning is of the second argument. I added the aterisk to make it explicit to disable validation. You can either use skip_schema_validations default value of False. Or, if you want to disable the validation, you can set it to true by keyword arguments:

@on("MeterValues", skip_schema_validation=True)
def on_meter_values(*args, **kwargs):
   pass

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Thanks for the explanation.

""" Function decorator to mark function as handler for specific action.

This hook's argument are the data that is in the payload for the specific
Expand All @@ -20,13 +20,18 @@ def on_boot_notification(charge_point_model, charge_point_vendor, **kwargs): # n
status="Accepted",
)

The decorator takes an optional argument `skip_schema_validation` which
defaults to False. Setting this argument to `True` will disable schema
validation of the request and the response of the specific route.

"""
def decorator(func):
@functools.wraps(func)
def inner(*args, **kwargs):
return func(*args, **kwargs)

inner._on_action = action
inner._skip_schema_validation = skip_schema_validation
return inner
return decorator

Expand Down Expand Up @@ -79,20 +84,29 @@ def after_boot_notification(self, *args, **kwargs):
Action.BootNotification: {
'_on_action': <reference to 'on_boot_notification'>,
'_after_action': <reference to 'after_boot_notification'>,
'_skip_schema_validation': False,
},
}

"""
routes = {}
for attr_name in dir(obj):
attr = getattr(obj, attr_name)
for hook in ['_on_action', '_after_action']:
for option in ['_on_action', '_after_action']:
try:
action = getattr(attr, hook)
action = getattr(attr, option)

if action not in routes:
routes[action] = {}
routes[action][hook] = attr

# Routes decorated with the `@on()` decorator can be configured
# to skip validation of the input and output. For more info see
# the docstring of `on()`.
if option == '_on_action':
routes[action]['_skip_schema_validation'] = \
getattr(attr, '_skip_schema_validation', False)

routes[action][option] = attr

except AttributeError:
continue
Expand Down
4 changes: 3 additions & 1 deletion tests/test_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def test_create_route_map():

"""
class ChargePoint:
@on(Action.Heartbeat)
@on(Action.Heartbeat, skip_schema_validation=True)
def on_heartbeat(self):
pass

Expand All @@ -31,8 +31,10 @@ def undecorated(self):
Action.Heartbeat: {
'_on_action': cp.on_heartbeat,
'_after_action': cp.after_heartbeat,
'_skip_schema_validation': True,
},
Action.MeterValues: {
'_on_action': cp.meter_values,
'_skip_schema_validation': False,
},
}
40 changes: 40 additions & 0 deletions tests/v16/test_v16_charge_point.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import pytest
import asyncio
from unittest import mock

from ocpp.exceptions import NotImplementedError
from ocpp.routing import on, after, create_route_map
Expand Down Expand Up @@ -52,6 +53,45 @@ def after_boot_notification(charge_point_model, charge_point_vendor,
)


@pytest.mark.asyncio
async def test_route_message_without_validation(base_central_system):
@on(Action.BootNotification, skip_schema_validation=True)
def on_boot_notification(**kwargs): # noqa
assert kwargs['firmware_version'] == "#1:3.4.0-2990#N:217H;1.0-223"

return call_result.BootNotificationPayload(
current_time='2018-05-29T17:37:05.495259',
interval=350,
# 'Yolo' is not a valid value for for field status.
status='Yolo',
)

setattr(base_central_system, 'on_boot_notification', on_boot_notification)
base_central_system.route_map = create_route_map(base_central_system)

await base_central_system.route_message(json.dumps([
2,
1,
"BootNotification",
{
# The payload is missing the required fields 'chargepointVendor'
# and 'chargePointModel'.
"firmwareVersion": "#1:3.4.0-2990#N:217H;1.0-223"
}
]))

base_central_system._connection.send.call_args == \
mock.call(json.dumps([
3,
"1",
{
'currentTime': '2018-05-29T17:37:05.495259',
'interval': 350,
'status': 'Yolo',
}
]))


@pytest.mark.asyncio
async def test_route_message_with_no_route(base_central_system,
heartbeat_call):
Expand Down