{"action":"created","comment":{"url":"https://api.github.com/repos/zooniverse/education-api/pulls/comments/68424498","id":68424498,"diff_hunk":"@@ -1,61 +1,142 @@\n-require 'uri'\n-require 'net/http'\n+require \"csv\"\n+require \"uri\"\n+require \"net/http\"\n+require \"json\"\n \n class CartoUploader\n   CARTODB_ACCOUNT = ENV[\"CARTODB_ACCOUNT\"]\n   CARTODB_APIKEY = ENV[\"CARTODB_APIKEY\"]\n   CARTODB_URI_GET = \"https://#{CARTODB_ACCOUNT}.cartodb.com/api/v2/sql?q=__SQLQUERY__&api_key=#{CARTODB_APIKEY}\"\n   CARTODB_URI_POST = \"https://#{CARTODB_ACCOUNT}.cartodb.com/api/v2/sql\"\n   CARTODB_TABLE = ENV[\"CARTODB_TABLE\"]\n-  \n-  CARTODB_INSERT_MODE_INDIVIDUAL = \"ind\"\n-  CARTODB_INSERT_MODE_MASS = \"mass\"\n+  CLASSIFICATIONS_PER_BATCH = 200\n \n-  def initialize(mode = CARTODB_INSERT_MODE_MASS)\n-    @mode = mode\n-  end\n-  \n   def upload(data)\n-    allVals = []  #For Mass Insertion mode only.\n-    keys = []     #Declaration for Mass Insertion mode.\n+    # Determine what's already in the database\n+    # --------------------------------\n+    latest_classification_id = get_latest_classification_id\n+    expected_insertions = 0\n+    successful_insertions = 0\n+\n+    # If we can't determine what's already in the database, DELETE AND START ANEW.\n+    if latest_classification_id == 0\n+      truncate_everything\n+    end\n+    # --------------------------------\n+\n+    arr_vals = []\n+    keys = []\n     data.each do |classification|\n-      keys = []  #Will be overwritten with the same data over and over again, but whatcha gonna do?\n+      # Only insert new Classifications\n+      # --------------------------------\n+      if Integer(classification[\"classification_id\"]) <= latest_classification_id\n+        next\n+      end\n+      # --------------------------------\n+\n+      # Take every key-val and package it into a single Classification to be Inserted\n+      # --------------------------------\n+      keys = []  # Will be overwritten with the same data over and over again, but whatcha gonna do?\n       vals = []\n-      #Take every key-val \n-      #--------------------------------\n       classification.each do |key, val|\n         keys.push(key)\n-        if (val.is_a?(NilClass))\n+        if val.is_a?(NilClass)\n           vals.push(\"''\")\n         else\n           vals.push(\"'\" + val.to_s.gsub(/'/, \"''\") + \"'\")\n         end\n       end\n       keys = keys.join(\",\")\n       vals = vals.join(\",\")\n-      allVals.push(\"(#{vals})\");  #For Mass Insertion mode only.\n-      #--------------------------------\n-\n-      #Option 1: Individual SQL Insertions\n-      #--------------------------------\n-      if @mode != CARTODB_INSERT_MODE_MASS\n-        sqlQuery = \"INSERT INTO #{CARTODB_TABLE} (#{keys}) VALUES (#{vals}) \"\n-        uri = URI(CARTODB_URI_GET.gsub(/__SQLQUERY__/, URI.escape(sqlQuery)))\n-        res = Net::HTTP.get(uri)\n-        puts res\n+      arr_vals.push(\"(#{vals})\")\n+      # --------------------------------\n+\n+      # If we have enough classifications queued up, do a batch Insert.\n+      # --------------------------------\n+      if arr_vals.length >= CLASSIFICATIONS_PER_BATCH\n+        expected_insertions += arr_vals.length\n+        successful_insertions += batch_insert(keys, arr_vals)\n+        arr_vals = []\n+        # Sanity check\n+        puts \"Inserted #{successful_insertions} Classifications out of #{expected_insertions}\"\n+        if expected_insertions != successful_insertions\n+          raise StandardError, \"Could not insert Classifications. Aborting attempt.\"\n+        end\n       end\n-      #--------------------------------\n+      # --------------------------------\n+    end\n+\n+    # Do a final batch Insert for any stragglers\n+    # --------------------------------\n+    unless arr_vals.empty?\n+      expected_insertions += arr_vals.length\n+      successful_insertions += batch_insert(keys, arr_vals)\n+      arr_vals = []\n     end\n+    # --------------------------------\n+\n+    # Final Sanity check\n+    # --------------------------------\n+    puts \"Inserted #{successful_insertions} Classifications out of #{expected_insertions}\"\n+    if expected_insertions != successful_insertions\n+      raise StandardError, \"Could not insert Classifications. Aborting attempt.\"\n+    end\n+    # --------------------------------\n+\n+  rescue StandardError => err\n+    # FINAL SAFETY NET\n+    # TODO: Log error?\n+    puts \"ERROR:\"\n+    puts err\n+  end\n+\n+  # Returns latest Classification ID.\n+  def get_latest_classification_id\n+    sql_query = \"SELECT classification_id FROM #{CARTODB_TABLE} ORDER BY classification_id DESC LIMIT 1\"\n+    uri = URI(CARTODB_URI_GET.gsub(/__SQLQUERY__/, URI.escape(sql_query)))\n+    res = Net::HTTP.get(uri)\n+    res = JSON.parse(res)\n+    if res[\"error\"]\n+      raise StandardError, res[\"error\"]\n+    end\n+    if res[\"rows\"] && res[\"rows\"][0] && res[\"rows\"][0] && res[\"rows\"][0][\"classification_id\"]\n+      return Integer(res[\"rows\"][0][\"classification_id\"])\n+    else\n+      return 0\n+  rescue StandardError => err\n+    # TODO: Log error?\n+    puts \"ERROR:\"\n+    puts err\n+    return 0\n+  end\n+\n+  # Uploads a batch of rows. Returns number of successful uploads.\n+  def batch_insert(keys, arr_values)\n+    all_vals = arr_values.join(\",\")\n+    sql_query = \"INSERT INTO #{CARTODB_TABLE} (#{keys}) VALUES #{all_vals} \"\n+    uri = URI(CARTODB_URI_POST)\n+    res = Net::HTTP.post_form(uri, \"q\" => sql_query, \"api_key\" => CARTODB_APIKEY)\n+    res = JSON.parse(res.body)\n+    if res[\"error\"]\n+      raise StandardError, res[\"error\"]\n+    end\n+    return (res[\"total_rows\"]) ? Integer(res[\"total_rows\"]) : 0\n+  rescue StandardError => err\n+    # TODO: Log error?\n+    puts \"ERROR:\"\n+    puts err\n+    return 0\n+  end\n \n-    #Option 2: Mass SQL Insertions\n-    #--------------------------------\n-    allVals = allVals.join(\",\")\n-    if @mode == CARTODB_INSERT_MODE_MASS\n-      sqlQuery = \"INSERT INTO #{CARTODB_TABLE} (#{keys}) VALUES #{allVals} \"\n-      uri = URI(CARTODB_URI_POST)\n-      res = Net::HTTP.post_form(uri, \"q\" => sqlQuery, \"api_key\" => CARTODB_APIKEY)\n-      puts res.body\n+  # Truncates the table. Raises an error if something goes wrong; no safeties.\n+  def truncate_everything\n+    sql_query = \"TRUNCATE #{CARTODB_TABLE}\"\n+    uri = URI(CARTODB_URI_GET.gsub(/__SQLQUERY__/, URI.escape(sql_query)))\n+    res = Net::HTTP.get(uri)\n+    res = JSON.parse(res)\n+    puts \"Truncated #{CARTODB_TABLE}\"\n+    if res[\"error\"]\n+      raise StandardError, res[\"error\"]\n     end\n-    #--------------------------------\n   end\n-end\n\\ No newline at end of file\n+end","path":"app/models/carto_uploader.rb","position":179,"original_position":179,"commit_id":"2593bea98fb82bfa084c0a410c01da211cfaa899","original_commit_id":"2593bea98fb82bfa084c0a410c01da211cfaa899","user":{"login":"houndci-bot","id":6697940,"avatar_url":"https://avatars.githubusercontent.com/u/6697940?v=3","gravatar_id":"","url":"https://api.github.com/users/houndci-bot","html_url":"https://github.com/houndci-bot","followers_url":"https://api.github.com/users/houndci-bot/followers","following_url":"https://api.github.com/users/houndci-bot/following{/other_user}","gists_url":"https://api.github.com/users/houndci-bot/gists{/gist_id}","starred_url":"https://api.github.com/users/houndci-bot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/houndci-bot/subscriptions","organizations_url":"https://api.github.com/users/houndci-bot/orgs","repos_url":"https://api.github.com/users/houndci-bot/repos","events_url":"https://api.github.com/users/houndci-bot/events{/privacy}","received_events_url":"https://api.github.com/users/houndci-bot/received_events","type":"User","site_admin":false},"body":"unexpected token $end\n(Using Ruby 2.3 parser; configure using `TargetRubyVersion` parameter, under `AllCops`)","created_at":"2016-06-24T16:32:54Z","updated_at":"2016-06-24T16:32:54Z","html_url":"https://github.com/zooniverse/education-api/pull/10#discussion_r68424498","pull_request_url":"https://api.github.com/repos/zooniverse/education-api/pulls/10","_links":{"self":{"href":"https://api.github.com/repos/zooniverse/education-api/pulls/comments/68424498"},"html":{"href":"https://github.com/zooniverse/education-api/pull/10#discussion_r68424498"},"pull_request":{"href":"https://api.github.com/repos/zooniverse/education-api/pulls/10"}}},"pull_request":{"url":"https://api.github.com/repos/zooniverse/education-api/pulls/10","id":69705430,"html_url":"https://github.com/zooniverse/education-api/pull/10","diff_url":"https://github.com/zooniverse/education-api/pull/10.diff","patch_url":"https://github.com/zooniverse/education-api/pull/10.patch","issue_url":"https://api.github.com/repos/zooniverse/education-api/issues/10","number":10,"state":"open","locked":false,"title":"Improve CartoDB Transformer & Uploader (Classifications) [Ready for review, needs 'live' testing]","user":{"login":"shaunanoordin","id":13952701,"avatar_url":"https://avatars.githubusercontent.com/u/13952701?v=3","gravatar_id":"","url":"https://api.github.com/users/shaunanoordin","html_url":"https://github.com/shaunanoordin","followers_url":"https://api.github.com/users/shaunanoordin/followers","following_url":"https://api.github.com/users/shaunanoordin/following{/other_user}","gists_url":"https://api.github.com/users/shaunanoordin/gists{/gist_id}","starred_url":"https://api.github.com/users/shaunanoordin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/shaunanoordin/subscriptions","organizations_url":"https://api.github.com/users/shaunanoordin/orgs","repos_url":"https://api.github.com/users/shaunanoordin/repos","events_url":"https://api.github.com/users/shaunanoordin/events{/privacy}","received_events_url":"https://api.github.com/users/shaunanoordin/received_events","type":"User","site_admin":false},"body":"Closes #9 \r\n\r\n* This PR is intended to improve the data sync between new Classifications data from Panoptes to CartoDB.\r\n* NOT addressed: anything to do with Aggregations or Subjects data.","created_at":"2016-05-11T16:49:58Z","updated_at":"2016-06-24T16:32:54Z","closed_at":null,"merged_at":null,"merge_commit_sha":"52acf2d0d5ea4861bd175d6d3fd6419d12beaee2","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/zooniverse/education-api/pulls/10/commits","review_comments_url":"https://api.github.com/repos/zooniverse/education-api/pulls/10/comments","review_comment_url":"https://api.github.com/repos/zooniverse/education-api/pulls/comments{/number}","comments_url":"https://api.github.com/repos/zooniverse/education-api/issues/10/comments","statuses_url":"https://api.github.com/repos/zooniverse/education-api/statuses/2593bea98fb82bfa084c0a410c01da211cfaa899","head":{"label":"zooniverse:cartodb-improved-classifications","ref":"cartodb-improved-classifications","sha":"2593bea98fb82bfa084c0a410c01da211cfaa899","user":{"login":"zooniverse","id":106856,"avatar_url":"https://avatars.githubusercontent.com/u/106856?v=3","gravatar_id":"","url":"https://api.github.com/users/zooniverse","html_url":"https://github.com/zooniverse","followers_url":"https://api.github.com/users/zooniverse/followers","following_url":"https://api.github.com/users/zooniverse/following{/other_user}","gists_url":"https://api.github.com/users/zooniverse/gists{/gist_id}","starred_url":"https://api.github.com/users/zooniverse/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zooniverse/subscriptions","organizations_url":"https://api.github.com/users/zooniverse/orgs","repos_url":"https://api.github.com/users/zooniverse/repos","events_url":"https://api.github.com/users/zooniverse/events{/privacy}","received_events_url":"https://api.github.com/users/zooniverse/received_events","type":"Organization","site_admin":false},"repo":{"id":49882447,"name":"education-api","full_name":"zooniverse/education-api","owner":{"login":"zooniverse","id":106856,"avatar_url":"https://avatars.githubusercontent.com/u/106856?v=3","gravatar_id":"","url":"https://api.github.com/users/zooniverse","html_url":"https://github.com/zooniverse","followers_url":"https://api.github.com/users/zooniverse/followers","following_url":"https://api.github.com/users/zooniverse/following{/other_user}","gists_url":"https://api.github.com/users/zooniverse/gists{/gist_id}","starred_url":"https://api.github.com/users/zooniverse/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zooniverse/subscriptions","organizations_url":"https://api.github.com/users/zooniverse/orgs","repos_url":"https://api.github.com/users/zooniverse/repos","events_url":"https://api.github.com/users/zooniverse/events{/privacy}","received_events_url":"https://api.github.com/users/zooniverse/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/zooniverse/education-api","description":"Backend for (at least) Wildcam Gorongosa educator portal","fork":false,"url":"https://api.github.com/repos/zooniverse/education-api","forks_url":"https://api.github.com/repos/zooniverse/education-api/forks","keys_url":"https://api.github.com/repos/zooniverse/education-api/keys{/key_id}","collaborators_url":"https://api.github.com/repos/zooniverse/education-api/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/zooniverse/education-api/teams","hooks_url":"https://api.github.com/repos/zooniverse/education-api/hooks","issue_events_url":"https://api.github.com/repos/zooniverse/education-api/issues/events{/number}","events_url":"https://api.github.com/repos/zooniverse/education-api/events","assignees_url":"https://api.github.com/repos/zooniverse/education-api/assignees{/user}","branches_url":"https://api.github.com/repos/zooniverse/education-api/branches{/branch}","tags_url":"https://api.github.com/repos/zooniverse/education-api/tags","blobs_url":"https://api.github.com/repos/zooniverse/education-api/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/zooniverse/education-api/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/zooniverse/education-api/git/refs{/sha}","trees_url":"https://api.github.com/repos/zooniverse/education-api/git/trees{/sha}","statuses_url":"https://api.github.com/repos/zooniverse/education-api/statuses/{sha}","languages_url":"https://api.github.com/repos/zooniverse/education-api/languages","stargazers_url":"https://api.github.com/repos/zooniverse/education-api/stargazers","contributors_url":"https://api.github.com/repos/zooniverse/education-api/contributors","subscribers_url":"https://api.github.com/repos/zooniverse/education-api/subscribers","subscription_url":"https://api.github.com/repos/zooniverse/education-api/subscription","commits_url":"https://api.github.com/repos/zooniverse/education-api/commits{/sha}","git_commits_url":"https://api.github.com/repos/zooniverse/education-api/git/commits{/sha}","comments_url":"https://api.github.com/repos/zooniverse/education-api/comments{/number}","issue_comment_url":"https://api.github.com/repos/zooniverse/education-api/issues/comments{/number}","contents_url":"https://api.github.com/repos/zooniverse/education-api/contents/{+path}","compare_url":"https://api.github.com/repos/zooniverse/education-api/compare/{base}...{head}","merges_url":"https://api.github.com/repos/zooniverse/education-api/merges","archive_url":"https://api.github.com/repos/zooniverse/education-api/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/zooniverse/education-api/downloads","issues_url":"https://api.github.com/repos/zooniverse/education-api/issues{/number}","pulls_url":"https://api.github.com/repos/zooniverse/education-api/pulls{/number}","milestones_url":"https://api.github.com/repos/zooniverse/education-api/milestones{/number}","notifications_url":"https://api.github.com/repos/zooniverse/education-api/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/zooniverse/education-api/labels{/name}","releases_url":"https://api.github.com/repos/zooniverse/education-api/releases{/id}","deployments_url":"https://api.github.com/repos/zooniverse/education-api/deployments","created_at":"2016-01-18T14:35:56Z","updated_at":"2016-01-18T15:05:35Z","pushed_at":"2016-06-24T16:32:47Z","git_url":"git://github.com/zooniverse/education-api.git","ssh_url":"git@github.com:zooniverse/education-api.git","clone_url":"https://github.com/zooniverse/education-api.git","svn_url":"https://github.com/zooniverse/education-api","homepage":"","size":150,"stargazers_count":0,"watchers_count":0,"language":"Ruby","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":6,"forks":0,"open_issues":6,"watchers":0,"default_branch":"master"}},"base":{"label":"zooniverse:master","ref":"master","sha":"8651574ff04448a6978c4d6d535d5519fe34829a","user":{"login":"zooniverse","id":106856,"avatar_url":"https://avatars.githubusercontent.com/u/106856?v=3","gravatar_id":"","url":"https://api.github.com/users/zooniverse","html_url":"https://github.com/zooniverse","followers_url":"https://api.github.com/users/zooniverse/followers","following_url":"https://api.github.com/users/zooniverse/following{/other_user}","gists_url":"https://api.github.com/users/zooniverse/gists{/gist_id}","starred_url":"https://api.github.com/users/zooniverse/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zooniverse/subscriptions","organizations_url":"https://api.github.com/users/zooniverse/orgs","repos_url":"https://api.github.com/users/zooniverse/repos","events_url":"https://api.github.com/users/zooniverse/events{/privacy}","received_events_url":"https://api.github.com/users/zooniverse/received_events","type":"Organization","site_admin":false},"repo":{"id":49882447,"name":"education-api","full_name":"zooniverse/education-api","owner":{"login":"zooniverse","id":106856,"avatar_url":"https://avatars.githubusercontent.com/u/106856?v=3","gravatar_id":"","url":"https://api.github.com/users/zooniverse","html_url":"https://github.com/zooniverse","followers_url":"https://api.github.com/users/zooniverse/followers","following_url":"https://api.github.com/users/zooniverse/following{/other_user}","gists_url":"https://api.github.com/users/zooniverse/gists{/gist_id}","starred_url":"https://api.github.com/users/zooniverse/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zooniverse/subscriptions","organizations_url":"https://api.github.com/users/zooniverse/orgs","repos_url":"https://api.github.com/users/zooniverse/repos","events_url":"https://api.github.com/users/zooniverse/events{/privacy}","received_events_url":"https://api.github.com/users/zooniverse/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/zooniverse/education-api","description":"Backend for (at least) Wildcam Gorongosa educator portal","fork":false,"url":"https://api.github.com/repos/zooniverse/education-api","forks_url":"https://api.github.com/repos/zooniverse/education-api/forks","keys_url":"https://api.github.com/repos/zooniverse/education-api/keys{/key_id}","collaborators_url":"https://api.github.com/repos/zooniverse/education-api/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/zooniverse/education-api/teams","hooks_url":"https://api.github.com/repos/zooniverse/education-api/hooks","issue_events_url":"https://api.github.com/repos/zooniverse/education-api/issues/events{/number}","events_url":"https://api.github.com/repos/zooniverse/education-api/events","assignees_url":"https://api.github.com/repos/zooniverse/education-api/assignees{/user}","branches_url":"https://api.github.com/repos/zooniverse/education-api/branches{/branch}","tags_url":"https://api.github.com/repos/zooniverse/education-api/tags","blobs_url":"https://api.github.com/repos/zooniverse/education-api/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/zooniverse/education-api/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/zooniverse/education-api/git/refs{/sha}","trees_url":"https://api.github.com/repos/zooniverse/education-api/git/trees{/sha}","statuses_url":"https://api.github.com/repos/zooniverse/education-api/statuses/{sha}","languages_url":"https://api.github.com/repos/zooniverse/education-api/languages","stargazers_url":"https://api.github.com/repos/zooniverse/education-api/stargazers","contributors_url":"https://api.github.com/repos/zooniverse/education-api/contributors","subscribers_url":"https://api.github.com/repos/zooniverse/education-api/subscribers","subscription_url":"https://api.github.com/repos/zooniverse/education-api/subscription","commits_url":"https://api.github.com/repos/zooniverse/education-api/commits{/sha}","git_commits_url":"https://api.github.com/repos/zooniverse/education-api/git/commits{/sha}","comments_url":"https://api.github.com/repos/zooniverse/education-api/comments{/number}","issue_comment_url":"https://api.github.com/repos/zooniverse/education-api/issues/comments{/number}","contents_url":"https://api.github.com/repos/zooniverse/education-api/contents/{+path}","compare_url":"https://api.github.com/repos/zooniverse/education-api/compare/{base}...{head}","merges_url":"https://api.github.com/repos/zooniverse/education-api/merges","archive_url":"https://api.github.com/repos/zooniverse/education-api/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/zooniverse/education-api/downloads","issues_url":"https://api.github.com/repos/zooniverse/education-api/issues{/number}","pulls_url":"https://api.github.com/repos/zooniverse/education-api/pulls{/number}","milestones_url":"https://api.github.com/repos/zooniverse/education-api/milestones{/number}","notifications_url":"https://api.github.com/repos/zooniverse/education-api/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/zooniverse/education-api/labels{/name}","releases_url":"https://api.github.com/repos/zooniverse/education-api/releases{/id}","deployments_url":"https://api.github.com/repos/zooniverse/education-api/deployments","created_at":"2016-01-18T14:35:56Z","updated_at":"2016-01-18T15:05:35Z","pushed_at":"2016-06-24T16:32:47Z","git_url":"git://github.com/zooniverse/education-api.git","ssh_url":"git@github.com:zooniverse/education-api.git","clone_url":"https://github.com/zooniverse/education-api.git","svn_url":"https://github.com/zooniverse/education-api","homepage":"","size":150,"stargazers_count":0,"watchers_count":0,"language":"Ruby","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":6,"forks":0,"open_issues":6,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/zooniverse/education-api/pulls/10"},"html":{"href":"https://github.com/zooniverse/education-api/pull/10"},"issue":{"href":"https://api.github.com/repos/zooniverse/education-api/issues/10"},"comments":{"href":"https://api.github.com/repos/zooniverse/education-api/issues/10/comments"},"review_comments":{"href":"https://api.github.com/repos/zooniverse/education-api/pulls/10/comments"},"review_comment":{"href":"https://api.github.com/repos/zooniverse/education-api/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/zooniverse/education-api/pulls/10/commits"},"statuses":{"href":"https://api.github.com/repos/zooniverse/education-api/statuses/2593bea98fb82bfa084c0a410c01da211cfaa899"}}}}
{"actor":{"display_login":"houndci-bot"}}