Skip to content

Instantly share code, notes, and snippets.

@dacr
Last active April 2, 2023 10:10
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/88966cba361061be20d50534711a6c2a to your computer and use it in GitHub Desktop.
Save dacr/88966cba361061be20d50534711a6c2a to your computer and use it in GitHub Desktop.
Publish code examples to remote repositories solutions (DEPRECATED see https://github.com/dacr/code-examples-manager). / published by https://github.com/dacr/code-examples-manager #82d07ca5-6b9c-4f6e-bc48-23ca7af16835/50301e440382f42c7f8f18d52ebeb513194dee79
#!/usr/bin/env amm
// summary : Publish code examples to remote repositories solutions (DEPRECATED see https://github.com/dacr/code-examples-manager).
// keywords : scala, github-api, gitlab-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 : 82d07ca5-6b9c-4f6e-bc48-23ca7af16835
// 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'
import $ivy.`com.github.pathikrit::better-files:3.8.0`
import $ivy.`org.scalatest::scalatest:3.0.8`
import $ivy.`com.softwaremill.sttp::core:1.6.4`
import $ivy.`com.softwaremill.sttp::json4s:1.6.4`
import $ivy.`com.softwaremill.sttp::okhttp-backend:1.6.4`
import $ivy.`org.json4s::json4s-native:3.6.7`
import better.files._
import better.files.Dsl._
import org.scalatest._
import org.scalatest.OptionValues._
import com.softwaremill.sttp.Uri
import com.softwaremill.sttp.json4s._
import com.softwaremill.sttp._
import scala.util.{Either, Left, Right}
import org.json4s.JValue
object Helpers {
implicit val serialization = org.json4s.native.Serialization
implicit val formats = org.json4s.DefaultFormats
def sha1(that: String): String = {
// Inspired from https://alvinalexander.com/source-code/scala-method-create-md5-hash-of-string
import java.security.MessageDigest
import java.math.BigInteger
val md = MessageDigest.getInstance("SHA-1")
val digest = md.digest(that.getBytes)
val bigInt = new BigInteger(1, digest)
val hashedString = bigInt.toString(16)
hashedString
}
}
import Helpers._
// ===================================================================================
// GITHUB MODEL
case class GistFileInfo(
filename: String,
`type`: String,
language: String,
raw_url: String,
size: Int,
)
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,
)
case class Gist(
id: String,
description: String,
html_url: String,
public: Boolean,
files: Map[String, GistFile],
)
case class GistFileSpec(
filename: String,
content: String
)
case class GistSpec(
description: String,
public: Boolean,
files: Map[String, GistFileSpec],
)
trait httpAPIOperations {
implicit val sttpBackend = com.softwaremill.sttp.okhttp.OkHttpSyncBackend()
}
object GitHubGistsOperations extends httpAPIOperations {
case class AuthToken(value: String) {
override def toString: String = value
}
def getUserGists(user: String)(implicit token: AuthToken): 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: AuthToken): 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: AuthToken): 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: AuthToken): 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]
}
}
}
// ===================================================================================
case class CodeExample(
file: File,
summary: Option[String],
keywords: List[String],
publish: List[String],
authors: List[String],
id: Option[String],
) {
def content: String = file.contentAsString
def filename: String = file.name
def fileExt: String = filename.split("[.]", 2).drop(1).headOption.getOrElse("")
}
object CodeExample {
def extractValue(from: String)(key: String): Option[String] = {
val RE = ("""(?m)(?i)^(?:(?://)|(?:##))\s+""" + key + """\s+:\s+(.*)$""").r
RE.findFirstIn(from).collect { case RE(value) => value.trim }
}
def extractValueList(from: String)(key: String): List[String] = {
extractValue(from)(key).map(_.split("""[ \t\r,;]+""").toList).getOrElse(Nil)
}
def apply(file: File): CodeExample = {
val content = file.contentAsString
CodeExample(
file = file,
summary = extractValue(content)("summary"),
keywords = extractValueList(content)("keywords"),
publish = extractValueList(content)("publish"),
authors = extractValueList(content)("authors"),
id = extractValue(content)("id"),
)
}
}
trait CodeExampleStatus {
val id: String
val example: CodeExample
val contentHash: String
}
case class GitHubGistCodeExampleStatus(
id:String,
example:CodeExample,
contentHash:String
) extends CodeExampleStatus
case class CodeExamples(
status: List[CodeExampleStatus]
)
object ExamplesManager {
def examples(): List[CodeExample] = {
pwd
.glob("*.{sc,sh}")
.map(CodeExample(_))
.toList
.filter(_.id.isDefined)
}
def updateGitHubGistRemoteExamples(): Unit = {
import GitHubGistsOperations._
implicit val token = AuthToken(scala.util.Properties.envOrElse("GIST_TOKEN", "invalid-token"))
val user = "dacr"
val remoteGists = getUserGists(user).toList
}
}
// ===================================================================================
object ExamplesManagerTest extends FlatSpec with Matchers {
override def suiteName: String = "ExamplesManagerTest"
"CodeExample" can "be constructed from a local file" in {
val ex1 = CodeExample(file"poc-1-examples-manager.sc")
ex1.fileExt shouldBe "sc"
ex1.filename shouldBe "poc-1-examples-manager.sc"
ex1.content should include regex "id [:] 82d07ca5-6b9c-4f6e-bc48-23ca7af16835"
}
"All examples" should "all have a summary" in {
for {example <- ExamplesManager.examples()} {
example.summary shouldBe 'defined
}
}
it should "have a distinct identifier" in {
val examplesById = ExamplesManager.examples().groupBy(_.id)
for { (id, examplesWithThisId) <- examplesById} {
examplesWithThisId should have size(1)
}
}
"ExamplesManager" should "be able to list locally available examples" in {
val examplesByFileExt =
ExamplesManager
.examples()
.groupBy(_.fileExt)
.mapValues(_.size)
examplesByFileExt.get("sc").get should be > 0
examplesByFileExt.get("sh").get should be > 0
}
it should "be able to get a gist example using its IDs" in {
}
it should "be able to synchronize remotes" in {
}
}
// ===================================================================================
run(ExamplesManagerTest)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment