{"action":"created","comment":{"url":"https://api.github.com/repos/mlflow/mlflow/pulls/comments/213791429","pull_request_review_id":150701573,"id":213791429,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIxMzc5MTQyOQ==","diff_hunk":"@@ -0,0 +1,242 @@\n+package org.mlflow.artifacts;\n+\n+import java.io.File;\n+import java.io.IOException;\n+import java.lang.reflect.Type;\n+import java.nio.charset.StandardCharsets;\n+import java.util.ArrayList;\n+import java.util.List;\n+import java.util.Map;\n+import java.util.concurrent.atomic.AtomicBoolean;\n+\n+import com.google.common.collect.Lists;\n+import com.google.gson.Gson;\n+import com.google.gson.reflect.TypeToken;\n+import com.google.protobuf.InvalidProtocolBufferException;\n+import com.google.protobuf.util.JsonFormat;\n+import org.apache.commons.io.IOUtils;\n+import org.apache.log4j.Logger;\n+\n+import org.mlflow.api.proto.Service;\n+import org.mlflow.tracking.MlflowClientException;\n+import org.mlflow.tracking.creds.MlflowHostCredsProvider;\n+\n+/**\n+ * Shells out to the 'mlflow' command line utility to upload, download, and list artifacts. This\n+ * is used as a fallback to implement any artifact repositories which are not natively supported\n+ * within Java.\n+ *\n+ * We require that 'mlflow' is available in the system path.\n+ */\n+public class CliBasedArtifactRepository implements ArtifactRepository {\n+  private static final Logger logger = Logger.getLogger(CliBasedArtifactRepository.class);\n+\n+  // Global check if we ever successfully loaded 'mlflow'. This allows us to print a more\n+  // helpful error message if the executable is not in the path.\n+  private static final AtomicBoolean mlflowSuccessfullyLoaded = new AtomicBoolean(false);\n+\n+  // Name of the mlflow CLI utility which can be exec'd directly.\n+  private final String mlflowExecutable = \"mlflow\";\n+\n+  // Base directory of the artifactory, used to let the user know why this repository was chosen.\n+  private final String artifactBaseDir;\n+\n+  // Run ID this repository is targeting.\n+  private final String runId;\n+\n+  // Used to pass the MLFLOW_TRACKING_URI on to the mlflow process.\n+  private final MlflowHostCredsProvider hostCredsProvider;\n+\n+  public CliBasedArtifactRepository(\n+      String artifactBaseDir,\n+      String runId,\n+      MlflowHostCredsProvider hostCredsProvider) {\n+    this.artifactBaseDir = artifactBaseDir;\n+    this.runId = runId;\n+    this.hostCredsProvider = hostCredsProvider;\n+  }\n+\n+  @Override\n+  public void logArtifact(File localFile, String artifactPath) {\n+    checkMlflowAccessible();\n+    if (!localFile.exists()) {\n+      throw new MlflowClientException(\"Local file does not exist: \" + localFile);\n+    }\n+    if (localFile.isDirectory()) {\n+      throw new MlflowClientException(\"Local path points to a directory. Use logArtifacts\" +\n+        \" instead: \" + localFile);\n+    }\n+\n+    List<String> baseCommand = Lists.newArrayList(\n+      mlflowExecutable, \"artifacts\", \"log-artifact\", \"--local-file\", localFile.toString());\n+    List<String> command = appendRunIdArtifactPath(baseCommand, runId, artifactPath);\n+    String tag = \"log file \" + localFile + \" to \" + getTargetIdentifier(artifactPath);\n+    forkProcess(command, tag);\n+  }\n+\n+  @Override\n+  public void logArtifact(File localFile) {\n+    logArtifact(localFile, null);\n+  }\n+\n+  @Override\n+  public void logArtifacts(File localDir, String artifactPath) {\n+    checkMlflowAccessible();\n+    if (!localDir.exists()) {\n+      throw new MlflowClientException(\"Local file does not exist: \" + localDir);\n+    }\n+    if (localDir.isFile()) {\n+      throw new MlflowClientException(\"Local path points to a file. Use logArtifact\" +\n+        \" instead: \" + localDir);\n+    }\n+\n+    List<String> baseCommand = Lists.newArrayList(\n+      mlflowExecutable, \"artifacts\", \"log-artifacts\", \"--local-dir\", localDir.toString());\n+    List<String> command = appendRunIdArtifactPath(baseCommand, runId, artifactPath);\n+    String tag = \"log dir \" + localDir + \" to \" + getTargetIdentifier(artifactPath);\n+    forkProcess(command, tag);\n+  }\n+\n+  public void logArtifacts(File localDir) {\n+    logArtifacts(localDir, null);\n+  }\n+\n+  @Override\n+  public File downloadArtifacts(String artifactPath) {\n+    checkMlflowAccessible();\n+    String tag = \"download artifacts for \" + getTargetIdentifier(artifactPath);\n+    List<String> command = appendRunIdArtifactPath(\n+      Lists.newArrayList(mlflowExecutable, \"artifacts\", \"download\"), runId, artifactPath);\n+    String localPath = forkProcess(command, tag).trim();\n+    return new File(localPath);\n+  }\n+\n+  public File downloadArtifacts() {\n+    return downloadArtifacts(null);\n+  }\n+\n+  @Override\n+  public List<Service.FileInfo> listArtifacts(String artifactPath) {\n+    checkMlflowAccessible();\n+    String tag = \"list artifacts in \" + getTargetIdentifier(artifactPath);\n+    List<String> command = appendRunIdArtifactPath(\n+      Lists.newArrayList(mlflowExecutable, \"artifacts\", \"list\"), runId, artifactPath);\n+    String jsonOutput = forkProcess(command, tag);\n+    return parseFileInfos(jsonOutput);\n+  }\n+\n+  public List<Service.FileInfo> listArtifacts() {\n+    return listArtifacts(null);\n+  }\n+\n+  /** Parses a list of JSON FileInfos, as returned by 'mlflow artifacts list'. */\n+  private List<Service.FileInfo> parseFileInfos(String json) {\n+    // The protobuf deserializer doesn't allow us to directly deserialize a list, so we\n+    // deserialize a list-of-dictionaries, and then reserialize each dictionary to pass it to\n+    // the protobuf deserializer.\n+    Gson gson = new Gson();\n+    Type type = new TypeToken<List<Map<String, Object>>>(){}.getType();\n+    List<Map<String, Object>> listOfDicts = gson.fromJson(json, type);\n+    List<Service.FileInfo> fileInfos = new ArrayList<>();\n+    for (Map<String, Object> dict: listOfDicts) {\n+      String fileInfoJson = gson.toJson(dict);\n+      try {\n+        Service.FileInfo.Builder builder = Service.FileInfo.newBuilder();\n+        JsonFormat.parser().merge(fileInfoJson, builder);\n+        fileInfos.add(builder.build());\n+      } catch (InvalidProtocolBufferException e) {\n+        throw new MlflowClientException(\"Failed to deserialize JSON into FileInfo: \" + json, e);\n+      }\n+    }\n+    return fileInfos;\n+  }\n+\n+  /**\n+   * Checks whether the 'mlflow' executable is available, and throws a nice error if not.\n+   * If this method has ever run successfully before (in the entire JVM), we will not rerun it.\n+   */\n+  private void checkMlflowAccessible() {\n+    if (mlflowSuccessfullyLoaded.get()) {\n+      return;\n+    }\n+\n+    try {\n+      String tag = \"get mlflow version\";\n+      String mlflowVersion = forkProcess(Lists.newArrayList(mlflowExecutable, \"--version\"), tag);\n+      logger.info(\"Found local mlflow executable with version=\" + mlflowVersion);\n+      mlflowSuccessfullyLoaded.set(true);\n+    } catch (MlflowClientException e) {\n+      String errorMessage = String.format(\"Failed to exec process %s, needed to access artifacts \" +\n+          \"within the non-Java-native artifact store at '%s'. Please make sure mlflow is \" +\n+          \"available on your local system path (e.g.,\" + \"from 'pip install mlflow')\",\n+        mlflowExecutable, artifactBaseDir);\n+      throw new MlflowClientException(errorMessage, e);\n+    }\n+  }\n+\n+  /**\n+   * Forks the given mlflow command and awaits for its successful completion.\n+   *\n+   * @param command Command used to fork the process.\n+   * @param tag User-facing tag which will be used to identify what we were trying to do\n+   *            in the case of a failure.\n+   * @return raw stdout of the process, decoded as a utf-8 string\n+   * @throws MlflowClientException if the process exits with a non-zero exit code, or anything\n+   *                               else goes wrong.\n+   */\n+  private String forkProcess(List<String> command, String tag) {\n+    String stdout;\n+    Process process = null;\n+    try {\n+      ProcessBuilder pb = new ProcessBuilder(command);\n+      // TODO(aaron) Figure out a way to pass the other fields of the host-creds.\n+      pb.environment().put(\"MLFLOW_TRACKING_URI\", hostCredsProvider.getHostCreds().getHost());\n+      process = pb.start();\n+      stdout = IOUtils.toString(process.getInputStream(), StandardCharsets.UTF_8);\n+      int exitValue = process.waitFor();\n+      if (exitValue != 0) {\n+        throw new MlflowClientException(\"Failed to \" + tag + \". Error: \" +","path":"mlflow/java/client/src/main/java/org/mlflow/artifacts/CliBasedArtifactRepository.java","position":201,"original_position":198,"commit_id":"13da06f0a0a3505bf96c122d9582b34932ced304","original_commit_id":"b27e729652d3efefc50c268cbe86b8de6f568d69","user":{"login":"mparkhe","id":6900999,"node_id":"MDQ6VXNlcjY5MDA5OTk=","avatar_url":"https://avatars0.githubusercontent.com/u/6900999?v=4","gravatar_id":"","url":"https://api.github.com/users/mparkhe","html_url":"https://github.com/mparkhe","followers_url":"https://api.github.com/users/mparkhe/followers","following_url":"https://api.github.com/users/mparkhe/following{/other_user}","gists_url":"https://api.github.com/users/mparkhe/gists{/gist_id}","starred_url":"https://api.github.com/users/mparkhe/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mparkhe/subscriptions","organizations_url":"https://api.github.com/users/mparkhe/orgs","repos_url":"https://api.github.com/users/mparkhe/repos","events_url":"https://api.github.com/users/mparkhe/events{/privacy}","received_events_url":"https://api.github.com/users/mparkhe/received_events","type":"User","site_admin":false},"body":"makes sense","created_at":"2018-08-29T18:42:06Z","updated_at":"2018-08-29T18:43:12Z","html_url":"https://github.com/mlflow/mlflow/pull/394#discussion_r213791429","pull_request_url":"https://api.github.com/repos/mlflow/mlflow/pulls/394","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/mlflow/mlflow/pulls/comments/213791429"},"html":{"href":"https://github.com/mlflow/mlflow/pull/394#discussion_r213791429"},"pull_request":{"href":"https://api.github.com/repos/mlflow/mlflow/pulls/394"}},"in_reply_to_id":213466098},"pull_request":{"url":"https://api.github.com/repos/mlflow/mlflow/pulls/394","id":211467416,"node_id":"MDExOlB1bGxSZXF1ZXN0MjExNDY3NDE2","html_url":"https://github.com/mlflow/mlflow/pull/394","diff_url":"https://github.com/mlflow/mlflow/pull/394.diff","patch_url":"https://github.com/mlflow/mlflow/pull/394.patch","issue_url":"https://api.github.com/repos/mlflow/mlflow/issues/394","number":394,"state":"open","locked":false,"title":"Add a CLI-based fallback ArtifactRepository in Java","user":{"login":"aarondav","id":1400247,"node_id":"MDQ6VXNlcjE0MDAyNDc=","avatar_url":"https://avatars2.githubusercontent.com/u/1400247?v=4","gravatar_id":"","url":"https://api.github.com/users/aarondav","html_url":"https://github.com/aarondav","followers_url":"https://api.github.com/users/aarondav/followers","following_url":"https://api.github.com/users/aarondav/following{/other_user}","gists_url":"https://api.github.com/users/aarondav/gists{/gist_id}","starred_url":"https://api.github.com/users/aarondav/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/aarondav/subscriptions","organizations_url":"https://api.github.com/users/aarondav/orgs","repos_url":"https://api.github.com/users/aarondav/repos","events_url":"https://api.github.com/users/aarondav/events{/privacy}","received_events_url":"https://api.github.com/users/aarondav/received_events","type":"User","site_admin":false},"body":"Adds a default ArtifactRepository in Java which allows use of arbitrary storage baceknds as long as the `mlflow` CLI is available on the local PATH. This is intended as a fallback mechanism to access backends which do not have native Java implementations (which is currently all backends).\r\n\r\nThis relies on #391 to be available locally, which also discusses the shortcomings of this approach.\r\n\r\nThis PR introduces the notion of a Java ArtifactRepository, but does not make it super convenient for a user to construct this (i.e., no layer of indirection given a Run) -- this PR is mostly intended for the review of the CLI-based ArtifactRepository.","created_at":"2018-08-28T15:38:00Z","updated_at":"2018-08-29T18:43:12Z","closed_at":null,"merged_at":null,"merge_commit_sha":"cb716333a9c4f7f5000d8ef3737a935abf546d4a","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/mlflow/mlflow/pulls/394/commits","review_comments_url":"https://api.github.com/repos/mlflow/mlflow/pulls/394/comments","review_comment_url":"https://api.github.com/repos/mlflow/mlflow/pulls/comments{/number}","comments_url":"https://api.github.com/repos/mlflow/mlflow/issues/394/comments","statuses_url":"https://api.github.com/repos/mlflow/mlflow/statuses/13da06f0a0a3505bf96c122d9582b34932ced304","head":{"label":"aarondav:java-remote-artifactory","ref":"java-remote-artifactory","sha":"13da06f0a0a3505bf96c122d9582b34932ced304","user":{"login":"aarondav","id":1400247,"node_id":"MDQ6VXNlcjE0MDAyNDc=","avatar_url":"https://avatars2.githubusercontent.com/u/1400247?v=4","gravatar_id":"","url":"https://api.github.com/users/aarondav","html_url":"https://github.com/aarondav","followers_url":"https://api.github.com/users/aarondav/followers","following_url":"https://api.github.com/users/aarondav/following{/other_user}","gists_url":"https://api.github.com/users/aarondav/gists{/gist_id}","starred_url":"https://api.github.com/users/aarondav/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/aarondav/subscriptions","organizations_url":"https://api.github.com/users/aarondav/orgs","repos_url":"https://api.github.com/users/aarondav/repos","events_url":"https://api.github.com/users/aarondav/events{/privacy}","received_events_url":"https://api.github.com/users/aarondav/received_events","type":"User","site_admin":false},"repo":{"id":138810489,"node_id":"MDEwOlJlcG9zaXRvcnkxMzg4MTA0ODk=","name":"mlflow","full_name":"aarondav/mlflow","owner":{"login":"aarondav","id":1400247,"node_id":"MDQ6VXNlcjE0MDAyNDc=","avatar_url":"https://avatars2.githubusercontent.com/u/1400247?v=4","gravatar_id":"","url":"https://api.github.com/users/aarondav","html_url":"https://github.com/aarondav","followers_url":"https://api.github.com/users/aarondav/followers","following_url":"https://api.github.com/users/aarondav/following{/other_user}","gists_url":"https://api.github.com/users/aarondav/gists{/gist_id}","starred_url":"https://api.github.com/users/aarondav/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/aarondav/subscriptions","organizations_url":"https://api.github.com/users/aarondav/orgs","repos_url":"https://api.github.com/users/aarondav/repos","events_url":"https://api.github.com/users/aarondav/events{/privacy}","received_events_url":"https://api.github.com/users/aarondav/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/aarondav/mlflow","description":"Open source platform for the complete machine learning lifecycle","fork":true,"url":"https://api.github.com/repos/aarondav/mlflow","forks_url":"https://api.github.com/repos/aarondav/mlflow/forks","keys_url":"https://api.github.com/repos/aarondav/mlflow/keys{/key_id}","collaborators_url":"https://api.github.com/repos/aarondav/mlflow/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/aarondav/mlflow/teams","hooks_url":"https://api.github.com/repos/aarondav/mlflow/hooks","issue_events_url":"https://api.github.com/repos/aarondav/mlflow/issues/events{/number}","events_url":"https://api.github.com/repos/aarondav/mlflow/events","assignees_url":"https://api.github.com/repos/aarondav/mlflow/assignees{/user}","branches_url":"https://api.github.com/repos/aarondav/mlflow/branches{/branch}","tags_url":"https://api.github.com/repos/aarondav/mlflow/tags","blobs_url":"https://api.github.com/repos/aarondav/mlflow/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/aarondav/mlflow/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/aarondav/mlflow/git/refs{/sha}","trees_url":"https://api.github.com/repos/aarondav/mlflow/git/trees{/sha}","statuses_url":"https://api.github.com/repos/aarondav/mlflow/statuses/{sha}","languages_url":"https://api.github.com/repos/aarondav/mlflow/languages","stargazers_url":"https://api.github.com/repos/aarondav/mlflow/stargazers","contributors_url":"https://api.github.com/repos/aarondav/mlflow/contributors","subscribers_url":"https://api.github.com/repos/aarondav/mlflow/subscribers","subscription_url":"https://api.github.com/repos/aarondav/mlflow/subscription","commits_url":"https://api.github.com/repos/aarondav/mlflow/commits{/sha}","git_commits_url":"https://api.github.com/repos/aarondav/mlflow/git/commits{/sha}","comments_url":"https://api.github.com/repos/aarondav/mlflow/comments{/number}","issue_comment_url":"https://api.github.com/repos/aarondav/mlflow/issues/comments{/number}","contents_url":"https://api.github.com/repos/aarondav/mlflow/contents/{+path}","compare_url":"https://api.github.com/repos/aarondav/mlflow/compare/{base}...{head}","merges_url":"https://api.github.com/repos/aarondav/mlflow/merges","archive_url":"https://api.github.com/repos/aarondav/mlflow/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/aarondav/mlflow/downloads","issues_url":"https://api.github.com/repos/aarondav/mlflow/issues{/number}","pulls_url":"https://api.github.com/repos/aarondav/mlflow/pulls{/number}","milestones_url":"https://api.github.com/repos/aarondav/mlflow/milestones{/number}","notifications_url":"https://api.github.com/repos/aarondav/mlflow/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/aarondav/mlflow/labels{/name}","releases_url":"https://api.github.com/repos/aarondav/mlflow/releases{/id}","deployments_url":"https://api.github.com/repos/aarondav/mlflow/deployments","created_at":"2018-06-27T00:53:01Z","updated_at":"2018-07-30T20:18:55Z","pushed_at":"2018-08-29T18:27:41Z","git_url":"git://github.com/aarondav/mlflow.git","ssh_url":"git@github.com:aarondav/mlflow.git","clone_url":"https://github.com/aarondav/mlflow.git","svn_url":"https://github.com/aarondav/mlflow","homepage":"https://mlflow.org","size":5360,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"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":0,"watchers":0,"default_branch":"master"}},"base":{"label":"mlflow:master","ref":"master","sha":"643ab70ef274b7dbab064be49dde3a31958cbf1e","user":{"login":"mlflow","id":39938107,"node_id":"MDEyOk9yZ2FuaXphdGlvbjM5OTM4MTA3","avatar_url":"https://avatars3.githubusercontent.com/u/39938107?v=4","gravatar_id":"","url":"https://api.github.com/users/mlflow","html_url":"https://github.com/mlflow","followers_url":"https://api.github.com/users/mlflow/followers","following_url":"https://api.github.com/users/mlflow/following{/other_user}","gists_url":"https://api.github.com/users/mlflow/gists{/gist_id}","starred_url":"https://api.github.com/users/mlflow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mlflow/subscriptions","organizations_url":"https://api.github.com/users/mlflow/orgs","repos_url":"https://api.github.com/users/mlflow/repos","events_url":"https://api.github.com/users/mlflow/events{/privacy}","received_events_url":"https://api.github.com/users/mlflow/received_events","type":"Organization","site_admin":false},"repo":{"id":136202695,"node_id":"MDEwOlJlcG9zaXRvcnkxMzYyMDI2OTU=","name":"mlflow","full_name":"mlflow/mlflow","owner":{"login":"mlflow","id":39938107,"node_id":"MDEyOk9yZ2FuaXphdGlvbjM5OTM4MTA3","avatar_url":"https://avatars3.githubusercontent.com/u/39938107?v=4","gravatar_id":"","url":"https://api.github.com/users/mlflow","html_url":"https://github.com/mlflow","followers_url":"https://api.github.com/users/mlflow/followers","following_url":"https://api.github.com/users/mlflow/following{/other_user}","gists_url":"https://api.github.com/users/mlflow/gists{/gist_id}","starred_url":"https://api.github.com/users/mlflow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mlflow/subscriptions","organizations_url":"https://api.github.com/users/mlflow/orgs","repos_url":"https://api.github.com/users/mlflow/repos","events_url":"https://api.github.com/users/mlflow/events{/privacy}","received_events_url":"https://api.github.com/users/mlflow/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/mlflow/mlflow","description":"Open source platform for the machine learning lifecycle","fork":false,"url":"https://api.github.com/repos/mlflow/mlflow","forks_url":"https://api.github.com/repos/mlflow/mlflow/forks","keys_url":"https://api.github.com/repos/mlflow/mlflow/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mlflow/mlflow/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mlflow/mlflow/teams","hooks_url":"https://api.github.com/repos/mlflow/mlflow/hooks","issue_events_url":"https://api.github.com/repos/mlflow/mlflow/issues/events{/number}","events_url":"https://api.github.com/repos/mlflow/mlflow/events","assignees_url":"https://api.github.com/repos/mlflow/mlflow/assignees{/user}","branches_url":"https://api.github.com/repos/mlflow/mlflow/branches{/branch}","tags_url":"https://api.github.com/repos/mlflow/mlflow/tags","blobs_url":"https://api.github.com/repos/mlflow/mlflow/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mlflow/mlflow/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mlflow/mlflow/git/refs{/sha}","trees_url":"https://api.github.com/repos/mlflow/mlflow/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mlflow/mlflow/statuses/{sha}","languages_url":"https://api.github.com/repos/mlflow/mlflow/languages","stargazers_url":"https://api.github.com/repos/mlflow/mlflow/stargazers","contributors_url":"https://api.github.com/repos/mlflow/mlflow/contributors","subscribers_url":"https://api.github.com/repos/mlflow/mlflow/subscribers","subscription_url":"https://api.github.com/repos/mlflow/mlflow/subscription","commits_url":"https://api.github.com/repos/mlflow/mlflow/commits{/sha}","git_commits_url":"https://api.github.com/repos/mlflow/mlflow/git/commits{/sha}","comments_url":"https://api.github.com/repos/mlflow/mlflow/comments{/number}","issue_comment_url":"https://api.github.com/repos/mlflow/mlflow/issues/comments{/number}","contents_url":"https://api.github.com/repos/mlflow/mlflow/contents/{+path}","compare_url":"https://api.github.com/repos/mlflow/mlflow/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mlflow/mlflow/merges","archive_url":"https://api.github.com/repos/mlflow/mlflow/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mlflow/mlflow/downloads","issues_url":"https://api.github.com/repos/mlflow/mlflow/issues{/number}","pulls_url":"https://api.github.com/repos/mlflow/mlflow/pulls{/number}","milestones_url":"https://api.github.com/repos/mlflow/mlflow/milestones{/number}","notifications_url":"https://api.github.com/repos/mlflow/mlflow/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mlflow/mlflow/labels{/name}","releases_url":"https://api.github.com/repos/mlflow/mlflow/releases{/id}","deployments_url":"https://api.github.com/repos/mlflow/mlflow/deployments","created_at":"2018-06-05T16:05:58Z","updated_at":"2018-08-29T18:39:05Z","pushed_at":"2018-08-29T18:39:01Z","git_url":"git://github.com/mlflow/mlflow.git","ssh_url":"git@github.com:mlflow/mlflow.git","clone_url":"https://github.com/mlflow/mlflow.git","svn_url":"https://github.com/mlflow/mlflow","homepage":"https://mlflow.org","size":4448,"stargazers_count":2060,"watchers_count":2060,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":315,"mirror_url":null,"archived":false,"open_issues_count":86,"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":315,"open_issues":86,"watchers":2060,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/mlflow/mlflow/pulls/394"},"html":{"href":"https://github.com/mlflow/mlflow/pull/394"},"issue":{"href":"https://api.github.com/repos/mlflow/mlflow/issues/394"},"comments":{"href":"https://api.github.com/repos/mlflow/mlflow/issues/394/comments"},"review_comments":{"href":"https://api.github.com/repos/mlflow/mlflow/pulls/394/comments"},"review_comment":{"href":"https://api.github.com/repos/mlflow/mlflow/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/mlflow/mlflow/pulls/394/commits"},"statuses":{"href":"https://api.github.com/repos/mlflow/mlflow/statuses/13da06f0a0a3505bf96c122d9582b34932ced304"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"mparkhe"}}