Skip to content

Commit d0aee0b

Browse files
committed
added sdk
1 parent c8efbe8 commit d0aee0b

18 files changed

+1791
-2
lines changed

README.md

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,54 @@
1-
# sdk-go
2-
Official Go SDK for Machine Box
1+
# Machine Box Go SDK
2+
3+
The official Machine Box Go SDK provides Go clients for each box.
4+
5+
## Usage
6+
7+
Go get the repo:
8+
9+
```
10+
go get github.com/machinebox/mb/exp/sdk-go
11+
```
12+
13+
Then import the package of the box you wish to use:
14+
15+
```go
16+
import "github.com/machinebox/mb/exp/sdk-go/facebox"
17+
```
18+
19+
Then create a client, providing the address of the running box.
20+
21+
(To get a box running locally, see the instructions at https://machinebox.io/account)
22+
23+
```go
24+
faceboxClient := facebox.New("http://localhost:8080")
25+
```
26+
27+
It is recommended that you consider the startup time a box needs before it
28+
is ready. The simplest approach is to use the boxutil.WaitForReady function:
29+
30+
```go
31+
err := boxutil.WaitForReady(ctx, faceboxClient)
32+
if err != nil {
33+
log.Fatalln("error waiting for box:", err)
34+
}
35+
```
36+
37+
A more advanced solution is to get notified whenever the status of a box changes
38+
using the boxutil.StatusChan feature:
39+
40+
```go
41+
go func(){
42+
statusChan := boxutil.StatusChan(ctx, faceboxClient)
43+
for {
44+
select {
45+
case status := <-statusChan:
46+
if !boxutil.IsReady(status) {
47+
log.Println("TODO: Pause work, the box isn't ready")
48+
} else {
49+
log.Println("TODO: resume work, the box is ready to go")
50+
}
51+
}
52+
}
53+
}()
54+
```

boxutil/info.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package boxutil
2+
3+
// Box represents a box client capable of returning
4+
// Info.
5+
type Box interface {
6+
Info() (*Info, error)
7+
}
8+
9+
// Info describes box information.
10+
type Info struct {
11+
Name string
12+
Version int
13+
Build string
14+
Status string
15+
}

boxutil/status.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package boxutil
2+
3+
import (
4+
"context"
5+
"errors"
6+
"time"
7+
)
8+
9+
// ErrCanceled is returned when the context cancels or times out
10+
// an operation.
11+
var ErrCanceled = errors.New("context is done")
12+
13+
// readyCheckInterval is the interval to wait between checking
14+
// the status in StatusChan.
15+
// Unexported because 1 second is sensible, but configurable to make
16+
// tests run quicker.
17+
var readyCheckInterval = 1 * time.Second
18+
19+
// StatusChan gets a channel that periodically gets the box info
20+
// and sends a message whenever the status changes.
21+
func StatusChan(ctx context.Context, i Box) <-chan string {
22+
statusChan := make(chan string)
23+
go func() {
24+
var lastStatus string
25+
for {
26+
select {
27+
case <-ctx.Done():
28+
return
29+
default:
30+
time.Sleep(readyCheckInterval)
31+
status := "unavailable"
32+
info, err := i.Info()
33+
if err == nil {
34+
status = info.Status
35+
}
36+
if status != lastStatus {
37+
lastStatus = status
38+
statusChan <- status
39+
}
40+
}
41+
}
42+
}()
43+
return statusChan
44+
}
45+
46+
// WaitForReady blocks until the Box is ready.
47+
func WaitForReady(ctx context.Context, i Box) error {
48+
ctx, cancel := context.WithCancel(ctx)
49+
defer cancel()
50+
statusChan := StatusChan(ctx, i)
51+
for {
52+
select {
53+
case <-ctx.Done():
54+
return ErrCanceled
55+
case status := <-statusChan:
56+
if IsReady(status) {
57+
return nil
58+
}
59+
}
60+
}
61+
}
62+
63+
// IsReady gets whether the box info status is ready or not.
64+
func IsReady(status string) bool {
65+
return status == "ready"
66+
}

boxutil/status_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package boxutil
2+
3+
import (
4+
"context"
5+
"errors"
6+
"sync"
7+
"testing"
8+
"time"
9+
10+
"github.com/matryer/is"
11+
)
12+
13+
func init() {
14+
// quicker for testing
15+
readyCheckInterval = 100 * time.Millisecond
16+
}
17+
18+
func TestStatusChan(t *testing.T) {
19+
is := is.New(t)
20+
21+
i := &testBox{}
22+
ctx, cancel := context.WithCancel(context.Background())
23+
defer cancel()
24+
25+
status := StatusChan(ctx, i)
26+
is.Equal(<-status, "starting...")
27+
i.setReady()
28+
is.Equal(<-status, "ready")
29+
i.setError()
30+
is.Equal(<-status, "unavailable")
31+
i.clearError()
32+
is.Equal(<-status, "ready")
33+
34+
}
35+
36+
func TestWaitForReady(t *testing.T) {
37+
is := is.New(t)
38+
i := &testBox{}
39+
ctx, cancel := context.WithCancel(context.Background())
40+
time.AfterFunc(300*time.Millisecond, cancel)
41+
go func() {
42+
time.Sleep(200 * time.Millisecond)
43+
i.setReady()
44+
}()
45+
err := WaitForReady(ctx, i)
46+
is.NoErr(err)
47+
}
48+
49+
func TestWaitForReadyTimeout(t *testing.T) {
50+
is := is.New(t)
51+
i := &testBox{}
52+
ctx, cancel := context.WithCancel(context.Background())
53+
time.AfterFunc(100*time.Millisecond, cancel)
54+
go func() {
55+
time.Sleep(200 * time.Millisecond)
56+
i.setReady()
57+
}()
58+
err := WaitForReady(ctx, i)
59+
is.Equal(err, ErrCanceled)
60+
}
61+
62+
type testBox struct {
63+
lock sync.Mutex
64+
ready bool
65+
err error
66+
}
67+
68+
func (i *testBox) setReady() {
69+
i.lock.Lock()
70+
defer i.lock.Unlock()
71+
i.ready = true
72+
}
73+
74+
func (i *testBox) setError() {
75+
i.lock.Lock()
76+
defer i.lock.Unlock()
77+
i.err = errors.New("cannot reach server")
78+
}
79+
80+
func (i *testBox) clearError() {
81+
i.lock.Lock()
82+
defer i.lock.Unlock()
83+
i.err = nil
84+
}
85+
86+
func (i *testBox) Info() (*Info, error) {
87+
i.lock.Lock()
88+
defer i.lock.Unlock()
89+
if i.err != nil {
90+
return nil, i.err
91+
}
92+
if i.ready {
93+
return &Info{Status: "ready"}, nil
94+
}
95+
return &Info{Status: "starting..."}, nil
96+
}

facebox/facebox.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Package facebox provides a client for accessing facebox services.
2+
package facebox
3+
4+
import (
5+
"encoding/json"
6+
"errors"
7+
"net/http"
8+
"net/url"
9+
"time"
10+
11+
"github.com/machinebox/mb/exp/sdk-go/boxutil"
12+
)
13+
14+
// Face represents a face in an image.
15+
type Face struct {
16+
Rect Rect
17+
ID string
18+
Name string
19+
Matched bool
20+
}
21+
22+
// Rect represents the coordinates of a face within an image.
23+
type Rect struct {
24+
Top, Left int
25+
Width, Height int
26+
}
27+
28+
// Similar represents a similar face.
29+
type Similar struct {
30+
ID string
31+
Name string
32+
}
33+
34+
// Client is an HTTP client that can make requests to the box.
35+
type Client struct {
36+
addr string
37+
38+
// HTTPClient is the http.Client that will be used to
39+
// make requests.
40+
HTTPClient *http.Client
41+
}
42+
43+
// make sure the Client implements boxutil.Box
44+
var _ boxutil.Box = (*Client)(nil)
45+
46+
// New creates a new Client.
47+
func New(addr string) *Client {
48+
return &Client{
49+
addr: addr,
50+
HTTPClient: &http.Client{
51+
Timeout: 10 * time.Second,
52+
},
53+
}
54+
}
55+
56+
// Info gets the details about the box.
57+
func (c *Client) Info() (*boxutil.Info, error) {
58+
var info boxutil.Info
59+
u, err := url.Parse(c.addr + "/info")
60+
if err != nil {
61+
return nil, err
62+
}
63+
if !u.IsAbs() {
64+
return nil, errors.New("box address must be absolute")
65+
}
66+
resp, err := c.HTTPClient.Get(u.String())
67+
if err != nil {
68+
return nil, err
69+
}
70+
defer resp.Body.Close()
71+
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
72+
return nil, err
73+
}
74+
return &info, nil
75+
}
76+
77+
// ErrFacebox represents an error from nudebox.
78+
type ErrFacebox string
79+
80+
func (e ErrFacebox) Error() string {
81+
return "facebox: " + string(e)
82+
}

facebox/facebox_check.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package facebox
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"io"
7+
"mime/multipart"
8+
"net/url"
9+
10+
"github.com/pkg/errors"
11+
)
12+
13+
// Check checks the image in the io.Reader for faces.
14+
func (c *Client) Check(image io.Reader) ([]Face, error) {
15+
var buf bytes.Buffer
16+
w := multipart.NewWriter(&buf)
17+
fw, err := w.CreateFormFile("file", "image.dat")
18+
if err != nil {
19+
return nil, err
20+
}
21+
_, err = io.Copy(fw, image)
22+
if err != nil {
23+
return nil, err
24+
}
25+
if err = w.Close(); err != nil {
26+
return nil, err
27+
}
28+
u, err := url.Parse(c.addr + "/facebox/check")
29+
if err != nil {
30+
return nil, err
31+
}
32+
if !u.IsAbs() {
33+
return nil, errors.New("box address must be absolute")
34+
}
35+
resp, err := c.HTTPClient.Post(u.String(), w.FormDataContentType(), &buf)
36+
if err != nil {
37+
return nil, err
38+
}
39+
defer resp.Body.Close()
40+
return c.parseCheckResponse(resp.Body)
41+
}
42+
43+
// CheckURL checks the image at the specified URL for faces.
44+
func (c *Client) CheckURL(imageURL *url.URL) ([]Face, error) {
45+
u, err := url.Parse(c.addr + "/facebox/check")
46+
if err != nil {
47+
return nil, err
48+
}
49+
if !u.IsAbs() {
50+
return nil, errors.New("box address must be absolute")
51+
}
52+
if !imageURL.IsAbs() {
53+
return nil, errors.New("url must be absolute")
54+
}
55+
form := url.Values{}
56+
form.Set("url", imageURL.String())
57+
resp, err := c.HTTPClient.PostForm(u.String(), form)
58+
if err != nil {
59+
return nil, err
60+
}
61+
defer resp.Body.Close()
62+
return c.parseCheckResponse(resp.Body)
63+
}
64+
65+
func (c *Client) parseCheckResponse(r io.Reader) ([]Face, error) {
66+
var checkResponse struct {
67+
Success bool
68+
Error string
69+
Faces []Face
70+
}
71+
if err := json.NewDecoder(r).Decode(&checkResponse); err != nil {
72+
return nil, errors.Wrap(err, "decoding response")
73+
}
74+
if !checkResponse.Success {
75+
return nil, ErrFacebox(checkResponse.Error)
76+
}
77+
return checkResponse.Faces, nil
78+
}

0 commit comments

Comments
 (0)