Add the carbon api (#351)

This commit is contained in:
Asim Aslam
2022-01-07 11:55:41 +00:00
committed by GitHub
parent a093abaf5e
commit f505d4b857
15 changed files with 650 additions and 2 deletions

2
carbon/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
carbon

3
carbon/Dockerfile Normal file
View File

@@ -0,0 +1,3 @@
FROM alpine
ADD carbon /carbon
ENTRYPOINT [ "/carbon" ]

28
carbon/Makefile Normal file
View File

@@ -0,0 +1,28 @@
GOPATH:=$(shell go env GOPATH)
.PHONY: init
init:
go get -u github.com/golang/protobuf/proto
go get -u github.com/golang/protobuf/protoc-gen-go
go get github.com/micro/micro/v3/cmd/protoc-gen-micro
go get github.com/micro/micro/v3/cmd/protoc-gen-openapi
.PHONY: api
api:
protoc --openapi_out=. --proto_path=. proto/carbon.proto
.PHONY: proto
proto:
protoc --proto_path=. --micro_out=. --go_out=:. proto/carbon.proto
.PHONY: build
build:
go build -o carbon *.go
.PHONY: test
test:
go test -v ./... -cover
.PHONY: docker
docker:
docker build . -t carbon:latest

9
carbon/README.md Normal file
View File

@@ -0,0 +1,9 @@
Purchase carbon offsets
# Carbon Service
Purchase carbon offsets to help climate change and achieve carbon neutrality.
Offset purchases are currently limited to 1kg (or 0.001 tonnes) per request.
Make multiple requests to purchase the required amount.
Powered by [Ecologi](https://ecologi.com/)

21
carbon/domain/domain.go Normal file
View File

@@ -0,0 +1,21 @@
package domain
type Project struct {
Name string `json:"name"`
Percentage float64 `json:"splitPercentage"`
Tonnes float64 `json:"splitAmountTonnes"`
}
type OffsetRequest struct {
Number int32 `json:"number"`
Units string `json:"units"`
}
type OffsetResponse struct {
Number int32 `json:"number"`
Units string `json:"units"`
Tonnes float64 `json:"numberInTonnes"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Projects []Project `json:"projectDetails"`
}

24
carbon/examples.json Normal file
View File

@@ -0,0 +1,24 @@
{
"offset": [{
"title": "Offset Carbon",
"run_check": false,
"request": {},
"response": {
"units": 1,
"metric": "KG",
"tonnes": 0.001,
"projects": [
{
"name": "Producing electricity from wind power in Northeast Thailand",
"percentage": 98,
"tonnes": 0.001
},
{
"name": "Wind power generation in Bac Lieu Province, Vietnam",
"percentage": 2,
"tonnes": 0
}
]
}
}]
}

3
carbon/generate.go Normal file
View File

@@ -0,0 +1,3 @@
package main
//go:generate make proto

78
carbon/handler/carbon.go Normal file
View File

@@ -0,0 +1,78 @@
package handler
import (
"context"
"github.com/micro/micro/v3/service/config"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/services/carbon/domain"
"github.com/micro/services/pkg/api"
pb "github.com/micro/services/carbon/proto"
)
type Carbon struct {
apiKey string
apiAddress string
}
func New() *Carbon {
v, err := config.Get("carbon.api_key")
if err != nil {
logger.Fatalf("carbon.api_key config not found: %v", err)
}
apiKey := v.String("")
if len(apiKey) == 0 {
logger.Fatal("carbon.api_key config not found")
}
api.SetKey("Authorization", "Bearer "+apiKey)
api.SetKey("Content-Type", "application/json")
v, err = config.Get("carbon.api_address")
if err != nil {
logger.Fatalf("carbon.api_address config not found: %v", err)
}
apiAddress := v.String("")
if len(apiKey) == 0 {
logger.Fatal("carbon.api_address config not found")
}
return &Carbon{
apiKey: apiKey,
apiAddress: apiAddress,
}
}
func (c *Carbon) Offset(ctx context.Context, req *pb.OffsetRequest, rsp *pb.OffsetResponse) error {
var resp domain.OffsetResponse
// currently do not support options
r := &domain.OffsetRequest{
Number: 1,
Units: "KG",
}
if err := api.Post(c.apiAddress+"/impact/carbon", r, &resp); err != nil {
logger.Error("Failed to purchase offsets: ", err.Error())
return errors.InternalServerError("carbon.offset", "failed to purchase offsets")
}
logger.Infof("Purchased %d %s: %v\n", r.Number, r.Units, resp)
rsp.Units = resp.Number
rsp.Metric = resp.Units
rsp.Tonnes = resp.Tonnes
//rsp.Cost = resp.Amount
//rsp.Currency = resp.Currency
for _, p := range resp.Projects {
rsp.Projects = append(rsp.Projects, &pb.Project{
Name: p.Name,
Percentage: p.Percentage,
Tonnes: p.Tonnes,
})
}
return nil
}

24
carbon/main.go Normal file
View File

@@ -0,0 +1,24 @@
package main
import (
"github.com/micro/micro/v3/service"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/services/carbon/handler"
pb "github.com/micro/services/carbon/proto"
)
func main() {
// Create service
srv := service.New(
service.Name("carbon"),
service.Version("latest"),
)
// Register handler
pb.RegisterCarbonHandler(srv.Server(), handler.New())
// Run service
if err := srv.Run(); err != nil {
logger.Fatal(err)
}
}

1
carbon/micro.mu Normal file
View File

@@ -0,0 +1 @@
service carbon

321
carbon/proto/carbon.pb.go Normal file
View File

@@ -0,0 +1,321 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.27.1
// protoc v3.15.6
// source: proto/carbon.proto
package carbon
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Project struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// name of the project
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// percentage that went to this
Percentage float64 `protobuf:"fixed64,2,opt,name=percentage,proto3" json:"percentage,omitempty"`
// amount in tonnes
Tonnes float64 `protobuf:"fixed64,3,opt,name=tonnes,proto3" json:"tonnes,omitempty"`
}
func (x *Project) Reset() {
*x = Project{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_carbon_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Project) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Project) ProtoMessage() {}
func (x *Project) ProtoReflect() protoreflect.Message {
mi := &file_proto_carbon_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Project.ProtoReflect.Descriptor instead.
func (*Project) Descriptor() ([]byte, []int) {
return file_proto_carbon_proto_rawDescGZIP(), []int{0}
}
func (x *Project) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Project) GetPercentage() float64 {
if x != nil {
return x.Percentage
}
return 0
}
func (x *Project) GetTonnes() float64 {
if x != nil {
return x.Tonnes
}
return 0
}
// Purchase 1kg (0.001 tonnes) of carbon offsets in a single request
type OffsetRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *OffsetRequest) Reset() {
*x = OffsetRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_carbon_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *OffsetRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*OffsetRequest) ProtoMessage() {}
func (x *OffsetRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_carbon_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use OffsetRequest.ProtoReflect.Descriptor instead.
func (*OffsetRequest) Descriptor() ([]byte, []int) {
return file_proto_carbon_proto_rawDescGZIP(), []int{1}
}
type OffsetResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// number of units purchased
Units int32 `protobuf:"varint,1,opt,name=units,proto3" json:"units,omitempty"`
// the metric used e.g KG or Tonnes
Metric string `protobuf:"bytes,2,opt,name=metric,proto3" json:"metric,omitempty"`
// number of tonnes
Tonnes float64 `protobuf:"fixed64,3,opt,name=tonnes,proto3" json:"tonnes,omitempty"`
// projects it was allocated to
Projects []*Project `protobuf:"bytes,4,rep,name=projects,proto3" json:"projects,omitempty"`
}
func (x *OffsetResponse) Reset() {
*x = OffsetResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_carbon_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *OffsetResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*OffsetResponse) ProtoMessage() {}
func (x *OffsetResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_carbon_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use OffsetResponse.ProtoReflect.Descriptor instead.
func (*OffsetResponse) Descriptor() ([]byte, []int) {
return file_proto_carbon_proto_rawDescGZIP(), []int{2}
}
func (x *OffsetResponse) GetUnits() int32 {
if x != nil {
return x.Units
}
return 0
}
func (x *OffsetResponse) GetMetric() string {
if x != nil {
return x.Metric
}
return ""
}
func (x *OffsetResponse) GetTonnes() float64 {
if x != nil {
return x.Tonnes
}
return 0
}
func (x *OffsetResponse) GetProjects() []*Project {
if x != nil {
return x.Projects
}
return nil
}
var File_proto_carbon_proto protoreflect.FileDescriptor
var file_proto_carbon_proto_rawDesc = []byte{
0x0a, 0x12, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x61, 0x72, 0x62, 0x6f, 0x6e, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x63, 0x61, 0x72, 0x62, 0x6f, 0x6e, 0x22, 0x55, 0x0a, 0x07,
0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70,
0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52,
0x0a, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x74,
0x6f, 0x6e, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x06, 0x74, 0x6f, 0x6e,
0x6e, 0x65, 0x73, 0x22, 0x0f, 0x0a, 0x0d, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x22, 0x83, 0x01, 0x0a, 0x0e, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x75, 0x6e, 0x69, 0x74, 0x73,
0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x75, 0x6e, 0x69, 0x74, 0x73, 0x12, 0x16, 0x0a,
0x06, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d,
0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x6f, 0x6e, 0x6e, 0x65, 0x73, 0x18,
0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x06, 0x74, 0x6f, 0x6e, 0x6e, 0x65, 0x73, 0x12, 0x2b, 0x0a,
0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x0f, 0x2e, 0x63, 0x61, 0x72, 0x62, 0x6f, 0x6e, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74,
0x52, 0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x32, 0x43, 0x0a, 0x06, 0x43, 0x61,
0x72, 0x62, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x06, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x15,
0x2e, 0x63, 0x61, 0x72, 0x62, 0x6f, 0x6e, 0x2e, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x63, 0x61, 0x72, 0x62, 0x6f, 0x6e, 0x2e, 0x4f,
0x66, 0x66, 0x73, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42,
0x10, 0x5a, 0x0e, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3b, 0x63, 0x61, 0x72, 0x62, 0x6f,
0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_proto_carbon_proto_rawDescOnce sync.Once
file_proto_carbon_proto_rawDescData = file_proto_carbon_proto_rawDesc
)
func file_proto_carbon_proto_rawDescGZIP() []byte {
file_proto_carbon_proto_rawDescOnce.Do(func() {
file_proto_carbon_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_carbon_proto_rawDescData)
})
return file_proto_carbon_proto_rawDescData
}
var file_proto_carbon_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_proto_carbon_proto_goTypes = []interface{}{
(*Project)(nil), // 0: carbon.Project
(*OffsetRequest)(nil), // 1: carbon.OffsetRequest
(*OffsetResponse)(nil), // 2: carbon.OffsetResponse
}
var file_proto_carbon_proto_depIdxs = []int32{
0, // 0: carbon.OffsetResponse.projects:type_name -> carbon.Project
1, // 1: carbon.Carbon.Offset:input_type -> carbon.OffsetRequest
2, // 2: carbon.Carbon.Offset:output_type -> carbon.OffsetResponse
2, // [2:3] is the sub-list for method output_type
1, // [1:2] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_proto_carbon_proto_init() }
func file_proto_carbon_proto_init() {
if File_proto_carbon_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_proto_carbon_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Project); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_carbon_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*OffsetRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_carbon_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*OffsetResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_carbon_proto_rawDesc,
NumEnums: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_proto_carbon_proto_goTypes,
DependencyIndexes: file_proto_carbon_proto_depIdxs,
MessageInfos: file_proto_carbon_proto_msgTypes,
}.Build()
File_proto_carbon_proto = out.File
file_proto_carbon_proto_rawDesc = nil
file_proto_carbon_proto_goTypes = nil
file_proto_carbon_proto_depIdxs = nil
}

View File

@@ -0,0 +1,93 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: proto/carbon.proto
package carbon
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
import (
context "context"
api "github.com/micro/micro/v3/service/api"
client "github.com/micro/micro/v3/service/client"
server "github.com/micro/micro/v3/service/server"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// Reference imports to suppress errors if they are not otherwise used.
var _ api.Endpoint
var _ context.Context
var _ client.Option
var _ server.Option
// Api Endpoints for Carbon service
func NewCarbonEndpoints() []*api.Endpoint {
return []*api.Endpoint{}
}
// Client API for Carbon service
type CarbonService interface {
Offset(ctx context.Context, in *OffsetRequest, opts ...client.CallOption) (*OffsetResponse, error)
}
type carbonService struct {
c client.Client
name string
}
func NewCarbonService(name string, c client.Client) CarbonService {
return &carbonService{
c: c,
name: name,
}
}
func (c *carbonService) Offset(ctx context.Context, in *OffsetRequest, opts ...client.CallOption) (*OffsetResponse, error) {
req := c.c.NewRequest(c.name, "Carbon.Offset", in)
out := new(OffsetResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Carbon service
type CarbonHandler interface {
Offset(context.Context, *OffsetRequest, *OffsetResponse) error
}
func RegisterCarbonHandler(s server.Server, hdlr CarbonHandler, opts ...server.HandlerOption) error {
type carbon interface {
Offset(ctx context.Context, in *OffsetRequest, out *OffsetResponse) error
}
type Carbon struct {
carbon
}
h := &carbonHandler{hdlr}
return s.Handle(s.NewHandler(&Carbon{h}, opts...))
}
type carbonHandler struct {
CarbonHandler
}
func (h *carbonHandler) Offset(ctx context.Context, in *OffsetRequest, out *OffsetResponse) error {
return h.CarbonHandler.Offset(ctx, in, out)
}

32
carbon/proto/carbon.proto Normal file
View File

@@ -0,0 +1,32 @@
syntax = "proto3";
package carbon;
option go_package = "./proto;carbon";
service Carbon {
rpc Offset(OffsetRequest) returns (OffsetResponse) {}
}
message Project {
// name of the project
string name = 1;
// percentage that went to this
double percentage = 2;
// amount in tonnes
double tonnes = 3;
}
// Purchase 1kg (0.001 tonnes) of carbon offsets in a single request
message OffsetRequest {}
message OffsetResponse {
// number of units purchased
int32 units = 1;
// the metric used e.g KG or Tonnes
string metric = 2;
// number of tonnes
double tonnes = 3;
// projects it was allocated to
repeated Project projects = 4;
}

9
carbon/publicapi.json Normal file
View File

@@ -0,0 +1,9 @@
{
"name": "carbon",
"icon": "🌋",
"category": "climate",
"display_name": "Carbon",
"pricing": {
"Carbon.Offset": 5000000
}
}

View File

@@ -40,7 +40,7 @@ func Get(url string, rsp interface{}) error {
return err
}
if resp.StatusCode != 200 {
if resp.StatusCode >= 400 {
return fmt.Errorf("Non 200 response %v: %v", resp.StatusCode, string(b))
}
@@ -78,7 +78,7 @@ func Post(url string, ureq, rsp interface{}) error {
return err
}
if resp.StatusCode != 200 {
if resp.StatusCode >= 400 {
return fmt.Errorf("Non 200 response %v: %v", resp.StatusCode, string(b))
}