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: String
和parameters: 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