{"action":"created","comment":{"url":"https://api.github.com/repos/Ensembl/ensembl-xref/pulls/comments/239004446","pull_request_review_id":181680056,"id":239004446,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIzOTAwNDQ0Ng==","diff_hunk":"@@ -65,267 +55,285 @@ sub new {\n     return bless {}, $class;\n }\n \n+=head2 fetch_files\n+\n+Arg 1      :  Hashref { \n+                dest_dir => $path_for_downloads, \n+                user_uris => [$uri,...], \n+                deletedownloaded => boolean, # Delete any existing files\n+                verbose => boolean \n+              }\n+Description:  Given a config, this method selects the relevant client and downloads\n+              the files from the supplies list of URLs. It uses a dispatch table to choose\n+              which method to run to fetch. It always tries to reuse existing downloaded\n+              files - set deletedownloaded if you don't want that to happen\n+Returntype :  List of paths on the local file system\n+\n+=cut\n+\n sub fetch_files {\n   my ($self, $arg_ref) = @_;\n \n-\n   my $dest_dir         = $arg_ref->{dest_dir};\n   my $user_uris        = $arg_ref->{user_uris};\n   my $deletedownloaded = $arg_ref->{del_down};\n-  my $checkdownload    = $arg_ref->{chk_down};\n-  my $verbose          = $arg_ref->{verbose} ;\n-\n-  my @processed_files;\n-\n-  foreach my $user_uri (@$user_uris) {\n+  my $verbose          = $arg_ref->{verbose};\n+\n+  my @processed_files; # to be returned to caller\n+  my %dispatch = (\n+    script => \\&script_handler,\n+    file => \\&file_handler,\n+    ftp => \\&ftp_handler,\n+    http => \\&http_handler,\n+    https => \\&http_handler,\n+    mysql => \\&script_handler,\n+  );\n+\n+  # Ensure there is a folder to receive downloaded files\n+  make_path($dest_dir); \n+\n+  foreach my $stringy_uri (@$user_uris) {\n     # Change old-style 'LOCAL:' URIs into 'file:'.\n-    $user_uri =~ s/^LOCAL:/file:/ix;\n-    my $uri = URI->new($user_uri);\n+    $stringy_uri =~ s/^LOCAL:/file:/ix;\n+    my $uri = URI->new($stringy_uri);\n \n-    if ( $uri->scheme() eq 'script' ) {\n-      push( @processed_files, $user_uri );\n-    } elsif ( $uri->scheme() eq 'file' ) {\n-\n-      # Deal with local files.\n+    unless (exists $dispatch{$uri->scheme()}) { \n+      croak 'Unrecognised fetch method from config file: '.$uri->scheme().' from '.$stringy_uri;\n+    }\n+    my $code = $dispatch{$uri->scheme()};\n+    my @downloaded_files = $self->$code($stringy_uri, $uri, $dest_dir, $deletedownloaded, $verbose);\n+    push @processed_files, @downloaded_files;\n+  }\n \n-      $user_uri =~ s/file://x;\n-      if ( -s $user_uri ) {\n-        push( @processed_files, $user_uri );\n-      } else {\n-        printf( \"==> Can not find file '%s' (or it is empty)\\n\",\n-                $user_uri );\n-        return ();\n+  if (!@processed_files) { croak \"No files were downloaded or existed already\"; }\n+  # Validate any compressed files we downloaded\n+  my @failures;\n+  foreach my $file (@processed_files) {\n+    if ($file =~ /\\.(gz|Z)$/x) {\n+      my $cmd = \"gzip -t $file\";\n+      my $return = system($cmd);\n+      if ($return != 0) {\n+        push @failures, $file;\n       }\n-    } elsif ( $uri->scheme() eq 'ftp' ) {\n-      # Deal with FTP files.\n+    }\n+  }\n \n-      my $file_path = catfile( $dest_dir, basename( $uri->path() ) );\n+  if (@failures) {\n+    my $error = sprintf \"Failed to validate: %s\\nThese files have been deleted\", join ',',@failures;\n+    foreach my $doomed (@failures) {\n+      unlink $doomed;\n+    }\n+    confess $error;\n+  }\n \n-      if ( $deletedownloaded && -e $file_path ) {\n-        if ($verbose) {\n-          printf( \"Deleting '%s'\\n\", $file_path );\n-        }\n-        unlink($file_path);\n-      }\n+  return @processed_files;\n+}\n \n-      if ( $checkdownload && -s $file_path ) {\n-        # The file is already there, no need to connect to a FTP\n-        # server.  This also means no file name globbing was\n-        # used (for globbing FTP URIs, we always need to connect\n-        # to a FTP site to see what files are there).\n-\n-        if ($verbose) {\n-          printf( \"File '%s' already exists\\n\", $file_path );\n-        }\n-        push( @processed_files, $file_path );\n-        next;\n-      }\n+=head2 script_handler\n \n-      if ( -e $file_path ) { unlink($file_path) }\n+Description:  A dud function, for the eventuality that we should ever want to\n+              not do nothing with script-type, mysql-type sources\n+Caller:       fetch_files\n \n-      if ($verbose) {\n-        printf( \"Connecting to FTP host '%s' for file '%s' \\n\",\n-                $uri->host(), $file_path );\n-      }\n+=cut\n \n-      my $ftp = $self->get_ftp($uri, 0);\n-      if(!defined($ftp) or ! $ftp->can('ls') or !$ftp->ls()){\n-\t$ftp =  $self->get_ftp($uri, 1);\n-      }\n-      foreach my $remote_file ( ( @{ $ftp->ls() } ) ) {\n-\t      if ( !match_glob( basename( $uri->path() ), $remote_file ) ) {\n-\t\t  next;\n-\t      }\n-\n-\t      $file_path = catfile( $dest_dir, basename($remote_file) );\n-\n-\t      if ( $deletedownloaded && -e $file_path ) {\n-\t\t  if ($verbose) {\n-\t\t      printf( \"Deleting '%s'\\n\", $file_path );\n-\t\t  }\n-\t\t  unlink($file_path);\n-\t      }\n-\n-\t      if ( $checkdownload && -s $file_path ) {\n-\t\t  if ($verbose) {\n-\t\t      printf( \"File '%s' already exists\\n\", $file_path );\n-\t\t  }\n-\t      } else {\n-\n-\t\t  if ( -e $file_path ) { unlink($file_path) }\n-\n-\t\t  if ( !-d dirname($file_path) ) {\n-\t\t      if ($verbose) {\n-\t\t\t  printf( \"Creating directory '%s'\\n\",\n-\t\t\t\t  dirname($file_path) );\n-\t\t      }\n-\t\t      if ( !mkdir( dirname($file_path) ) ) {\n-\t\t\t  printf( \"==> Can not create directory '%s': %s\",\n-\t\t\t\t  dirname($file_path), $! );\n-\t\t\t  return ();\n-\t\t      }\n-\t\t  }\n-\n-\t\t  if ($verbose) {\n-\t\t      printf( \"Fetching '%s' (size = %s)\\n\",\n-\t\t\t      $remote_file,\n-\t\t\t      $ftp->size($remote_file) || '(unknown)' );\n-\t\t      printf( \"Local file is '%s'\\n\", $file_path );\n-\t\t  }\n-\n-\t\t  if ( !$ftp->get( $remote_file, $file_path ) ) {\n-\t\t      printf( \"==> Could not get '%s': %s\\n\",\n-\t\t\t      basename( $uri->path() ), $ftp->message() );\n-\t\t      return ();\n-\t\t  }\n-\t      } ## end else [ if ( $checkdownload &&...)]\n-\n-\t      if ( $file_path =~ /\\.(gz|Z)$/x ) {\n-\t\t  # Read from zcat pipe\n-\t\t  #\n-\t\t  my $cmd = \"gzip -t $file_path\";\n-\t\t  if ( system($cmd) != 0 ) {\n-\t\t      printf( \"system command '%s' failed: %s - \"\n-\t\t\t      . \"Checking of gzip file failed - \"\n-\t\t\t      . \"FILE CORRUPTED ?\\n\\n\",\n-\t\t\t      $cmd, $? );\n-\n-\t\t      if ( -e $file_path ) {\n-\t\t\t  if ($verbose) {\n-\t\t\t      printf( \"Deleting '%s'\\n\", $file_path );\n-\t\t\t  }\n-\t\t\t  unlink($file_path);\n-\t\t      }\n-\t\t      return ();\n-\t\t  } else {\n-\t\t      if ($verbose) {\n-\t\t\t  printf( \"'%s' passed (gzip -t) corruption test.\\n\",\n-\t\t\t\t  $file_path );\n-\t\t      }\n-\t\t  }\n-\t      }\n-\t      push( @processed_files, $file_path );\n-\n-      } ## end foreach my $remote_file ( (...))\n-    if (!@processed_files) { printf (\"No files found matching $uri\") ; }\n-\n-\n-    } elsif ( $uri->scheme() eq 'http' || $uri->scheme eq 'https') {\n-      # Deal with HTTP files.\n-\n-      my $filename = basename ($uri->path() );\n-      if ($uri->path eq '') { $filename = \"index.html\"; }\n-\n-      my $file_path = catfile( $dest_dir, $filename );\n-\n-      if ( $deletedownloaded && -e $file_path ) {\n-        if ($verbose) {\n-          printf( \"Deleting '%s'\\n\", $file_path );\n-        }\n-        unlink($file_path);\n-      }\n+sub script_handler {\n+  my ($self, $user_uri) = @_;\n+  $user_uri =~ s/\\Ascript://x;\n+  return $user_uri;\n+}\n \n-      if ( $checkdownload && -s $file_path ) {\n-        # The file is already there, no need to connect to a\n-        # HTTP server.\n+=head2 file_handler\n \n-        if ($verbose) {\n-          printf( \"File '%s' already exists\\n\", $file_path );\n-        }\n-        push( @processed_files, $file_path );\n-        next;\n-      }\n+Description:  Handles file system paths, by checking if they're present\n+Caller:       fetch_files\n \n-      if ( -e $file_path ) { unlink($file_path) }\n-\n-      if ( !-d dirname($file_path) ) {\n-        if ($verbose) {\n-          printf( \"Creating directory '%s'\\n\", dirname($file_path) );\n-        }\n-        if ( !mkdir( dirname($file_path) ) ) {\n-          printf( \"==> Can not create directory '%s': %s\",\n-                  dirname($file_path), $! );\n-          return ();\n-        }\n-      }\n+=cut\n \n-      if ($verbose) {\n-        printf( \"Connecting to HTTP host '%s'\\n\", $uri->host() );\n-        printf( \"Fetching '%s'\\n\",                $uri->path() );\n-      }\n+sub file_handler {\n+  my ($self, $user_uri) = @_;\n \n-      if ( $checkdownload && -s $file_path ) {\n-        if ($verbose) {\n-          printf( \"File '%s' already exists\\n\", $file_path );\n-        }\n-      } else {\n+  $user_uri =~ s/\\Afile://x;\n+  if ( -s $user_uri ) {\n+    return $user_uri;\n+  } else {\n+    confess \"Claimed file does not exist or is empty: $user_uri\";\n+  }\n+}\n \n-        if ($verbose) {\n-          printf( \"Local file is '%s'\\n\", $file_path );\n-        }\n+=head2 ftp_handler\n \n-        if ( -e $file_path ) { unlink($file_path) }\n+Description:  Fetches glob-matchable files from an FTP site. It is able to\n+              avoid downloading by looking for the expected file.\n+              Could not be handled in Bio::EnsEMBL::Utils::Net, due to needing\n+              a manifest of files in order to do glob-matching\n+Caller:       fetch_files\n \n-        open OUT, \">$file_path\" or die \"Couldn't open file $file_path $!\";\n-        my $http = HTTP::Tiny->new();\n+=cut\n \n-        my $response = $http->get($uri->as_string());\n+sub ftp_handler {\n+  my ($self, $user_uri, $uri, $dest_dir, $delete, $verbose) = @_;\n+  \n+  # In case of single file fetch\n+  my $path = $self->check_download($dest_dir, basename($uri->path()), $delete, $verbose);\n+  return $path if defined $path;\n+  # From here on we are pulling sets of files from the FTP site, either by glob pattern\n+  # or just getting all of them\n \n-        if ( !$response->{success} ) {\n-          printf( \"==> Could not get '%s': %s\\n\",\n-                  basename( $uri->path() ), $response->{content} );\n-          return ();\n-        }\n-        print OUT $response->{content};\n-        close OUT;\n-      }\n+  my @ftp_manifest = @{ $self->list_ftp_files($uri) };\n \n-      push( @processed_files, $file_path );\n+  my @download_manifest; # A buffer of file paths\n \n-    } elsif ( $uri->scheme() eq 'mysql' ) {\n-      # Just leave MySQL data untouched for now.\n-      push( @processed_files, $user_uri );\n+  my $ftp = $self->get_ftp($uri);\n+\n+  foreach my $remote_file (@ftp_manifest) {\n+    # Skip if the pattern in the URI does not match individual files?\n+    # Probably for removing .. and such\n+    next if ( !match_glob( basename( $uri->path() ), $remote_file ) );\n+\n+    my $file_path = $self->check_download($dest_dir, basename($remote_file), $delete, $verbose);\n+    if ($file_path) {\n+      push @download_manifest, $file_path;\n     } else {\n-      printf( \"==> Unknown URI scheme '%s' in URI '%s'\\n\",\n-              $uri->scheme(), $uri->as_string() );\n-      return ();\n+      print \"Fetching $remote_file to $dest_dir\\n\" if $verbose;\n+      my $status = $ftp->get( $remote_file, catfile( $dest_dir, basename($remote_file)) );\n+      if ($status) {\n+        push @download_manifest, catfile($dest_dir,basename($remote_file));\n+      } else {\n+        confess \"Failed to download $remote_file via FTP: \".$ftp->message();\n+      }\n     }\n-  } ## end foreach my $user_uri (@user_uris)\n+  }\n+  $ftp->quit();\n+  \n+  return @download_manifest;\n+}\n \n-  return @processed_files;\n-} ## end sub fetch_files\n+=head2 http_handler\n \n+Description:  Fetches a single URL. Will check for existing files by default\n+Caller:       fetch_files\n \n-sub get_ftp{\n-  my ($self, $uri, $passive) = @_;\n-  my $ftp;\n+=cut\n \n-  if($passive){\n-    $ftp = Net::FTP->new( $uri->host(), 'Debug' => 0, Passive => 1);\n-  }\n-  else{\n-    $ftp = Net::FTP->new( $uri->host(), 'Debug' => 0);\n+sub http_handler {\n+  my ($self, $uri_string, $uri, $dest_dir, $delete, $verbose) = @_;\n+  my $remote_file_name;\n+  if ($uri->path eq '/') { $remote_file_name = \"index.html\"; } # failover to default webpage\n+  $remote_file_name //= basename($uri->path());\n+\n+  my $path = $self->check_download($dest_dir, $remote_file_name, $delete, $verbose);\n+  return $path if defined $path;\n+\n+  print \"Fetching $uri_string\\n\" if $verbose;\n+  my $download_path = catfile($dest_dir,$remote_file_name);\n+  \n+  my $ua = LWP::UserAgent->new();\n+  $ua->ssl_opts( verify_hostname => 0);\n+  my $response = $ua->get($uri_string, ':content_file' => $download_path);\n+  if (! $response->is_success) {\n+    confess \"Failed when downloading $uri_string with response \".$response->status_line;\n   }\n \n-  if ( !defined($ftp) ) {\n-    printf( \"==> Can not open FTP connection: %s\\n\", $@ );\n-    return ();\n-  }\n+  return $download_path;\n+}\n \n-  if ( !$ftp->login( 'anonymous', '-anonymous@' ) ) {\n-    printf( \"==> Can not log in on FTP host: %s\\n\",\n-\t    $ftp->message() );\n-    return ();\n-\t}\n \n-  if ( !$ftp->cwd( dirname( $uri->path() ) ) ) {\n-    printf( \"== Can not change directory to '%s': %s\\n\",\n-\t\t  dirname( $uri->path() ), $ftp->message() );\n-    return ();\n-  }\n+=head2 get_ftp\n+\n+Arg 1:        URI::ftp instance\n+Arg 2:        Passive option for Net::FTP\n+Arg 3:        FTP user account\n+Arg 4:        FTP password should it be necessary\n+Description:  Provides an FTP client configured to download files from the given URI\n+              It would be a lot more robust if we could use LWP::UserAgent, but this\n+              code requires an FTP->ls() call, hence a real FTP client is needed\n+ReturnType:   Net::FTP instance\n+\n+=cut\n+\n+sub get_ftp {\n+  my ($self, $uri, $passive, $user, $pass) = @_;\n+  my $ftp;\n+  $passive //= 1; # Default use of passive mode for FTP (better for tight firewalls)\n+  $user //= 'anonymous';\n+  $pass //= '-anonymous@';\n+\n+  $ftp = Net::FTP->new( $uri->host(), Debug => 0, Passive => $passive);\n+\n+  croak sprintf(\"Cannot open FTP connection: %s\\n\", $@) if !defined($ftp);\n+\n+  my $state = $ftp->login( $user, $pass );\n+  croak sprintf( \"Cannot log in on FTP host: %s\\n\", $ftp->message() ) if !$state;\n \n+  my ($filename, $path, $extras) = fileparse( $uri->path() ); # fileparse is a bit safer than dirname\n+  $state = $ftp->cwd( $path );\n+\n+  if (!$state) {\n+    croak sprintf( \"== Can not change directory to '%s': %s\\n\", $path, $ftp->message() );  ","path":"modules/Bio/EnsEMBL/Xref/FetchFiles.pm","position":488,"original_position":488,"commit_id":"33a0ace191a82219373f6ae0462bf4b558674338","original_commit_id":"33a0ace191a82219373f6ae0462bf4b558674338","user":{"login":"nerdstrike","id":5434501,"node_id":"MDQ6VXNlcjU0MzQ1MDE=","avatar_url":"https://avatars3.githubusercontent.com/u/5434501?v=4","gravatar_id":"","url":"https://api.github.com/users/nerdstrike","html_url":"https://github.com/nerdstrike","followers_url":"https://api.github.com/users/nerdstrike/followers","following_url":"https://api.github.com/users/nerdstrike/following{/other_user}","gists_url":"https://api.github.com/users/nerdstrike/gists{/gist_id}","starred_url":"https://api.github.com/users/nerdstrike/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nerdstrike/subscriptions","organizations_url":"https://api.github.com/users/nerdstrike/orgs","repos_url":"https://api.github.com/users/nerdstrike/repos","events_url":"https://api.github.com/users/nerdstrike/events{/privacy}","received_events_url":"https://api.github.com/users/nerdstrike/received_events","type":"User","site_admin":false},"body":"I think in both these cases a croak is reasonable. The calling code that tried to access FTP will be fingered by the error. It would only need to be a confess if people were doing FTP activities in the middle of hugely complex operations which is in itself a risky proposition.","created_at":"2018-12-05T10:16:04Z","updated_at":"2018-12-05T10:16:04Z","html_url":"https://github.com/Ensembl/ensembl-xref/pull/17#discussion_r239004446","pull_request_url":"https://api.github.com/repos/Ensembl/ensembl-xref/pulls/17","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/Ensembl/ensembl-xref/pulls/comments/239004446"},"html":{"href":"https://github.com/Ensembl/ensembl-xref/pull/17#discussion_r239004446"},"pull_request":{"href":"https://api.github.com/repos/Ensembl/ensembl-xref/pulls/17"}},"in_reply_to_id":236684200},"pull_request":{"url":"https://api.github.com/repos/Ensembl/ensembl-xref/pulls/17","id":233587782,"node_id":"MDExOlB1bGxSZXF1ZXN0MjMzNTg3Nzgy","html_url":"https://github.com/Ensembl/ensembl-xref/pull/17","diff_url":"https://github.com/Ensembl/ensembl-xref/pull/17.diff","patch_url":"https://github.com/Ensembl/ensembl-xref/pull/17.patch","issue_url":"https://api.github.com/repos/Ensembl/ensembl-xref/issues/17","number":17,"state":"open","locked":false,"title":"Feature/fetch_files cleanup","user":{"login":"nerdstrike","id":5434501,"node_id":"MDQ6VXNlcjU0MzQ1MDE=","avatar_url":"https://avatars3.githubusercontent.com/u/5434501?v=4","gravatar_id":"","url":"https://api.github.com/users/nerdstrike","html_url":"https://github.com/nerdstrike","followers_url":"https://api.github.com/users/nerdstrike/followers","following_url":"https://api.github.com/users/nerdstrike/following{/other_user}","gists_url":"https://api.github.com/users/nerdstrike/gists{/gist_id}","starred_url":"https://api.github.com/users/nerdstrike/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nerdstrike/subscriptions","organizations_url":"https://api.github.com/users/nerdstrike/orgs","repos_url":"https://api.github.com/users/nerdstrike/repos","events_url":"https://api.github.com/users/nerdstrike/events{/privacy}","received_events_url":"https://api.github.com/users/nerdstrike/received_events","type":"User","site_admin":false},"body":"## Description\r\n\r\nFix the horror that was FetchFiles, and provide a unit test for it. run_script-type parsers will benefit from these improvements. Update ArrayExpressParser to use the improvements. Remove database access code that the eHive pipeline makes redundant. Work on RFAM to follow built on these changes.\r\n\r\n## Use case\r\n\r\nFetchFiles module was horrible. run_script-type parsers were all replicating the same clumsy code to determine whether the current species is supported by the given source.\r\n\r\n## Benefits\r\n\r\nTestable now, more comments, cleaner run_script parsers, improved behaviour around HTTP retrieval, and tighter behaviour around \"checkdownload\".\r\n\r\n## Possible Drawbacks\r\n\r\nFTP tests on Travis are pretty flaky, so the fetchfiles.t needs to be enabled to make it run. checkdownload is now happening by default, and must be turned off.\r\n\r\n## Testing\r\n\r\nNew test for FetchFiles, changes to arrayexpress.t\r\n\r\nfetchfiles.t does not run on Travis by default due to network reliability issues\r\n","created_at":"2018-11-26T15:00:45Z","updated_at":"2018-12-05T10:16:04Z","closed_at":null,"merged_at":null,"merge_commit_sha":"4bb49f387b19f29fd05951b7cd8683dcab45213f","assignee":null,"assignees":[],"requested_reviewers":[{"login":"premanand17","id":10760269,"node_id":"MDQ6VXNlcjEwNzYwMjY5","avatar_url":"https://avatars1.githubusercontent.com/u/10760269?v=4","gravatar_id":"","url":"https://api.github.com/users/premanand17","html_url":"https://github.com/premanand17","followers_url":"https://api.github.com/users/premanand17/followers","following_url":"https://api.github.com/users/premanand17/following{/other_user}","gists_url":"https://api.github.com/users/premanand17/gists{/gist_id}","starred_url":"https://api.github.com/users/premanand17/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/premanand17/subscriptions","organizations_url":"https://api.github.com/users/premanand17/orgs","repos_url":"https://api.github.com/users/premanand17/repos","events_url":"https://api.github.com/users/premanand17/events{/privacy}","received_events_url":"https://api.github.com/users/premanand17/received_events","type":"User","site_admin":false}],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/Ensembl/ensembl-xref/pulls/17/commits","review_comments_url":"https://api.github.com/repos/Ensembl/ensembl-xref/pulls/17/comments","review_comment_url":"https://api.github.com/repos/Ensembl/ensembl-xref/pulls/comments{/number}","comments_url":"https://api.github.com/repos/Ensembl/ensembl-xref/issues/17/comments","statuses_url":"https://api.github.com/repos/Ensembl/ensembl-xref/statuses/33a0ace191a82219373f6ae0462bf4b558674338","head":{"label":"Ensembl:feature/rfam_parser","ref":"feature/rfam_parser","sha":"33a0ace191a82219373f6ae0462bf4b558674338","user":{"login":"Ensembl","id":5832463,"node_id":"MDEyOk9yZ2FuaXphdGlvbjU4MzI0NjM=","avatar_url":"https://avatars3.githubusercontent.com/u/5832463?v=4","gravatar_id":"","url":"https://api.github.com/users/Ensembl","html_url":"https://github.com/Ensembl","followers_url":"https://api.github.com/users/Ensembl/followers","following_url":"https://api.github.com/users/Ensembl/following{/other_user}","gists_url":"https://api.github.com/users/Ensembl/gists{/gist_id}","starred_url":"https://api.github.com/users/Ensembl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Ensembl/subscriptions","organizations_url":"https://api.github.com/users/Ensembl/orgs","repos_url":"https://api.github.com/users/Ensembl/repos","events_url":"https://api.github.com/users/Ensembl/events{/privacy}","received_events_url":"https://api.github.com/users/Ensembl/received_events","type":"Organization","site_admin":false},"repo":{"id":154531202,"node_id":"MDEwOlJlcG9zaXRvcnkxNTQ1MzEyMDI=","name":"ensembl-xref","full_name":"Ensembl/ensembl-xref","private":false,"owner":{"login":"Ensembl","id":5832463,"node_id":"MDEyOk9yZ2FuaXphdGlvbjU4MzI0NjM=","avatar_url":"https://avatars3.githubusercontent.com/u/5832463?v=4","gravatar_id":"","url":"https://api.github.com/users/Ensembl","html_url":"https://github.com/Ensembl","followers_url":"https://api.github.com/users/Ensembl/followers","following_url":"https://api.github.com/users/Ensembl/following{/other_user}","gists_url":"https://api.github.com/users/Ensembl/gists{/gist_id}","starred_url":"https://api.github.com/users/Ensembl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Ensembl/subscriptions","organizations_url":"https://api.github.com/users/Ensembl/orgs","repos_url":"https://api.github.com/users/Ensembl/repos","events_url":"https://api.github.com/users/Ensembl/events{/privacy}","received_events_url":"https://api.github.com/users/Ensembl/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/Ensembl/ensembl-xref","description":null,"fork":false,"url":"https://api.github.com/repos/Ensembl/ensembl-xref","forks_url":"https://api.github.com/repos/Ensembl/ensembl-xref/forks","keys_url":"https://api.github.com/repos/Ensembl/ensembl-xref/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Ensembl/ensembl-xref/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Ensembl/ensembl-xref/teams","hooks_url":"https://api.github.com/repos/Ensembl/ensembl-xref/hooks","issue_events_url":"https://api.github.com/repos/Ensembl/ensembl-xref/issues/events{/number}","events_url":"https://api.github.com/repos/Ensembl/ensembl-xref/events","assignees_url":"https://api.github.com/repos/Ensembl/ensembl-xref/assignees{/user}","branches_url":"https://api.github.com/repos/Ensembl/ensembl-xref/branches{/branch}","tags_url":"https://api.github.com/repos/Ensembl/ensembl-xref/tags","blobs_url":"https://api.github.com/repos/Ensembl/ensembl-xref/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Ensembl/ensembl-xref/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Ensembl/ensembl-xref/git/refs{/sha}","trees_url":"https://api.github.com/repos/Ensembl/ensembl-xref/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Ensembl/ensembl-xref/statuses/{sha}","languages_url":"https://api.github.com/repos/Ensembl/ensembl-xref/languages","stargazers_url":"https://api.github.com/repos/Ensembl/ensembl-xref/stargazers","contributors_url":"https://api.github.com/repos/Ensembl/ensembl-xref/contributors","subscribers_url":"https://api.github.com/repos/Ensembl/ensembl-xref/subscribers","subscription_url":"https://api.github.com/repos/Ensembl/ensembl-xref/subscription","commits_url":"https://api.github.com/repos/Ensembl/ensembl-xref/commits{/sha}","git_commits_url":"https://api.github.com/repos/Ensembl/ensembl-xref/git/commits{/sha}","comments_url":"https://api.github.com/repos/Ensembl/ensembl-xref/comments{/number}","issue_comment_url":"https://api.github.com/repos/Ensembl/ensembl-xref/issues/comments{/number}","contents_url":"https://api.github.com/repos/Ensembl/ensembl-xref/contents/{+path}","compare_url":"https://api.github.com/repos/Ensembl/ensembl-xref/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Ensembl/ensembl-xref/merges","archive_url":"https://api.github.com/repos/Ensembl/ensembl-xref/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Ensembl/ensembl-xref/downloads","issues_url":"https://api.github.com/repos/Ensembl/ensembl-xref/issues{/number}","pulls_url":"https://api.github.com/repos/Ensembl/ensembl-xref/pulls{/number}","milestones_url":"https://api.github.com/repos/Ensembl/ensembl-xref/milestones{/number}","notifications_url":"https://api.github.com/repos/Ensembl/ensembl-xref/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Ensembl/ensembl-xref/labels{/name}","releases_url":"https://api.github.com/repos/Ensembl/ensembl-xref/releases{/id}","deployments_url":"https://api.github.com/repos/Ensembl/ensembl-xref/deployments","created_at":"2018-10-24T16:12:02Z","updated_at":"2018-12-03T16:42:44Z","pushed_at":"2018-12-05T10:07:24Z","git_url":"git://github.com/Ensembl/ensembl-xref.git","ssh_url":"git@github.com:Ensembl/ensembl-xref.git","clone_url":"https://github.com/Ensembl/ensembl-xref.git","svn_url":"https://github.com/Ensembl/ensembl-xref","homepage":null,"size":1310,"stargazers_count":0,"watchers_count":0,"language":"Perl","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":4,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":0,"open_issues":4,"watchers":0,"default_branch":"master"}},"base":{"label":"Ensembl:master","ref":"master","sha":"5f734ab36482b1cc301a1a1bf55e54f7d6450252","user":{"login":"Ensembl","id":5832463,"node_id":"MDEyOk9yZ2FuaXphdGlvbjU4MzI0NjM=","avatar_url":"https://avatars3.githubusercontent.com/u/5832463?v=4","gravatar_id":"","url":"https://api.github.com/users/Ensembl","html_url":"https://github.com/Ensembl","followers_url":"https://api.github.com/users/Ensembl/followers","following_url":"https://api.github.com/users/Ensembl/following{/other_user}","gists_url":"https://api.github.com/users/Ensembl/gists{/gist_id}","starred_url":"https://api.github.com/users/Ensembl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Ensembl/subscriptions","organizations_url":"https://api.github.com/users/Ensembl/orgs","repos_url":"https://api.github.com/users/Ensembl/repos","events_url":"https://api.github.com/users/Ensembl/events{/privacy}","received_events_url":"https://api.github.com/users/Ensembl/received_events","type":"Organization","site_admin":false},"repo":{"id":154531202,"node_id":"MDEwOlJlcG9zaXRvcnkxNTQ1MzEyMDI=","name":"ensembl-xref","full_name":"Ensembl/ensembl-xref","private":false,"owner":{"login":"Ensembl","id":5832463,"node_id":"MDEyOk9yZ2FuaXphdGlvbjU4MzI0NjM=","avatar_url":"https://avatars3.githubusercontent.com/u/5832463?v=4","gravatar_id":"","url":"https://api.github.com/users/Ensembl","html_url":"https://github.com/Ensembl","followers_url":"https://api.github.com/users/Ensembl/followers","following_url":"https://api.github.com/users/Ensembl/following{/other_user}","gists_url":"https://api.github.com/users/Ensembl/gists{/gist_id}","starred_url":"https://api.github.com/users/Ensembl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Ensembl/subscriptions","organizations_url":"https://api.github.com/users/Ensembl/orgs","repos_url":"https://api.github.com/users/Ensembl/repos","events_url":"https://api.github.com/users/Ensembl/events{/privacy}","received_events_url":"https://api.github.com/users/Ensembl/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/Ensembl/ensembl-xref","description":null,"fork":false,"url":"https://api.github.com/repos/Ensembl/ensembl-xref","forks_url":"https://api.github.com/repos/Ensembl/ensembl-xref/forks","keys_url":"https://api.github.com/repos/Ensembl/ensembl-xref/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Ensembl/ensembl-xref/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Ensembl/ensembl-xref/teams","hooks_url":"https://api.github.com/repos/Ensembl/ensembl-xref/hooks","issue_events_url":"https://api.github.com/repos/Ensembl/ensembl-xref/issues/events{/number}","events_url":"https://api.github.com/repos/Ensembl/ensembl-xref/events","assignees_url":"https://api.github.com/repos/Ensembl/ensembl-xref/assignees{/user}","branches_url":"https://api.github.com/repos/Ensembl/ensembl-xref/branches{/branch}","tags_url":"https://api.github.com/repos/Ensembl/ensembl-xref/tags","blobs_url":"https://api.github.com/repos/Ensembl/ensembl-xref/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Ensembl/ensembl-xref/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Ensembl/ensembl-xref/git/refs{/sha}","trees_url":"https://api.github.com/repos/Ensembl/ensembl-xref/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Ensembl/ensembl-xref/statuses/{sha}","languages_url":"https://api.github.com/repos/Ensembl/ensembl-xref/languages","stargazers_url":"https://api.github.com/repos/Ensembl/ensembl-xref/stargazers","contributors_url":"https://api.github.com/repos/Ensembl/ensembl-xref/contributors","subscribers_url":"https://api.github.com/repos/Ensembl/ensembl-xref/subscribers","subscription_url":"https://api.github.com/repos/Ensembl/ensembl-xref/subscription","commits_url":"https://api.github.com/repos/Ensembl/ensembl-xref/commits{/sha}","git_commits_url":"https://api.github.com/repos/Ensembl/ensembl-xref/git/commits{/sha}","comments_url":"https://api.github.com/repos/Ensembl/ensembl-xref/comments{/number}","issue_comment_url":"https://api.github.com/repos/Ensembl/ensembl-xref/issues/comments{/number}","contents_url":"https://api.github.com/repos/Ensembl/ensembl-xref/contents/{+path}","compare_url":"https://api.github.com/repos/Ensembl/ensembl-xref/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Ensembl/ensembl-xref/merges","archive_url":"https://api.github.com/repos/Ensembl/ensembl-xref/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Ensembl/ensembl-xref/downloads","issues_url":"https://api.github.com/repos/Ensembl/ensembl-xref/issues{/number}","pulls_url":"https://api.github.com/repos/Ensembl/ensembl-xref/pulls{/number}","milestones_url":"https://api.github.com/repos/Ensembl/ensembl-xref/milestones{/number}","notifications_url":"https://api.github.com/repos/Ensembl/ensembl-xref/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Ensembl/ensembl-xref/labels{/name}","releases_url":"https://api.github.com/repos/Ensembl/ensembl-xref/releases{/id}","deployments_url":"https://api.github.com/repos/Ensembl/ensembl-xref/deployments","created_at":"2018-10-24T16:12:02Z","updated_at":"2018-12-03T16:42:44Z","pushed_at":"2018-12-05T10:07:24Z","git_url":"git://github.com/Ensembl/ensembl-xref.git","ssh_url":"git@github.com:Ensembl/ensembl-xref.git","clone_url":"https://github.com/Ensembl/ensembl-xref.git","svn_url":"https://github.com/Ensembl/ensembl-xref","homepage":null,"size":1310,"stargazers_count":0,"watchers_count":0,"language":"Perl","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":4,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":0,"open_issues":4,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/Ensembl/ensembl-xref/pulls/17"},"html":{"href":"https://github.com/Ensembl/ensembl-xref/pull/17"},"issue":{"href":"https://api.github.com/repos/Ensembl/ensembl-xref/issues/17"},"comments":{"href":"https://api.github.com/repos/Ensembl/ensembl-xref/issues/17/comments"},"review_comments":{"href":"https://api.github.com/repos/Ensembl/ensembl-xref/pulls/17/comments"},"review_comment":{"href":"https://api.github.com/repos/Ensembl/ensembl-xref/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/Ensembl/ensembl-xref/pulls/17/commits"},"statuses":{"href":"https://api.github.com/repos/Ensembl/ensembl-xref/statuses/33a0ace191a82219373f6ae0462bf4b558674338"}},"author_association":"MEMBER"}}
{"actor":{"display_login":"nerdstrike"}}