{"action":"created","comment":{"url":"https://api.github.com/repos/OICR/softeng-blog/pulls/comments/176765857","pull_request_review_id":106512627,"id":176765857,"diff_hunk":"@@ -0,0 +1,349 @@\n+---\n+layout: post\n+title:  \"Introducing the Kids-First ETL\"\n+breadcrumb: true\n+author: grant_guo\n+date: 2018-03-19\n+categories: grant_guo\n+tags:\n+    - Functional Programming(FP), Scala\n+teaser:\n+    info: An introduction of how to apply Scala's functional programming features to the Kids-First ETL\n+    image: lambda.png\n+header:\n+    version: small\n+    title: Software Engineering Blog\n+    image: header-logo-crop.png\n+    icon: icon-blog\n+---\n+\n+## Abstractions\n+\n+The Kids-First ETL (ETL for short) consists of a set of consecutive data transformations. Transformations are grouped into logical units which are modelled as `Processors`. Processors join together to form a `Pipeline`, which is the top level abstraction of the ETL. The `Pipleline` manages `Processors` and global resources through `Context`. \n+\n+Each `Processor` manages subordinate but important abstractions for the purpose of separation of concerns. These abstractions serve as `functions` taking  specific inputs and returning as an output the results of transformation. \n+* `source`: defines how to load the processor-specific data\n+* `transform`: defines how to make the processor-specific data transformations\n+* `sink`: defines whether and how to write the transformed data to the external storage\n+* `ouput`: defines how to pass the transformed data to the next processor in the `Pipeline`\n+* `context`: maintains the processor-scoped resources for the execution of the current `Processor`\n+* `step`: defines the smallest transformation unit executed in a finer-grained way\n+* `inject`: creates the instances of the above abstractions in a non-invasive way\n+\n+We can visualize all of these parts as a Class diagram: \n+<figure>\n+    <img src=\"{{site.urlimg}}grant_guo/kf-etl-classes.jpg\" />\n+    <figcaption>Kids-First ETL Classes</figcaption>\n+</figure>\n+\n+Most of the processors have been defined as Scala [Function1[-T1, +R]](http://www.scala-lang.org/api/2.11.12/#scala.Function1)\n+\n+## Functional programming in Scala\n+\n+Scala is both a Functional and Object-Oriented programming language. In Scala, functions are *first-class* language constructs and are part of the type system. Functions can be:\n+* Assigned to variables\n+* Passed as parameters to functions, constructors, etc\n+* Returned as results of functions\n+* Written as function literals. \n+\n+In functional programming, a function is required to be *pure*, which means forbidding changing-state and mutable data. If the execution of a function depends on not only the inputs, but also the context, the function probably has some hidden internal behaviors which means side effects and impurity.\n+\n+For the ETL pricess, we have used several of the Scala functional features. \n+\n+### Immutability\n+\n+Scala encourages developers to define immutable data structures and variables. Immutability helps keep control of the application states, avoid unexpected values, and avoid concurrent state issues. Collection types defined in the package `scala.collection.immutable` are immutable. Some functions in those types return a new instance of the same type instead of altering the collection in-place. This feature makes the method chaining possible. For example:\n+```\n+List(\"Jan\", \"Feb\", \"Mar\", ..)\n+    .filter(m => {???})\n+    .map(m => {???})\n+    .collect(m => {???})\n+```\n+\n+Another interesting and  powerful language construct is `case classes` as the following definition:\n+```\n+case class Address(number:Int, street:String, city:String, province:String)\n+```\n+Scala compiler will add some syntactic conveniences to the class:\n+* A factory method with the name of the class. This means that, for example, you can write `Address(661, \"University Ave\", \"Toronto\", \"ON\")` to construct a Address object. Under the hood, the `apply(...)` method generated in the companion object makes this magic possible.\n+* All arguments in the parameter list of a case class implicitly get a `val` prefix\n+* The \"natural\" implementations of methods `toString`, `hashCode`, and `equals`\n+* A `copy` method, which is useful for making a new instance of the class that is the same as another one except that one or two attributes are different. For example:\n+```\n+val addr = Address(661, \"University Ave\", \"Toronto\", \"ON\")\n+val new_addr = addr.copy(province = \"Ontario\")\n+```\n+\n+### Higher-order functions\n+\n+In Scala, a function can be defined in the standard way:\n+```def plusOne(x:Int):Int = {x + 1}```\n+or as a functional literal:\n+```val func = (x:Int) => x + 1```\n+\n+Basicaly, a higher-order function can have one the three forms:\n+* one or more of its parameters is a function, and it returns some value\n+* it returns a function, but none of its parameters are a function\n+* both of the above\n+\n+Functions like `map`, `filter` or `flatMap` defined in collection types are the first type. In the section [Implementation Details]() we will examine how the ETL uses the other two types, especially function composition to make function chaining possible.\n+\n+### Pattern Matching\n+\n+Pattern Matching in Scala is amazingly powerful. Here are some examples:\n+* wildcard, varable, constructor patterns:\n+```\n+expr match {\n+    case Address(number, street, _, _) => println(s\"${number}, ${street}\")\n+    ...\n+}\n+```\n+* constant patterns:\n+```\n+expr match {\n+    case 10 => \"ten\"\n+    case false => \"false\"\n+    case \"hello\" => \"hi!\"\n+    case Nil => \"the empty list\"\n+    case _ => \"something else\"\n+}\n+```\n+* sequence patterns:\n+```\n+list match {\n+    case List(0, _, _) => println(\"the list starts with 0\")\n+    case _ => \n+}\n+```\n+* typed patterns\n+```\n+expr match {\n+    case s: String => s.length\n+    case m: Map[_, _] => m.size\n+    case _ => -1\n+}\n+```\n+* variable binding:\n+```\n+case class Company(addr: Address, contact:String)\n+\n+company match {\n+    case Company(oicr @ Address(661, \"University Ave\", \"Toronto\", \"ON\"), _) => println(oicr)\n+    case _ =>\n+}\n+```\n+\n+Under the hood, the `apply(...)` function plays very important role in pattern matching.\n+\n+### Handling errors without exceptions\n+\n+The Scala way to handle exceptions is to represent failtures and exceptions with ordinary values, and the developers can write higher-order functions that abstract out common patterns of error handling and recovery.\n+\n+The `Try` class is defined for this purpose. A `Try[T]` represents a computation that may result in a value of type `T`. If the computation is successful, a `Success[T]` is returned. Or a `Failture[T]` is returned. We can make use of the pattern matching as the following:\n+```\n+val computation = Try(....)\n+computation match {\n+    case Success(ret) => { logic for successful operation }\n+    case Failture(failed) => { error handling } \n+}\n+\n+```\n+\n+## Implementation Details\n+\n+This section explains how the ETL applies Scala's functional features.\n+<figure>\n+    <img src=\"{{site.urlimg}}grant_guo/pipeline.png\" />\n+    <figcaption>Kids-First ETL Pipeline</figcaption>\n+</figure>\n+\n+### Data Models\n+\n+The ETL uses [ScalaPB](https://scalapb.github.io/), a Scala implementation of Google [Protocol Buffers](https://developers.google.com/protocol-buffers/), as the representation of the internal data model. [Protocol Buffers](https://developers.google.com/protocol-buffers/) are  language-neutral, platform-neutral extensible mechanisms for serializing structured data. It ensures that all exchanged data follows predefined formats and increases  data consistency.\n+\n+ScalaPB is built on top of the Protocol Buffer compiler and has many advantages that the ETL values including:\n+* Supports both proto2 and proto3. The ETL uses `proto2` because some fields of the source data are optional and only `proto2` supports the keywords `optional` and `required`. \n+* Supports SparkSQL\n+* Supports converting to and from JSON. The ETL uses this to directly generate JSON strings.\n+* Supports generating case classes for all messages. Applying case classes to Spark Dataset is intuitive, so that no extra [Encoder](http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.Encoder)s are needed.\n+* Supports introspection of generated case classes \n+\n+### Context\n+\n+`Context` holds globally shared resources like `SparkSession`, `PostgresQL`, and `Elasticsearch` and is defined like this:\n+\n+```\n+object Context extends ContextTrait with ClasspathURLEnabler{\n+  lazy val (hdfs, rootPath) = getHDFS()\n+  lazy val sparkSession = getSparkSession()\n+  lazy val postgresql = getPostgresql()\n+  lazy val esClient = getESClient()\n+  \n+  ...\n+}\n+```\n+\n+### Processor\n+\n+`Processor` is the key abstraction in the ETL and is defined like this:\n+```\n+trait Processor[I, O] extends Function1[I, O]{\n+  override def apply(input: I): O = {\n+    process(input)\n+  }\n+  def process(input:I): O\n+}\n+```\n+\n+The `Processor` is derived from Scala's [Function1[-T1, +R]](http://www.scala-lang.org/api/2.11.12/#scala.Function1) which basically means that a `Processor` is just a function. Any concrete `Processor` fills its logic by overriding the function `process(input:I):O`. Technically, a `Processor` could be implemented in any direct or indirect way. However, the reference implementations follow this convention:\n+\n+> A Processor is composed of source, transform, sink and output. A transform can be composed of Steps. The ProcessorContext and  StepContext hold shared resources for a Processor and a Step respectively.\n+\n+`Source`, `transform`, `sink` and `output` are types, but the ETL doesn't define the particular base interface respectively. Instead, they are all derived from Scala [Function1[-T1, +R]](http://www.scala-lang.org/api/2.11.12/#scala.Function1). This means that a `Processor` can be implemented in a very consistent and simple way like this:\n+\n+```\n+class FileCentricProcessor(context: FileCentricContext,\n+                           source: Repository => DatasetsFromDBTables,\n+                           transform: DatasetsFromDBTables => Dataset[FileCentric],\n+                           sink: Dataset[FileCentric] => Unit,\n+                           output: Unit => (String,Repository)) extends Processor[Repository, (String,Repository)]{\n+\n+  override def process(input: Repository):(String,Repository) = {\n+    source.andThen(transform).andThen(sink).andThen(output)(input)\n+  }\n+\n+}\n+``` \n+\n+The key point is the usage of the `andThen` function defined in Scala [Function1[-T1, +R]](http://www.scala-lang.org/api/2.11.12/#scala.Function1). `andThen` allows  function calls to be changed together sequentially rather than nested together. \n+\n+#### `inject` in Processor\n+\n+Another convention in `Processor` is:\n+\n+> Every Processor provides a dependency injection module backed by Google Guice. At runtime, the Pipeline will load all of the inject module instances annotated with @GuiceModule(name = \"...\") and instantiate the corresponding components \n+\n+An inject module has its base class defined as the following:\n+```\n+abstract class ProcessorInjectModule(val sparkSession: SparkSession,\n+                                     val hdfs: HDFS,\n+                                     val appRootPath: String,\n+                                     val config: Option[Config]) extends AbstractModule{\n+  type CONTEXT\n+  type PROCESSOR\n+  type SOURCE\n+  type SINK\n+  type TRANSFORMER\n+  type OUTPUT\n+\n+  def getContext(): CONTEXT\n+  def getProcessor(): PROCESSOR\n+\n+  def getSource(context: CONTEXT): SOURCE\n+  def getSink(context: CONTEXT): SINK\n+  def getTransformer(context: CONTEXT): TRANSFORMER\n+  def getOutput(context: CONTEXT): OUTPUT\n+\n+}\n+```\n+\n+An implementation module should provide the concrete type definitions for all the `type ***` declarations. For example:\n+\n+```\n+@GuiceModule(name = \"download\")\n+class DownloadInjectModule(sparkSession: SparkSession,\n+                           hdfs: HDFS,\n+                           appRootPath: String,\n+                           config: Option[Config]) extends ProcessorInjectModule(sparkSession, hdfs, appRootPath, config) {\n+  type CONTEXT = DownloadContext\n+  type PROCESSOR = DownloadProcessor\n+  type SOURCE = DownloadSource\n+  type SINK = DownloadSink\n+  type TRANSFORMER = DownloadTransformer\n+  type OUTPUT = DownloadOutput\n+  \n+  ...\n+}\n+```\n+\n+\n+#### `transform` and `step` in Processor\n+\n+As you can guess,  `Step` is also a Scala [Function1[-T1, +R]](http://www.scala-lang.org/api/2.11.12/#scala.Function1).  It is the smallest executable unit in the ETL and performs a very specific data transformation logic.\n+\n+```\n+case class Step[I, O](val description: String, val handler: StepExecutable[I, O], val posthandler: StepExecutable[O, O] = DefaultPostHandler[O]()) extends Function1[I, O] {\n+  override def apply(input: I): O = {\n+    handler.andThen(posthandler)(input)\n+  }\n+}\n+\n+abstract class StepExecutable[-I, +O] extends Function1[I, O] with Serializable {\n+  override def apply(v1: I): O = {\n+    process(v1)\n+  }\n+\n+  def process(input: I):O\n+  def ctx():StepContext\n+}\n+``` \n+\n+`Step` itself is a case class composed of a description, a handler, and a posthandler. `Step`'s behavior is defined through `StepExecutable`, which is a Scala [Function1[-T1, +R]](http://www.scala-lang.org/api/2.11.12/#scala.Function1) of course.\n+\n+Let's take a look at how to chain `Step`s together:\n+\n+```\n+class FileCentricTransformer(val context: FileCentricContext) {\n+\n+  def transform(input: DatasetsFromDBTables): Dataset[FileCentric] = {\n+\n+    ... ...\n+    \n+    Function.chain(\n+      Seq(\n+        Step[Dataset[Participant], Dataset[Participant]](\"01. merge Study into Participant\", new MergeStudy(ctx), posthandler1(\"step1\")),\n+        Step[Dataset[Participant], Dataset[Participant]](\"02. merge Demographic into Participant\", new MergeDemographic(ctx), posthandler1(\"step2\")),\n+        Step[Dataset[Participant], Dataset[Participant]](\"03. merge Diagnosis into Participant\", new MergeDiagnosis(ctx), posthandler1(\"step3\")),\n+        Step[Dataset[Participant], Dataset[Participant]](\"04. compute HPO reference data and then merge Phenotype into Participant\", new MergePhenotype(ctx), posthandler1(\"step4\")),\n+        Step[Dataset[Participant], Dataset[Participant]](\"05. merge 'availableDataTypes' into Participant\", new MergeAvailableDataTypesForParticipant(ctx), posthandler1(\"step5\")),\n+        Step[Dataset[Participant], Dataset[Participant]](\"06. merge family member into Participant\", new MergeFamilyMember(ctx), posthandler1(\"step6\"))\n+      )\n+    ).andThen(\n+      {\n+        val ph = (filename:String) => new WriteKfModelToJsonFile[GfId_Participants](ctx)\n+        Step[Dataset[Participant], Dataset[GfId_Participants]](\"07. merge Sample, Aliquot into Participant\", new MergeSampleAliquot(ctx), ph(\"step7\"))\n+      }\n+    ).andThen(\n+      Step[Dataset[GfId_Participants], Dataset[FileCentric]](\"08. build final FileCentric\", new BuildFileCentricNew(ctx), posthandler2)\n+    )(context.sparkSession.emptyDataset[Participant])\n+\n+  }\n+\n+}\n+\n+``` \n+\n+This time, we don't use the `andThen` function only but instead we compose the pipeline from the `Function.chain` function. The difference is that `Function.chain` accepts a sequence of [Function1[-T1, +R]](http://www.scala-lang.org/api/2.11.12/#scala.Function1) calls which have the same input and output types. `andThen` is used to chain functions with different input and output types.\n+\n+### Pipeline\n+\n+A `Pipeline` defines the workflow and is responsible for creating `Processor`s through their `inject`s. \n+\n+## Summary\n+\n+The Kids-First ETL is an attempt to combine both FP and OOP paradigms to build  large-scale, data-intensive, and distributed applications. Although it only uses a small set of Scala's functional features, the FP helps simplify the design and implementation and provides a beneficial supplement to OOP.  \n+\n+## Next Steps\n+\n+There are some potential enhancements we could do in the future.","path":"_posts/grant_guo/2018-03-22-kf-etl.md","position":346,"original_position":338,"commit_id":"0a75eb097d6b5f3aa3f892a28d32e967b396bf8f","original_commit_id":"f32b1779a763ff685f1f676965c6e43af77926a0","user":{"login":"KevinFHartmann","id":26552896,"avatar_url":"https://avatars1.githubusercontent.com/u/26552896?v=4","gravatar_id":"","url":"https://api.github.com/users/KevinFHartmann","html_url":"https://github.com/KevinFHartmann","followers_url":"https://api.github.com/users/KevinFHartmann/followers","following_url":"https://api.github.com/users/KevinFHartmann/following{/other_user}","gists_url":"https://api.github.com/users/KevinFHartmann/gists{/gist_id}","starred_url":"https://api.github.com/users/KevinFHartmann/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/KevinFHartmann/subscriptions","organizations_url":"https://api.github.com/users/KevinFHartmann/orgs","repos_url":"https://api.github.com/users/KevinFHartmann/repos","events_url":"https://api.github.com/users/KevinFHartmann/events{/privacy}","received_events_url":"https://api.github.com/users/KevinFHartmann/received_events","type":"User","site_admin":false},"body":"\"Here are ... in the future:\"","created_at":"2018-03-23T15:10:05Z","updated_at":"2018-03-23T15:15:05Z","html_url":"https://github.com/OICR/softeng-blog/pull/114#discussion_r176765857","pull_request_url":"https://api.github.com/repos/OICR/softeng-blog/pulls/114","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/OICR/softeng-blog/pulls/comments/176765857"},"html":{"href":"https://github.com/OICR/softeng-blog/pull/114#discussion_r176765857"},"pull_request":{"href":"https://api.github.com/repos/OICR/softeng-blog/pulls/114"}}},"pull_request":{"url":"https://api.github.com/repos/OICR/softeng-blog/pulls/114","id":176898551,"html_url":"https://github.com/OICR/softeng-blog/pull/114","diff_url":"https://github.com/OICR/softeng-blog/pull/114.diff","patch_url":"https://github.com/OICR/softeng-blog/pull/114.patch","issue_url":"https://api.github.com/repos/OICR/softeng-blog/issues/114","number":114,"state":"open","locked":false,"title":"Kids-First ETL design","user":{"login":"grant-guo","id":9614747,"avatar_url":"https://avatars0.githubusercontent.com/u/9614747?v=4","gravatar_id":"","url":"https://api.github.com/users/grant-guo","html_url":"https://github.com/grant-guo","followers_url":"https://api.github.com/users/grant-guo/followers","following_url":"https://api.github.com/users/grant-guo/following{/other_user}","gists_url":"https://api.github.com/users/grant-guo/gists{/gist_id}","starred_url":"https://api.github.com/users/grant-guo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/grant-guo/subscriptions","organizations_url":"https://api.github.com/users/grant-guo/orgs","repos_url":"https://api.github.com/users/grant-guo/repos","events_url":"https://api.github.com/users/grant-guo/events{/privacy}","received_events_url":"https://api.github.com/users/grant-guo/received_events","type":"User","site_admin":false},"body":"","created_at":"2018-03-22T20:21:50Z","updated_at":"2018-03-23T15:15:05Z","closed_at":null,"merged_at":null,"merge_commit_sha":"8931dfdf2628e2c5d33b0a90c0e33570d3c28a78","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/OICR/softeng-blog/pulls/114/commits","review_comments_url":"https://api.github.com/repos/OICR/softeng-blog/pulls/114/comments","review_comment_url":"https://api.github.com/repos/OICR/softeng-blog/pulls/comments{/number}","comments_url":"https://api.github.com/repos/OICR/softeng-blog/issues/114/comments","statuses_url":"https://api.github.com/repos/OICR/softeng-blog/statuses/0a75eb097d6b5f3aa3f892a28d32e967b396bf8f","head":{"label":"OICR:grant-etl","ref":"grant-etl","sha":"0a75eb097d6b5f3aa3f892a28d32e967b396bf8f","user":{"login":"OICR","id":13154011,"avatar_url":"https://avatars0.githubusercontent.com/u/13154011?v=4","gravatar_id":"","url":"https://api.github.com/users/OICR","html_url":"https://github.com/OICR","followers_url":"https://api.github.com/users/OICR/followers","following_url":"https://api.github.com/users/OICR/following{/other_user}","gists_url":"https://api.github.com/users/OICR/gists{/gist_id}","starred_url":"https://api.github.com/users/OICR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OICR/subscriptions","organizations_url":"https://api.github.com/users/OICR/orgs","repos_url":"https://api.github.com/users/OICR/repos","events_url":"https://api.github.com/users/OICR/events{/privacy}","received_events_url":"https://api.github.com/users/OICR/received_events","type":"Organization","site_admin":false},"repo":{"id":70279096,"name":"softeng-blog","full_name":"OICR/softeng-blog","owner":{"login":"OICR","id":13154011,"avatar_url":"https://avatars0.githubusercontent.com/u/13154011?v=4","gravatar_id":"","url":"https://api.github.com/users/OICR","html_url":"https://github.com/OICR","followers_url":"https://api.github.com/users/OICR/followers","following_url":"https://api.github.com/users/OICR/following{/other_user}","gists_url":"https://api.github.com/users/OICR/gists{/gist_id}","starred_url":"https://api.github.com/users/OICR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OICR/subscriptions","organizations_url":"https://api.github.com/users/OICR/orgs","repos_url":"https://api.github.com/users/OICR/repos","events_url":"https://api.github.com/users/OICR/events{/privacy}","received_events_url":"https://api.github.com/users/OICR/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/OICR/softeng-blog","description":"Software engineering team blog","fork":false,"url":"https://api.github.com/repos/OICR/softeng-blog","forks_url":"https://api.github.com/repos/OICR/softeng-blog/forks","keys_url":"https://api.github.com/repos/OICR/softeng-blog/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OICR/softeng-blog/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OICR/softeng-blog/teams","hooks_url":"https://api.github.com/repos/OICR/softeng-blog/hooks","issue_events_url":"https://api.github.com/repos/OICR/softeng-blog/issues/events{/number}","events_url":"https://api.github.com/repos/OICR/softeng-blog/events","assignees_url":"https://api.github.com/repos/OICR/softeng-blog/assignees{/user}","branches_url":"https://api.github.com/repos/OICR/softeng-blog/branches{/branch}","tags_url":"https://api.github.com/repos/OICR/softeng-blog/tags","blobs_url":"https://api.github.com/repos/OICR/softeng-blog/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OICR/softeng-blog/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OICR/softeng-blog/git/refs{/sha}","trees_url":"https://api.github.com/repos/OICR/softeng-blog/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OICR/softeng-blog/statuses/{sha}","languages_url":"https://api.github.com/repos/OICR/softeng-blog/languages","stargazers_url":"https://api.github.com/repos/OICR/softeng-blog/stargazers","contributors_url":"https://api.github.com/repos/OICR/softeng-blog/contributors","subscribers_url":"https://api.github.com/repos/OICR/softeng-blog/subscribers","subscription_url":"https://api.github.com/repos/OICR/softeng-blog/subscription","commits_url":"https://api.github.com/repos/OICR/softeng-blog/commits{/sha}","git_commits_url":"https://api.github.com/repos/OICR/softeng-blog/git/commits{/sha}","comments_url":"https://api.github.com/repos/OICR/softeng-blog/comments{/number}","issue_comment_url":"https://api.github.com/repos/OICR/softeng-blog/issues/comments{/number}","contents_url":"https://api.github.com/repos/OICR/softeng-blog/contents/{+path}","compare_url":"https://api.github.com/repos/OICR/softeng-blog/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OICR/softeng-blog/merges","archive_url":"https://api.github.com/repos/OICR/softeng-blog/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OICR/softeng-blog/downloads","issues_url":"https://api.github.com/repos/OICR/softeng-blog/issues{/number}","pulls_url":"https://api.github.com/repos/OICR/softeng-blog/pulls{/number}","milestones_url":"https://api.github.com/repos/OICR/softeng-blog/milestones{/number}","notifications_url":"https://api.github.com/repos/OICR/softeng-blog/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OICR/softeng-blog/labels{/name}","releases_url":"https://api.github.com/repos/OICR/softeng-blog/releases{/id}","deployments_url":"https://api.github.com/repos/OICR/softeng-blog/deployments","created_at":"2016-10-07T20:06:27Z","updated_at":"2018-03-23T13:52:39Z","pushed_at":"2018-03-23T14:56:36Z","git_url":"git://github.com/OICR/softeng-blog.git","ssh_url":"git@github.com:OICR/softeng-blog.git","clone_url":"https://github.com/OICR/softeng-blog.git","svn_url":"https://github.com/OICR/softeng-blog","homepage":"http://softeng.oicr.on.ca/","size":57824,"stargazers_count":1,"watchers_count":1,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":7,"mirror_url":null,"archived":false,"open_issues_count":6,"license":{"key":"other","name":"Other","spdx_id":null,"url":null},"forks":7,"open_issues":6,"watchers":1,"default_branch":"gh-pages"}},"base":{"label":"OICR:gh-pages","ref":"gh-pages","sha":"64566d6f64521401ce01227aa6fd076ac94f1520","user":{"login":"OICR","id":13154011,"avatar_url":"https://avatars0.githubusercontent.com/u/13154011?v=4","gravatar_id":"","url":"https://api.github.com/users/OICR","html_url":"https://github.com/OICR","followers_url":"https://api.github.com/users/OICR/followers","following_url":"https://api.github.com/users/OICR/following{/other_user}","gists_url":"https://api.github.com/users/OICR/gists{/gist_id}","starred_url":"https://api.github.com/users/OICR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OICR/subscriptions","organizations_url":"https://api.github.com/users/OICR/orgs","repos_url":"https://api.github.com/users/OICR/repos","events_url":"https://api.github.com/users/OICR/events{/privacy}","received_events_url":"https://api.github.com/users/OICR/received_events","type":"Organization","site_admin":false},"repo":{"id":70279096,"name":"softeng-blog","full_name":"OICR/softeng-blog","owner":{"login":"OICR","id":13154011,"avatar_url":"https://avatars0.githubusercontent.com/u/13154011?v=4","gravatar_id":"","url":"https://api.github.com/users/OICR","html_url":"https://github.com/OICR","followers_url":"https://api.github.com/users/OICR/followers","following_url":"https://api.github.com/users/OICR/following{/other_user}","gists_url":"https://api.github.com/users/OICR/gists{/gist_id}","starred_url":"https://api.github.com/users/OICR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OICR/subscriptions","organizations_url":"https://api.github.com/users/OICR/orgs","repos_url":"https://api.github.com/users/OICR/repos","events_url":"https://api.github.com/users/OICR/events{/privacy}","received_events_url":"https://api.github.com/users/OICR/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/OICR/softeng-blog","description":"Software engineering team blog","fork":false,"url":"https://api.github.com/repos/OICR/softeng-blog","forks_url":"https://api.github.com/repos/OICR/softeng-blog/forks","keys_url":"https://api.github.com/repos/OICR/softeng-blog/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OICR/softeng-blog/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OICR/softeng-blog/teams","hooks_url":"https://api.github.com/repos/OICR/softeng-blog/hooks","issue_events_url":"https://api.github.com/repos/OICR/softeng-blog/issues/events{/number}","events_url":"https://api.github.com/repos/OICR/softeng-blog/events","assignees_url":"https://api.github.com/repos/OICR/softeng-blog/assignees{/user}","branches_url":"https://api.github.com/repos/OICR/softeng-blog/branches{/branch}","tags_url":"https://api.github.com/repos/OICR/softeng-blog/tags","blobs_url":"https://api.github.com/repos/OICR/softeng-blog/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OICR/softeng-blog/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OICR/softeng-blog/git/refs{/sha}","trees_url":"https://api.github.com/repos/OICR/softeng-blog/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OICR/softeng-blog/statuses/{sha}","languages_url":"https://api.github.com/repos/OICR/softeng-blog/languages","stargazers_url":"https://api.github.com/repos/OICR/softeng-blog/stargazers","contributors_url":"https://api.github.com/repos/OICR/softeng-blog/contributors","subscribers_url":"https://api.github.com/repos/OICR/softeng-blog/subscribers","subscription_url":"https://api.github.com/repos/OICR/softeng-blog/subscription","commits_url":"https://api.github.com/repos/OICR/softeng-blog/commits{/sha}","git_commits_url":"https://api.github.com/repos/OICR/softeng-blog/git/commits{/sha}","comments_url":"https://api.github.com/repos/OICR/softeng-blog/comments{/number}","issue_comment_url":"https://api.github.com/repos/OICR/softeng-blog/issues/comments{/number}","contents_url":"https://api.github.com/repos/OICR/softeng-blog/contents/{+path}","compare_url":"https://api.github.com/repos/OICR/softeng-blog/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OICR/softeng-blog/merges","archive_url":"https://api.github.com/repos/OICR/softeng-blog/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OICR/softeng-blog/downloads","issues_url":"https://api.github.com/repos/OICR/softeng-blog/issues{/number}","pulls_url":"https://api.github.com/repos/OICR/softeng-blog/pulls{/number}","milestones_url":"https://api.github.com/repos/OICR/softeng-blog/milestones{/number}","notifications_url":"https://api.github.com/repos/OICR/softeng-blog/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OICR/softeng-blog/labels{/name}","releases_url":"https://api.github.com/repos/OICR/softeng-blog/releases{/id}","deployments_url":"https://api.github.com/repos/OICR/softeng-blog/deployments","created_at":"2016-10-07T20:06:27Z","updated_at":"2018-03-23T13:52:39Z","pushed_at":"2018-03-23T14:56:36Z","git_url":"git://github.com/OICR/softeng-blog.git","ssh_url":"git@github.com:OICR/softeng-blog.git","clone_url":"https://github.com/OICR/softeng-blog.git","svn_url":"https://github.com/OICR/softeng-blog","homepage":"http://softeng.oicr.on.ca/","size":57824,"stargazers_count":1,"watchers_count":1,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":7,"mirror_url":null,"archived":false,"open_issues_count":6,"license":{"key":"other","name":"Other","spdx_id":null,"url":null},"forks":7,"open_issues":6,"watchers":1,"default_branch":"gh-pages"}},"_links":{"self":{"href":"https://api.github.com/repos/OICR/softeng-blog/pulls/114"},"html":{"href":"https://github.com/OICR/softeng-blog/pull/114"},"issue":{"href":"https://api.github.com/repos/OICR/softeng-blog/issues/114"},"comments":{"href":"https://api.github.com/repos/OICR/softeng-blog/issues/114/comments"},"review_comments":{"href":"https://api.github.com/repos/OICR/softeng-blog/pulls/114/comments"},"review_comment":{"href":"https://api.github.com/repos/OICR/softeng-blog/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/OICR/softeng-blog/pulls/114/commits"},"statuses":{"href":"https://api.github.com/repos/OICR/softeng-blog/statuses/0a75eb097d6b5f3aa3f892a28d32e967b396bf8f"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"KevinFHartmann"}}