{"action":"created","comment":{"url":"https://api.github.com/repos/miraculixx/pyrules/pulls/comments/29110439","id":29110439,"diff_hunk":"@@ -1,93 +1,161 @@\n-from pyrules.dictobj import DictObject\n-from pyrules.language import Translator\n+import json\n+import yaml\n+from .conditions import LogicEvaluator\n+from .dictobj import DictObject\n+from .language import Translator\n+\n      \n class Rule(object):\n     \"\"\"\n-    basic rule\n+    Base rule\n     \"\"\"\n+    name = None\n+\n     def should_trigger(self, context):\n-        pass\n+        return True\n+\n     def perform(self, context):\n-        pass\n+        raise NotImplementedError\n+\n     def record(self, context, result):\n         context._executed.append((self.ruleid, result))\n+\n     @property\n     def ruleid(self):\n-        return self.__class__.__name__.split('.')[-1]\n-    \n-class LambdaRule(Rule):\n+        return self.name or self.__class__.__name__.rsplit('.', 1)[-1]\n+\n+\n+class ConditionalRule(Rule):\n     \"\"\"\n-    LambdaRules define two class-variable lambdas: condition and action\n+    ConditionalRule defines receives two functions as parameters: condition and\n+    action.\n     \n-    condition = lambda self, context: <some condition returning True or False>\n-    action = lambda self, context: <return a dict to update the context with>\n+    @param condition: lambda context: <some condition returning True or False>\n+    @param action: lambda context: <return a dict to update the context with>\n     \n     Example:\n     \n-    class MyLambdaRule(Rule):\n-        # always run and return 5\n-        condition = lambda self, context: True\n-        action = lambda self, context: { 'result': 5 }\n+    >>> rule = ConditionalRule(\n+    ...     condition=lambda context: True,\n+    ...     action=lambda context: {'result': 5})\n     \"\"\"\n-    condition = lambda self, context: False\n-    action = lambda self, context: None \n+    def __init__(self, condition=None, action=None):\n+        self._condition = condition\n+        self._action = action\n+\n+    def condition(self, context):\n+        \"\"\"\n+        Condition for executing this rule.\n+\n+        Override in subclasses if necessary. Should return boolean value that\n+        determines if rule is used.\n+        \"\"\"\n+        return self._condition(self, context)\n+\n+    def action(self, context):\n+        \"\"\"\n+        Action for executing this rule.\n+\n+        Override in subclasses if necessary. Should return dictionary with\n+        results that will be added to context.\n+        \"\"\"\n+        return self._action(self, context)\n+\n     def should_trigger(self, context):\n         return self.condition(context)\n+\n     def perform(self, context):\n         result = self.action(context) \n         context.update(result)\n         return result\n-    \n-class TableRuleset(Rule):\n+\n+\n+class TableRule(Rule):\n     \"\"\"\n-    a table ruleset created from a list of dict objects of the following format\n+    A table rule is created from a list of dict objects of the following format:\n     \n     [ \n       {\n-        'if' : ['condition1', ...],\n+        'if' : {'logic': '1 | 2', 'conditions': ['foo', {'bar': 10}]},\n         'then' : ['action1', ...],\n         'target' : ['target1', ...]\n       }, \n       ...\n     ]\n     \n-    Each rule is only executed if all conditions are met. In conditions and actions, use context. \n-    to reference variables. targets implicitly reference context. (target 'xy' means 'context.xy').\n-    The result of the nth 'then' action is stored in the nth context.variable as defined in target.\n+    Each rule is only executed if all conditions are met. In actions, use\n+    'context.' to reference variables. Targets and conditions implicitly reference\n+    'context.' (target 'xy' means 'context.xy'). Logic can be omitted, which\n+    would imply \"&\" operation for all conditions. Condition can be a dictionary or\n+    a single value, so 'value' is equivalent to {'value': True}\n+\n+    The result of the nth 'then' action is stored in the nth 'context.variable'\n+    as defined in target.\n     \"\"\"\n-    def __init__(self, rules, translations=None):\n+    def __init__(self, rules, name=None):\n         self.rules = rules or {}\n-        if translations:\n-            translator = Translator(translations)\n-            for rule in self.rules:\n-                for key in rule.keys():\n-                    rule[key] = [translator.replace(item) for item in rule[key]]\n-    def should_trigger(self, context):\n-        return True\n+        if name:\n+            self.name = name\n+        self._current_ruleid = None\n+\n     def perform(self, context):\n         count = 0\n         for rule in self.rules:\n-            if all([eval(condition, context.as_dict) for condition in rule['if']]):\n+            evaluator = LogicEvaluator(\n+                rule['if'].get('logic'), rule['if']['conditions'])\n+            if evaluator.evaluate(context):\n                 count = count + 1\n-                self._current_ruleid = rule.get('id', count)\n+                self._current_ruleid = rule.get('rule') or count\n                 for action, target in zip(rule['then'], rule['target']):\n-                    if context._translations:\n-                        action = context._translations.replace(action)\n-                        target = context._translations.replace(target)\n-                    result = context[target.replace('context.', '').strip()] = eval(action, context.as_dict)\n+                    #if context._translations:\n+                    #    action = context._translations.replace(action)\n+                    #    target = context._translations.replace(target)\n+                    result = \\\n+                        context[target.replace('context.', '').strip()] = (\n+                            eval(action, context.as_dict)\n+                            if isinstance(action, basestring)\n+                            else action)\n                     self.record(context, result)\n             else:\n-                break\n+                continue\n         else:\n             self._current_ruleid = None\n             return True\n         return False\n+\n     @property\n     def ruleid(self):\n         if self._current_ruleid:\n-            return \"%s.%s\" % (super(TableRuleset, self).ruleid, self._current_ruleid)\n-        return super(TableRuleset, self).ruleid\n-    \n+            return \"%s.%s\" % (super(TableRule, self).ruleid, self._current_ruleid)\n+        return super(TableRule, self).ruleid\n+\n+    @classmethod\n+    def from_yaml(cls, text):\n+        return cls._from_data(yaml.load(text))\n+        \n+    @classmethod\n+    def from_json(cls, text):\n+        return cls._from_data(json.loads(text))\n+\n+    @classmethod\n+    def _from_data(cls, data):\n+        # We have to convert non-string data in clauses back to strings,\n+        # because they will be eval-ed\n+        rules = []\n+        for rule in data['rules']:\n+            obj = {\n+                'rule': rule.get('rule'),\n+                'then': rule['then'],\n+                'target': rule['target']}\n+            if_clause = {}\n+            # Convert conditions to dictionaries, i.e. \"foo\" becomes {\"foo\": True}\n+            conditions = rule['if'].get('conditions', [])","path":"pyrules/rules.py","position":194,"original_position":194,"commit_id":"19fa22926e93a2d2b022c8e46ad5c9fa6d97ca9c","original_commit_id":"19fa22926e93a2d2b022c8e46ad5c9fa6d97ca9c","user":{"login":"miraculixx","id":859611,"avatar_url":"https://avatars.githubusercontent.com/u/859611?v=3","gravatar_id":"","url":"https://api.github.com/users/miraculixx","html_url":"https://github.com/miraculixx","followers_url":"https://api.github.com/users/miraculixx/followers","following_url":"https://api.github.com/users/miraculixx/following{/other_user}","gists_url":"https://api.github.com/users/miraculixx/gists{/gist_id}","starred_url":"https://api.github.com/users/miraculixx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/miraculixx/subscriptions","organizations_url":"https://api.github.com/users/miraculixx/orgs","repos_url":"https://api.github.com/users/miraculixx/repos","events_url":"https://api.github.com/users/miraculixx/events{/privacy}","received_events_url":"https://api.github.com/users/miraculixx/received_events","type":"User","site_admin":false},"body":"if `conditions` is not present, it should assume the list in `if` as conditions, i.e. this should be valid\r\n\r\n```\r\nif:\r\n  - c1\r\n  - c2\r\n```\r\n\r\nequivalent of \r\n\r\nif:\r\n  logic: 1 & 2\r\n  conditions: \r\n  - c1\r\n  - c2\r\n```\r\n","created_at":"2015-04-26T14:41:49Z","updated_at":"2015-04-26T14:41:49Z","html_url":"https://github.com/miraculixx/pyrules/pull/18#discussion_r29110439","pull_request_url":"https://api.github.com/repos/miraculixx/pyrules/pulls/18","_links":{"self":{"href":"https://api.github.com/repos/miraculixx/pyrules/pulls/comments/29110439"},"html":{"href":"https://github.com/miraculixx/pyrules/pull/18#discussion_r29110439"},"pull_request":{"href":"https://api.github.com/repos/miraculixx/pyrules/pulls/18"}}},"pull_request":{"url":"https://api.github.com/repos/miraculixx/pyrules/pulls/18","id":33693914,"html_url":"https://github.com/miraculixx/pyrules/pull/18","diff_url":"https://github.com/miraculixx/pyrules/pull/18.diff","patch_url":"https://github.com/miraculixx/pyrules/pull/18.patch","issue_url":"https://api.github.com/repos/miraculixx/pyrules/issues/18","number":18,"state":"open","locked":false,"title":"Django app and lots of tests","user":{"login":"antisvin","id":940639,"avatar_url":"https://avatars.githubusercontent.com/u/940639?v=3","gravatar_id":"","url":"https://api.github.com/users/antisvin","html_url":"https://github.com/antisvin","followers_url":"https://api.github.com/users/antisvin/followers","following_url":"https://api.github.com/users/antisvin/following{/other_user}","gists_url":"https://api.github.com/users/antisvin/gists{/gist_id}","starred_url":"https://api.github.com/users/antisvin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/antisvin/subscriptions","organizations_url":"https://api.github.com/users/antisvin/orgs","repos_url":"https://api.github.com/users/antisvin/repos","events_url":"https://api.github.com/users/antisvin/events{/privacy}","received_events_url":"https://api.github.com/users/antisvin/received_events","type":"User","site_admin":false},"body":"#2 #5 #9 #14 #15","created_at":"2015-04-20T18:45:01Z","updated_at":"2015-04-26T14:41:49Z","closed_at":null,"merged_at":null,"merge_commit_sha":"0031c08046c27d219c3341e6f505b487858ea552","assignee":{"login":"miraculixx","id":859611,"avatar_url":"https://avatars.githubusercontent.com/u/859611?v=3","gravatar_id":"","url":"https://api.github.com/users/miraculixx","html_url":"https://github.com/miraculixx","followers_url":"https://api.github.com/users/miraculixx/followers","following_url":"https://api.github.com/users/miraculixx/following{/other_user}","gists_url":"https://api.github.com/users/miraculixx/gists{/gist_id}","starred_url":"https://api.github.com/users/miraculixx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/miraculixx/subscriptions","organizations_url":"https://api.github.com/users/miraculixx/orgs","repos_url":"https://api.github.com/users/miraculixx/repos","events_url":"https://api.github.com/users/miraculixx/events{/privacy}","received_events_url":"https://api.github.com/users/miraculixx/received_events","type":"User","site_admin":false},"milestone":null,"commits_url":"https://api.github.com/repos/miraculixx/pyrules/pulls/18/commits","review_comments_url":"https://api.github.com/repos/miraculixx/pyrules/pulls/18/comments","review_comment_url":"https://api.github.com/repos/miraculixx/pyrules/pulls/comments{/number}","comments_url":"https://api.github.com/repos/miraculixx/pyrules/issues/18/comments","statuses_url":"https://api.github.com/repos/miraculixx/pyrules/statuses/19fa22926e93a2d2b022c8e46ad5c9fa6d97ca9c","head":{"label":"miraculixx:stas","ref":"stas","sha":"19fa22926e93a2d2b022c8e46ad5c9fa6d97ca9c","user":{"login":"miraculixx","id":859611,"avatar_url":"https://avatars.githubusercontent.com/u/859611?v=3","gravatar_id":"","url":"https://api.github.com/users/miraculixx","html_url":"https://github.com/miraculixx","followers_url":"https://api.github.com/users/miraculixx/followers","following_url":"https://api.github.com/users/miraculixx/following{/other_user}","gists_url":"https://api.github.com/users/miraculixx/gists{/gist_id}","starred_url":"https://api.github.com/users/miraculixx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/miraculixx/subscriptions","organizations_url":"https://api.github.com/users/miraculixx/orgs","repos_url":"https://api.github.com/users/miraculixx/repos","events_url":"https://api.github.com/users/miraculixx/events{/privacy}","received_events_url":"https://api.github.com/users/miraculixx/received_events","type":"User","site_admin":false},"repo":{"id":26932347,"name":"pyrules","full_name":"miraculixx/pyrules","owner":{"login":"miraculixx","id":859611,"avatar_url":"https://avatars.githubusercontent.com/u/859611?v=3","gravatar_id":"","url":"https://api.github.com/users/miraculixx","html_url":"https://github.com/miraculixx","followers_url":"https://api.github.com/users/miraculixx/followers","following_url":"https://api.github.com/users/miraculixx/following{/other_user}","gists_url":"https://api.github.com/users/miraculixx/gists{/gist_id}","starred_url":"https://api.github.com/users/miraculixx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/miraculixx/subscriptions","organizations_url":"https://api.github.com/users/miraculixx/orgs","repos_url":"https://api.github.com/users/miraculixx/repos","events_url":"https://api.github.com/users/miraculixx/events{/privacy}","received_events_url":"https://api.github.com/users/miraculixx/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/miraculixx/pyrules","description":"Python Rules Engine","fork":false,"url":"https://api.github.com/repos/miraculixx/pyrules","forks_url":"https://api.github.com/repos/miraculixx/pyrules/forks","keys_url":"https://api.github.com/repos/miraculixx/pyrules/keys{/key_id}","collaborators_url":"https://api.github.com/repos/miraculixx/pyrules/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/miraculixx/pyrules/teams","hooks_url":"https://api.github.com/repos/miraculixx/pyrules/hooks","issue_events_url":"https://api.github.com/repos/miraculixx/pyrules/issues/events{/number}","events_url":"https://api.github.com/repos/miraculixx/pyrules/events","assignees_url":"https://api.github.com/repos/miraculixx/pyrules/assignees{/user}","branches_url":"https://api.github.com/repos/miraculixx/pyrules/branches{/branch}","tags_url":"https://api.github.com/repos/miraculixx/pyrules/tags","blobs_url":"https://api.github.com/repos/miraculixx/pyrules/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/miraculixx/pyrules/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/miraculixx/pyrules/git/refs{/sha}","trees_url":"https://api.github.com/repos/miraculixx/pyrules/git/trees{/sha}","statuses_url":"https://api.github.com/repos/miraculixx/pyrules/statuses/{sha}","languages_url":"https://api.github.com/repos/miraculixx/pyrules/languages","stargazers_url":"https://api.github.com/repos/miraculixx/pyrules/stargazers","contributors_url":"https://api.github.com/repos/miraculixx/pyrules/contributors","subscribers_url":"https://api.github.com/repos/miraculixx/pyrules/subscribers","subscription_url":"https://api.github.com/repos/miraculixx/pyrules/subscription","commits_url":"https://api.github.com/repos/miraculixx/pyrules/commits{/sha}","git_commits_url":"https://api.github.com/repos/miraculixx/pyrules/git/commits{/sha}","comments_url":"https://api.github.com/repos/miraculixx/pyrules/comments{/number}","issue_comment_url":"https://api.github.com/repos/miraculixx/pyrules/issues/comments{/number}","contents_url":"https://api.github.com/repos/miraculixx/pyrules/contents/{+path}","compare_url":"https://api.github.com/repos/miraculixx/pyrules/compare/{base}...{head}","merges_url":"https://api.github.com/repos/miraculixx/pyrules/merges","archive_url":"https://api.github.com/repos/miraculixx/pyrules/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/miraculixx/pyrules/downloads","issues_url":"https://api.github.com/repos/miraculixx/pyrules/issues{/number}","pulls_url":"https://api.github.com/repos/miraculixx/pyrules/pulls{/number}","milestones_url":"https://api.github.com/repos/miraculixx/pyrules/milestones{/number}","notifications_url":"https://api.github.com/repos/miraculixx/pyrules/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/miraculixx/pyrules/labels{/name}","releases_url":"https://api.github.com/repos/miraculixx/pyrules/releases{/id}","created_at":"2014-11-20T21:18:21Z","updated_at":"2014-11-20T23:47:17Z","pushed_at":"2015-04-24T21:25:07Z","git_url":"git://github.com/miraculixx/pyrules.git","ssh_url":"git@github.com:miraculixx/pyrules.git","clone_url":"https://github.com/miraculixx/pyrules.git","svn_url":"https://github.com/miraculixx/pyrules","homepage":null,"size":252,"stargazers_count":1,"watchers_count":1,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":18,"forks":0,"open_issues":18,"watchers":1,"default_branch":"master"}},"base":{"label":"miraculixx:master","ref":"master","sha":"b762d92ae040fb741ccec1cd3b26d5511391e9a1","user":{"login":"miraculixx","id":859611,"avatar_url":"https://avatars.githubusercontent.com/u/859611?v=3","gravatar_id":"","url":"https://api.github.com/users/miraculixx","html_url":"https://github.com/miraculixx","followers_url":"https://api.github.com/users/miraculixx/followers","following_url":"https://api.github.com/users/miraculixx/following{/other_user}","gists_url":"https://api.github.com/users/miraculixx/gists{/gist_id}","starred_url":"https://api.github.com/users/miraculixx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/miraculixx/subscriptions","organizations_url":"https://api.github.com/users/miraculixx/orgs","repos_url":"https://api.github.com/users/miraculixx/repos","events_url":"https://api.github.com/users/miraculixx/events{/privacy}","received_events_url":"https://api.github.com/users/miraculixx/received_events","type":"User","site_admin":false},"repo":{"id":26932347,"name":"pyrules","full_name":"miraculixx/pyrules","owner":{"login":"miraculixx","id":859611,"avatar_url":"https://avatars.githubusercontent.com/u/859611?v=3","gravatar_id":"","url":"https://api.github.com/users/miraculixx","html_url":"https://github.com/miraculixx","followers_url":"https://api.github.com/users/miraculixx/followers","following_url":"https://api.github.com/users/miraculixx/following{/other_user}","gists_url":"https://api.github.com/users/miraculixx/gists{/gist_id}","starred_url":"https://api.github.com/users/miraculixx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/miraculixx/subscriptions","organizations_url":"https://api.github.com/users/miraculixx/orgs","repos_url":"https://api.github.com/users/miraculixx/repos","events_url":"https://api.github.com/users/miraculixx/events{/privacy}","received_events_url":"https://api.github.com/users/miraculixx/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/miraculixx/pyrules","description":"Python Rules Engine","fork":false,"url":"https://api.github.com/repos/miraculixx/pyrules","forks_url":"https://api.github.com/repos/miraculixx/pyrules/forks","keys_url":"https://api.github.com/repos/miraculixx/pyrules/keys{/key_id}","collaborators_url":"https://api.github.com/repos/miraculixx/pyrules/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/miraculixx/pyrules/teams","hooks_url":"https://api.github.com/repos/miraculixx/pyrules/hooks","issue_events_url":"https://api.github.com/repos/miraculixx/pyrules/issues/events{/number}","events_url":"https://api.github.com/repos/miraculixx/pyrules/events","assignees_url":"https://api.github.com/repos/miraculixx/pyrules/assignees{/user}","branches_url":"https://api.github.com/repos/miraculixx/pyrules/branches{/branch}","tags_url":"https://api.github.com/repos/miraculixx/pyrules/tags","blobs_url":"https://api.github.com/repos/miraculixx/pyrules/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/miraculixx/pyrules/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/miraculixx/pyrules/git/refs{/sha}","trees_url":"https://api.github.com/repos/miraculixx/pyrules/git/trees{/sha}","statuses_url":"https://api.github.com/repos/miraculixx/pyrules/statuses/{sha}","languages_url":"https://api.github.com/repos/miraculixx/pyrules/languages","stargazers_url":"https://api.github.com/repos/miraculixx/pyrules/stargazers","contributors_url":"https://api.github.com/repos/miraculixx/pyrules/contributors","subscribers_url":"https://api.github.com/repos/miraculixx/pyrules/subscribers","subscription_url":"https://api.github.com/repos/miraculixx/pyrules/subscription","commits_url":"https://api.github.com/repos/miraculixx/pyrules/commits{/sha}","git_commits_url":"https://api.github.com/repos/miraculixx/pyrules/git/commits{/sha}","comments_url":"https://api.github.com/repos/miraculixx/pyrules/comments{/number}","issue_comment_url":"https://api.github.com/repos/miraculixx/pyrules/issues/comments{/number}","contents_url":"https://api.github.com/repos/miraculixx/pyrules/contents/{+path}","compare_url":"https://api.github.com/repos/miraculixx/pyrules/compare/{base}...{head}","merges_url":"https://api.github.com/repos/miraculixx/pyrules/merges","archive_url":"https://api.github.com/repos/miraculixx/pyrules/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/miraculixx/pyrules/downloads","issues_url":"https://api.github.com/repos/miraculixx/pyrules/issues{/number}","pulls_url":"https://api.github.com/repos/miraculixx/pyrules/pulls{/number}","milestones_url":"https://api.github.com/repos/miraculixx/pyrules/milestones{/number}","notifications_url":"https://api.github.com/repos/miraculixx/pyrules/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/miraculixx/pyrules/labels{/name}","releases_url":"https://api.github.com/repos/miraculixx/pyrules/releases{/id}","created_at":"2014-11-20T21:18:21Z","updated_at":"2014-11-20T23:47:17Z","pushed_at":"2015-04-24T21:25:07Z","git_url":"git://github.com/miraculixx/pyrules.git","ssh_url":"git@github.com:miraculixx/pyrules.git","clone_url":"https://github.com/miraculixx/pyrules.git","svn_url":"https://github.com/miraculixx/pyrules","homepage":null,"size":252,"stargazers_count":1,"watchers_count":1,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":18,"forks":0,"open_issues":18,"watchers":1,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/miraculixx/pyrules/pulls/18"},"html":{"href":"https://github.com/miraculixx/pyrules/pull/18"},"issue":{"href":"https://api.github.com/repos/miraculixx/pyrules/issues/18"},"comments":{"href":"https://api.github.com/repos/miraculixx/pyrules/issues/18/comments"},"review_comments":{"href":"https://api.github.com/repos/miraculixx/pyrules/pulls/18/comments"},"review_comment":{"href":"https://api.github.com/repos/miraculixx/pyrules/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/miraculixx/pyrules/pulls/18/commits"},"statuses":{"href":"https://api.github.com/repos/miraculixx/pyrules/statuses/19fa22926e93a2d2b022c8e46ad5c9fa6d97ca9c"}}}}