Created
August 24, 2017 08:08
-
-
Save rjsvaljean/183e3122180859da5de69a07b9a3c788 to your computer and use it in GitHub Desktop.
Script to find distribution of PR age
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| scalaVersion := "2.12.2" | |
| name := "graphql-playground" | |
| libraryDependencies ++= Seq( | |
| "org.sangria-graphql" %% "sangria" % "1.3.0", | |
| "org.http4s" %% "http4s-dsl" % "0.17.0-RC1", | |
| "org.http4s" %% "http4s-blaze-client" % "0.17.0-RC1", | |
| "org.http4s" %% "http4s-circe" % "0.17.0-RC1", | |
| "io.circe" %% "circe-generic" % "0.6.1", | |
| "io.circe" %% "circe-literal" % "0.6.1", | |
| "io.circe" %% "circe-parser" % "0.6.1", | |
| "ch.qos.logback" % "logback-classic" % "1.2.3", | |
| "joda-time" % "joda-time" % "2.9.9", | |
| "org.typelevel" %% "cats-core" % "1.0.0-MF", | |
| "com.quantifind" %% "wisp" % "0.0.4" | |
| ) | |
| resolvers += Resolver.sonatypeRepo("releases") | |
| addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package rxvl | |
| import java.text.SimpleDateFormat | |
| import org.http4s.client.blaze._ | |
| import java.util.Date | |
| import cats.instances.map._ | |
| import cats.instances.int._ | |
| import cats.instances.tuple._ | |
| import cats.kernel.Semigroup | |
| import cats.syntax.semigroup._ | |
| import io.circe._ | |
| import io.circe.generic.semiauto._ | |
| import io.circe.syntax._ | |
| import org.http4s._ | |
| import org.http4s.circe._ | |
| import sangria._ | |
| import sangria.macros._ | |
| import sangria.renderer.QueryRenderer | |
| import fs2._ | |
| import org.joda.time.{DateTime, Days} | |
| object models { | |
| private val is08601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX") | |
| implicit val dateEncoder: Encoder[Date] = Encoder.instance[Date](_.toString.asJson) | |
| implicit val dateDecoder: Decoder[Date] = Decoder.decodeString.map(is08601.parse) | |
| case class Commits(totalCount: Int) | |
| case class PullRequestNode(createdAt: Date, mergedAt: Option[Date], commits: Commits) | |
| case class PullRequestEdge(cursor: String, node: PullRequestNode) | |
| case class PullRequest(edges: List[PullRequestEdge]) | |
| case class Repo(pullRequests: PullRequest) | |
| case class Data(repository: Repo) | |
| case class RepoGQLResponse(data: Data) | |
| object Commits {implicit val encoder: Encoder[Commits] = deriveEncoder[Commits];implicit val decoder: Decoder[Commits] = deriveDecoder[Commits];implicit val entityEncoder: EntityEncoder[Commits] = jsonEncoderOf(encoder);implicit val entityDecoder: EntityDecoder[Commits] = jsonOf(decoder)} | |
| object PullRequestNode {implicit val encoder: Encoder[PullRequestNode] = deriveEncoder[PullRequestNode];implicit val decoder: Decoder[PullRequestNode] = deriveDecoder[PullRequestNode];implicit val entityEncoder: EntityEncoder[PullRequestNode] = jsonEncoderOf(encoder);implicit val entityDecoder: EntityDecoder[PullRequestNode] = jsonOf(decoder)} | |
| object PullRequestEdge {implicit val encoder: Encoder[PullRequestEdge] = deriveEncoder[PullRequestEdge];implicit val decoder: Decoder[PullRequestEdge] = deriveDecoder[PullRequestEdge];implicit val entityEncoder: EntityEncoder[PullRequestEdge] = jsonEncoderOf(encoder);implicit val entityDecoder: EntityDecoder[PullRequestEdge] = jsonOf(decoder)} | |
| object PullRequest {implicit val encoder: Encoder[PullRequest] = deriveEncoder[PullRequest];implicit val decoder: Decoder[PullRequest] = deriveDecoder[PullRequest];implicit val entityEncoder: EntityEncoder[PullRequest] = jsonEncoderOf(encoder);implicit val entityDecoder: EntityDecoder[PullRequest] = jsonOf(decoder)} | |
| object Repo {implicit val encoder: Encoder[Repo] = deriveEncoder[Repo];implicit val decoder: Decoder[Repo] = deriveDecoder[Repo];implicit val entityEncoder: EntityEncoder[Repo] = jsonEncoderOf(encoder);implicit val entityDecoder: EntityDecoder[Repo] = jsonOf(decoder)} | |
| object Data {implicit val encoder: Encoder[Data] = deriveEncoder[Data];implicit val decoder: Decoder[Data] = deriveDecoder[Data];implicit val entityEncoder: EntityEncoder[Data] = jsonEncoderOf(encoder);implicit val entityDecoder: EntityDecoder[Data] = jsonOf(decoder)} | |
| object RepoGQLResponse {implicit val encoder: Encoder[RepoGQLResponse] = deriveEncoder[RepoGQLResponse];implicit val decoder: Decoder[RepoGQLResponse] = deriveDecoder[RepoGQLResponse];implicit val entityEncoder: EntityEncoder[RepoGQLResponse] = jsonEncoderOf(encoder);implicit val entityDecoder: EntityDecoder[RepoGQLResponse] = jsonOf(decoder)} | |
| } | |
| case class Average(total: Int, n: Int) { | |
| override def toString: String = "[Avg: " + (total.toDouble / n).formatted("%.2f") + "]" | |
| } | |
| object Average { | |
| implicit val semigroup: Semigroup[Average] = (x: Average, y: Average) => Average(x.total + y.total, x.n + y.n) | |
| } | |
| object Main { | |
| import models._ | |
| val GithubAccessToken = "nice try" | |
| def main(args: Array[String]): Unit = (for { | |
| buckets <- getPages.flatMap(toNodes).map(toBucket).reduce(_ |+| _).runLast.map(_.get) | |
| _ <- client.shutdown | |
| } yield buckets.toSeq.sortBy(_._1).foreach(println)).unsafeRun() | |
| private def getPage(cursor: Option[String]): Task[RepoGQLResponse] = for { | |
| reqWithBody <- prsStartingAt(cursor) | |
| req = reqWithBody.withHeaders(Headers( | |
| Header("Authorization", "bearer " + GithubAccessToken), | |
| Header("Accept", "application/vnd.github.v4.raw+json"))) | |
| res <- client.fetchAs[RepoGQLResponse](req) | |
| } yield res | |
| private def getPages: Stream[Task, RepoGQLResponse] = | |
| Stream.unfoldEval[Task, Option[String], RepoGQLResponse](Option.empty[String]) { cursorOpt => | |
| getPage(cursorOpt).map(res => | |
| if (res.data.repository.pullRequests.edges.isEmpty) None | |
| else Some((res, Some(res.data.repository.pullRequests.edges.last.cursor)))) | |
| } | |
| private val printStartEnd: Sink[Task, PullRequestNode] = | |
| _.map(n => println("Created at: " + n.createdAt + " and Merged at: " + n.mergedAt.fold("NOT MERGED")(_.toString))) | |
| def toBucket(node: PullRequestNode): Map[Int, (Int, Average)] = { | |
| val days = node.mergedAt.map { mAt => | |
| val startAsLocalDate = new DateTime(node.createdAt).toLocalDate | |
| val endAsLocalDate = new DateTime(mAt).toLocalDate | |
| Days.daysBetween(startAsLocalDate, endAsLocalDate).getDays | |
| }.getOrElse(999) | |
| Map(days -> (1, Average(node.commits.totalCount, 1))) | |
| } | |
| private def toNodes(res: RepoGQLResponse): Stream[Task, PullRequestNode] = | |
| Stream.emits(res.data.repository.pullRequests.edges.map(_.node)) | |
| private def prsStartingAt(cursor: Option[String]) = { | |
| Request(Method.POST, Uri.uri("https://api.github.com/graphql")).withBody(GraphQLQuery(query, variables(cursor))) | |
| } | |
| val query = | |
| graphql""" | |
| query($$cursorPosition: String) { | |
| repository(owner: "rjsvaljean", name: "some-repo") { | |
| pullRequests(first: 10, after: $$cursorPosition) { | |
| edges { | |
| cursor | |
| node { | |
| createdAt | |
| mergedAt | |
| commits { | |
| totalCount | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| """ | |
| private def variables(cursor: Option[String]) = Json.obj("cursorPosition" -> cursor.fold(Json.Null)(_.asJson)) | |
| val client = PooledHttp1Client() | |
| } | |
| case class GraphQLQuery(query: ast.Document, variables: Json) | |
| object GraphQLQuery { | |
| implicit val queryEncoder: Encoder[ast.Document] = | |
| Encoder.instance((doc: ast.Document) => QueryRenderer.render(doc).asJson) | |
| implicit val encoder: Encoder[GraphQLQuery] = deriveEncoder[GraphQLQuery] | |
| implicit val entityEncoder: EntityEncoder[GraphQLQuery] = jsonEncoderOf(encoder) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment