diff --git a/README.md b/README.md index 817c0a4..9e3cc25 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ sudo make install ## Upgrade -If you're using macOS and you previously installed the CLI with Homebrew, run the following: +If you previously installed the CLI with Homebrew, +you can upgrade to the latest version by running the following command: ```console brew upgrade replicate diff --git a/go.mod b/go.mod index ab1d284..6673c64 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/getkin/kin-openapi v0.123.0 github.com/golangci/golangci-lint v1.56.1 github.com/mattn/go-isatty v0.0.20 - github.com/replicate/replicate-go v0.15.0 + github.com/replicate/replicate-go v0.16.1 github.com/schollz/progressbar/v3 v3.13.1 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.8.4 diff --git a/go.sum b/go.sum index f840db8..56d2ed4 100644 --- a/go.sum +++ b/go.sum @@ -505,8 +505,10 @@ github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= -github.com/replicate/replicate-go v0.15.0 h1:irjAv6PI9H2o4H3sKkaHuQ35mKNC/0OQqNleYWNA4Sg= -github.com/replicate/replicate-go v0.15.0/go.mod h1:otIrl1vDmyjNhTzmVmp/mQU3Wt1+3387gFNEsAZq0ig= +github.com/replicate/replicate-go v0.16.0 h1:N4/BcRtMhDB6G2e2ecYXPQUxK5bCt1cm6KnriN7u290= +github.com/replicate/replicate-go v0.16.0/go.mod h1:otIrl1vDmyjNhTzmVmp/mQU3Wt1+3387gFNEsAZq0ig= +github.com/replicate/replicate-go v0.16.1 h1:LbImDfB6ef0yEfWbBNQdnC5CoKmHxonoa/UUJ6YrFC8= +github.com/replicate/replicate-go v0.16.1/go.mod h1:otIrl1vDmyjNhTzmVmp/mQU3Wt1+3387gFNEsAZq0ig= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= diff --git a/go.work.sum b/go.work.sum index f690501..4835dfc 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,6 +1,11 @@ +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/bombsimon/wsl/v3 v3.4.0/go.mod h1:KkIB+TXkqy6MvK9BDZVbZxKNYsE1/oLRJbIFtf14qqo= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -10,16 +15,22 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= @@ -28,6 +39,7 @@ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZ github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= @@ -49,6 +61,8 @@ github.com/replicate/replicate-go v0.8.1 h1:Mza5hWR/R1akZRKwXtA/CQJ2pY4/B9fSCYX+ github.com/replicate/replicate-go v0.8.1/go.mod h1:k9C4+PaYa9+hMRjn4D7ZPHOCUFb8P4jhytsCqcGa2vU= github.com/replicate/replicate-go v0.13.0 h1:DWpSw8ck+dVK79jcVbg0iWJt4/ajcDaYX7FmiqSh2iI= github.com/replicate/replicate-go v0.13.0/go.mod h1:k9C4+PaYa9+hMRjn4D7ZPHOCUFb8P4jhytsCqcGa2vU= +github.com/replicate/replicate-go v0.16.1 h1:LbImDfB6ef0yEfWbBNQdnC5CoKmHxonoa/UUJ6YrFC8= +github.com/replicate/replicate-go v0.16.1/go.mod h1:otIrl1vDmyjNhTzmVmp/mQU3Wt1+3387gFNEsAZq0ig= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/shirou/gopsutil/v3 v3.23.10/go.mod h1:JIE26kpucQi+innVlAUnIEOSBhBUkirr5b44yr55+WE= @@ -64,8 +78,14 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3 go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.tmz.dev/musttag v0.7.1/go.mod h1:oJLkpR56EsIryktZJk/B0IroSMi37YWver47fibGh5U= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= diff --git a/internal/cmd/prediction/create.go b/internal/cmd/prediction/create.go index 17f88e2..d0ab4c1 100644 --- a/internal/cmd/prediction/create.go +++ b/internal/cmd/prediction/create.go @@ -59,7 +59,7 @@ var CreateCmd = &cobra.Command{ } separator := cmd.Flag("separator").Value.String() - inputs, err := util.ParseInputs(ctx, args[1:], stdin, separator) + inputs, err := util.ParseInputs(ctx, r8, args[1:], stdin, separator) if err != nil { return fmt.Errorf("failed to parse inputs: %w", err) } diff --git a/internal/cmd/training/create.go b/internal/cmd/training/create.go index a8e5192..0bbffbd 100644 --- a/internal/cmd/training/create.go +++ b/internal/cmd/training/create.go @@ -69,7 +69,7 @@ var CreateCmd = &cobra.Command{ } separator := cmd.Flag("separator").Value.String() - inputs, err := util.ParseInputs(ctx, args[1:], stdin, separator) + inputs, err := util.ParseInputs(ctx, r8, args[1:], stdin, separator) if err != nil { return fmt.Errorf("failed to parse inputs: %w", err) } diff --git a/internal/util/optparse.go b/internal/util/optparse.go index b167c69..76c4828 100644 --- a/internal/util/optparse.go +++ b/internal/util/optparse.go @@ -11,9 +11,10 @@ import ( "strings" "github.com/PaesslerAG/jsonpath" + "github.com/replicate/replicate-go" ) -func ParseInputs(ctx context.Context, args []string, stdin string, sep string) (map[string]string, error) { +func ParseInputs(ctx context.Context, r8 *replicate.Client, args []string, stdin string, sep string) (map[string]string, error) { re := regexp.MustCompile(`{{(.*?)}}`) inputs := make(map[string]string) @@ -55,9 +56,15 @@ func ParseInputs(ctx context.Context, args []string, stdin string, sep string) ( // Read from file if strings.HasPrefix(v, "@") { path := strings.TrimSpace(v[1:]) - downloadURL, err := UploadFile(ctx, path) + + file, err := r8.CreateFileFromPath(ctx, path, nil) if err != nil { - return nil, fmt.Errorf("failed to upload file: %w", err) + return nil, fmt.Errorf("failed to create file from path: %w", err) + } + + downloadURL := file.URLs["get"] + if downloadURL == "" { + return nil, fmt.Errorf("failed to get download URL for file") } v = downloadURL diff --git a/internal/util/upload.go b/internal/util/upload.go deleted file mode 100644 index 67d67f1..0000000 --- a/internal/util/upload.go +++ /dev/null @@ -1,109 +0,0 @@ -package util - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "os" - "path/filepath" - - "github.com/replicate/cli/internal/config" -) - -// uploadFile uploads a file to Replicate's experimental DreamBooth API and returns the URL -func UploadFile(ctx context.Context, path string) (string, error) { - // Open the file - file, err := os.Open(path) - if err != nil { - return "", fmt.Errorf("failed to open file: %w", err) - } - defer file.Close() - - // Get the upload URL - filename := filepath.Base(path) - request, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("https://dreambooth-api-experimental.replicate.com/v1/upload/%s", filename), nil) - if err != nil { - return "", fmt.Errorf("failed to create upload request: %w", err) - } - - token, exists := os.LookupEnv("REPLICATE_API_TOKEN") - if !exists { - var err error - token, err = config.GetAPIToken() - if err != nil { - return "", fmt.Errorf("failed to get API token: %w", err) - } - } - - request.Header.Set("Authorization", fmt.Sprintf("Token %s", token)) - resp, err := http.DefaultClient.Do(request) - if err != nil { - return "", fmt.Errorf("failed to get upload URL: %w", err) - } - if resp == nil { - return "", fmt.Errorf("failed to get upload URL") - } - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to get upload URL: %s", resp.Status) - } - - bodyBytes, err := io.ReadAll(resp.Body) - defer func() { _ = resp.Body.Close() }() - if err != nil { - return "", fmt.Errorf("failed to read upload response: %w", err) - } - - uploadResponse := &struct { - UploadURL string `json:"upload_url"` - ServingURL string `json:"serving_url"` - }{} - err = json.Unmarshal(bodyBytes, uploadResponse) - if err != nil { - return "", fmt.Errorf("failed to parse upload response: %w", err) - } - - // Upload the file - uploadURL, _ := url.Parse(uploadResponse.UploadURL) - servingURL, _ := url.Parse(uploadResponse.ServingURL) - if uploadURL == nil || servingURL == nil { - return "", fmt.Errorf("failed to parse upload URL: %w", err) - } - - request, err = http.NewRequestWithContext(ctx, "PUT", uploadURL.String(), nil) - if err != nil { - return "", fmt.Errorf("failed to create file upload request: %w", err) - } - - // Detect the content type - buff := make([]byte, 512) - _, err = file.Read(buff) - if err != nil { - return "", fmt.Errorf("failed to read file: %w", err) - } - contentType := http.DetectContentType(buff) - request.Header.Set("Content-Type", contentType) - - // Reset the file pointer - _, err = file.Seek(0, io.SeekStart) - if err != nil { - return "", fmt.Errorf("failed to reset file pointer: %w", err) - } - - request.Body = file - - resp, err = http.DefaultClient.Do(request) - if err != nil { - return "", fmt.Errorf("failed to upload data: %w", err) - } - if resp == nil { - return "", fmt.Errorf("failed to upload data") - } - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to upload data: %s", resp.Status) - } - - return servingURL.String(), nil -} diff --git a/internal/util/util_test.go b/internal/util/util_test.go index fc48511..2432346 100644 --- a/internal/util/util_test.go +++ b/internal/util/util_test.go @@ -2,24 +2,83 @@ package util_test import ( "context" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" "testing" "github.com/getkin/kin-openapi/openapi3" "github.com/replicate/cli/internal/util" + "github.com/replicate/replicate-go" "github.com/stretchr/testify/assert" ) func TestParseInputs(t *testing.T) { + ctx := context.Background() + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" && r.URL.Path == "/files" { + file := &replicate.File{ + URLs: map[string]string{ + "get": "https://api.replicate.com/v1/files/123", + }, + } + + w.WriteHeader(http.StatusCreated) + + w.Header().Set("Content-Type", "application/json") + data, err := json.Marshal(file) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write(data) + return + } + w.WriteHeader(http.StatusNotFound) + })) + defer mockServer.Close() + + httpClient := &http.Client{ + Transport: &http.Transport{ + Proxy: func(_ *http.Request) (*url.URL, error) { + return url.Parse(mockServer.URL) + }, + }, + } + + r8, err := replicate.NewClient( + replicate.WithBaseURL(mockServer.URL), + replicate.WithToken("test-token"), + replicate.WithHTTPClient(httpClient), + ) + if err != nil { + t.Fatalf("failed to create replicate client: %v", err) + } + + tempDir := t.TempDir() + tmpFilePath := filepath.Join(tempDir, "hello.txt") + if err := os.WriteFile(tmpFilePath, []byte("Hello, world!"), 0o666); err != nil { + t.Fatalf("failed to write to temp file: %v", err) + } + args := []string{ "integer=1", "number=1.0", "boolean=true", "string=hello", "array_of_integers=[1,2,3]", + "file=@" + tmpFilePath, } - ctx := context.Background() - inputs, err := util.ParseInputs(ctx, args, "", "=") + inputs, err := util.ParseInputs(ctx, r8, args, "", "=") + if err != nil { + t.Fatalf("failed to parse inputs: %v", err) + } assert.NoError(t, err) assert.Equal(t, map[string]string{ @@ -28,6 +87,7 @@ func TestParseInputs(t *testing.T) { "boolean": "true", "string": "hello", "array_of_integers": "[1,2,3]", + "file": "https://api.replicate.com/v1/files/123", }, inputs) }