diff --git a/tests/pyrest_tests/basic_smoketest.yaml b/tests/pyrest_tests/basic_smoketest.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..282d730aa1ed0637aa9083fa5506ef573bd60aa2
--- /dev/null
+++ b/tests/pyrest_tests/basic_smoketest.yaml
@@ -0,0 +1,9 @@
+- config:
+  - testset: "Basic hivemind API Smoke Test"
+
+- test:
+  - name: "Test ip address"
+  - group: "basic_smoketest"
+  - url: "/rpc"
+  - method: "POST"
+  - body: '{}'
diff --git a/tests/pyrest_tests/bridge_api/bridge_api_test.yaml b/tests/pyrest_tests/bridge_api/bridge_api_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..3be66891ba4b6ea515330deb0c5249945a84266c
--- /dev/null
+++ b/tests/pyrest_tests/bridge_api/bridge_api_test.yaml
@@ -0,0 +1,155 @@
+---
+  - config:
+    - testset: "Bridge API Tests"
+    - api: &api "bridge_api"
+    - variable_binds:
+      - api: *api
+    - generators:
+      - test_id: {type: 'number_sequence', start: 1}
+
+  - base_test: &base_test
+    - generator_binds:
+      - test_id: test_id
+    - group: *api
+    - url: "/rpc"
+    - method: "POST"
+    - body: {template: {file: "../templates/request_template.json"}}
+    - validators:
+      - extract_test: {jsonpath_mini: "error", test: "not_exists"}
+      - extract_test: {jsonpath_mini: "result", test: "exists"}
+      - compare: {jsonpath_mini: "id", comparator: "str_eq", expected: {template: $test_id}}
+      - json_file_validator: {jsonpath_mini: "result", comparator: "json_compare", expected: {template: '$api/$method'}}
+
+  - test:
+    - name: "normalize_post"
+    - variable_binds:
+      - method: "normalize_post"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_post_header"
+    - variable_binds:
+      - method: "get_post_header"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussion"
+    - variable_binds:
+      - method: "get_discussion"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_post"
+    - variable_binds:
+      - method: "get_post"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_account_posts"
+    - variable_binds:
+      - method: "get_account_posts"
+      - args: {}
+    - <<: *base_test
+
+
+  - test:
+    - name: "get_ranked_posts"
+    - variable_binds:
+      - method: "get_ranked_posts"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_profile"
+    - variable_binds:
+      - method: "get_profile"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_trending_topics"
+    - variable_binds:
+      - method: "get_trending_topics"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "post_notifications"
+    - variable_binds:
+      - method: "post_notifications"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "account_notifications"
+    - variable_binds:
+      - method: "account_notifications"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "unread_notifications"
+    - variable_binds:
+      - method: "unread_notifications"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_payout_stats"
+    - variable_binds:
+      - method: "get_payout_stats"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_community"
+    - variable_binds:
+      - method: "get_community"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_community_context"
+    - variable_binds:
+      - method: "get_community_context"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "list_communities"
+    - variable_binds:
+      - method: "list_communities"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "list_pop_communities"
+    - variable_binds:
+      - method: "list_pop_communities"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "list_community_roles"
+    - variable_binds:
+      - method: "list_community_roles"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "list_subscribers"
+    - variable_binds:
+      - method: "list_subscribers"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "list_all_subscriptions"
+    - variable_binds:
+      - method: "list_all_subscriptions"
+      - args: {}
+    - <<: *base_test
\ No newline at end of file
diff --git a/tests/pyrest_tests/comparator_contain.py b/tests/pyrest_tests/comparator_contain.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a8751db2bed774c3e9db1c5449efce2340b0680
--- /dev/null
+++ b/tests/pyrest_tests/comparator_contain.py
@@ -0,0 +1,26 @@
+def dict_contain(response, pattern):
+    for key in pattern.keys():
+        if (not key in response) or (pattern[key] != response[key]):
+            return False
+    return True
+
+def list_contain(response, pattern):
+    for item in pattern:
+        if item not in response:
+            return False
+    return True
+
+def contain(response, pattern):
+    if not isinstance(response, pattern):
+        return False
+
+    if isinstance(response, dict):
+        return dict_contain(response, pattern)
+    if isinstance(response, list):
+        return list_contain(response, pattern)
+    if isinstance(response):
+        return pattern in response
+   # all other types
+    return pattern == response
+
+COMPARATORS = {'json_compare': contain}
diff --git a/tests/pyrest_tests/comparator_equal.py b/tests/pyrest_tests/comparator_equal.py
new file mode 100644
index 0000000000000000000000000000000000000000..02fe80971cd3d04a687eb7abc3b7a747e82a18f0
--- /dev/null
+++ b/tests/pyrest_tests/comparator_equal.py
@@ -0,0 +1,3 @@
+import operator
+
+COMPARATORS = {'json_compare': operator.eq}
diff --git a/tests/pyrest_tests/condenser_api/condenser_api_test.yaml b/tests/pyrest_tests/condenser_api/condenser_api_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..47c106124b558ee4df83eaae3d8c8953ee0eece7
--- /dev/null
+++ b/tests/pyrest_tests/condenser_api/condenser_api_test.yaml
@@ -0,0 +1,189 @@
+---
+  - config:
+    - testset: "Condenser API Tests"
+    - api: &api "condenser_api"
+    - variable_binds:
+      - api: *api
+    - generators:
+      - test_id: {type: 'number_sequence', start: 1}
+  
+  - base_test: &base_test
+    - generator_binds:
+      - test_id: test_id
+    - group: *api
+    - url: "/rpc"
+    - method: "POST"
+    - body: {template: {file: "../templates/request_template.json"}}
+    - validators:
+      - extract_test: {jsonpath_mini: "error", test: "not_exists"}
+      - extract_test: {jsonpath_mini: "result", test: "exists"}
+      - compare: {jsonpath_mini: "id", comparator: "str_eq", expected: {template: $test_id}}
+      - json_file_validator: {jsonpath_mini: "result", comparator: "json_compare", expected: {template: '$api/$method'}}
+
+  - test:
+    - name: "get_followers"
+    - variable_binds:
+      - method: "get_followers"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_following"
+    - variable_binds:
+      - method: "get_following"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_follow_count"
+    - variable_binds:
+      - method: "get_follow_count"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_content"
+    - variable_binds:
+      - method: "get_content"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_content_replies"
+    - variable_binds:
+      - method: "get_content_replies"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_state"
+    - variable_binds:
+      - method: "get_state"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_trending_tags"
+    - variable_binds:
+      - method: "get_trending_tags"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_trending"
+    - variable_binds:
+      - method: "get_discussions_by_trending"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_hot"
+    - variable_binds:
+      - method: "get_discussions_by_hot"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_promoted"
+    - variable_binds:
+      - method: "get_discussions_by_promoted"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_created"
+    - variable_binds:
+      - method: "get_discussions_by_created"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_blog"
+    - variable_binds:
+      - method: "get_discussions_by_blog"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_feed"
+    - variable_binds:
+      - method: "get_discussions_by_feed"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_comments"
+    - variable_binds:
+      - method: "get_discussions_by_comments"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_replies_by_last_update"
+    - variable_binds:
+      - method: "get_replies_by_last_update"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_author_before_date"
+    - variable_binds:
+      - method: "get_discussions_by_author_before_date"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_post_discussions_by_payout"
+    - variable_binds:
+      - method: "get_post_discussions_by_payout"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_comment_discussions_by_payout"
+    - variable_binds:
+      - method: "get_comment_discussions_by_payout"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_blog"
+    - variable_binds:
+      - method: "get_blog"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_blog_entries"
+    - variable_binds:
+      - method: "get_blog_entries"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_account_reputations"
+    - variable_binds:
+      - method: "get_account_reputations"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_reblogged_by"
+    - variable_binds:
+      - method: "get_reblogged_by"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_accounts"
+    - variable_binds:
+      - method: "get_accounts"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_account_votes"
+    - variable_binds:
+      - method: "get_account_votes"
+      - args: {}
+    - <<: *base_test
\ No newline at end of file
diff --git a/tests/pyrest_tests/database_api/database_api_test.yaml b/tests/pyrest_tests/database_api/database_api_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..27d3e01aba6e56af21026cefed158bd43b882001
--- /dev/null
+++ b/tests/pyrest_tests/database_api/database_api_test.yaml
@@ -0,0 +1,35 @@
+---
+- config:
+  - testset: "Database API Tests"
+  - api: &api "database_api"
+  - variable_binds:
+    - api: *api
+  - generators:
+    - test_id: {type: 'number_sequence', start: 1}
+
+- base_test: &base_test
+  - generator_binds:
+    - test_id: test_id
+  - group: *api
+  - url: "/rpc"
+  - method: "POST"
+  - body: {template: {file: "../templates/request_template.json"}}
+  - validators:
+    - extract_test: {jsonpath_mini: "error", test: "not_exists"}
+    - extract_test: {jsonpath_mini: "result", test: "exists"}
+    - compare: {jsonpath_mini: "id", comparator: "str_eq", expected: {template: $test_id}}
+    - json_file_validator: {jsonpath_mini: "result", comparator: "json_compare", expected: {template: '$api/$method'}}
+
+- test:
+  - name: "list_comments"
+  - variable_binds:
+    - method: "list_comments"
+    - args: {}
+  - <<: *base_test
+
+- test:
+  - name: "find_comments"
+  - variable_binds:
+    - method: "find_comments"
+    - args: {}
+  - <<: *base_test
diff --git a/tests/pyrest_tests/follow_api/follow_api_test.yaml b/tests/pyrest_tests/follow_api/follow_api_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..59bc3f0059b9a6fefd3cbd6f9e9f2fc093a21c28
--- /dev/null
+++ b/tests/pyrest_tests/follow_api/follow_api_test.yaml
@@ -0,0 +1,91 @@
+---
+  - config:
+    - testset: "Follow API Tests"
+    - api: &api "follow_api"
+    - variable_binds:
+      - api: *api
+    - generators:
+      - test_id: {type: 'number_sequence', start: 1}
+
+  - base_test: &base_test
+    - generator_binds:
+      - test_id: test_id
+    - group: *api
+    - url: "/rpc"
+    - method: "POST"
+    - body: {template: {file: "../templates/request_template.json"}}
+    - validators:
+      - extract_test: {jsonpath_mini: "error", test: "not_exists"}
+      - extract_test: {jsonpath_mini: "result", test: "exists"}
+      - compare: {jsonpath_mini: "id", comparator: "str_eq", expected: {template: $test_id}}
+      - json_file_validator: {jsonpath_mini: "result", comparator: "json_compare", expected: {template: '$api/$method'}}
+
+  - test:
+    - name: "get_followers"
+    - variable_binds:
+      - method: "get_followers"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_following"
+    - variable_binds:
+      - method: "get_following"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_follow_count"
+    - variable_binds:
+      - method: "get_follow_count"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_account_reputations"
+    - variable_binds:
+      - method: "get_account_reputations"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_blog"
+    - variable_binds:
+      - method: "get_blog"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_blog_entries"
+    - variable_binds:
+      - method: "get_blog_entries"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_reblogged_by"
+    - variable_binds:
+      - method: "get_reblogged_by"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_feed_entries"
+    - variable_binds:
+      - method: "get_feed_entries"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_feed"
+    - variable_binds:
+      - method: "get_feed"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_blog_authors"
+    - variable_binds:
+      - method: "get_blog_authors"
+      - args: {}
+    - <<: *base_test
\ No newline at end of file
diff --git a/tests/pyrest_tests/hive_api/hive_api_test.yaml b/tests/pyrest_tests/hive_api/hive_api_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..9884da30c547e89e18eeb73807490d7335c1f139
--- /dev/null
+++ b/tests/pyrest_tests/hive_api/hive_api_test.yaml
@@ -0,0 +1,28 @@
+---
+  - config:
+    - testset: "Hive API Tests"
+    - api: &api "hive"
+    - variable_binds:
+      - api: *api
+    - generators:
+      - test_id: {type: 'number_sequence', start: 1}
+  
+  - base_test: &base_test
+    - generator_binds:
+      - test_id: test_id
+    - group: *api
+    - url: "/rpc"
+    - method: "POST"
+    - body: {template: {file: "../templates/request_template.json"}}
+    - validators:
+      - extract_test: {jsonpath_mini: "error", test: "not_exists"}
+      - extract_test: {jsonpath_mini: "result", test: "exists"}
+      - compare: {jsonpath_mini: "id", comparator: "str_eq", expected: {template: $test_id}}
+      - json_file_validator: {jsonpath_mini: "result", comparator: "json_compare", expected: {template: '$api/$method'}}
+  
+  - test:
+    - name: "db_head_state"
+    - variable_binds:
+      - method: "db_head_state"
+      - args: {}
+    - <<: *base_test
\ No newline at end of file
diff --git a/tests/pyrest_tests/run_api_tests.sh b/tests/pyrest_tests/run_api_tests.sh
new file mode 100644
index 0000000000000000000000000000000000000000..bbf42080cffcecd124480eb917a1dc79b6df6c47
--- /dev/null
+++ b/tests/pyrest_tests/run_api_tests.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+NODE='http://127.0.0.1'
+RPC_PORT=8080
+EXIT_CODE=0
+COMPARATOR=''
+
+if [ $1 == 'equal' ]
+then
+   COMPARATOR='comparator_equal'
+elif [ $1 == 'contain' ]
+then
+   COMPARATOR='comparator_contain'
+else
+   echo FATAL: $1 is not a valid comparator! && exit -1
+fi
+
+echo COMPARATOR: $COMPARATOR
+
+pyresttest $NODE:$RPC_PORT ./basic_smoketest.yaml
+[ $? -ne 0 ] && echo FATAL: hivemind not running? && exit -1
+
+pyresttest $NODE:$RPC_PORT ./bridge_api/bridge_api_test.yaml --import_extensions='validator_ex;'$COMPARATOR
+[ $? -ne 0 ] && EXIT_CODE=-1
+
+pyresttest $NODE:$RPC_PORT ./condenser_api/condenser_api_test.yaml --import_extensions='validator_ex;'$COMPARATOR
+[ $? -ne 0 ] && EXIT_CODE=-1
+
+pyresttest $NODE:$RPC_PORT ./database_api/database_api_test.yaml --import_extensions='validator_ex;'$COMPARATOR
+[ $? -ne 0 ] && EXIT_CODE=-1
+
+pyresttest $NODE:$RPC_PORT ./follow_api/follow_api_test.yaml --import_extensions='validator_ex;'$COMPARATOR
+[ $? -ne 0 ] && EXIT_CODE=-1
+
+pyresttest $NODE:$RPC_PORT ./hive_api/hive_api_test.yaml --import_extensions='validator_ex;'$COMPARATOR
+[ $? -ne 0 ] && EXIT_CODE=-1
+
+pyresttest $NODE:$RPC_PORT ./tags_api/tags_api_test.yaml --import_extensions='validator_ex;'$COMPARATOR
+[ $? -ne 0 ] && EXIT_CODE=-1
+
+exit $EXIT_CODE
diff --git a/tests/pyrest_tests/tags_api/tags_api_test.yaml b/tests/pyrest_tests/tags_api/tags_api_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b30c99d38f71330016449cea07829b8fd53dffce
--- /dev/null
+++ b/tests/pyrest_tests/tags_api/tags_api_test.yaml
@@ -0,0 +1,147 @@
+---
+  - config:
+    - testset: "Tags API Tests"
+    - api: &api "tags_api"
+    - variable_binds:
+      - api: *api
+    - generators:
+      - test_id: {type: 'number_sequence', start: 1}
+  
+  - base_test: &base_test
+    - generator_binds:
+      - test_id: test_id
+    - group: *api
+    - url: "/rpc"
+    - method: "POST"
+    - body: {template: {file: "../templates/request_template.json"}}
+    - validators:
+      - extract_test: {jsonpath_mini: "error", test: "not_exists"}
+      - extract_test: {jsonpath_mini: "result", test: "exists"}
+      - compare: {jsonpath_mini: "id", comparator: "str_eq", expected: {template: $test_id}}
+      - json_file_validator: {jsonpath_mini: "result", comparator: "json_compare", expected: {template: '$api/$method'}}
+
+  - test:
+    - name: "get_discussion"
+    - variable_binds:
+      - method: "get_discussion"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_content_replies"
+    - variable_binds:
+      - method: "get_content_replies"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_trending"
+    - variable_binds:
+      - method: "get_discussions_by_trending"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_hot"
+    - variable_binds:
+      - method: "get_discussions_by_hot"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_promoted"
+    - variable_binds:
+      - method: "get_discussions_by_promoted"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_created"
+    - variable_binds:
+      - method: "get_discussions_by_created"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_blog"
+    - variable_binds:
+      - method: "get_discussions_by_blog"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_comments"
+    - variable_binds:
+      - method: "get_discussions_by_comments"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_author_before_date"
+    - variable_binds:
+      - method: "get_discussions_by_author_before_date"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_post_discussions_by_payout"
+    - variable_binds:
+      - method: "get_post_discussions_by_payout"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_comment_discussions_by_payout"
+    - variable_binds:
+      - method: "get_comment_discussions_by_payout"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_active_votes"
+    - variable_binds:
+      - method: "get_active_votes"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_tags_used_by_author"
+    - variable_binds:
+      - method: "get_tags_used_by_author"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_active"
+    - variable_binds:
+      - method: "get_discussions_by_active"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_cashout"
+    - variable_binds:
+      - method: "get_discussions_by_cashout"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_votes"
+    - variable_binds:
+      - method: "get_discussions_by_votes"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_discussions_by_children"
+    - variable_binds:
+      - method: "get_discussions_by_children"
+      - args: {}
+    - <<: *base_test
+
+  - test:
+    - name: "get_account_votes"
+    - variable_binds:
+      - method: "get_account_votes"
+      - args: {}
+    - <<: *base_test
\ No newline at end of file
diff --git a/tests/pyrest_tests/templates/request_template.json b/tests/pyrest_tests/templates/request_template.json
new file mode 100644
index 0000000000000000000000000000000000000000..30833b0377fffb2d9c7e7dc911648db46269d104
--- /dev/null
+++ b/tests/pyrest_tests/templates/request_template.json
@@ -0,0 +1,6 @@
+{
+    "jsonrpc": "2.0",
+    "id": $test_id,
+    "method": "$api.$method",
+    "params": $args
+}
diff --git a/tests/pyrest_tests/validator_ex.py b/tests/pyrest_tests/validator_ex.py
new file mode 100644
index 0000000000000000000000000000000000000000..29aebd243d4bbd4f40dc7c52501753d0f876e613
--- /dev/null
+++ b/tests/pyrest_tests/validator_ex.py
@@ -0,0 +1,172 @@
+import os
+import sys
+import traceback
+import string
+import ast
+import json
+import logging
+
+from six import binary_type
+from six import text_type
+
+# Python 3 compatibility
+PYTHON_MAJOR_VERSION = sys.version_info[0]
+if PYTHON_MAJOR_VERSION > 2:
+    from past.builtins import basestring
+    from past.builtins import long
+
+try:  # First try to load pyresttest from global namespace
+    from pyresttest import validators
+    from pyresttest import parsing
+except ImportError:  # Then try a relative import if possible
+    logging.error("Cannot import module")
+
+def dict_str_eq(x, y):
+    """ Check if dict object is equal to string object """
+    assert isinstance(x, dict)
+    assert isinstance(y, str)
+    y = ast.literal_eval(y)
+    assert isinstance(y, dict)
+    return x == y
+
+COMPARATORS = {'dict_str_eq': dict_str_eq}
+PATTERN_FILE_EXT = ".json.pat"
+OUTPUT_FILE_EXT = ".json.out"
+
+def dump_output(output_file_name, output):
+    """ Dump JSON output to the file. """
+    try:
+        with open(output_file_name, "w") as f:
+            json.dump(output, f, sort_keys=True, indent=4)
+    except Exception:
+        logging.error("Cannot dump output to file {0}.".format(output_file_name))
+        logging.info(traceback.format_exc())
+
+class JSONFileValidator(validators.AbstractValidator):
+    """ Does extract response body and compare with given my_file_name.pat.json.
+        If comparison failed response is save into my_file_name.out.json file.
+    """
+
+    name = 'ComparatorValidator'
+    config = None   # Configuration text, if parsed
+    extractor = None
+    comparator = None
+    comparator_name = ""
+    expected = None
+    isTemplateExpected = False
+
+    def get_readable_config(self, context=None):
+        """ Get a human-readable config string """
+        string_frags = list()
+        string_frags.append(
+            "Extractor: " + self.extractor.get_readable_config(context=context))
+        if isinstance(self.expected, validators.AbstractExtractor):
+            string_frags.append("Expected value extractor: " +
+                                self.expected.get_readable_config(context=context))
+        elif self.isTemplateExpected:
+            string_frags.append(
+                'Expected is templated, raw value: {0}'.format(self.expected))
+        return os.linesep.join(string_frags)
+
+    def validate(self, body=None, headers=None, context=None):
+        try:
+            extracted_val = self.extractor.extract(
+                body=body, headers=headers, context=context)
+        except Exception:
+            trace = traceback.format_exc()
+            return validators.Failure(message="Extractor threw exception", details=trace,
+                                      validator=self,
+                                      failure_type=validators.FAILURE_EXTRACTOR_EXCEPTION)
+
+        # Compute expected output, either templating or using expected value
+        file_name = None
+        if self.isTemplateExpected and context:
+            file_name = string.Template(
+                self.expected).safe_substitute(context.get_values())
+        else:
+            file_name = self.expected
+
+        expected_val = None
+        expected_file_name = file_name + PATTERN_FILE_EXT
+        output_file_name = file_name + OUTPUT_FILE_EXT
+        try:
+            with open(expected_file_name, "r") as f:
+                expected_val = json.load(f)
+        except Exception:
+            trace = traceback.format_exc()
+            dump_output(output_file_name, extracted_val)
+            return validators.Failure(message="Cannot load pattern file {0}.".format(expected_file_name), details=trace, validator=self, failure_type=validators.FAILURE_VALIDATOR_EXCEPTION)
+
+        # Handle a bytes-based body and a unicode expected value seamlessly
+        if isinstance(extracted_val, binary_type) and isinstance(expected_val, text_type):
+            expected_val = expected_val.encode('utf-8')
+        comparison = self.comparator(extracted_val, expected_val)
+
+        if not comparison:
+            failure = validators.Failure(validator=self)
+            failure.message = "Comparison failed, evaluating {0}({1}, {2}) returned False".format(
+                self.comparator_name, extracted_val, expected_val)
+            failure.details = self.get_readable_config(context=context)
+            failure.failure_type = validators.FAILURE_VALIDATOR_FAILED
+            dump_output(output_file_name, extracted_val)
+            return failure
+        return True
+
+    @staticmethod
+    def parse(config):
+        """ Create a validator that does an extract from body and applies a comparator,
+            Then does comparison vs expected value
+            Syntax sample:
+              { jsonpath_mini: 'node.child',
+                operator: 'eq',
+                expected: 'my_file_name'
+              }
+        """
+
+        output = JSONFileValidator()
+        config = parsing.lowercase_keys(parsing.flatten_dictionaries(config))
+        output.config = config
+
+        # Extract functions are called by using defined extractor names
+        output.extractor = validators._get_extractor(config)
+
+        if output.extractor is None:
+            raise ValueError(
+                "Extract function for comparison is not valid or not found!")
+
+        if 'comparator' not in config:  # Equals comparator if unspecified
+            output.comparator_name = 'eq'
+        else:
+            output.comparator_name = config['comparator'].lower()
+        output.comparator = validators.COMPARATORS[output.comparator_name]
+        if not output.comparator:
+            raise ValueError("Invalid comparator given!")
+
+        try:
+            expected = config['expected']
+        except KeyError:
+            raise ValueError(
+                "No expected value found in comparator validator config, one must be!")
+
+        # Expected value can be base or templated string contains file name without extension.
+        if isinstance(expected, (basestring, int, long, float, complex)):
+            output.expected = expected
+        elif isinstance(expected, dict):
+            expected = parsing.lowercase_keys(expected)
+            template = expected.get('template')
+            if template:  # Templated string
+                if not isinstance(template, basestring):
+                    raise ValueError(
+                        "Can't template a comparator-validator unless template value is a string")
+                output.isTemplateExpected = True
+                output.expected = template
+            else:  # Extractor to compare against
+                raise ValueError(
+                    "Can't supply a non-template, non-extract dictionary to comparator-validator")
+
+        return output
+
+VALIDATORS = {
+    'json_file_validator': JSONFileValidator.parse,
+    'json_file_validate': JSONFileValidator.parse
+}