Skip to content

Commit

Permalink
Validate messages now performs primitive type matching (#169)
Browse files Browse the repository at this point in the history
* Validate messages command now validates depth=1 field names

* remove time module from IT, add user agent to dps

* added help for central_dns_suffix

* fixed lint issues, removed capability-model command

* added linting guidance to CONTRIBUTING.md

* Validate messages now performs primitive type matching

* fixed lint issue, moved to using better parser

* Move to using 'template' instead of 'DCM'
  • Loading branch information
prbans authored Apr 29, 2020
1 parent 8f9bd05 commit acff680
Show file tree
Hide file tree
Showing 3 changed files with 358 additions and 102 deletions.
134 changes: 92 additions & 42 deletions azext_iot/common/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import ast
import base64
import isodate
import json
import os
import sys
Expand All @@ -34,13 +35,13 @@ def parse_entity(entity, filter_none=False):
result (dict): a dictionary of attributes from the function input.
"""
result = {}
attributes = [attr for attr in dir(entity) if not attr.startswith('_')]
attributes = [attr for attr in dir(entity) if not attr.startswith("_")]
for attribute in attributes:
value = getattr(entity, attribute, None)
if filter_none and not value:
continue
value_behavior = dir(value)
if '__call__' not in value_behavior:
if "__call__" not in value_behavior:
result[attribute] = value
return result

Expand Down Expand Up @@ -73,19 +74,25 @@ def verify_transform(subject, mapping):
verifies that subject[k] is of type mapping[k]
"""
import jmespath

for k in mapping.keys():
result = jmespath.search(k, subject)

if result is None:
raise AttributeError('The property "{}" is required'.format(k))
if not isinstance(result, mapping[k]):
supplemental_info = ''
supplemental_info = ""
if mapping[k] == dict:
wiki_link = 'https://github.com/Azure/azure-iot-cli-extension/wiki/Tips'
supplemental_info = 'Review inline JSON examples here --> {}'.format(wiki_link)

raise TypeError('The property "{}" must be of {} but is {}. Input: {}. {}'.format(
k, str(mapping[k]), str(type(result)), result, supplemental_info))
wiki_link = "https://github.com/Azure/azure-iot-cli-extension/wiki/Tips"
supplemental_info = "Review inline JSON examples here --> {}".format(
wiki_link
)

raise TypeError(
'The property "{}" must be of {} but is {}. Input: {}. {}'.format(
k, str(mapping[k]), str(type(result)), result, supplemental_info
)
)


def validate_key_value_pairs(string):
Expand All @@ -99,8 +106,8 @@ def validate_key_value_pairs(string):
"""
result = None
if string:
kv_list = [x for x in string.split(';') if '=' in x] # key-value pairs
result = dict(x.split('=', 1) for x in kv_list)
kv_list = [x for x in string.split(";") if "=" in x] # key-value pairs
result = dict(x.split("=", 1) for x in kv_list)
return result


Expand Down Expand Up @@ -139,21 +146,27 @@ def shell_safe_json_parse(json_or_dict_string, preserve_order=False):
if not preserve_order:
return json.loads(json_or_dict_string)
from collections import OrderedDict

return json.loads(json_or_dict_string, object_pairs_hook=OrderedDict)
except ValueError as json_ex:
try:
return ast.literal_eval(json_or_dict_string)
except SyntaxError:
raise CLIError(json_ex)
except ValueError as ex:
logger.debug(ex) # log the exception which could be a python dict parsing error.
raise CLIError(json_ex) # raise json_ex error which is more readable and likely.
logger.debug(
ex
) # log the exception which could be a python dict parsing error.
raise CLIError(
json_ex
) # raise json_ex error which is more readable and likely.


def read_file_content(file_path, allow_binary=False):
from codecs import open as codecs_open

# Note, always put 'utf-8-sig' first, so that BOM in WinOS won't cause trouble.
for encoding in ['utf-8-sig', 'utf-8', 'utf-16', 'utf-16le', 'utf-16be']:
for encoding in ["utf-8-sig", "utf-8", "utf-16", "utf-16le", "utf-16be"]:
try:
with codecs_open(file_path, encoding=encoding) as f:
logger.debug("attempting to read file %s as %s", file_path, encoding)
Expand All @@ -163,19 +176,19 @@ def read_file_content(file_path, allow_binary=False):

if allow_binary:
try:
with open(file_path, 'rb') as input_file:
with open(file_path, "rb") as input_file:
logger.debug("attempting to read file %s as binary", file_path)
return base64.b64encode(input_file.read()).decode("utf-8")
except Exception: # pylint: disable=broad-except
pass
raise CLIError('Failed to decode file {} - unknown decoding'.format(file_path))
raise CLIError("Failed to decode file {} - unknown decoding".format(file_path))


def trim_from_start(s, substring):
""" Trims a substring from the target string (if it exists) returning the trimmed string.
Otherwise returns original target string. """
if s.startswith(substring):
s = s[len(substring):]
s = s[len(substring) :]
return s


Expand All @@ -186,12 +199,17 @@ def validate_min_python_version(major, minor, error_msg=None, exit_on_fail=True)
if version.major > major:
return True
if major == version.major:
result = (version.minor >= minor)
result = version.minor >= minor

if not result:
if exit_on_fail:
msg = error_msg if error_msg else 'Python version {}.{} or higher required for this functionality.'.format(
major, minor)
msg = (
error_msg
if error_msg
else "Python version {}.{} or higher required for this functionality.".format(
major, minor
)
)
sys.exit(msg)

return result
Expand All @@ -205,10 +223,10 @@ def unicode_binary_map(target):
for k in target:
key = k
if isinstance(k, bytes):
key = str(k, 'utf8')
key = str(k, "utf8")

if isinstance(target[k], bytes):
result[key] = str(target[k], 'utf8')
result[key] = str(target[k], "utf8")
else:
result[key] = target[k]

Expand All @@ -232,11 +250,11 @@ def execute_onthread(**kwargs):
Event(), Thread(): Event object to set the cancellation flag, Executing Thread object
"""

interval = kwargs.get('interval')
method = kwargs.get('method')
method_args = kwargs.get('args')
max_runs = kwargs.get('max_runs')
handle = kwargs.get('return_handle')
interval = kwargs.get("interval")
method = kwargs.get("method")
method_args = kwargs.get("args")
max_runs = kwargs.get("max_runs")
handle = kwargs.get("return_handle")

if not interval:
interval = 2
Expand Down Expand Up @@ -292,6 +310,7 @@ def url_encode_str(s, plus=False):
def test_import(package):
""" Used to determine if a dependency is loading correctly """
import importlib

try:
importlib.import_module(package)
except ImportError:
Expand All @@ -302,10 +321,10 @@ def test_import(package):
def unpack_pnp_http_error(e):
error = unpack_msrest_error(e)
if isinstance(error, dict):
if error.get('error'):
error = error['error']
if error.get('stackTrace'):
error.pop('stackTrace')
if error.get("error"):
error = error["error"]
if error.get("stackTrace"):
error.pop("stackTrace")
return error


Expand Down Expand Up @@ -340,13 +359,13 @@ def init_monitoring(cmd, timeout, properties, enqueued_time, repair, yes):
validate_min_python_version(3, 5)

if timeout < 0:
raise CLIError('Monitoring timeout must be 0 (inf) or greater.')
timeout = (timeout * 1000)
raise CLIError("Monitoring timeout must be 0 (inf) or greater.")
timeout = timeout * 1000

config = cmd.cli_ctx.config
output = cmd.cli_ctx.invocation.data.get("output", None)
if not output:
output = 'json'
output = "json"
ensure_uamqp(config, yes, repair)

if not properties:
Expand All @@ -359,14 +378,19 @@ def init_monitoring(cmd, timeout, properties, enqueued_time, repair, yes):


def get_sas_token(target):
from azext_iot.common.digitaltwin_sas_token_auth import DigitalTwinSasTokenAuthentication
token = ''
if target.get('repository_id'):
token = DigitalTwinSasTokenAuthentication(target["repository_id"],
target["entity"],
target["policy"],
target["primarykey"]).generate_sas_token()
return {'Authorization': '{}'.format(token)}
from azext_iot.common.digitaltwin_sas_token_auth import (
DigitalTwinSasTokenAuthentication,
)

token = ""
if target.get("repository_id"):
token = DigitalTwinSasTokenAuthentication(
target["repository_id"],
target["entity"],
target["policy"],
target["primarykey"],
).generate_sas_token()
return {"Authorization": "{}".format(token)}


def dict_clean(d):
Expand Down Expand Up @@ -396,7 +420,7 @@ def looks_like_file(element):
".java",
".ts",
".js",
".cs"
".cs",
)
)

Expand All @@ -412,3 +436,29 @@ def ensure_pkg_resources_entries():
pkg_resources.working_set.add_entry(extension_path)

return


class ISO8601Validator:
def is_iso8601_date(self, to_validate) -> bool:
try:
return bool(isodate.parse_date(to_validate))
except Exception:
return False

def is_iso8601_datetime(self, to_validate: str) -> bool:
try:
return bool(isodate.parse_datetime(to_validate))
except Exception:
return False

def is_iso8601_duration(self, to_validate: str) -> bool:
try:
return bool(isodate.parse_duration(to_validate))
except Exception:
return False

def is_iso8601_time(self, to_validate: str) -> bool:
try:
return bool(isodate.parse_time(to_validate))
except Exception:
return False
Loading

0 comments on commit acff680

Please sign in to comment.