Skip to content

Instantly share code, notes, and snippets.

@dacr
Last active April 2, 2023 10:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dacr/c525e743825ef10afe1e6bc72737554b to your computer and use it in GitHub Desktop.
Save dacr/c525e743825ef10afe1e6bc72737554b to your computer and use it in GitHub Desktop.
Publish code sample as github gist (DEPRECATED see https://github.com/dacr/code-examples-manager). / published by https://github.com/dacr/code-examples-manager #e4b1937b-e327-447c-ac75-9df5d4ede340/1118dfd208014e270e8a608143c3479c7f5363ca
#!/usr/bin/env amm
// summary : Publish code sample as github gist (DEPRECATED see https://github.com/dacr/code-examples-manager).
// keywords : scala, github-api, automation, sttp, json4s, betterfiles, gists-api
// publish : gist
// authors : David Crosson
// license : Apache NON-AI License Version 2.0 (https://raw.githubusercontent.com/non-ai-licenses/non-ai-licenses/main/NON-AI-APACHE2)
// id : e4b1937b-e327-447c-ac75-9df5d4ede340
// created-on : 2020-05-31T19:54:52Z
// managed-by : https://github.com/dacr/code-examples-manager
// execution : scala 2.12 ammonite script (http://ammonite.io/) - run as follow 'amm scriptname.sc'
/* Get an authorized access to github gist API :
- list authorizations : curl --user "dacr" https://api.github.com/authorizations
- create token : curl https://api.github.com/authorizations --user "dacr" --data '{"scopes":["gist"],"note":"testoauth"}'
- setup GIST_TOKEN environment variable with the previously generated token
- get token : not possible of course
- interesting link : https://gist.github.com/joyrexus/85bf6b02979d8a7b0308#oauth
*/
import $ivy.`com.softwaremill.sttp::core:1.5.17`
import $ivy.`com.softwaremill.sttp::json4s:1.5.17`
import $ivy.`com.softwaremill.sttp::okhttp-backend:1.5.17`
import $ivy.`org.json4s::json4s-native:3.6.5`
import $ivy.`org.scalatest::scalatest:3.0.6`
import $ivy.`com.github.pathikrit::better-files:3.7.1`
import com.softwaremill.sttp.Uri
import com.softwaremill.sttp.json4s._
import com.softwaremill.sttp._
import scala.util.{Either, Left, Right}
import org.json4s.JValue
import better.files._
import better.files.Dsl._
implicit val sttpBackend = com.softwaremill.sttp.okhttp.OkHttpSyncBackend()
implicit val serialization = org.json4s.native.Serialization
implicit val formats = org.json4s.DefaultFormats
trait CommonBase {
def filename:String
val basename = filename.split("[.]",2).head
}
trait CommonContentBase extends CommonBase{
def content:String
def hash:Int = scala.util.hashing.MurmurHash3.stringHash(basename + content)
def uuid:String = {
val RE = ("""(?m)(?i)^//\s+id\s+:\s+(.*)$""").r
RE.findFirstIn(content).collect { case RE(value) => value.trim }.get // Make it fail if no uuid was provided
}
}
case class GistFileInfo(
filename: String,
`type`: String,
language: String,
raw_url: String,
size: Int,
) extends CommonBase
case class GistInfo(
id: String,
description: String,
html_url: String,
public: Boolean,
files: Map[String, GistFileInfo],
)
case class GistFile(
filename: String,
`type`: String,
language: String,
raw_url: String,
size: Int,
truncated: Boolean,
content: String,
) extends CommonContentBase
case class Gist(
id: String,
description: String,
html_url: String,
public: Boolean,
files: Map[String, GistFile],
)
case class GistFileSpec(
filename: String,
content: String
) extends CommonContentBase
case class GistSpec(
description: String,
public: Boolean,
files: Map[String, GistFileSpec],
)
case class Token(value: String) {
override def toString: String = value
}
def getUserGists(user: String)(implicit token: Token): Stream[GistInfo] = {
val nextLinkRE =""".*<([^>]+)>; rel="next".*""".r
def worker(nextQuery: Option[Uri], currentRemaining: Iterable[GistInfo]): Stream[GistInfo] = {
(nextQuery, currentRemaining) match {
case (None, Nil) => Stream.empty
case (_, head :: tail) => head #:: worker(nextQuery, tail)
case (Some(query), Nil) =>
val response = {
sttp
.get(query)
.header("Authorization", s"token $token")
.response(asJson[Array[GistInfo]])
.send()
}
response.body match {
case Left(message) =>
System.err.println(s"List gists - Something wrong has happened : $message")
Stream.empty
case Right(gistsArray) =>
val next = response.header("Link") // it provides the link for the next & last page :)
val newNextQuery = next.collect { case nextLinkRE(uri) => uri"$uri" }
worker(newNextQuery, gistsArray.toList)
}
}
}
val count = 10
val startQuery = uri"https://api.github.com/users/$user/gists?page=1&per_page=$count"
worker(Some(startQuery), Nil)
}
def getGist(id: String)(implicit token: Token): Option[Gist] = {
val query = uri"https://api.github.com/gists/$id"
val response = {
sttp
.get(query)
.header("Authorization", s"token $token")
.response(asJson[Gist])
.send()
}
response.body match {
case Left(message) =>
System.err.println(s"Get gist - Something wrong has happened : $message")
None
case Right(gist) =>
Some(gist)
}
}
def addGist(gist: GistSpec)(implicit token: Token): Option[String] = {
val query = uri"https://api.github.com/gists"
val response = {
sttp
.body(gist)
.post(query)
.header("Authorization", s"token $token")
.response(asJson[JValue])
.send()
}
response.body match {
case Left(message) =>
System.err.println(s"Add gist - Something wrong has happened : $message")
None
case Right(jvalue) =>
(jvalue \ "id").extractOpt[String]
}
}
def updateGist(id: String, gist: GistSpec)(implicit token: Token): Option[String] = {
val query = uri"https://api.github.com/gists/$id"
val response = {
sttp
.body(gist)
.patch(query)
.header("Authorization", s"token $token")
.response(asJson[JValue])
.send()
}
response.body match {
case Left(message) =>
System.err.println(s"Update gist - Something wrong has happened : $message")
None
case Right(jvalue) =>
(jvalue \ "id").extractOpt[String]
}
}
implicit val token = Token(scala.util.Properties.envOrElse("GIST_TOKEN", "invalid-token"))
val user = "dacr"
val remoteGists = getUserGists(user).toList
case class LocalScript(
filename: String,
summary: Option[String],
keywords: List[String],
publish: List[String],
authors: List[String],
id: Option[String],
content: String) extends CommonContentBase
object LocalScript {
def apply(filename: String, content: String): LocalScript = {
def extractValue(key: String): Option[String] = {
val RE = ("""(?m)(?i)^//\s+"""+key+"""\s+:\s+(.*)$""").r
RE.findFirstIn(content).collect { case RE(value) => value.trim }
}
def extractValueList(key: String): List[String] = {
extractValue(key).map(_.split("""[ \t\r,;]+""").toList).getOrElse(Nil)
}
val summary: Option[String] = extractValue("summary")
val keywords: List[String] = extractValueList("keywords")
val publish: List[String] = extractValueList("publish")
val authors: List[String] = extractValueList("authors")
val id: Option[String] = extractValue("id")
LocalScript(
filename = filename,
summary = summary,
keywords = keywords,
publish = publish,
authors = authors,
id = id,
content = content,
)
}
}
def hasDuplicateUuids(scripts:List[LocalScript]):Boolean = {
val scriptsByUuid = scripts.filter(_.id.isDefined).groupBy(_.id)
scriptsByUuid.exists{case (Some(k), v) => v.size > 1}
}
def fileHasChanged(script:LocalScript, remote:Gist):Boolean = {
// it only manages one file by gist !
remote.files.values.headOption.filter(_.hash != script.hash).isDefined
}
// TODO : REFACTOR, that code smells bad
def checkAndOperateGist(script: LocalScript, remoteGists:List[GistInfo]):Unit = {
// TODO : base update check on UUID instead of file basename !!
val remoteGistInfoOption = remoteGists.find(_.files.values.exists(_.basename == script.basename))
val filename = script.basename+".scala"
val summary = script.summary.getOrElse("")
remoteGistInfoOption match {
case Some(gistInfo) => // Then check for update
getGist(gistInfo.id) match {
case None =>
println(s"Couldn't get ${gistInfo.id} gist details :( ")
case Some(remoteGist) if fileHasChanged(script, remoteGist) =>
val updatedGistFilename = filename
val updatedGistFile = GistFileSpec(updatedGistFilename, script.content)
val updatedGist = GistSpec(
description = summary,
public = true, // TODO : manage public:private gists
files = Map(updatedGistFilename -> updatedGistFile)
)
val result = updateGist(remoteGist.id, updatedGist)
println(s"Just updated $filename, gistId : $result")
case Some(remoteGist) =>
//println(s"No change for $filename, gistId : ${remoteGist.id}")
}
case None => //Then Add
val spec = GistSpec(
summary,
true, // TODO : manage public:private gists
Map(
filename -> GistFileSpec(filename, script.content)
)
)
val generatedId:Option[String] = addGist(spec)
println(s"Just added $filename, gistId : $generatedId")
}
}
val scripts = pwd.glob("*.sc").map(file => LocalScript(file.name, file.contentAsString)).toList
val gistScripts = scripts.filter(_.publish.contains("gist"))
val foundDuplicatedUuids = hasDuplicateUuids(scripts)
assert(foundDuplicatedUuids == false)
//scripts.foreach{sc => println(sc.filename+" : "+sc.summary.getOrElse("N/A") + "("+sc.keywords.mkString(",")+")")}
println("Not at all published scripts :")
scripts.filter(_.summary.isEmpty).sortBy(_.basename).foreach{ sc =>
println(" "+sc.filename)
}
println("available scripts : " + scripts.size)
println("duplicates check : " + foundDuplicatedUuids)
println("publishable gists : " + gistScripts.size)
for {
gistScript <- gistScripts //.filter(_.basename == "testing-pi")
} {
// script disabled (DEPRECATED see https://github.com/dacr/code-examples-manager).
// checkAndOperateGist(gistScript, remoteGists)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment