python zio_如何使用ZIO和Http4s创建简单的API客户端

python zio

与巴西朋友讨论了我们国家的情况后,我们意识到查找有关公共支出的信息有多么困难,并且在有可用信息的情况下,很难对此进行推理。 我们决定联合起来,探索巴西政府提供的一些数据,以期提供一种更简便的方法来可视化和了解如何使用公共资源。

起点是:找到一些数据进行分析,这些数据相对容易收集(至少从开发人员的角度而言)。 最好的候选者是Portal daTransparência (直译为Transparency Portal),该倡议旨在通过API或下载CSV文件提供公共数据。

是否有比编写客户端更好的方法来学习API? 因此,让我们使用ZIO + http4s客户端来实现

为什么选择ZIO?

在Scala UA中演讲后,有人问我最近在Scala生态系统中引起我注意的是什么。 我相信ZIO可以改变游戏规则,因为它“不适合功能性程序员”。 即使它严格基于功能原理,也不认为用户已经了解功能概念(这只是Monad !),这对于新加入的人员可能会感到恐惧。

ZIO提供的所有强大功能中,易于使用和采用的功能,就我而言,到目前为止,这是其最佳功能。 #Scala感谢您的 ZIO团队!

该写代码了,让我们开始定义一个ZIO模块

HttpClient模块

该API仅支持GET请求,这使得特征定义非常简单:

package pdt.http

import io.circe. Decoder
import org.http4s.client. Client
import zio._

object HttpClient  {
  type HttpClient  = Has [ Service ]

  trait Service  {
    protected val rootUrl = "http://www.transparencia.gov.br/api-de-dados/"

    def get [ T ](uri: String , parameters: Map [ String , String ])
              ( implicit d: Decoder [ T ]): Task [ T ]
  }

  def http4s : ZLayer [ Has [ Client [ Task ]], Nothing , HttpClient ] = ???
}

Service只有一种方法get[T] ,其参数resource: Stringparameters: Map[String, String] ,它将以"resource?key=value"格式成为url的一部分。 它也需要一个隐式的io.circe.Decoder[T] ,用于将json结果解码为T

get[T]返回zio.Task[T] ,它ZIO[Any, Throwable, T]的类型别名 ,它表示没有要求的效果,并且可能会以Throwable值失败或以T成功。

按照模块配方 ,我们有:

type HttpClient  = Has [ Service ]

简单来说, Has允许我们将Service用作依赖项。 下一行使您更容易理解:

def http4s : ZLayer [ Has [ Client [ Task ]], Nothing , HttpClient ] = ???

http4s方法将创建一个ZLayer ,它与ZIO数据类型非常相似; 它需要构建一个Has[Client[Task]] ,不会产生任何错误(这就是Nothing意思),并且将返回Service的实现: HttpClient ,这是我们使用Has定义的。

我们应该使用类型别名来使ZLayer更具表现力。 知道我们的层不会失败,我们可以使用URLayer

def http4s : URLayer [ Has [ Client [ Task ]], HttpClient ] = ???

http4s实际上会返回什么? 为了回答这个问题,我们需要首先实现HttpClient.Service

Http4s实现

实现get请求很简单:

package pdt.http

import io.circe. Decoder
import org.http4s. Uri
import org.http4s.circe. CirceEntityCodec .circeEntityDecoder
import org.http4s.client. Client
import org.http4s.client.dsl. Http4sClientDsl
import zio._
import zio.interop.catz._

private [http] final case class Http4s ( client: Client [ Task ] )
  extends HttpClient . Service with Http4sClientDsl [ Task ] {

  def get [ T ](resource: String , parameters: Map [ String , String ])
            ( implicit d: Decoder [ T ]): Task [ T ] = {
    val uri = Uri (path = rootUrl + resource).withQueryParams(parameters)

    client.expect[ T ](uri.toString())
  }

也许由于import zio.interop.catz._import zio.interop.catz._http4s构建在Cats Effect堆栈的顶部,因此我们需要interop-catz模块以实现互操作性

此类的实例不能在http包之外创建; 该实例将通过我们的ZLayer提供。 让我们回到HttpClient.http4s ,现在是实现它的时候了!

通过ZLayer提供HttpClient.Service

具有服务定义, ZLayer.fromService似乎合适:

object HttpClient  {

  def http4s : URLayer [ Has [ Client [ Task ]], HttpClient ] =
    ZLayer .fromService[ Client [ Task ], Service ] { http4sClient =>
      Http4s (http4sClient)
    }
}

好的,这一层使我们的HttpClient可用。 我们如何访问它? 让我们开始定义使用客户端的东西,一个具体的例子总是使学习变得容易:)

几个有用的助手

第一个资源, Acordos deLeniência是一个很好的候选人:

  • GET /acordos-leniencia/{id}返回一个对象;
  • GET /acordos-leniencia (使用过滤器作为查询参数)返回对象列表;

该API的其余部分对于其他资源公开的内容基本相同,只是具有更多的过滤器。 知道了这一点,我们可以定义两个助手,每个案例一个:

object HttpClient  {
  // ...

  def get [ T ](resource: String , id: Long )
            ( implicit d: Decoder [ T ]): RIO [ HttpClient , T ] =
    RIO .accessM[ HttpClient ](_.get.get[ T ]( s" $resource / $id " , Map ()))

  def get [ T ](resource: String , parameters: Map [ String , String ] = Map ())
            ( implicit d: Decoder [ T ]): RIO [ HttpClient , List [ T ]] =
    RIO .accessM[ HttpClient ](_.get.get[ List [ T ]](resource, parameters))
}
RIO.accessM[HttpClient]有效地访问了我们效果的环境,为我们提供了Has[HttpClient.Service] ,因此我们调用第一个get来访问由Has包装的效果-我们的Service-而第二个get是实际的get请求。

为了清楚起见,如果我们有一个post方法,代码将是:

RIO .accessM[ HttpClient ](_.get.post[ T ](resource, parameters))

好吧,让我们使整个工作正常!

一个具体的HttpClient…客户端(?!?!?)

同样, Acordos deLeniência是我们的资源。 这是一个case类,用于其可能的过滤器(巴西api,葡萄牙语名称):

case class AcordoLenienciaRequest ( 
              cnpjSancionado: Option [ String ] = None ,
              nomeSancionado: Option [ String ] = None ,
              situacao: Option [ String ] = None ,
              dataInicialSancao: Option [ LocalDate ] = None ,
              dataFinalSancao: Option [ LocalDate ] = None ,
              pagina: Int = 1 )

以及响应:

case class AcordoLeniencia ( 
              id: Long ,
              nomeEmpresa: String ,
              dataInicioAcordo: LocalDate ,
              dataFimAcordo: LocalDate ,
              orgaoResponsavel: String ,
              cnpj: String ,
              razaoSocial: String ,
              nomeFantasia: String ,
              ufEmpresa: String ,
              situacaoAcordo: String ,
              quantidade: Int )

AcordosLenienciaClient再简单不过了:

import io.circe.generic.auto._
import pdt.client.decoders.localDateDecoder
import pdt.http. HttpClient .{ HttpClient , get}
import pdt.domain.{ AcordoLeniencia , AcordoLenienciaRequest => ALRequest }
import pdt.http.implicits. HttpRequestOps
import zio._

object AcordosLenienciaClient  {

  def by (id: Long ): RIO [ HttpClient , AcordoLeniencia ] =
    get[ AcordoLeniencia ]( "acordos-leniencia" , id)

  def by (request: ALRequest ): RIO [ HttpClient , List [ AcordoLeniencia ]] =
    get[ AcordoLeniencia ]( "acordos-leniencia" , request.parameters)
}
隐式方法HttpRequestOps.parameters将任何请求转换为Map[String, String]看看我是如何使用无定形的

现在我们只需要将所有部分放在一起,整理出依存关系,
这种事情。 那发生在世界末日……也被称为Main

ZIO模块,组装!

这是一个请求获取AcordoLeniencia列表的AcordoLeniencia

val program = for {
  result <- AcordosLeniencia .by( AcordoLenienciaRequest ())
  _ <- putStrLn(result.toString())
} yield ()

它需要ZLayer生成HttpClient ,该Client[Task]具有Client[Task]作为其自己的依赖项。 首先,将Client[Task]创建为托管资源

private def makeHttpClient : UIO [ TaskManaged [ Client [ Task ]]] =
  ZIO .runtime[ Any ].map { implicit rts =>
      BlazeClientBuilder
        .apply[ Task ]( Implicits .global)
        .resource
        .toManaged
    }

现在我们可以整理图层:

val httpClientLayer = makeHttpClient.toLayer.orDie
val http4sClientLayer = httpClientLayer >>> HttpClient .http4s

最后,为我们的program提供所需的层:

program.provideSomeLayer[ZEnv ](http4sClientLayer)

准备好出发:

program.foldM(
  e => putStrLn(s"Execution failed with: ${e.printStackTrace()} " ) *> ZIO .succeed( 1 ),
  _ => ZIO .succeed( 0 )
)

这就是我的构建方式。 为了学习目的,此处的某些代码与原始代码有所不同。 您可以在Github上找到代码

在得出结论之前,让我们巩固所学到的知识,添加一个新的依赖项,一个logger ,该logger在控制台中打印所请求的url,以及如果失败则显示错误消息。

逐步添加新的依赖项

Logger模块定义:

object Logger  {
  type Logger  = Has [ Service ]

  trait Service  {
    def info (message: => String ): UIO [ Unit ]
    def error (t: Throwable )(message: => String ): UIO [ Unit ]
  }
}

实现,打印到控制台:

import zio.console.{ Console => ConsoleZIO }

case class Console ( console: ConsoleZIO . Service )
  extends Logger . Service {

  def info (message: => String ): UIO [ Unit ] =
    console.putStrLn(message)

  def error (t: Throwable )(message: => String ): UIO [ Unit ] =
    for {
      _ <- console.putStrLn(message)
      _ <- console.putStrLn(t.stackTrace)
    } yield ()
}

Logger通过ZLayer使实现可用:

object Logger  {

  def console : URLayer [ ConsoleZIO , Logger ] =
    ZLayer .fromService[ ConsoleZIO . Service , Service ] { console =>
      Console (console)
    }
}

Http4s现在可以接收和使用logger实例:

private [http] final case class Http4s ( logger: Logger . Service , client: Client [ Task ] )
  extends HttpClient . Service with Http4sClientDsl [ Task ] {

  def get [ T ](resource: String , parameters: Map [ String , String ])
            ( implicit d: Decoder [ T ]): Task [ T ] = {
    val uri = Uri (path = rootUrl + resource).withQueryParams(parameters)

    logger.info( s"GET REQUEST: $uri " ) *>
      client
        .expect[ T ](uri.toString())
        .foldM(
          e => logger.error(e)( "Request failed" ) *> IO .fail(e),
          ZIO .succeed(_))
  }
}

http4s层需要适应:

object HttpClient  {

  def http4s : URLayer [ Logger with Has [ Client [ Task ]], HttpClient ] =
    ZLayer .fromServices[ Logger . Service , Client [ Task ], Service ] {
      (logger, http4sClient) =>
        Http4s (logger, http4sClient)
    }
}

让我们为program提供新的依赖项。 更改位于提供的层中:

val http4sClientLayer = (loggerLayer ++ httpClientLayer) >>> HttpClient .http4s

做完了!

摘要

我第一次接触ZIO的经历非常愉快。 为了解决依赖关系,编译器在我们这边发挥作用。 每次缺少某些内容时,我们都会在编译时出错,并明确指出缺少的内容。 此外, ZLayer使依赖关系解析极其简单和可扩展(例如考虑添加FileLogger ),而没有任何魔术。

有什么建议可以改进该代码吗? 请分享!

参考资料

最初于 2020年4月20日 发布于 https://juliano-alves.com

翻译自: https://hackernoon.com/how-to-create-simple-api-client-with-zio-and-http4s-ed1r3uey

python zio

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值