{"action":"created","comment":{"url":"https://api.github.com/repos/amplab/keystone/pulls/comments/32888192","id":32888192,"diff_hunk":"@@ -0,0 +1,197 @@\n+package workflow\n+\n+import org.apache.spark.rdd.RDD\n+import pipelines.Logging\n+\n+trait Pipeline[A, B] {\n+  private[workflow] val nodes: Seq[Node]\n+  private[workflow] val dataDeps: Seq[Seq[Int]]\n+  private[workflow] val fitDeps: Seq[Seq[Int]]\n+  private[workflow] val sink: Int\n+\n+  /** validates (returns an exception if false) that\n+    - nodes.size = dataDeps.size == fitDeps.size\n+\n+    - there is a valid sink\n+    - data nodes must have no deps\n+    - estimators may not have fit deps, must have data deps\n+    - transformers must have data deps, allowed to have fit deps\n+\n+    - data deps may not point at estimators\n+    - fit deps may only point to estimators\n+    */\n+  private[workflow] def validate(): Unit = {\n+    require(nodes.size == dataDeps.size && nodes.size == fitDeps.size, \"nodes.size must equal dataDeps.size and fitDeps.size\")\n+    require(sink == Pipeline.SOURCE || (sink >= 0 && sink < nodes.size), \"Sink must point at a valid node\")\n+\n+    val nodeTuples = nodes.zip(dataDeps).zip(fitDeps).map(x => (x._1._1, x._1._2, x._2))\n+    require(nodeTuples.forall(x => if (x._1.isInstanceOf[DataNode] && (x._2.nonEmpty || x._3.nonEmpty)) false else true), \"DataNodes may not have dependencies\")\n+    require(nodeTuples.forall(x => if (x._1.isInstanceOf[EstimatorNode] && x._3.nonEmpty) false else true), \"Estimators may not have fit dependencies\")\n+    require(nodeTuples.forall(x => if (x._1.isInstanceOf[EstimatorNode] && x._2.isEmpty) false else true), \"Estimators must have data dependencies\")\n+    require(nodeTuples.forall(x => if (x._1.isInstanceOf[TransformerNode[_]] && x._2.isEmpty) false else true), \"Transformers must have data dependencies\")\n+\n+    require(dataDeps.forall(_.forall(x => !nodes(x).isInstanceOf[EstimatorNode])), \"Data dependencies may not point at Estimators\")\n+    require(fitDeps.forall(_.forall(x => nodes(x).isInstanceOf[EstimatorNode])), \"Fit dependencies may only point at Estimators\")\n+\n+    /*TODO: Validate that\n+    - there is a data path from sink to in\n+    - there are no fit paths from sink to in\n+    */\n+  }\n+\n+  def apply(in: A): B\n+\n+  def apply(in: RDD[A]): RDD[B]\n+\n+  final def andThen[C](next: Pipeline[B, C]): Pipeline[A, C] = {\n+    val nodes = this.nodes ++ next.nodes\n+    val dataDeps = this.dataDeps ++ next.dataDeps.map(_.map {\n+      x => if (x == Pipeline.SOURCE) this.sink else x + this.nodes.size\n+    })\n+    val fitDeps = this.fitDeps ++ next.fitDeps.map(_.map {\n+      x => if (x == Pipeline.SOURCE) this.sink else x + this.nodes.size\n+    })\n+    val sink = next.sink + this.nodes.size\n+\n+    Pipeline(nodes, dataDeps, fitDeps, sink)\n+  }\n+\n+  final def andThen[C](est: Estimator[B, C], data: RDD[A]): PipelineWithFittedTransformer[A, B, C] = {\n+    val transformerLabel = est.label + \".fit\"\n+\n+    val newNodes = this.nodes :+ est :+ DataNode(data) :+ new DelegatingTransformer[C](transformerLabel)\n+    val newDataDeps = this.dataDeps.map(_.map {\n+      x => if (x == Pipeline.SOURCE) this.nodes.size + 1 else x\n+    }) :+ Seq(this.sink) :+ Seq() :+ Seq(Pipeline.SOURCE)\n+    val newFitDeps = this.fitDeps :+ Seq() :+ Seq() :+ Seq(this.nodes.size)\n+    val newSink = newNodes.size - 1\n+\n+    val fittedTransformer = Pipeline[B, C](newNodes, newDataDeps, newFitDeps, newSink)\n+    val totalOut = this andThen fittedTransformer\n+    new PipelineWithFittedTransformer(totalOut.nodes, totalOut.dataDeps, totalOut.fitDeps, totalOut.sink, fittedTransformer)\n+  }\n+\n+  final def andThen[C, L](est: LabelEstimator[B, C, L], data: RDD[A], labels: RDD[L]): PipelineWithFittedTransformer[A, B, C] = {\n+    val transformerLabel = est.label + \".fit\"\n+\n+    val newNodes = this.nodes :+ est :+ DataNode(data) :+ DataNode(labels) :+ new DelegatingTransformer[C](transformerLabel)\n+    val newDataDeps = this.dataDeps.map(_.map {\n+      x => if (x == Pipeline.SOURCE) this.nodes.size + 1 else x\n+    }) :+ Seq(this.sink, this.nodes.size + 2) :+ Seq() :+ Seq() :+ Seq(Pipeline.SOURCE)\n+    val newFitDeps = this.fitDeps :+ Seq() :+ Seq() :+ Seq() :+ Seq(this.nodes.size)\n+    val newSink = newNodes.size - 1\n+\n+    val fittedTransformer = Pipeline[B, C](newNodes, newDataDeps, newFitDeps, newSink)\n+    val totalOut = this andThen fittedTransformer\n+    new PipelineWithFittedTransformer(totalOut.nodes, totalOut.dataDeps, totalOut.fitDeps, totalOut.sink, fittedTransformer)\n+  }\n+\n+\n+  final def toDOTString: String = {\n+    val nodeLabels: Seq[String] = \"-1 [label=\\\"In\\\" shape=\\\"Msquare\\\"]\" +: nodes.zipWithIndex.map {\n+      case (data: DataNode, id)  => s\"$id [label=${'\"' + data.label + '\"'} shape=${\"\\\"box\\\"\"} style=${\"\\\"filled\\\"\"}]\"\n+      case (transformer: TransformerNode[_], id) => s\"$id [label=${'\"' + transformer.label + '\"'}]\"\n+      case (estimator: EstimatorNode, id) => s\"$id [label=${'\"' + estimator.label + '\"'} shape=${\"\\\"box\\\"\"}]\"\n+    } :+ s\"${nodes.size} [label=${\"\\\"Out\\\"\"} shape=${\"\\\"Msquare\\\"\"}]\"\n+\n+    val dataEdges: Seq[String] = dataDeps.zipWithIndex.flatMap {\n+      case (deps, id) => deps.map(x => s\"$x -> $id\")\n+    } :+ s\"$sink -> ${nodes.size}\"\n+\n+    val fitEdges: Seq[String] = fitDeps.zipWithIndex.flatMap {\n+      case (deps, id) => deps.map(x => s\"$x -> $id [dir=${\"\\\"none\\\"\"} style=${\"\\\"dashed\\\"\"}]\")\n+    }\n+\n+    val ranks = fitDeps.zipWithIndex.flatMap {\n+      case (deps, id) => deps.map(x => s\"{rank=same; $x $id}\")\n+    }\n+\n+    val lines = nodeLabels ++ dataEdges ++ fitEdges ++ ranks\n+    lines.mkString(\"digraph pipeline {\\n  rankdir=LR;\\n  \", \"\\n  \", \"\\n}\")\n+  }\n+\n+  final private[workflow] def planEquals(pipeline: Pipeline[A, B]): Boolean = {\n+    this.eq(pipeline) || (\n+        (nodes == pipeline.nodes) &&\n+        (dataDeps == pipeline.dataDeps) &&\n+        (fitDeps == pipeline.fitDeps) &&\n+        (sink == pipeline.sink))\n+  }\n+}\n+\n+object Pipeline {\n+  val SOURCE: Int = -1\n+\n+  def apply[T](): Pipeline[T, T] = new ConcretePipeline(Seq(), Seq(), Seq(), SOURCE)\n+  private[workflow] def apply[A, B](nodes: Seq[Node], dataDeps: Seq[Seq[Int]], fitDeps: Seq[Seq[Int]], sink: Int): Pipeline[A, B] = new ConcretePipeline(nodes, dataDeps, fitDeps, sink)\n+}\n+\n+private[workflow] class ConcretePipeline[A, B](\n+  private[workflow] override val nodes: Seq[Node],\n+  private[workflow] override val dataDeps: Seq[Seq[Int]],\n+  private[workflow] override val fitDeps: Seq[Seq[Int]],\n+  private[workflow] override val sink: Int) extends Pipeline[A, B] with Logging {\n+\n+  private val fitCache: Array[Option[TransformerNode[_]]] = nodes.map(_ => None).toArray\n+\n+  private def fitEstimator(node: Int): TransformerNode[_] = fitCache(node).getOrElse {\n+    nodes(node) match {\n+      case _: DataNode =>\n+        throw new RuntimeException(\"Pipeline DAG error: Cannot have a fit dependency on a DataNode\")\n+      case _: TransformerNode[_] =>\n+        throw new RuntimeException(\"Pipeline DAG error: Cannot have a data dependency on a Transformer\")\n+      case estimator: EstimatorNode =>\n+        val nodeDataDeps = dataDeps(node).map(x => rddDataEval(x, null))\n+        logInfo(s\"Fitting '${estimator.label}' [$node]\")\n+        val fitOut = estimator.fit(nodeDataDeps)\n+        fitCache(node) = Some(fitOut)\n+        logInfo(s\"Finished fitting '${estimator.label}' [$node]\")\n+        fitOut\n+    }\n+  }\n+\n+  private def singleDataEval(node: Int, in: A): Any = {\n+    if (node == Pipeline.SOURCE) {\n+      in\n+    } else {\n+      nodes(node) match {\n+        case transformer: TransformerNode[_] =>\n+          val nodeFitDeps = fitDeps(node).map(fitEstimator)\n+          val nodeDataDeps = dataDeps(node).map(x => singleDataEval(x, in))\n+          transformer.transform(nodeDataDeps, nodeFitDeps)\n+        case _: DataNode =>\n+          throw new RuntimeException(\"Pipeline DAG error: came across an RDD data dependency when trying to do a single item apply\")\n+        case _: EstimatorNode =>\n+          throw new RuntimeException(\"Pipeline DAG error: Cannot have a data dependency on an Estimator\")\n+      }\n+    }\n+  }\n+\n+  private def rddDataEval(node: Int, in: RDD[A]): RDD[_] = {","path":"src/main/scala/workflow/Pipeline.scala","position":170,"original_position":170,"commit_id":"af2f9ec0550d0a442cbcd3c268585bc348cab6c2","original_commit_id":"af2f9ec0550d0a442cbcd3c268585bc348cab6c2","user":{"login":"etrain","id":1326181,"avatar_url":"https://avatars.githubusercontent.com/u/1326181?v=3","gravatar_id":"","url":"https://api.github.com/users/etrain","html_url":"https://github.com/etrain","followers_url":"https://api.github.com/users/etrain/followers","following_url":"https://api.github.com/users/etrain/following{/other_user}","gists_url":"https://api.github.com/users/etrain/gists{/gist_id}","starred_url":"https://api.github.com/users/etrain/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/etrain/subscriptions","organizations_url":"https://api.github.com/users/etrain/orgs","repos_url":"https://api.github.com/users/etrain/repos","events_url":"https://api.github.com/users/etrain/events{/privacy}","received_events_url":"https://api.github.com/users/etrain/received_events","type":"User","site_admin":false},"body":"I'm planning to use this (and possibly its non-RDD brother) in my caching stuff. Any good reason not to mark it private[workflow]?","created_at":"2015-06-20T21:22:41Z","updated_at":"2015-06-20T21:22:41Z","html_url":"https://github.com/amplab/keystone/pull/146#discussion_r32888192","pull_request_url":"https://api.github.com/repos/amplab/keystone/pulls/146","_links":{"self":{"href":"https://api.github.com/repos/amplab/keystone/pulls/comments/32888192"},"html":{"href":"https://github.com/amplab/keystone/pull/146#discussion_r32888192"},"pull_request":{"href":"https://api.github.com/repos/amplab/keystone/pulls/146"}}},"pull_request":{"url":"https://api.github.com/repos/amplab/keystone/pulls/146","id":37636025,"html_url":"https://github.com/amplab/keystone/pull/146","diff_url":"https://github.com/amplab/keystone/pull/146.diff","patch_url":"https://github.com/amplab/keystone/pull/146.patch","issue_url":"https://api.github.com/repos/amplab/keystone/issues/146","number":146,"state":"open","locked":false,"title":"Dag construction v2","user":{"login":"tomerk","id":676357,"avatar_url":"https://avatars.githubusercontent.com/u/676357?v=3","gravatar_id":"","url":"https://api.github.com/users/tomerk","html_url":"https://github.com/tomerk","followers_url":"https://api.github.com/users/tomerk/followers","following_url":"https://api.github.com/users/tomerk/following{/other_user}","gists_url":"https://api.github.com/users/tomerk/gists{/gist_id}","starred_url":"https://api.github.com/users/tomerk/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tomerk/subscriptions","organizations_url":"https://api.github.com/users/tomerk/orgs","repos_url":"https://api.github.com/users/tomerk/repos","events_url":"https://api.github.com/users/tomerk/events{/privacy}","received_events_url":"https://api.github.com/users/tomerk/received_events","type":"User","site_admin":false},"body":"@etrain @shivaram Not yet ready for merging (and plenty of cleanup to be done), but feel free to start taking a look","created_at":"2015-06-14T04:25:48Z","updated_at":"2015-06-20T21:22:41Z","closed_at":null,"merged_at":null,"merge_commit_sha":"62b2291261425aaf19702d422fb4f74c7c8a35a8","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/amplab/keystone/pulls/146/commits","review_comments_url":"https://api.github.com/repos/amplab/keystone/pulls/146/comments","review_comment_url":"https://api.github.com/repos/amplab/keystone/pulls/comments{/number}","comments_url":"https://api.github.com/repos/amplab/keystone/issues/146/comments","statuses_url":"https://api.github.com/repos/amplab/keystone/statuses/af2f9ec0550d0a442cbcd3c268585bc348cab6c2","head":{"label":"tomerk:dag-construction-v2","ref":"dag-construction-v2","sha":"af2f9ec0550d0a442cbcd3c268585bc348cab6c2","user":{"login":"tomerk","id":676357,"avatar_url":"https://avatars.githubusercontent.com/u/676357?v=3","gravatar_id":"","url":"https://api.github.com/users/tomerk","html_url":"https://github.com/tomerk","followers_url":"https://api.github.com/users/tomerk/followers","following_url":"https://api.github.com/users/tomerk/following{/other_user}","gists_url":"https://api.github.com/users/tomerk/gists{/gist_id}","starred_url":"https://api.github.com/users/tomerk/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tomerk/subscriptions","organizations_url":"https://api.github.com/users/tomerk/orgs","repos_url":"https://api.github.com/users/tomerk/repos","events_url":"https://api.github.com/users/tomerk/events{/privacy}","received_events_url":"https://api.github.com/users/tomerk/received_events","type":"User","site_admin":false},"repo":{"id":36905160,"name":"keystone","full_name":"tomerk/keystone","owner":{"login":"tomerk","id":676357,"avatar_url":"https://avatars.githubusercontent.com/u/676357?v=3","gravatar_id":"","url":"https://api.github.com/users/tomerk","html_url":"https://github.com/tomerk","followers_url":"https://api.github.com/users/tomerk/followers","following_url":"https://api.github.com/users/tomerk/following{/other_user}","gists_url":"https://api.github.com/users/tomerk/gists{/gist_id}","starred_url":"https://api.github.com/users/tomerk/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tomerk/subscriptions","organizations_url":"https://api.github.com/users/tomerk/orgs","repos_url":"https://api.github.com/users/tomerk/repos","events_url":"https://api.github.com/users/tomerk/events{/privacy}","received_events_url":"https://api.github.com/users/tomerk/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/tomerk/keystone","description":"Simplifying robust end-to-end machine learning on Apache Spark.","fork":true,"url":"https://api.github.com/repos/tomerk/keystone","forks_url":"https://api.github.com/repos/tomerk/keystone/forks","keys_url":"https://api.github.com/repos/tomerk/keystone/keys{/key_id}","collaborators_url":"https://api.github.com/repos/tomerk/keystone/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/tomerk/keystone/teams","hooks_url":"https://api.github.com/repos/tomerk/keystone/hooks","issue_events_url":"https://api.github.com/repos/tomerk/keystone/issues/events{/number}","events_url":"https://api.github.com/repos/tomerk/keystone/events","assignees_url":"https://api.github.com/repos/tomerk/keystone/assignees{/user}","branches_url":"https://api.github.com/repos/tomerk/keystone/branches{/branch}","tags_url":"https://api.github.com/repos/tomerk/keystone/tags","blobs_url":"https://api.github.com/repos/tomerk/keystone/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/tomerk/keystone/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/tomerk/keystone/git/refs{/sha}","trees_url":"https://api.github.com/repos/tomerk/keystone/git/trees{/sha}","statuses_url":"https://api.github.com/repos/tomerk/keystone/statuses/{sha}","languages_url":"https://api.github.com/repos/tomerk/keystone/languages","stargazers_url":"https://api.github.com/repos/tomerk/keystone/stargazers","contributors_url":"https://api.github.com/repos/tomerk/keystone/contributors","subscribers_url":"https://api.github.com/repos/tomerk/keystone/subscribers","subscription_url":"https://api.github.com/repos/tomerk/keystone/subscription","commits_url":"https://api.github.com/repos/tomerk/keystone/commits{/sha}","git_commits_url":"https://api.github.com/repos/tomerk/keystone/git/commits{/sha}","comments_url":"https://api.github.com/repos/tomerk/keystone/comments{/number}","issue_comment_url":"https://api.github.com/repos/tomerk/keystone/issues/comments{/number}","contents_url":"https://api.github.com/repos/tomerk/keystone/contents/{+path}","compare_url":"https://api.github.com/repos/tomerk/keystone/compare/{base}...{head}","merges_url":"https://api.github.com/repos/tomerk/keystone/merges","archive_url":"https://api.github.com/repos/tomerk/keystone/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/tomerk/keystone/downloads","issues_url":"https://api.github.com/repos/tomerk/keystone/issues{/number}","pulls_url":"https://api.github.com/repos/tomerk/keystone/pulls{/number}","milestones_url":"https://api.github.com/repos/tomerk/keystone/milestones{/number}","notifications_url":"https://api.github.com/repos/tomerk/keystone/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/tomerk/keystone/labels{/name}","releases_url":"https://api.github.com/repos/tomerk/keystone/releases{/id}","created_at":"2015-06-05T01:29:58Z","updated_at":"2015-06-05T01:30:54Z","pushed_at":"2015-06-18T22:00:51Z","git_url":"git://github.com/tomerk/keystone.git","ssh_url":"git@github.com:tomerk/keystone.git","clone_url":"https://github.com/tomerk/keystone.git","svn_url":"https://github.com/tomerk/keystone","homepage":"http://keystone-ml.org/","size":16559,"stargazers_count":0,"watchers_count":0,"language":"Scala","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"amplab:master","ref":"master","sha":"a8cbf141697b291a126feda278e25e8edb852407","user":{"login":"amplab","id":1438454,"avatar_url":"https://avatars.githubusercontent.com/u/1438454?v=3","gravatar_id":"","url":"https://api.github.com/users/amplab","html_url":"https://github.com/amplab","followers_url":"https://api.github.com/users/amplab/followers","following_url":"https://api.github.com/users/amplab/following{/other_user}","gists_url":"https://api.github.com/users/amplab/gists{/gist_id}","starred_url":"https://api.github.com/users/amplab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/amplab/subscriptions","organizations_url":"https://api.github.com/users/amplab/orgs","repos_url":"https://api.github.com/users/amplab/repos","events_url":"https://api.github.com/users/amplab/events{/privacy}","received_events_url":"https://api.github.com/users/amplab/received_events","type":"Organization","site_admin":false},"repo":{"id":33748726,"name":"keystone","full_name":"amplab/keystone","owner":{"login":"amplab","id":1438454,"avatar_url":"https://avatars.githubusercontent.com/u/1438454?v=3","gravatar_id":"","url":"https://api.github.com/users/amplab","html_url":"https://github.com/amplab","followers_url":"https://api.github.com/users/amplab/followers","following_url":"https://api.github.com/users/amplab/following{/other_user}","gists_url":"https://api.github.com/users/amplab/gists{/gist_id}","starred_url":"https://api.github.com/users/amplab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/amplab/subscriptions","organizations_url":"https://api.github.com/users/amplab/orgs","repos_url":"https://api.github.com/users/amplab/repos","events_url":"https://api.github.com/users/amplab/events{/privacy}","received_events_url":"https://api.github.com/users/amplab/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/amplab/keystone","description":"Simplifying robust end-to-end machine learning on Apache Spark.","fork":false,"url":"https://api.github.com/repos/amplab/keystone","forks_url":"https://api.github.com/repos/amplab/keystone/forks","keys_url":"https://api.github.com/repos/amplab/keystone/keys{/key_id}","collaborators_url":"https://api.github.com/repos/amplab/keystone/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/amplab/keystone/teams","hooks_url":"https://api.github.com/repos/amplab/keystone/hooks","issue_events_url":"https://api.github.com/repos/amplab/keystone/issues/events{/number}","events_url":"https://api.github.com/repos/amplab/keystone/events","assignees_url":"https://api.github.com/repos/amplab/keystone/assignees{/user}","branches_url":"https://api.github.com/repos/amplab/keystone/branches{/branch}","tags_url":"https://api.github.com/repos/amplab/keystone/tags","blobs_url":"https://api.github.com/repos/amplab/keystone/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/amplab/keystone/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/amplab/keystone/git/refs{/sha}","trees_url":"https://api.github.com/repos/amplab/keystone/git/trees{/sha}","statuses_url":"https://api.github.com/repos/amplab/keystone/statuses/{sha}","languages_url":"https://api.github.com/repos/amplab/keystone/languages","stargazers_url":"https://api.github.com/repos/amplab/keystone/stargazers","contributors_url":"https://api.github.com/repos/amplab/keystone/contributors","subscribers_url":"https://api.github.com/repos/amplab/keystone/subscribers","subscription_url":"https://api.github.com/repos/amplab/keystone/subscription","commits_url":"https://api.github.com/repos/amplab/keystone/commits{/sha}","git_commits_url":"https://api.github.com/repos/amplab/keystone/git/commits{/sha}","comments_url":"https://api.github.com/repos/amplab/keystone/comments{/number}","issue_comment_url":"https://api.github.com/repos/amplab/keystone/issues/comments{/number}","contents_url":"https://api.github.com/repos/amplab/keystone/contents/{+path}","compare_url":"https://api.github.com/repos/amplab/keystone/compare/{base}...{head}","merges_url":"https://api.github.com/repos/amplab/keystone/merges","archive_url":"https://api.github.com/repos/amplab/keystone/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/amplab/keystone/downloads","issues_url":"https://api.github.com/repos/amplab/keystone/issues{/number}","pulls_url":"https://api.github.com/repos/amplab/keystone/pulls{/number}","milestones_url":"https://api.github.com/repos/amplab/keystone/milestones{/number}","notifications_url":"https://api.github.com/repos/amplab/keystone/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/amplab/keystone/labels{/name}","releases_url":"https://api.github.com/repos/amplab/keystone/releases{/id}","created_at":"2015-04-10T20:16:11Z","updated_at":"2015-06-18T11:07:23Z","pushed_at":"2015-06-18T22:00:51Z","git_url":"git://github.com/amplab/keystone.git","ssh_url":"git@github.com:amplab/keystone.git","clone_url":"https://github.com/amplab/keystone.git","svn_url":"https://github.com/amplab/keystone","homepage":"http://keystone-ml.org/","size":17129,"stargazers_count":144,"watchers_count":144,"language":"Scala","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":26,"mirror_url":null,"open_issues_count":26,"forks":26,"open_issues":26,"watchers":144,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/amplab/keystone/pulls/146"},"html":{"href":"https://github.com/amplab/keystone/pull/146"},"issue":{"href":"https://api.github.com/repos/amplab/keystone/issues/146"},"comments":{"href":"https://api.github.com/repos/amplab/keystone/issues/146/comments"},"review_comments":{"href":"https://api.github.com/repos/amplab/keystone/pulls/146/comments"},"review_comment":{"href":"https://api.github.com/repos/amplab/keystone/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/amplab/keystone/pulls/146/commits"},"statuses":{"href":"https://api.github.com/repos/amplab/keystone/statuses/af2f9ec0550d0a442cbcd3c268585bc348cab6c2"}}}}