From 77f076e718436d945bc0dc49636a5748ced0399f Mon Sep 17 00:00:00 2001 From: marceloeatworld <20625497+marceloeatworld@users.noreply.github.com> Date: Wed, 28 Feb 2024 15:27:21 +0000 Subject: [PATCH 1/8] PostDeploymentPrediction From 40d3724a540c74251752e9fc692143cb6b46bec6 Mon Sep 17 00:00:00 2001 From: negativepromptman <20625497+marceloeatworld@users.noreply.github.com> Date: Wed, 28 Feb 2024 13:29:46 +0100 Subject: [PATCH 2/8] PostDeployment --- src/Data/PredictionData.php | 2 + src/Data/PredictionsData.php | 1 + src/PredictionsResource.php | 32 ++++++++++++++ src/Requests/PostDeploymentPrediction.php | 52 +++++++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 src/Requests/PostDeploymentPrediction.php diff --git a/src/Data/PredictionData.php b/src/Data/PredictionData.php index bd56cb9..340e479 100644 --- a/src/Data/PredictionData.php +++ b/src/Data/PredictionData.php @@ -16,6 +16,7 @@ final class PredictionData */ public function __construct( public string $id, + public string $model, public string $version, public string $createdAt, public ?string $completedAt, @@ -39,6 +40,7 @@ public static function fromResponse(Response $response): self return new self( id: $data['id'], + model: $data['model'], version: $data['version'], createdAt: $data['created_at'], completedAt: $data['completed_at'] ?? null, diff --git a/src/Data/PredictionsData.php b/src/Data/PredictionsData.php index 24197ee..06c6117 100644 --- a/src/Data/PredictionsData.php +++ b/src/Data/PredictionsData.php @@ -28,6 +28,7 @@ public static function fromResponse(Response $response): self foreach ($data['results'] as $result) { $results[] = new PredictionData( id: $result['id'], + model: $data['model'], version: $result['version'], createdAt: $result['created_at'], completedAt: $result['completed_at'], diff --git a/src/PredictionsResource.php b/src/PredictionsResource.php index 93e8738..d266007 100644 --- a/src/PredictionsResource.php +++ b/src/PredictionsResource.php @@ -7,6 +7,7 @@ use BenBjurstrom\Replicate\Requests\GetPrediction; use BenBjurstrom\Replicate\Requests\GetPredictions; use BenBjurstrom\Replicate\Requests\PostPrediction; +use BenBjurstrom\Replicate\Requests\PostDeploymentPrediction; use Exception; class PredictionsResource extends Resource @@ -84,4 +85,35 @@ public function withWebhook(string $url, ?array $events = ['completed']): self return $this; } + /** + * Creates a prediction for a specific deployment. + * + * @param string $fullDeploymentName Full name of the deployment. + * @param array $input The input data for the prediction. + * @return PredictionData The data of the created prediction. + * @throws Exception If the response does not contain the expected data. + */ + public function createForDeployment(string $fullDeploymentName, array $input): PredictionData + { + $request = new PostDeploymentPrediction($input, $fullDeploymentName); + + if ($this->webhookUrl) { + $request->body()->merge([ + 'webhook' => $this->webhookUrl, + 'webhook_events_filter' => $this->webhookEvents, + ]); + } + + $response = $this->connector->send($request); + + $data = $response->dtoOrFail(); + if (! $data instanceof PredictionData) { + throw new Exception('Unexpected data type'); + } + + return $data; + } + + + } diff --git a/src/Requests/PostDeploymentPrediction.php b/src/Requests/PostDeploymentPrediction.php new file mode 100644 index 0000000..9755c16 --- /dev/null +++ b/src/Requests/PostDeploymentPrediction.php @@ -0,0 +1,52 @@ + $input + * @param string $fullDeploymentName + */ + public function __construct( + protected array $input, + string $fullDeploymentName, + + ) { + $this->fullDeploymentName = $fullDeploymentName; + + } + + public function resolveEndpoint(): string + { + return sprintf('/deployments/%s/predictions', $this->fullDeploymentName); + } + + /** + * @return array + */ + protected function defaultBody(): array + { + return [ + 'input' => $this->input, + ]; + } + + public function createDtoFromResponse(Response $response): PredictionData + { + return PredictionData::fromResponse($response); + } +} \ No newline at end of file From bad360eb0cfbceb7fb84fcfe25724ee80cd6db3b Mon Sep 17 00:00:00 2001 From: negativepromptman <20625497+marceloeatworld@users.noreply.github.com> Date: Wed, 28 Feb 2024 16:59:31 +0100 Subject: [PATCH 3/8] Readme --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index d16b009..b68d1eb 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,29 @@ $data->id; // yfv4cakjzvh2lexxv7o5qzymqy Note that the input parameters will vary depending on what version (model) you're using. In this example version [db21e45d3f7023abc2a46ee38a23973f6dce16bb082a930b0c49861f96d1e5bf](https://replicate.com/stability-ai/stable-diffusion/versions/db21e45d3f7023abc2a46ee38a23973f6dce16bb082a930b0c49861f96d1e5bf) is a Stable Diffusion 2.1 model optimized for speed. ### +## Create a prediction for a specific deployment +ou can now create a prediction for a specific deployment using the createForDeployment method. This method takes the full name of the deployment and an array of data as input, sends the HTTP request, and returns the data of the created prediction. + +Here's an example of how to use it: +```php +$deploymentName = 'my-deployment'; +$input = [ + 'model' => 'stable-diffusion-2-1', + 'prompt' => 'a photo of an astronaut riding a horse on mars', + 'negative_prompt' => 'moon, alien, spaceship', + 'width' => 768, + 'height' => 768, + 'num_inference_steps' => 50, + 'guidance_scale' => 7.5, + 'scheduler' => 'DPMSolverMultistep', + 'seed' => null, +]; + +$data = $api->predictions()->createForDeployment($deploymentName, $input); +$data->id; // yfv4cakjzvh2lexxv7o5qzymqy +``` +### + ## Using with Laravel Begin by adding your credentials to your services config file. ```php From 0bc52abc8c9710dc340e1ab575b6625b2700c12b Mon Sep 17 00:00:00 2001 From: Marcelo <20625497+marceloeatworld@users.noreply.github.com> Date: Wed, 20 Nov 2024 02:46:40 +0000 Subject: [PATCH 4/8] Refactor namespace from BenBjurstrom to MarceloEatWorld and update README and composer.json --- README.md | 8 ++-- composer.json | 52 +++++++++++++++++------ src/Data/PredictionData.php | 2 +- src/Data/PredictionsData.php | 2 +- src/PredictionsResource.php | 16 +++---- src/Replicate.php | 2 +- src/Requests/GetPrediction.php | 4 +- src/Requests/GetPredictions.php | 4 +- src/Requests/PostDeploymentPrediction.php | 4 +- src/Requests/PostPrediction.php | 4 +- src/Requests/PostPredictionCancel.php | 4 +- src/Resource.php | 2 +- 12 files changed, 66 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index b68d1eb..bf28095 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ composer require benbjurstrom/replicate-php Create a new api instance. ```php -use BenBjurstrom\Replicate\Replicate; +use MarceloEatWorld\Replicate\Replicate; ... $api = new Replicate( @@ -146,7 +146,7 @@ $data->id; // la5xlbbrfzg57ip5jlx6obmm5y ### get() Use to get details about an existing prediction. If the prediction has completed the results will be under the output property. ```php -use BenBjurstrom\Replicate\Data\PredictionData; +use MarceloEatWorld\Replicate\Data\PredictionData; ... $id = 'la5xlbbrfzg57ip5jlx6obmm5y' /* @var PredictionData $data */ @@ -157,7 +157,7 @@ $data->output[0]; // https://replicate.delivery/pbxt/6UFOVtl1xCJPAFFiTB2tfveYBNR ### list() Use to get a cursor paginated list of predictions. Returns an PredictionsData object. ```php -use BenBjurstrom\Replicate\Data\PredictionsData +use MarceloEatWorld\Replicate\Data\PredictionsData ... /* @var PredictionsData $data */ @@ -171,7 +171,7 @@ $data->results[0]->id; // la5xlbbrfzg57ip5jlx6obmm5y ### create() Use to create a new prediction (invoke a model). Returns an PredictionData object. ```php -use BenBjurstrom\Replicate\Data\PredictionData; +use MarceloEatWorld\Replicate\Data\PredictionData; ... $version = '5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa'; $input = [ diff --git a/composer.json b/composer.json index 3a7c47b..4180a70 100644 --- a/composer.json +++ b/composer.json @@ -1,33 +1,57 @@ { - "name": "benbjurstrom/replicate-php", - "description": "A PHP client for the Replicate API", - "keywords": ["replicate", "php", "package"], + "name": "marceloeatworld/replicate-php", + "description": "A PHP client for the Replicate API with enhanced deployment support", + "keywords": [ + "replicate", + "php", + "package", + "api", + "deployment", + "ai", + "machine learning" + ], + "type": "library", "license": "MIT", "authors": [ { - "name": "Ben Bjurstrom", - "email": "bbjurstrom@gmail.com" + "name": "Marcelo", + "email": "votre@email.com", + "role": "Developer" } ], "require": { - "php": "^8.1.0", - "saloonphp/saloon": "^3.0" + "php": "^8.1", + "saloonphp/saloon": "^3.0", + "illuminate/support": "^9.0|^10.0", + "guzzlehttp/guzzle": "^7.0" }, "require-dev": { "laravel/pint": "^1.4", - "pestphp/pest": "^2.0.0", + "pestphp/pest": "^2.0", "pestphp/pest-plugin-arch": "2.5.0", "phpstan/phpstan": "^1.9.11", - "symfony/var-dumper": "^6.2.3" + "symfony/var-dumper": "^6.2.3", + "orchestra/testbench": "^7.0|^8.0", + "mockery/mockery": "^1.5" }, "autoload": { "psr-4": { - "BenBjurstrom\\Replicate\\": "src/" + "MarceloEatWorld\\Replicate\\": "src/" } }, "autoload-dev": { "psr-4": { - "Tests\\": "tests/" + "MarceloEatWorld\\Replicate\\Tests\\": "tests/" + } + }, + "extra": { + "laravel": { + "providers": [ + "MarceloEatWorld\\Replicate\\ReplicateServiceProvider" + ], + "aliases": { + "Replicate": "MarceloEatWorld\\Replicate\\Facades\\Replicate" + } } }, "minimum-stability": "dev", @@ -49,5 +73,9 @@ "@test:types", "@test:unit" ] + }, + "support": { + "issues": "https://github.com/Marceloeatworld/replicate-php/issues", + "source": "https://github.com/Marceloeatworld/replicate-php" } -} +} \ No newline at end of file diff --git a/src/Data/PredictionData.php b/src/Data/PredictionData.php index 340e479..307bab8 100644 --- a/src/Data/PredictionData.php +++ b/src/Data/PredictionData.php @@ -1,6 +1,6 @@ Date: Wed, 20 Nov 2024 02:58:10 +0000 Subject: [PATCH 5/8] Remove deprecated test files and update composer.json for cleaner structure --- composer.json | 46 ++------- tests/ArchTest.php | 5 - tests/Fixtures/Saloon/getPrediction.json | 1 - .../Saloon/getPredictionAstronaut.json | 1 - .../Fixtures/Saloon/getPredictionFailed.json | 1 - .../Saloon/getPredictionMultiple.json | 1 - tests/Fixtures/Saloon/getPredictions.json | 1 - .../Fixtures/Saloon/postPredictionAlice.json | 1 - .../Saloon/postPredictionAstronaut.json | 1 - .../Fixtures/Saloon/postPredictionCancel.json | 1 - tests/Pest.php | 35 ------- tests/PredictionResourceTest.php | 54 ----------- tests/Requests/GetPredictionTest.php | 93 ------------------- tests/Requests/GetPredictionsTest.php | 26 ------ tests/Requests/PostPredictionCancelTest.php | 21 ----- tests/Requests/PostPredictionTest.php | 52 ----------- 16 files changed, 9 insertions(+), 331 deletions(-) delete mode 100644 tests/ArchTest.php delete mode 100644 tests/Fixtures/Saloon/getPrediction.json delete mode 100644 tests/Fixtures/Saloon/getPredictionAstronaut.json delete mode 100644 tests/Fixtures/Saloon/getPredictionFailed.json delete mode 100644 tests/Fixtures/Saloon/getPredictionMultiple.json delete mode 100644 tests/Fixtures/Saloon/getPredictions.json delete mode 100644 tests/Fixtures/Saloon/postPredictionAlice.json delete mode 100644 tests/Fixtures/Saloon/postPredictionAstronaut.json delete mode 100644 tests/Fixtures/Saloon/postPredictionCancel.json delete mode 100644 tests/Pest.php delete mode 100644 tests/PredictionResourceTest.php delete mode 100644 tests/Requests/GetPredictionTest.php delete mode 100644 tests/Requests/GetPredictionsTest.php delete mode 100644 tests/Requests/PostPredictionCancelTest.php delete mode 100644 tests/Requests/PostPredictionTest.php diff --git a/composer.json b/composer.json index 4180a70..0c84aa3 100644 --- a/composer.json +++ b/composer.json @@ -1,38 +1,24 @@ { "name": "marceloeatworld/replicate-php", - "description": "A PHP client for the Replicate API with enhanced deployment support", - "keywords": [ - "replicate", - "php", - "package", - "api", - "deployment", - "ai", - "machine learning" - ], - "type": "library", + "description": "A PHP client for the Replicate API", + "keywords": ["replicate", "php", "package"], "license": "MIT", "authors": [ { - "name": "Marcelo", - "email": "votre@email.com", - "role": "Developer" + "name": "Marcelo Pereira", + "email": "diagngo@gmail.com" } ], "require": { - "php": "^8.1", - "saloonphp/saloon": "^3.0", - "illuminate/support": "^9.0|^10.0", - "guzzlehttp/guzzle": "^7.0" + "php": "^8.1.0", + "saloonphp/saloon": "^3.0" }, "require-dev": { "laravel/pint": "^1.4", - "pestphp/pest": "^2.0", + "pestphp/pest": "^2.0.0", "pestphp/pest-plugin-arch": "2.5.0", "phpstan/phpstan": "^1.9.11", - "symfony/var-dumper": "^6.2.3", - "orchestra/testbench": "^7.0|^8.0", - "mockery/mockery": "^1.5" + "symfony/var-dumper": "^6.2.3" }, "autoload": { "psr-4": { @@ -41,17 +27,7 @@ }, "autoload-dev": { "psr-4": { - "MarceloEatWorld\\Replicate\\Tests\\": "tests/" - } - }, - "extra": { - "laravel": { - "providers": [ - "MarceloEatWorld\\Replicate\\ReplicateServiceProvider" - ], - "aliases": { - "Replicate": "MarceloEatWorld\\Replicate\\Facades\\Replicate" - } + "Tests\\": "tests/" } }, "minimum-stability": "dev", @@ -73,9 +49,5 @@ "@test:types", "@test:unit" ] - }, - "support": { - "issues": "https://github.com/Marceloeatworld/replicate-php/issues", - "source": "https://github.com/Marceloeatworld/replicate-php" } } \ No newline at end of file diff --git a/tests/ArchTest.php b/tests/ArchTest.php deleted file mode 100644 index e9a2655..0000000 --- a/tests/ArchTest.php +++ /dev/null @@ -1,5 +0,0 @@ -expect(['dd', 'dump']) - ->each->not->toBeUsed(); diff --git a/tests/Fixtures/Saloon/getPrediction.json b/tests/Fixtures/Saloon/getPrediction.json deleted file mode 100644 index f6332b6..0000000 --- a/tests/Fixtures/Saloon/getPrediction.json +++ /dev/null @@ -1 +0,0 @@ -{"statusCode":200,"headers":{"Date":"Wed, 01 Mar 2023 00:00:00 GMT","Content-Type":"application\/json"},"data":"{\"completed_at\":\"2023-01-01T00:00:00.656763Z\",\"created_at\":\"2023-01-01T00:00:00.528143Z\",\"error\":null,\"id\":\"123\",\"input\":{\"text\":\"Alice\"},\"logs\":\"\",\"metrics\":{\"predict_time\":0.057882},\"output\":\"hello Alice\",\"started_at\":\"2023-01-01T00:00:00.598881Z\",\"status\":\"succeeded\",\"urls\":{\"get\":\"https:\/\/api.replicate.com\/v1\/predictions\/123\",\"cancel\":\"https:\/\/api.replicate.com\/v1\/predictions\/123\/cancel\"},\"version\":\"5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa\",\"webhook_completed\":null}"} diff --git a/tests/Fixtures/Saloon/getPredictionAstronaut.json b/tests/Fixtures/Saloon/getPredictionAstronaut.json deleted file mode 100644 index 612c80a..0000000 --- a/tests/Fixtures/Saloon/getPredictionAstronaut.json +++ /dev/null @@ -1 +0,0 @@ -{"statusCode":200,"headers":{"Date":"Mon, 13 Mar 2023 17:00:50 GMT","Content-Type":"application\/json","Content-Length":"2000","Connection":"keep-alive","allow":"GET, OPTIONS","cross-origin-opener-policy":"same-origin","referrer-policy":"same-origin","vary":"Cookie, Origin","x-content-type-options":"nosniff","x-frame-options":"DENY","via":"1.1 vegur, 1.1 google"},"data":"{\"completed_at\":\"2023-03-13T17:00:25.905318Z\",\"created_at\":\"2023-03-13T17:00:22.340263Z\",\"error\":null,\"id\":\"la5xlbbrfzg57ip5jlx6obmm5y\",\"input\":{\"seed\":null,\"prompt\":\"a photo of an astronaut riding a horse on mars\",\"scheduler\":\"DPMSolverMultistep\",\"guidance_scale\":7.5,\"negative_prompt\":\"moon, alien, spaceship\",\"num_inference_steps\":50},\"logs\":\"Using seed: 65101\\ninput_shape: torch.Size([1, 77])\\n 0%| | 0\/50 [00:00\n\n\n\n \n \n Page not found \u2013\u00a0Replicate<\/title>\n\n \n\n <link rel=\"shortcut icon\"\n href=\"\/static\/favicon.c3e4e1c1e57e.ico\">\n <link rel=\"icon\"\n href=\"\/static\/favicon.78c995b286d3.svg\"\n type=\"image\/svg+xml\">\n <link rel=\"mask-icon\"\n href=\"\/static\/safari-pinned-tab.4c32b8e091a9.svg\"\n color=\"#FFFFFF\">\n <link rel=\"apple-touch-icon\"\n sizes=\"180x180\"\n href=\"\/apple-touch-icon.png\" \/>\n\n <link rel=\"stylesheet\"\n href=\"https:\/\/fonts.googleapis.com\/css?family=Space+Grotesk\">\n <link rel=\"stylesheet\"\n href=\"\/static\/dist\/index.5f875005094d.css\">\n\n <link rel=\"alternate\"\n type=\"application\/rss+xml\"\n title=\"RSS\"\n href=\"\/blog\/rss\">\n <link rel=\"alternate\"\n type=\"application\/atom+xml\"\n title=\"Atom\"\n href=\"\/blog\/atom\">\n\n <link rel=\"dns-prefetch\"\n href=\"https:\/\/replicate.delivery\">\n\n \n\n \n\n <link rel=\"canonical\"\n href=\"https:\/\/internal.replicate.com\/_api-internal\/api-proxy\/v1\/predictions\/rrwu2qktznb7feez4slr2o67qm\/cancel%0A\">\n\n \n\n <script type=\"text\/javascript\">\n \/* beautify ignore:start *\/\n \n!function(){var e=window.rudderanalytics=window.rudderanalytics||[];e.methods=[\"load\",\"page\",\"track\",\"identify\",\"alias\",\"group\",\"ready\",\"reset\",\"getAnonymousId\",\"setAnonymousId\"],e.factory=function(t){return function(){var r=Array.prototype.slice.call(arguments);return r.unshift(t),e.push(r),e}};for(var t=0;t<e.methods.length;t++){var r=e.methods[t];e[r]=e.factory(r)}e.loadJS=function(e,t){var r=document.createElement(\"script\");r.type=\"text\/javascript\",r.async=!0,r.src=\"https:\/\/cdn.rudderlabs.com\/v1\/rudder-analytics.min.js\";var a=document.getElementsByTagName(\"script\")[0];a.parentNode.insertBefore(r,a)},e.loadJS(),\ne.load(\"23hcIqxUfr6QoeygnbNdOMvIZWG\",\"https:\/\/replicateor.dataplane.rudderstack.com\"),\ne.setAnonymousId(\"1bd03ce5-18ca-424b-a62d-2aee11a3cb77\"),\n\ne.page(null, null, null, null, function() {\n \/\/ remove utm_* query parameters\n function paramIsNotUtm(param) { return param.slice(0, 4) !== 'utm_'; }\n if (history && history.replaceState && location.search) {\n var params = location.search.slice(1).split('&');\n var newParams = params.filter(paramIsNotUtm);\n if (newParams.length < params.length) {\n var search = newParams.length ? '?' + newParams.join('&') : '';\n var url = location.pathname + search + location.hash;\n history.replaceState(null, null, url);\n }\n }\n})}();\n \n\n function replicate_track(event, properties) {\n \n var options = {};\n \n\n rudderanalytics.track(event, properties, options);\n \n }\n \/* beautify ignore:end *\/\n\n<\/script>\n\n<\/head>\n\n\n\n<body class=\"font-sans overflow-y-scroll antialiased flex min-h-screen flex-col\">\n \n\n \n\n <div class=\"p-lh px-2lh\">\n \n\n<header class=\"mt-4\">\n <div class=\"md:flex\">\n <h1>\n <a href=\"\/\"\n title=\"Replicate\"\n style=\"position: relative; top: -6px\"\n \n class=\"inline-block\">\n \n <svg version=\"1.1\"\n id=\"Layer_1\"\n xmlns=\"http:\/\/www.w3.org\/2000\/svg\"\n xmlns:xlink=\"http:\/\/www.w3.org\/1999\/xlink\"\n x=\"0px\"\n y=\"0px\"\n viewBox=\"0 0 1000 1000\"\n class=\"fill-current\"\n style=\"enable-background:new 0 0 1000 1000; width: 30px\"\n xml:space=\"preserve\">\n <g>\n <polygon points=\"1000,427.6 1000,540.6 603.4,540.6 603.4,1000 477,1000 477,427.6 \t\" \/>\n <polygon points=\"1000,213.8 1000,327 364.8,327 364.8,1000 238.4,1000 238.4,213.8 \t\" \/>\n <polygon points=\"1000,0 1000,113.2 126.4,113.2 126.4,1000 0,1000 0,0 \t\" \/>\n <\/g>\n<\/svg>\n\n \n <\/a>\n <\/h1>\n\n <div class=\"hidden md:block text-right flex-grow\">\n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<nav class=\"md:ml-lh flex justify-end items-start\">\n <div class=\"glass flex flex-wrap justify-end\">\n \n\n <a href=\"\/explore\"\n class=\"nav-link \">Explore<\/a>\n\n <a href=\"\/pricing\"\n class=\"nav-link \">Pricing<\/a>\n\n <a href=\"\/docs\"\n class=\"nav-link \">Docs<\/a>\n\n <a href=\"\/blog\"\n class=\"nav-link \">Blog<\/a>\n\n <a href=\"\/changelog\"\n class=\"nav-link \">Changelog<\/a>\n\n \n\n \n <a href=\"\/signin?next=\/_api-internal\/api-proxy\/v1\/predictions\/rrwu2qktznb7feez4slr2o67qm\/cancel\n\"\n class=\"nav-link\">Sign\u00a0in<\/a>\n\n <a href=\"\/docs\"\n class=\"nav-link-primary\">Get\u00a0started<\/a>\n \n <\/div>\n\n \n<\/nav>\n\n <\/div>\n <\/div>\n<\/header>\n\n\n \n <div class=\"block md:hidden flex-grow mt-lh\">\n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<nav class=\"md:ml-lh flex justify-end items-start\">\n <div class=\"glass flex flex-wrap justify-end\">\n \n\n <a href=\"\/explore\"\n class=\"nav-link \">Explore<\/a>\n\n <a href=\"\/pricing\"\n class=\"nav-link \">Pricing<\/a>\n\n <a href=\"\/docs\"\n class=\"nav-link \">Docs<\/a>\n\n <a href=\"\/blog\"\n class=\"nav-link \">Blog<\/a>\n\n <a href=\"\/changelog\"\n class=\"nav-link \">Changelog<\/a>\n\n \n\n \n <a href=\"\/signin?next=\/_api-internal\/api-proxy\/v1\/predictions\/rrwu2qktznb7feez4slr2o67qm\/cancel\n\"\n class=\"nav-link\">Sign\u00a0in<\/a>\n\n <a href=\"\/docs\"\n class=\"nav-link-primary\">Get\u00a0started<\/a>\n \n <\/div>\n\n \n<\/nav>\n\n <\/div>\n\n <\/div>\n\n \n\n \n <div class=\"px-2lh\">\n \n <\/div>\n\n \n\n <main class=\"flex-1\">\n \n\n<div class=\"full-container pb-40\">\n <div class=\"max-w-md\">\n <h3 class=\"mb-lh\">Page not found<\/h3>\n\n <p class=\"my-lh\">\n Sorry, we can't find the page you're looking for. If you followed a broken link from somewhere, you can hop into our\n <a href=\"https:\/\/discord.gg\/replicate\">Discord channel<\/a> or\n <a href=\"mailto:team@replicate.com\">email us<\/a> and let us know.\n <\/p>\n\n <p class=\"my-lh\">\n If you're browsing around, why not\n <a href=\"\/explore\">explore the models on Replicate<\/a>.\n \n Or just look at this one we've picked out for you!\n \n <\/p>\n <\/div>\n\n \n <div class=\"h-80 mb-2\">\n \n\n\n\n\n<div class=\"grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 grid-flow-row auto-rows-max gap-2lh \">\n \n <a class=\"no-default flex flex-col no-focus \"\n href=\"\/stability-ai\/stable-diffusion\"\n title=\"stability-ai\/stable-diffusion\">\n <div class=\"h-80 mb-2\">\n \n\n<div class=\" h-full w-full overflow-hidden\">\n \n \n \n <img data-src=\"https:\/\/bucketeer-be99e627-94e7-4e5b-a292-54eeb40ac303.s3.amazonaws.com\/public\/models_models_featured_image\/07ab2a80-df3b-4ed1-9ff2-545774b36dfa\/stable-diffusion.jpeg\"\n alt=\"\"\n role=\"presentation\"\n class=\"object-cover object-center w-full h-full lazy\" \/>\n \n<\/div>\n\n\n <\/div>\n <div>\n\n <div class=\"flex\">\n <h4 class=\"flex-shrink overflow-hidden overflow-ellipsis\"><span class=\"text-shade\">stability-ai<\/span><span class=\"text-shade px-1\">\/<\/span><\/span>stable-diffusion<\/h4>\n <\/div>\n\n <p class=\"mb-1\">\n A latent text-to-image diffusion model capable of generating photo-realistic images given any text input\n <\/p>\n <div class=\"text-shade text-sm\">\n <span class=\"float-right\">\n \n<svg class=\"icon\"\n xmlns=\"http:\/\/www.w3.org\/2000\/svg\"\n viewBox=\"0 0 24 24\"\n width=\"24\"\n height=\"24\">\n <path fill-rule=\"evenodd\"\n d=\"M20.322.75a10.75 10.75 0 00-7.373 2.926l-1.304 1.23A23.743 23.743 0 0010.103 6.5H5.066a1.75 1.75 0 00-1.5.85l-2.71 4.514a.75.75 0 00.49 1.12l4.571.963c.039.049.082.096.129.14L8.04 15.96l1.872 1.994c.044.047.091.09.14.129l.963 4.572a.75.75 0 001.12.488l4.514-2.709a1.75 1.75 0 00.85-1.5v-5.038a23.741 23.741 0 001.596-1.542l1.228-1.304a10.75 10.75 0 002.925-7.374V2.499A1.75 1.75 0 0021.498.75h-1.177zM16 15.112c-.333.248-.672.487-1.018.718l-3.393 2.262.678 3.223 3.612-2.167a.25.25 0 00.121-.214v-3.822zm-10.092-2.7L8.17 9.017c.23-.346.47-.685.717-1.017H5.066a.25.25 0 00-.214.121l-2.167 3.612 3.223.679zm8.07-7.644a9.25 9.25 0 016.344-2.518h1.177a.25.25 0 01.25.25v1.176a9.25 9.25 0 01-2.517 6.346l-1.228 1.303a22.248 22.248 0 01-3.854 3.257l-3.288 2.192-1.743-1.858a.764.764 0 00-.034-.034l-1.859-1.744 2.193-3.29a22.248 22.248 0 013.255-3.851l1.304-1.23zM17.5 8a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zm-11 13c.9-.9.9-2.6 0-3.5-.9-.9-2.6-.9-3.5 0-1.209 1.209-1.445 3.901-1.49 4.743a.232.232 0 00.247.247c.842-.045 3.534-.281 4.743-1.49z\"><\/path>\n<\/svg>\n 81M runs\n <\/span>\n <\/div>\n\n\n <\/div>\n <\/a>\n \n<\/div>\n\n <\/div>\n \n<\/div>\n\n\n <\/main>\n\n\n <div class=\"p-lh md:p-2lh border-hairline border-t\">\n <footer class=\"md:flex\">\n <p>Replicate<\/p>\n <div class=\"mt-0 sm:mt-4 md:mt-0 md:text-right flex-grow\">\n <a href=\"\/about\"\n class=\"mr-3\">About<\/a>\n <a href=\"\/docs\"\n class=\"mr-3\">Docs<\/a>\n <a href=\"\/terms\"\n class=\"mr-3\">Terms<\/a>\n <a href=\"\/privacy\"\n class=\"mr-3\">Privacy<\/a>\n <a href=\"https:\/\/github.com\/replicate\"\n class=\"mr-3\">GitHub<\/a>\n <a href=\"https:\/\/twitter.com\/replicatehq\"\n class=\"mr-3\">Twitter<\/a>\n <a href=\"https:\/\/discord.gg\/replicate\"\n class=\"mr-3\">Discord<\/a>\n <a href=\"mailto:team@replicate.com\"\n class=\"mr-3\">Email<\/a>\n <\/div>\n<\/footer>\n\n <\/div>\n\n \n\n <script src=\"\/static\/dist\/index.62d821824e67.js\"><\/script>\n <script>\n \/* beautify ignore:start *\/\n window.replicateInit({\"SENTRY_DSN_JS\": \"https:\/\/3dc017e574684610bbc7fd3b5519a4e8@o255771.ingest.sentry.io\/5909364\"})\n \/* beautify ignore:end *\/\n\n <\/script>\n \n\n<\/body>\n\n<\/html>\n"} diff --git a/tests/Pest.php b/tests/Pest.php deleted file mode 100644 index 6a72a81..0000000 --- a/tests/Pest.php +++ /dev/null @@ -1,35 +0,0 @@ -<?php - -use BenBjurstrom\Replicate\Replicate; -use Saloon\Exceptions\DirectoryNotFoundException; -use Saloon\Exceptions\InvalidMockResponseCaptureMethodException; -use Saloon\Http\Faking\MockClient; -use Saloon\Http\Faking\MockResponse; - -function getConnector(): Replicate -{ - $apiToken = getenv('REPLICATE_API_TOKEN'); - - return new Replicate( - apiToken: $apiToken ? $apiToken : '', - ); -} - -/** - * @throws DirectoryNotFoundException - * @throws InvalidMockResponseCaptureMethodException - */ -function getMockClient(string $class, string $fixture): MockClient -{ - return new MockClient([ - $class => MockResponse::fixture($fixture), - ]); -} - -function getMockConnector(string $class, string $fixture): Replicate -{ - $mockClient = getMockClient($class, $fixture); - $connector = getConnector(); - - return $connector->withMockClient($mockClient); -} diff --git a/tests/PredictionResourceTest.php b/tests/PredictionResourceTest.php deleted file mode 100644 index 0c2881c..0000000 --- a/tests/PredictionResourceTest.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php - -use BenBjurstrom\Replicate\Requests\GetPrediction; -use BenBjurstrom\Replicate\Requests\GetPredictions; -use BenBjurstrom\Replicate\Requests\PostPrediction; -use Saloon\Http\Faking\MockClient; -use Saloon\Http\Faking\MockResponse; - -test('predictions list', function () { - $mockClient = new MockClient([ - GetPredictions::class => MockResponse::fixture('getPredictions'), - ]); - - $connector = getConnector(); - $connector->withMockClient($mockClient); - - $cursor = '123'; - $data = $connector->predictions()->list($cursor); - - expect($data->results[0]->id) - ->toBe('yfv4cakjzvh2lexxv7o5qzymqy'); -}); - -test('predictions get', function () { - $mockClient = new MockClient([ - GetPrediction::class => MockResponse::fixture('getPrediction'), - ]); - - $connector = getConnector(); - $connector->withMockClient($mockClient); - - $data = $connector->predictions()->get('123'); - - expect($data->id) - ->toBe('123'); -}); - -test('predictions create', function () { - $mockClient = new MockClient([ - PostPrediction::class => MockResponse::fixture('postPredictionAlice'), - ]); - - $connector = getConnector(); - $connector->withMockClient($mockClient); - - $data = $connector->predictions() - ->withWebhook('https://example.com/webhook') - ->create('123', [ - 'text' => 'Alice', - ]); - - expect($data->id) - ->toBe('123'); -}); diff --git a/tests/Requests/GetPredictionTest.php b/tests/Requests/GetPredictionTest.php deleted file mode 100644 index 361f5ab..0000000 --- a/tests/Requests/GetPredictionTest.php +++ /dev/null @@ -1,93 +0,0 @@ -<?php - -use BenBjurstrom\Replicate\Data\PredictionData; -use BenBjurstrom\Replicate\Requests\GetPrediction; -use Saloon\Http\Faking\MockClient; -use Saloon\Http\Faking\MockResponse; - -beforeEach(function () { - echo 'beforeEach'; -}); - -test('get prediction endpoint', function () { - $mockClient = new MockClient([ - GetPrediction::class => MockResponse::fixture('getPrediction'), - ]); - - $connector = getConnector(); - $connector->withMockClient($mockClient); - - $request = new GetPrediction('123'); - $response = $connector->send($request); - - /* @var PredictionData $data */ - $data = $response->dtoOrFail(); - - expect($response->ok()) - ->toBeTrue() - ->and($data->id) - ->toBe('123'); -}); - -test('get prediction endpoint failed', function () { - $mockClient = new MockClient([ - GetPrediction::class => MockResponse::fixture('getPredictionFailed'), - ]); - - $connector = getConnector(); - $connector->withMockClient($mockClient); - - $id = 'c6fng3kkerguvg3aobpv2lquaa'; - $request = new GetPrediction($id); - $response = $connector->send($request); - - /* @var PredictionData $data */ - $data = $response->dtoOrFail(); - - expect($response->ok()) - ->toBeTrue() - ->and($data->id) - ->toBe('c6fng3kkerguvg3aobpv2lquaa'); -}); - -test('get prediction endpoint multiple outputs', function () { - $mockClient = new MockClient([ - GetPrediction::class => MockResponse::fixture('getPredictionMultiple'), - ]); - - $connector = getConnector(); - $connector->withMockClient($mockClient); - - $id = '3uff6ygnljbr7htjs2lkefcx3a'; - $request = new GetPrediction($id); - $response = $connector->send($request); - - /* @var PredictionData $data */ - $data = $response->dtoOrFail(); - - expect($response->ok()) - ->toBeTrue() - ->and($data->id) - ->toBe('3uff6ygnljbr7htjs2lkefcx3a'); -}); - -test('get prediction astronaut', function () { - $mockClient = new MockClient([ - GetPrediction::class => MockResponse::fixture('getPredictionAstronaut'), - ]); - - $connector = getConnector(); - $connector->withMockClient($mockClient); - - $id = 'la5xlbbrfzg57ip5jlx6obmm5y'; - $request = new GetPrediction($id); - $response = $connector->send($request); - - /* @var PredictionData $data */ - $data = $response->dtoOrFail(); - - expect($response->ok()) - ->toBeTrue() - ->and($data->id) - ->toBe('la5xlbbrfzg57ip5jlx6obmm5y'); -}); diff --git a/tests/Requests/GetPredictionsTest.php b/tests/Requests/GetPredictionsTest.php deleted file mode 100644 index 8f1924c..0000000 --- a/tests/Requests/GetPredictionsTest.php +++ /dev/null @@ -1,26 +0,0 @@ -<?php - -use BenBjurstrom\Replicate\Data\PredictionsData; -use BenBjurstrom\Replicate\Requests\GetPredictions; -use Saloon\Http\Faking\MockClient; -use Saloon\Http\Faking\MockResponse; - -test('get predictions endpoint', function () { - $mockClient = new MockClient([ - GetPredictions::class => MockResponse::fixture('getPredictions'), - ]); - - $connector = getConnector(); - $connector->withMockClient($mockClient); - - $request = new GetPredictions(); - $response = $connector->send($request); - - /* @var PredictionsData $data */ - $data = $response->dtoOrFail(); - - expect($response->ok()) - ->toBeTrue() - ->and($data->results[0]->id) - ->toBe('yfv4cakjzvh2lexxv7o5qzymqy'); -}); diff --git a/tests/Requests/PostPredictionCancelTest.php b/tests/Requests/PostPredictionCancelTest.php deleted file mode 100644 index 3229665..0000000 --- a/tests/Requests/PostPredictionCancelTest.php +++ /dev/null @@ -1,21 +0,0 @@ -<?php - -use BenBjurstrom\Replicate\Requests\PostPredictionCancel; -use Saloon\Http\Faking\MockClient; -use Saloon\Http\Faking\MockResponse; - -test('post predictions cancel endpoint', function () { - $mockClient = new MockClient([ - PostPredictionCancel::class => MockResponse::fixture('postPredictionCancel'), - ]); - - $connector = getConnector(); - $connector->withMockClient($mockClient); - - $id = 'rrwu2qktznb7feez4slr2o67qm'; - $request = new PostPredictionCancel($id); - $response = $connector->send($request); - - expect($response->ok()) - ->toBeTrue(); -})->skip('Cancellation endpoint always returns a 404'); diff --git a/tests/Requests/PostPredictionTest.php b/tests/Requests/PostPredictionTest.php deleted file mode 100644 index 532ca30..0000000 --- a/tests/Requests/PostPredictionTest.php +++ /dev/null @@ -1,52 +0,0 @@ -<?php - -use BenBjurstrom\Replicate\Data\PredictionData; -use BenBjurstrom\Replicate\Requests\PostPrediction; - -test('post prediction endpoint hello', function () { - $connector = getMockConnector( - PostPrediction::class, - 'postPredictionAlice' - ); - - $version = '5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa'; - $input = [ - 'text' => 'Alice', - ]; - $request = new PostPrediction($version, $input); - $response = $connector->send($request); - - /* @var PredictionData $data */ - $data = $response->dtoOrFail(); - - expect($data->id) - ->toBe('123'); -}); - -it('sends a post prediction request for a stable diffusion model', function () { - $connector = getMockConnector( - PostPrediction::class, - 'postPredictionAstronaut' - ); - - $version = 'db21e45d3f7023abc2a46ee38a23973f6dce16bb082a930b0c49861f96d1e5bf'; - $input = [ - 'model' => 'stable-diffusion-2-1', - 'prompt' => 'a photo of an astronaut riding a horse on mars', - 'negative_prompt' => 'moon, alien, spaceship', - 'width' => 768, - 'height' => 768, - 'num_inference_steps' => 50, - 'guidance_scale' => 7.5, - 'scheduler' => 'DPMSolverMultistep', - 'seed' => null, - ]; - $request = new PostPrediction($version, $input); - $response = $connector->send($request); - - /* @var PredictionData $data */ - $data = $response->dtoOrFail(); - - expect($data->id) - ->toBe('la5xlbbrfzg57ip5jlx6obmm5y'); -}); From 2f0cc7d24ef623babc100cd932984fdaf0d437bf Mon Sep 17 00:00:00 2001 From: Marcelo <20625497+marceloeatworld@users.noreply.github.com> Date: Sun, 29 Mar 2026 22:58:43 +0100 Subject: [PATCH 6/8] Rewrite with full Replicate API coverage and Saloon v4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Upgrade saloonphp/saloon v3 → v4, PHP 8.1 → 8.2+ - Add all Replicate API endpoints: models, deployments, trainings, files, collections, hardware, webhooks, account - Fix auth to Bearer token, fix cancel endpoint URL bug, fix list DTO null safety - Reorganize into resource subdirectories with typed DTOs - Update README, LICENSE, CHANGELOG for independent package --- .gitattributes | 5 - .github/FUNDING.yml | 3 - .github/workflows/formats.yml | 33 +- .github/workflows/tests.yml | 25 +- CHANGELOG.md | 40 +- LICENSE.md | 1 + README.md | 418 ++++++++++++------ composer.json | 20 +- phpstan.neon.dist | 2 +- src/Data/AccountData.php | 29 ++ src/Data/CollectionData.php | 54 +++ src/Data/CollectionsData.php | 43 ++ src/Data/DeploymentData.php | 40 ++ src/Data/DeploymentsData.php | 43 ++ src/Data/FileData.php | 60 +++ src/Data/FilesData.php | 43 ++ src/Data/HardwareData.php | 35 ++ src/Data/ModelData.php | 65 +++ src/Data/ModelVersionData.php | 43 ++ src/Data/ModelVersionsData.php | 43 ++ src/Data/ModelsData.php | 43 ++ src/Data/PredictionData.php | 82 ++-- src/Data/PredictionsData.php | 45 +- src/Data/TrainingData.php | 70 +++ src/Data/TrainingsData.php | 43 ++ src/Data/WebhookSecretData.php | 23 + src/PredictionsResource.php | 119 ----- src/Replicate.php | 64 ++- src/Requests/Account/GetAccount.php | 25 ++ src/Requests/Collections/GetCollection.php | 29 ++ src/Requests/Collections/GetCollections.php | 25 ++ src/Requests/Deployments/DeleteDeployment.php | 23 + src/Requests/Deployments/GetDeployment.php | 30 ++ src/Requests/Deployments/GetDeployments.php | 25 ++ src/Requests/Deployments/PatchDeployment.php | 46 ++ src/Requests/Deployments/PostDeployment.php | 53 +++ .../Deployments/PostDeploymentPrediction.php | 75 ++++ src/Requests/Files/DeleteFile.php | 22 + src/Requests/Files/GetFile.php | 29 ++ src/Requests/Files/GetFiles.php | 25 ++ src/Requests/Files/PostFile.php | 67 +++ src/Requests/Hardware/GetHardware.php | 18 + src/Requests/Models/DeleteModel.php | 23 + src/Requests/Models/DeleteModelVersion.php | 24 + src/Requests/Models/GetModel.php | 30 ++ src/Requests/Models/GetModelVersion.php | 31 ++ src/Requests/Models/GetModelVersions.php | 30 ++ src/Requests/Models/GetModels.php | 25 ++ src/Requests/Models/PatchModel.php | 46 ++ src/Requests/Models/PostModel.php | 53 +++ src/Requests/Models/PostModelPrediction.php | 75 ++++ src/Requests/PostDeploymentPrediction.php | 52 --- src/Requests/PostPrediction.php | 47 -- src/Requests/PostPredictionCancel.php | 33 -- .../{ => Predictions}/GetPrediction.php | 9 +- .../{ => Predictions}/GetPredictions.php | 4 +- src/Requests/Predictions/PostPrediction.php | 75 ++++ .../Predictions/PostPredictionCancel.php | 22 + src/Requests/Trainings/GetTraining.php | 29 ++ src/Requests/Trainings/GetTrainings.php | 25 ++ src/Requests/Trainings/PostTraining.php | 61 +++ src/Requests/Trainings/PostTrainingCancel.php | 22 + src/Requests/Webhooks/GetWebhookSecret.php | 25 ++ src/Resource.php | 13 - src/Resources/AccountResource.php | 19 + src/Resources/CollectionsResource.php | 30 ++ src/Resources/DeploymentsResource.php | 91 ++++ src/Resources/FilesResource.php | 55 +++ src/Resources/HardwareResource.php | 22 + src/Resources/ModelsResource.php | 118 +++++ src/Resources/PredictionsResource.php | 60 +++ src/Resources/TrainingsResource.php | 62 +++ src/Resources/WebhooksResource.php | 17 + 73 files changed, 2722 insertions(+), 532 deletions(-) delete mode 100644 .github/FUNDING.yml create mode 100644 src/Data/AccountData.php create mode 100644 src/Data/CollectionData.php create mode 100644 src/Data/CollectionsData.php create mode 100644 src/Data/DeploymentData.php create mode 100644 src/Data/DeploymentsData.php create mode 100644 src/Data/FileData.php create mode 100644 src/Data/FilesData.php create mode 100644 src/Data/HardwareData.php create mode 100644 src/Data/ModelData.php create mode 100644 src/Data/ModelVersionData.php create mode 100644 src/Data/ModelVersionsData.php create mode 100644 src/Data/ModelsData.php create mode 100644 src/Data/TrainingData.php create mode 100644 src/Data/TrainingsData.php create mode 100644 src/Data/WebhookSecretData.php delete mode 100644 src/PredictionsResource.php create mode 100644 src/Requests/Account/GetAccount.php create mode 100644 src/Requests/Collections/GetCollection.php create mode 100644 src/Requests/Collections/GetCollections.php create mode 100644 src/Requests/Deployments/DeleteDeployment.php create mode 100644 src/Requests/Deployments/GetDeployment.php create mode 100644 src/Requests/Deployments/GetDeployments.php create mode 100644 src/Requests/Deployments/PatchDeployment.php create mode 100644 src/Requests/Deployments/PostDeployment.php create mode 100644 src/Requests/Deployments/PostDeploymentPrediction.php create mode 100644 src/Requests/Files/DeleteFile.php create mode 100644 src/Requests/Files/GetFile.php create mode 100644 src/Requests/Files/GetFiles.php create mode 100644 src/Requests/Files/PostFile.php create mode 100644 src/Requests/Hardware/GetHardware.php create mode 100644 src/Requests/Models/DeleteModel.php create mode 100644 src/Requests/Models/DeleteModelVersion.php create mode 100644 src/Requests/Models/GetModel.php create mode 100644 src/Requests/Models/GetModelVersion.php create mode 100644 src/Requests/Models/GetModelVersions.php create mode 100644 src/Requests/Models/GetModels.php create mode 100644 src/Requests/Models/PatchModel.php create mode 100644 src/Requests/Models/PostModel.php create mode 100644 src/Requests/Models/PostModelPrediction.php delete mode 100644 src/Requests/PostDeploymentPrediction.php delete mode 100644 src/Requests/PostPrediction.php delete mode 100644 src/Requests/PostPredictionCancel.php rename src/Requests/{ => Predictions}/GetPrediction.php (79%) rename src/Requests/{ => Predictions}/GetPredictions.php (84%) create mode 100644 src/Requests/Predictions/PostPrediction.php create mode 100644 src/Requests/Predictions/PostPredictionCancel.php create mode 100644 src/Requests/Trainings/GetTraining.php create mode 100644 src/Requests/Trainings/GetTrainings.php create mode 100644 src/Requests/Trainings/PostTraining.php create mode 100644 src/Requests/Trainings/PostTrainingCancel.php create mode 100644 src/Requests/Webhooks/GetWebhookSecret.php delete mode 100644 src/Resource.php create mode 100644 src/Resources/AccountResource.php create mode 100644 src/Resources/CollectionsResource.php create mode 100644 src/Resources/DeploymentsResource.php create mode 100644 src/Resources/FilesResource.php create mode 100644 src/Resources/HardwareResource.php create mode 100644 src/Resources/ModelsResource.php create mode 100644 src/Resources/PredictionsResource.php create mode 100644 src/Resources/TrainingsResource.php create mode 100644 src/Resources/WebhooksResource.php diff --git a/.gitattributes b/.gitattributes index 7b50fc8..038d707 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,9 +1,5 @@ -/art export-ignore -/docs export-ignore /tests export-ignore -/scripts export-ignore /.github export-ignore -/.php_cs export-ignore .editorconfig export-ignore .gitattributes export-ignore .gitignore export-ignore @@ -11,4 +7,3 @@ phpstan.neon.dist export-ignore phpunit.xml.dist export-ignore CHANGELOG.md export-ignore CONTRIBUTING.md export-ignore -README.md export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 1d764b0..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -# These are supported funding model platforms - -github: benbjurstrom diff --git a/.github/workflows/formats.yml b/.github/workflows/formats.yml index f7d0a38..85aeb6d 100644 --- a/.github/workflows/formats.yml +++ b/.github/workflows/formats.yml @@ -4,41 +4,26 @@ on: ['push', 'pull_request'] jobs: ci: - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest - strategy: - fail-fast: true - matrix: - os: [ubuntu-latest] - php: [8.2] - dependency-version: [prefer-stable] - - name: Formats P${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }} + name: Lint & Static Analysis steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Cache dependencies - uses: actions/cache@v1 - with: - path: ~/.composer/cache/files - key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php }} + php-version: '8.2' extensions: dom, mbstring, zip - tools: prestissimo - coverage: pcov + coverage: none - - name: Install Composer dependencies - run: composer update --${{ matrix.dependency-version }} --no-interaction --prefer-dist + - name: Install dependencies + run: composer install --no-interaction --prefer-dist - - name: Coding Style Checks + - name: Coding Style run: composer test:lint - - name: Type Checks + - name: Static Analysis run: composer test:types diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 410150a..f863aeb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,26 +4,17 @@ on: ['push', 'pull_request'] jobs: ci: - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest strategy: fail-fast: true matrix: - os: [ubuntu-latest] - php: [8.2, 8.1] - dependency-version: [prefer-stable] + php: ['8.2', '8.3', '8.4'] - name: Tests P${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }} + name: PHP ${{ matrix.php }} steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Cache dependencies - uses: actions/cache@v1 - with: - path: ~/.composer/cache/files - key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -32,8 +23,8 @@ jobs: extensions: dom, mbstring, zip coverage: none - - name: Install Composer dependencies - run: composer update --${{ matrix.dependency-version }} --no-interaction --prefer-dist + - name: Install dependencies + run: composer install --no-interaction --prefer-dist - - name: Integration Tests - run: php ./vendor/bin/pest + - name: Run tests + run: vendor/bin/pest diff --git a/CHANGELOG.md b/CHANGELOG.md index 15dd6ee..d5a7d36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,44 @@ # Changelog + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [Unreleased] -- Adds first version +## [1.0.0] - 2026-03-29 + +### Changed +- Upgraded to Saloon v4 (patches CVE-2026-33942, CVE-2026-33182, CVE-2026-33183) +- Bumped minimum PHP version to 8.2 +- Authentication now uses Bearer token (was Token prefix) +- Reorganized requests into subdirectory structure per resource +- Resources now extend Saloon's BaseResource +- All DTOs use readonly properties with proper type narrowing +- Updated dev dependencies: Pest v3, PHPStan v2, symfony/var-dumper v7 +- Updated CI workflows for PHP 8.2/8.3/8.4 + +### Added +- Models resource: list, get, create, update, delete, createPrediction +- Model versions: getVersion, listVersions, deleteVersion +- Deployments resource: list, get, create, update, delete, createPrediction +- Trainings resource: list, get, create, cancel +- Files resource: list, get, upload, delete +- Collections resource: list, get +- Hardware resource: list +- Webhooks resource: getSecret +- Account resource: get +- Prediction cancel endpoint (was orphaned in previous version) +- Synchronous predictions via `wait` parameter (Prefer: wait header) +- Streaming support via `stream` parameter +- Webhook support directly on create methods +- 17 typed DTOs covering every API response +- `fromArray()` static factory on entity DTOs for nested construction + +### Fixed +- Trailing newline in PostPredictionCancel endpoint URL +- PredictionsData reading `model` from wrong JSON level +- Missing null-coalescing on nullable fields in list responses + +### Removed +- Custom Resource base class (replaced by Saloon's BaseResource) +- Mutable webhook state on PredictionsResource (replaced by direct parameters) diff --git a/LICENSE.md b/LICENSE.md index a030d2e..7816f57 100755 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,5 +1,6 @@ The MIT License (MIT) +Copyright (c) Marcelo Pereira <diagngo@gmail.com> Copyright (c) Ben Bjurstrom <bbjurstrom@gmail.com> Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/README.md b/README.md index bf28095..a8f43b6 100644 --- a/README.md +++ b/README.md @@ -1,195 +1,347 @@ -# Replicate PHP client -This is a framework-agnostic PHP client for [Replicate.com](https://replicate.com/) built on the amazing [Saloon v3](https://docs.saloon.dev/) 🤠 library. Use it to easily interact with machine learning models such as Stable Diffusion right from your PHP application. +# Replicate PHP -[![Latest Version on Packagist](https://img.shields.io/packagist/v/benbjurstrom/replicate-php.svg?style=flat-square)](https://packagist.org/packages/benbjurstrom/replicate-php) -[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/benbjurstrom/replicate-php/tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/benbjurstrom/replicate-php/actions?query=workflow%3tests+branch%3Amain) +[![Latest Version on Packagist](https://img.shields.io/packagist/v/marceloeatworld/replicate-php.svg?style=flat-square)](https://packagist.org/packages/marceloeatworld/replicate-php) +[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/marceloeatworld/replicate-php/tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/marceloeatworld/replicate-php/actions?query=workflow%3Atests+branch%3Amain) +[![PHPStan](https://img.shields.io/github/actions/workflow/status/marceloeatworld/replicate-php/formats.yml?branch=main&label=phpstan&style=flat-square)](https://github.com/marceloeatworld/replicate-php/actions?query=workflow%3Aformats+branch%3Amain) -## Table of contents -- [Quick Start](https://github.com/benbjurstrom/replicate-php#-quick-start) -- [Using with Laravel](https://github.com/benbjurstrom/replicate-php#using-with-laravel) -- [Response Data](https://github.com/benbjurstrom/replicate-php#response-data) -- [Webhooks](https://github.com/benbjurstrom/replicate-php#webhooks) -- [Prediction Methods](https://github.com/benbjurstrom/replicate-php#available-prediction-methods) - - [get](https://github.com/benbjurstrom/replicate-php#get) - - [list](https://github.com/benbjurstrom/replicate-php#list) - - [create](https://github.com/benbjurstrom/replicate-php#create) +A framework-agnostic PHP client for the [Replicate API](https://replicate.com/) built on [Saloon v4](https://docs.saloon.dev/). -## 🚀 Quick start +Full coverage of the Replicate HTTP API: predictions, models, deployments, trainings, files, collections, hardware, webhooks, and account. -Install with composer. +> This package is a fork of [benbjurstrom/replicate-php](https://github.com/benbjurstrom/replicate-php) which only covered predictions. This version has been entirely rewritten with full API coverage, Saloon v4, PHP 8.2+, and typed DTOs for every endpoint. + +## Requirements + +- PHP 8.2+ + +## Installation ```bash -composer require benbjurstrom/replicate-php +composer require marceloeatworld/replicate-php ``` -### -Create a new api instance. +## Quick Start + ```php use MarceloEatWorld\Replicate\Replicate; -... -$api = new Replicate( +$replicate = new Replicate( apiToken: $_ENV['REPLICATE_API_TOKEN'], ); ``` -### -Then use it to invoke your model (or in replicate terms "create a prediction"). +### Create a prediction + ```php -$version = 'db21e45d3f7023abc2a46ee38a23973f6dce16bb082a930b0c49861f96d1e5bf'; -$input = [ - 'model' => 'stable-diffusion-2-1', - 'prompt' => 'a photo of an astronaut riding a horse on mars', - 'negative_prompt' => 'moon, alien, spaceship', - 'width' => 768, - 'height' => 768, - 'num_inference_steps' => 50, - 'guidance_scale' => 7.5, - 'scheduler' => 'DPMSolverMultistep', - 'seed' => null, -]; +$prediction = $replicate->predictions()->create( + version: 'stability-ai/sdxl:c221b2b8ef527988fb59bf24a8b97c4561f1c671f73bd389f866bfb27c061316', + input: ['prompt' => 'a photo of an astronaut riding a horse on mars'], +); + +$prediction->id; // "xyz123" +$prediction->status; // "starting" +``` + +### Create a prediction using an official model -$data = $api->predictions()->create($version, $input); -$data->id; // yfv4cakjzvh2lexxv7o5qzymqy +```php +$prediction = $replicate->models()->createPrediction( + owner: 'meta', + name: 'meta-llama-3-70b-instruct', + input: ['prompt' => 'Write a haiku about PHP'], +); ``` -Note that the input parameters will vary depending on what version (model) you're using. In this example version [db21e45d3f7023abc2a46ee38a23973f6dce16bb082a930b0c49861f96d1e5bf](https://replicate.com/stability-ai/stable-diffusion/versions/db21e45d3f7023abc2a46ee38a23973f6dce16bb082a930b0c49861f96d1e5bf) is a Stable Diffusion 2.1 model optimized for speed. -### -## Create a prediction for a specific deployment -ou can now create a prediction for a specific deployment using the createForDeployment method. This method takes the full name of the deployment and an array of data as input, sends the HTTP request, and returns the data of the created prediction. +### Synchronous predictions (wait for result) -Here's an example of how to use it: ```php -$deploymentName = 'my-deployment'; -$input = [ - 'model' => 'stable-diffusion-2-1', - 'prompt' => 'a photo of an astronaut riding a horse on mars', - 'negative_prompt' => 'moon, alien, spaceship', - 'width' => 768, - 'height' => 768, - 'num_inference_steps' => 50, - 'guidance_scale' => 7.5, - 'scheduler' => 'DPMSolverMultistep', - 'seed' => null, -]; +$prediction = $replicate->predictions()->create( + version: 'stability-ai/sdxl:c221b2b8ef527988fb59bf24a8b97c4561f1c671f73bd389f866bfb27c061316', + input: ['prompt' => 'a painting of a cat'], + wait: 60, // wait up to 60 seconds for completion +); -$data = $api->predictions()->createForDeployment($deploymentName, $input); -$data->id; // yfv4cakjzvh2lexxv7o5qzymqy +if ($prediction->status === 'succeeded') { + $prediction->output; // result is ready +} ``` -### -## Using with Laravel -Begin by adding your credentials to your services config file. +### Get prediction status + ```php -// config/services.php -'replicate' => [ - 'api_token' => env('REPLICATE_API_TOKEN'), -], +$prediction = $replicate->predictions()->get('xyz123'); +$prediction->status; // "succeeded" +$prediction->output; // ["https://replicate.delivery/..."] ``` -### -Bind the `Replicate` class in a service provider. +### List predictions + ```php -// app/Providers/AppServiceProvider.php -public function register() -{ - $this->app->bind(Replicate::class, function () { - return new Replicate( - apiToken: config('services.replicate.api_token'), - ); - }); -} -```` -### +$list = $replicate->predictions()->list(); +$list->results; // array of PredictionData +$list->next; // cursor for next page + +// Paginate +$nextPage = $replicate->predictions()->list(cursor: $list->next); +``` + +### Cancel a prediction -And use anywhere in your application. ```php -$data = app(Replicate::class)->predictions()->get($id); +$replicate->predictions()->cancel('xyz123'); ``` -### -Test your integration using Saloon's amazing [response recording](https://docs.saloon.dev/testing/recording-requests#fixture-path). +## Webhooks + +Pass webhook parameters directly to creation methods: + ```php -use Saloon\Laravel\Saloon; // composer require sammyjo20/saloon-laravel "^2.0" -... -Saloon::fake([ - MockResponse::fixture('getPrediction'), -]); +$prediction = $replicate->predictions()->create( + version: 'owner/model:version', + input: ['prompt' => 'hello'], + webhook: 'https://example.com/webhook', + webhookEventsFilter: ['completed'], +); +``` -$id = 'yfv4cakjzvh2lexxv7o5qzymqy'; +Get the webhook signing secret for verification: -// The initial request will check if a fixture called "getPrediction" -// exists. Because it doesn't exist yet, the real request will be -// sent and the response will be recorded to tests/Fixtures/Saloon/getPrediction.json. -$data = app(Replicate::class)->predictions()->get($id); +```php +$secret = $replicate->webhooks()->getSecret(); +$secret->key; // "whsec_..." +``` -// However, the next time the request is made, the fixture will -// exist, and Saloon will not make the request again. -$data = app(Replicate::class)->predictions()->get($id); +## Streaming + +```php +$prediction = $replicate->predictions()->create( + version: 'owner/model:version', + input: ['prompt' => 'hello'], + stream: true, +); + +// If the model supports streaming, use the stream URL +$prediction->urls['stream']; // SSE endpoint URL ``` -## Response Data -All responses are returned as data objects. Detailed information can be found by inspecting the following class properties: +## Models + +```php +// List public models +$models = $replicate->models()->list(); + +// Get a model +$model = $replicate->models()->get('stability-ai', 'sdxl'); + +// Create a model +$model = $replicate->models()->create( + owner: 'your-username', + name: 'my-model', + hardware: 'gpu-a40-large', + visibility: 'private', +); -* [PredictionData](https://github.com/benbjurstrom/replicate-php/blob/main/src/Data/PredictionData.php) -* [PredictionsData](https://github.com/benbjurstrom/replicate-php/blob/main/src/Data/PredictionsData.php) +// Update a model +$model = $replicate->models()->update('your-username', 'my-model', [ + 'description' => 'Updated description', +]); -## Webhooks -Replicate allows you to configure a webhook to be called when your prediction is complete. To do so chain `withWebhook($url)` onto your api instance before calling the `create` method. For example: +// Delete a model (must be private, no versions) +$replicate->models()->delete('your-username', 'my-model'); +``` + +### Model Versions ```php -$api->predictions()->withWebhook('https://www.example.com/webhook')->create($version, $input); -$data->id; // la5xlbbrfzg57ip5jlx6obmm5y +$versions = $replicate->models()->listVersions('stability-ai', 'sdxl'); +$version = $replicate->models()->getVersion('stability-ai', 'sdxl', 'abc123'); +$replicate->models()->deleteVersion('your-username', 'my-model', 'abc123'); ``` -## Available Prediction Methods -### get() -Use to get details about an existing prediction. If the prediction has completed the results will be under the output property. +## Deployments + ```php -use MarceloEatWorld\Replicate\Data\PredictionData; -... -$id = 'la5xlbbrfzg57ip5jlx6obmm5y' -/* @var PredictionData $data */ -$data = $api->predictions()->get($id); -$data->output[0]; // https://replicate.delivery/pbxt/6UFOVtl1xCJPAFFiTB2tfveYBNRLhLmJz8yMQAYCOeZSFhOhA/out-0.png +// List deployments +$deployments = $replicate->deployments()->list(); + +// Get a deployment +$deployment = $replicate->deployments()->get('your-username', 'my-deployment'); + +// Create a deployment +$deployment = $replicate->deployments()->create( + name: 'my-deployment', + model: 'your-username/my-model', + version: 'abc123...', + hardware: 'gpu-a40-large', + minInstances: 1, + maxInstances: 3, +); + +// Update a deployment +$deployment = $replicate->deployments()->update('your-username', 'my-deployment', [ + 'min_instances' => 2, + 'max_instances' => 5, +]); + +// Create prediction on a deployment +$prediction = $replicate->deployments()->createPrediction( + owner: 'your-username', + name: 'my-deployment', + input: ['prompt' => 'hello world'], +); + +// Delete a deployment +$replicate->deployments()->delete('your-username', 'my-deployment'); ``` -### list() -Use to get a cursor paginated list of predictions. Returns an PredictionsData object. +## Trainings + ```php -use MarceloEatWorld\Replicate\Data\PredictionsData -... +// Create a training +$training = $replicate->trainings()->create( + owner: 'stability-ai', + name: 'sdxl', + versionId: 'abc123...', + destination: 'your-username/my-trained-model', + input: ['train_data' => 'https://example.com/data.zip'], + webhook: 'https://example.com/training-done', +); + +// Get training status +$training = $replicate->trainings()->get($training->id); + +// List trainings +$trainings = $replicate->trainings()->list(); + +// Cancel a training +$replicate->trainings()->cancel($training->id); +``` + +## Files -/* @var PredictionsData $data */ -$data = $api->predictions()->list( - cursor: '123', // optional +```php +// Upload a file +$file = $replicate->files()->upload( + content: file_get_contents('/path/to/image.jpg'), + filename: 'image.jpg', + contentType: 'image/jpeg', ); -$data->results[0]->id; // la5xlbbrfzg57ip5jlx6obmm5y +// Get file metadata +$file = $replicate->files()->get($file->id); + +// List files +$files = $replicate->files()->list(); + +// Delete a file +$replicate->files()->delete($file->id); +``` + +## Collections + +```php +// List collections +$collections = $replicate->collections()->list(); + +// Get a collection with its models +$collection = $replicate->collections()->get('text-to-image'); +$collection->models; // array of ModelData +``` + +## Hardware + +```php +// List available hardware +$hardware = $replicate->hardware()->list(); +// Returns array of HardwareData with name and sku +``` + +## Account + +```php +$account = $replicate->account()->get(); +$account->username; +$account->type; // "user" or "organization" +``` + +## Using with Laravel + +Add your credentials to your services config: + +```php +// config/services.php +'replicate' => [ + 'api_token' => env('REPLICATE_API_TOKEN'), +], +``` + +Bind in a service provider: + +```php +// app/Providers/AppServiceProvider.php +public function register(): void +{ + $this->app->bind(Replicate::class, fn () => new Replicate( + apiToken: config('services.replicate.api_token'), + )); +} +``` + +Use anywhere: +```php +$prediction = app(Replicate::class)->predictions()->get($id); ``` -### create() -Use to create a new prediction (invoke a model). Returns an PredictionData object. + +## Testing + +Use Saloon's built-in mocking: + ```php -use MarceloEatWorld\Replicate\Data\PredictionData; -... -$version = '5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa'; -$input = [ - 'text' => 'Alice' -]; +use Saloon\Http\Faking\MockClient; +use Saloon\Http\Faking\MockResponse; +use MarceloEatWorld\Replicate\Requests\Predictions\GetPrediction; -/* @var PredictionData $data */ -$data = $api->predictions() - ->withWebhook('https://www.example.com/webhook') // optional - ->create($version, $input); -$data->id; // la5xlbbrfzg57ip5jlx6obmm5y +$mockClient = new MockClient([ + GetPrediction::class => MockResponse::make(['id' => 'xyz', 'status' => 'succeeded']), +]); + +$replicate = new Replicate('test-token'); +$replicate->withMockClient($mockClient); + +$prediction = $replicate->predictions()->get('xyz'); +$prediction->status; // "succeeded" ``` +## Response Data + +All responses are returned as typed data objects: + +| DTO | Description | +|-----|-------------| +| `AccountData` | Account info | +| `PredictionData` | Single prediction | +| `PredictionsData` | Paginated prediction list | +| `ModelData` | Single model | +| `ModelsData` | Paginated model list | +| `ModelVersionData` | Single model version | +| `ModelVersionsData` | Paginated version list | +| `CollectionData` | Single collection with models | +| `CollectionsData` | Paginated collection list | +| `DeploymentData` | Single deployment | +| `DeploymentsData` | Paginated deployment list | +| `TrainingData` | Single training | +| `TrainingsData` | Paginated training list | +| `FileData` | Single file metadata | +| `FilesData` | Paginated file list | +| `HardwareData` | Hardware option (name + SKU) | +| `WebhookSecretData` | Webhook signing secret | + ## Credits -- [Ben Bjurstrom](https://github.com/benbjurstrom) -- [All Contributors](../../contributors) +- [Marcelo Pereira](https://github.com/marceloeatworld) +- Originally forked from [benbjurstrom/replicate-php](https://github.com/benbjurstrom/replicate-php) ## License -The MIT License (MIT). Please see [License File](LICENSE.md) for more information. +The MIT License (MIT). See [License File](LICENSE.md) for more information. diff --git a/composer.json b/composer.json index 0c84aa3..e35aa7d 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "marceloeatworld/replicate-php", - "description": "A PHP client for the Replicate API", - "keywords": ["replicate", "php", "package"], + "description": "A PHP client for the Replicate API built on Saloon v4", + "keywords": ["replicate", "php", "ai", "machine-learning", "api-client", "saloon"], "license": "MIT", "authors": [ { @@ -10,15 +10,15 @@ } ], "require": { - "php": "^8.1.0", - "saloonphp/saloon": "^3.0" + "php": "^8.2", + "saloonphp/saloon": "^4.0" }, "require-dev": { - "laravel/pint": "^1.4", - "pestphp/pest": "^2.0.0", - "pestphp/pest-plugin-arch": "2.5.0", - "phpstan/phpstan": "^1.9.11", - "symfony/var-dumper": "^6.2.3" + "laravel/pint": "^1.18", + "pestphp/pest": "^3.7", + "pestphp/pest-plugin-arch": "^3.0", + "phpstan/phpstan": "^2.1", + "symfony/var-dumper": "^7.2" }, "autoload": { "psr-4": { @@ -50,4 +50,4 @@ "@test:unit" ] } -} \ No newline at end of file +} diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 5fd25fc..a2d4a82 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: max + level: 6 paths: - src diff --git a/src/Data/AccountData.php b/src/Data/AccountData.php new file mode 100644 index 0000000..5d5c864 --- /dev/null +++ b/src/Data/AccountData.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Data; + +use Saloon\Http\Response; + +final class AccountData +{ + public function __construct( + public readonly string $type, + public readonly string $username, + public readonly string $name, + public readonly string $githubUrl, + ) {} + + public static function fromResponse(Response $response): self + { + $data = $response->json(); + + return new self( + type: (string) ($data['type'] ?? ''), + username: (string) ($data['username'] ?? ''), + name: (string) ($data['name'] ?? ''), + githubUrl: (string) ($data['github_url'] ?? ''), + ); + } +} diff --git a/src/Data/CollectionData.php b/src/Data/CollectionData.php new file mode 100644 index 0000000..6fab76d --- /dev/null +++ b/src/Data/CollectionData.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Data; + +use Saloon\Http\Response; + +final class CollectionData +{ + /** + * @param array<int, ModelData> $models + */ + public function __construct( + public readonly string $slug, + public readonly string $name, + public readonly string $description, + public readonly ?string $fullDescription, + public readonly array $models, + ) {} + + public static function fromResponse(Response $response): self + { + $data = $response->json(); + + return self::fromArray($data); + } + + /** + * @param array<string, mixed> $data + */ + public static function fromArray(array $data): self + { + $fullDescription = $data['full_description'] ?? null; + $rawModels = $data['models'] ?? []; + + $models = []; + if (is_array($rawModels)) { + foreach ($rawModels as $model) { + if (is_array($model)) { + $models[] = ModelData::fromArray($model); + } + } + } + + return new self( + slug: (string) ($data['slug'] ?? ''), + name: (string) ($data['name'] ?? ''), + description: (string) ($data['description'] ?? ''), + fullDescription: is_string($fullDescription) ? $fullDescription : null, + models: $models, + ); + } +} diff --git a/src/Data/CollectionsData.php b/src/Data/CollectionsData.php new file mode 100644 index 0000000..5887fa5 --- /dev/null +++ b/src/Data/CollectionsData.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Data; + +use Saloon\Http\Response; + +final class CollectionsData +{ + /** + * @param array<int, CollectionData> $results + */ + public function __construct( + public readonly ?string $previous, + public readonly ?string $next, + public readonly array $results, + ) {} + + public static function fromResponse(Response $response): self + { + $data = $response->json(); + + $previous = $data['previous'] ?? null; + $next = $data['next'] ?? null; + $rawResults = $data['results'] ?? []; + + $results = []; + if (is_array($rawResults)) { + foreach ($rawResults as $item) { + if (is_array($item)) { + $results[] = CollectionData::fromArray($item); + } + } + } + + return new self( + previous: is_string($previous) ? $previous : null, + next: is_string($next) ? $next : null, + results: $results, + ); + } +} diff --git a/src/Data/DeploymentData.php b/src/Data/DeploymentData.php new file mode 100644 index 0000000..d3cdd65 --- /dev/null +++ b/src/Data/DeploymentData.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Data; + +use Saloon\Http\Response; + +final class DeploymentData +{ + /** + * @param array<string, mixed>|null $currentRelease + */ + public function __construct( + public readonly string $owner, + public readonly string $name, + public readonly ?array $currentRelease, + ) {} + + public static function fromResponse(Response $response): self + { + $data = $response->json(); + + return self::fromArray($data); + } + + /** + * @param array<string, mixed> $data + */ + public static function fromArray(array $data): self + { + $currentRelease = $data['current_release'] ?? null; + + return new self( + owner: (string) ($data['owner'] ?? ''), + name: (string) ($data['name'] ?? ''), + currentRelease: is_array($currentRelease) ? $currentRelease : null, + ); + } +} diff --git a/src/Data/DeploymentsData.php b/src/Data/DeploymentsData.php new file mode 100644 index 0000000..456f5e1 --- /dev/null +++ b/src/Data/DeploymentsData.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Data; + +use Saloon\Http\Response; + +final class DeploymentsData +{ + /** + * @param array<int, DeploymentData> $results + */ + public function __construct( + public readonly ?string $previous, + public readonly ?string $next, + public readonly array $results, + ) {} + + public static function fromResponse(Response $response): self + { + $data = $response->json(); + + $previous = $data['previous'] ?? null; + $next = $data['next'] ?? null; + $rawResults = $data['results'] ?? []; + + $results = []; + if (is_array($rawResults)) { + foreach ($rawResults as $item) { + if (is_array($item)) { + $results[] = DeploymentData::fromArray($item); + } + } + } + + return new self( + previous: is_string($previous) ? $previous : null, + next: is_string($next) ? $next : null, + results: $results, + ); + } +} diff --git a/src/Data/FileData.php b/src/Data/FileData.php new file mode 100644 index 0000000..884eb86 --- /dev/null +++ b/src/Data/FileData.php @@ -0,0 +1,60 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Data; + +use Saloon\Http\Response; + +final class FileData +{ + /** + * @param array<string, string>|null $checksums + * @param array<string, mixed>|null $metadata + * @param array<string, string>|null $urls + */ + public function __construct( + public readonly string $id, + public readonly string $name, + public readonly string $contentType, + public readonly int $size, + public readonly ?string $etag, + public readonly ?array $checksums, + public readonly ?array $metadata, + public readonly string $createdAt, + public readonly ?string $expiresAt, + public readonly ?array $urls, + ) {} + + public static function fromResponse(Response $response): self + { + $data = $response->json(); + + return self::fromArray($data); + } + + /** + * @param array<string, mixed> $data + */ + public static function fromArray(array $data): self + { + $etag = $data['etag'] ?? null; + $checksums = $data['checksums'] ?? null; + $metadata = $data['metadata'] ?? null; + $expiresAt = $data['expires_at'] ?? null; + $urls = $data['urls'] ?? null; + + return new self( + id: (string) ($data['id'] ?? ''), + name: (string) ($data['name'] ?? ''), + contentType: (string) ($data['content_type'] ?? ''), + size: (int) ($data['size'] ?? 0), + etag: is_string($etag) ? $etag : null, + checksums: is_array($checksums) ? $checksums : null, + metadata: is_array($metadata) ? $metadata : null, + createdAt: (string) ($data['created_at'] ?? ''), + expiresAt: is_string($expiresAt) ? $expiresAt : null, + urls: is_array($urls) ? $urls : null, + ); + } +} diff --git a/src/Data/FilesData.php b/src/Data/FilesData.php new file mode 100644 index 0000000..a7f7904 --- /dev/null +++ b/src/Data/FilesData.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Data; + +use Saloon\Http\Response; + +final class FilesData +{ + /** + * @param array<int, FileData> $results + */ + public function __construct( + public readonly ?string $previous, + public readonly ?string $next, + public readonly array $results, + ) {} + + public static function fromResponse(Response $response): self + { + $data = $response->json(); + + $previous = $data['previous'] ?? null; + $next = $data['next'] ?? null; + $rawResults = $data['results'] ?? []; + + $results = []; + if (is_array($rawResults)) { + foreach ($rawResults as $item) { + if (is_array($item)) { + $results[] = FileData::fromArray($item); + } + } + } + + return new self( + previous: is_string($previous) ? $previous : null, + next: is_string($next) ? $next : null, + results: $results, + ); + } +} diff --git a/src/Data/HardwareData.php b/src/Data/HardwareData.php new file mode 100644 index 0000000..cf67345 --- /dev/null +++ b/src/Data/HardwareData.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Data; + +use Saloon\Http\Response; + +final class HardwareData +{ + public function __construct( + public readonly string $name, + public readonly string $sku, + ) {} + + /** + * @return array<int, self> + */ + public static function collectionFromResponse(Response $response): array + { + $data = $response->json(); + + $results = []; + foreach ($data as $item) { + if (is_array($item)) { + $results[] = new self( + name: (string) ($item['name'] ?? ''), + sku: (string) ($item['sku'] ?? ''), + ); + } + } + + return $results; + } +} diff --git a/src/Data/ModelData.php b/src/Data/ModelData.php new file mode 100644 index 0000000..ee9dab2 --- /dev/null +++ b/src/Data/ModelData.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Data; + +use Saloon\Http\Response; + +final class ModelData +{ + /** + * @param array<string, mixed>|null $defaultExample + * @param array<string, mixed>|null $latestVersion + */ + public function __construct( + public readonly string $url, + public readonly string $owner, + public readonly string $name, + public readonly ?string $description, + public readonly string $visibility, + public readonly ?string $githubUrl, + public readonly ?string $paperUrl, + public readonly ?string $licenseUrl, + public readonly int $runCount, + public readonly ?string $coverImageUrl, + public readonly ?array $defaultExample, + public readonly ?array $latestVersion, + ) {} + + public static function fromResponse(Response $response): self + { + $data = $response->json(); + + return self::fromArray($data); + } + + /** + * @param array<string, mixed> $data + */ + public static function fromArray(array $data): self + { + $description = $data['description'] ?? null; + $githubUrl = $data['github_url'] ?? null; + $paperUrl = $data['paper_url'] ?? null; + $licenseUrl = $data['license_url'] ?? null; + $coverImageUrl = $data['cover_image_url'] ?? null; + $defaultExample = $data['default_example'] ?? null; + $latestVersion = $data['latest_version'] ?? null; + + return new self( + url: (string) ($data['url'] ?? ''), + owner: (string) ($data['owner'] ?? ''), + name: (string) ($data['name'] ?? ''), + description: is_string($description) ? $description : null, + visibility: (string) ($data['visibility'] ?? ''), + githubUrl: is_string($githubUrl) ? $githubUrl : null, + paperUrl: is_string($paperUrl) ? $paperUrl : null, + licenseUrl: is_string($licenseUrl) ? $licenseUrl : null, + runCount: (int) ($data['run_count'] ?? 0), + coverImageUrl: is_string($coverImageUrl) ? $coverImageUrl : null, + defaultExample: is_array($defaultExample) ? $defaultExample : null, + latestVersion: is_array($latestVersion) ? $latestVersion : null, + ); + } +} diff --git a/src/Data/ModelVersionData.php b/src/Data/ModelVersionData.php new file mode 100644 index 0000000..9fb6ea6 --- /dev/null +++ b/src/Data/ModelVersionData.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Data; + +use Saloon\Http\Response; + +final class ModelVersionData +{ + /** + * @param array<string, mixed>|null $openapiSchema + */ + public function __construct( + public readonly string $id, + public readonly string $createdAt, + public readonly ?string $cogVersion, + public readonly ?array $openapiSchema, + ) {} + + public static function fromResponse(Response $response): self + { + $data = $response->json(); + + return self::fromArray($data); + } + + /** + * @param array<string, mixed> $data + */ + public static function fromArray(array $data): self + { + $cogVersion = $data['cog_version'] ?? null; + $openapiSchema = $data['openapi_schema'] ?? null; + + return new self( + id: (string) ($data['id'] ?? ''), + createdAt: (string) ($data['created_at'] ?? ''), + cogVersion: is_string($cogVersion) ? $cogVersion : null, + openapiSchema: is_array($openapiSchema) ? $openapiSchema : null, + ); + } +} diff --git a/src/Data/ModelVersionsData.php b/src/Data/ModelVersionsData.php new file mode 100644 index 0000000..7fddcfb --- /dev/null +++ b/src/Data/ModelVersionsData.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Data; + +use Saloon\Http\Response; + +final class ModelVersionsData +{ + /** + * @param array<int, ModelVersionData> $results + */ + public function __construct( + public readonly ?string $previous, + public readonly ?string $next, + public readonly array $results, + ) {} + + public static function fromResponse(Response $response): self + { + $data = $response->json(); + + $previous = $data['previous'] ?? null; + $next = $data['next'] ?? null; + $rawResults = $data['results'] ?? []; + + $results = []; + if (is_array($rawResults)) { + foreach ($rawResults as $item) { + if (is_array($item)) { + $results[] = ModelVersionData::fromArray($item); + } + } + } + + return new self( + previous: is_string($previous) ? $previous : null, + next: is_string($next) ? $next : null, + results: $results, + ); + } +} diff --git a/src/Data/ModelsData.php b/src/Data/ModelsData.php new file mode 100644 index 0000000..a3359a8 --- /dev/null +++ b/src/Data/ModelsData.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Data; + +use Saloon\Http\Response; + +final class ModelsData +{ + /** + * @param array<int, ModelData> $results + */ + public function __construct( + public readonly ?string $previous, + public readonly ?string $next, + public readonly array $results, + ) {} + + public static function fromResponse(Response $response): self + { + $data = $response->json(); + + $previous = $data['previous'] ?? null; + $next = $data['next'] ?? null; + $rawResults = $data['results'] ?? []; + + $results = []; + if (is_array($rawResults)) { + foreach ($rawResults as $item) { + if (is_array($item)) { + $results[] = ModelData::fromArray($item); + } + } + } + + return new self( + previous: is_string($previous) ? $previous : null, + next: is_string($next) ? $next : null, + results: $results, + ); + } +} diff --git a/src/Data/PredictionData.php b/src/Data/PredictionData.php index 307bab8..24cee3e 100644 --- a/src/Data/PredictionData.php +++ b/src/Data/PredictionData.php @@ -1,55 +1,69 @@ <?php +declare(strict_types=1); + namespace MarceloEatWorld\Replicate\Data; -use Exception; use Saloon\Http\Response; final class PredictionData { /** - * @param array<string, string|int|float> $input - * @param array<string, string|int|float> $metrics + * @param array<string, mixed> $input + * @param array<string, mixed>|null $metrics * @param array<string, string> $urls - * @param string|array<int, string> $output - * @param null|array<string, string> $error */ public function __construct( - public string $id, - public string $model, - public string $version, - public string $createdAt, - public ?string $completedAt, - public ?string $startedAt, - public string $status, - public ?bool $webhookCompleted, - public array $input, - public ?array $metrics, - public array $urls, - public array|string|null $error, - public string|array|null $output, - ) { - } + public readonly string $id, + public readonly ?string $model, + public readonly ?string $version, + public readonly string $createdAt, + public readonly ?string $completedAt, + public readonly ?string $startedAt, + public readonly string $status, + public readonly ?bool $webhookCompleted, + public readonly array $input, + public readonly ?string $logs, + public readonly ?array $metrics, + public readonly array $urls, + public readonly mixed $error, + public readonly mixed $output, + ) {} public static function fromResponse(Response $response): self { $data = $response->json(); - if (! is_array($data)) { - throw new Exception('Invalid response'); - } + + return self::fromArray($data); + } + + /** + * @param array<string, mixed> $data + */ + public static function fromArray(array $data): self + { + $model = $data['model'] ?? null; + $version = $data['version'] ?? null; + $completedAt = $data['completed_at'] ?? null; + $startedAt = $data['started_at'] ?? null; + $webhookCompleted = $data['webhook_completed'] ?? null; + $logs = $data['logs'] ?? null; + $metrics = $data['metrics'] ?? null; + $urls = $data['urls'] ?? []; return new self( - id: $data['id'], - model: $data['model'], - version: $data['version'], - createdAt: $data['created_at'], - completedAt: $data['completed_at'] ?? null, - startedAt: $data['started_at'] ?? null, - status: $data['status'], - webhookCompleted: $data['webhook_completed'] ?? null, - input: $data['input'], - metrics: $data['metrics'] ?? null, - urls: $data['urls'], + id: (string) ($data['id'] ?? ''), + model: is_string($model) ? $model : null, + version: is_string($version) ? $version : null, + createdAt: (string) ($data['created_at'] ?? ''), + completedAt: is_string($completedAt) ? $completedAt : null, + startedAt: is_string($startedAt) ? $startedAt : null, + status: (string) ($data['status'] ?? ''), + webhookCompleted: is_bool($webhookCompleted) ? $webhookCompleted : null, + input: is_array($data['input'] ?? null) ? $data['input'] : [], + logs: is_string($logs) ? $logs : null, + metrics: is_array($metrics) ? $metrics : null, + urls: is_array($urls) ? $urls : [], error: $data['error'] ?? null, output: $data['output'] ?? null, ); diff --git a/src/Data/PredictionsData.php b/src/Data/PredictionsData.php index 4ed40e6..28a4911 100644 --- a/src/Data/PredictionsData.php +++ b/src/Data/PredictionsData.php @@ -1,8 +1,9 @@ <?php +declare(strict_types=1); + namespace MarceloEatWorld\Replicate\Data; -use Exception; use Saloon\Http\Response; final class PredictionsData @@ -11,41 +12,31 @@ final class PredictionsData * @param array<int, PredictionData> $results */ public function __construct( - public ?string $previous, - public ?string $next, - public array $results - ) { - } + public readonly ?string $previous, + public readonly ?string $next, + public readonly array $results, + ) {} public static function fromResponse(Response $response): self { $data = $response->json(); - if (! is_array($data)) { - throw new Exception('Invalid response'); - } + + $previous = $data['previous'] ?? null; + $next = $data['next'] ?? null; + $rawResults = $data['results'] ?? []; $results = []; - foreach ($data['results'] as $result) { - $results[] = new PredictionData( - id: $result['id'], - model: $data['model'], - version: $result['version'], - createdAt: $result['created_at'], - completedAt: $result['completed_at'], - startedAt: $result['started_at'], - status: $result['status'], - webhookCompleted: $result['webhook_completed'], - input: $result['input'], - metrics: $result['metrics'], - urls: $result['urls'], - error: $result['error'], - output: $result['output'], - ); + if (is_array($rawResults)) { + foreach ($rawResults as $item) { + if (is_array($item)) { + $results[] = PredictionData::fromArray($item); + } + } } return new self( - previous: $data['previous'], - next: $data['next'], + previous: is_string($previous) ? $previous : null, + next: is_string($next) ? $next : null, results: $results, ); } diff --git a/src/Data/TrainingData.php b/src/Data/TrainingData.php new file mode 100644 index 0000000..43b2663 --- /dev/null +++ b/src/Data/TrainingData.php @@ -0,0 +1,70 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Data; + +use Saloon\Http\Response; + +final class TrainingData +{ + /** + * @param array<string, mixed> $input + * @param array<string, mixed>|null $output + * @param array<string, mixed>|null $metrics + * @param array<string, string> $urls + */ + public function __construct( + public readonly string $id, + public readonly ?string $model, + public readonly ?string $version, + public readonly string $status, + public readonly array $input, + public readonly ?array $output, + public readonly ?string $logs, + public readonly mixed $error, + public readonly ?array $metrics, + public readonly string $createdAt, + public readonly ?string $startedAt, + public readonly ?string $completedAt, + public readonly array $urls, + ) {} + + public static function fromResponse(Response $response): self + { + $data = $response->json(); + + return self::fromArray($data); + } + + /** + * @param array<string, mixed> $data + */ + public static function fromArray(array $data): self + { + $model = $data['model'] ?? null; + $version = $data['version'] ?? null; + $output = $data['output'] ?? null; + $logs = $data['logs'] ?? null; + $metrics = $data['metrics'] ?? null; + $startedAt = $data['started_at'] ?? null; + $completedAt = $data['completed_at'] ?? null; + $urls = $data['urls'] ?? []; + + return new self( + id: (string) ($data['id'] ?? ''), + model: is_string($model) ? $model : null, + version: is_string($version) ? $version : null, + status: (string) ($data['status'] ?? ''), + input: is_array($data['input'] ?? null) ? $data['input'] : [], + output: is_array($output) ? $output : null, + logs: is_string($logs) ? $logs : null, + error: $data['error'] ?? null, + metrics: is_array($metrics) ? $metrics : null, + createdAt: (string) ($data['created_at'] ?? ''), + startedAt: is_string($startedAt) ? $startedAt : null, + completedAt: is_string($completedAt) ? $completedAt : null, + urls: is_array($urls) ? $urls : [], + ); + } +} diff --git a/src/Data/TrainingsData.php b/src/Data/TrainingsData.php new file mode 100644 index 0000000..c309707 --- /dev/null +++ b/src/Data/TrainingsData.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Data; + +use Saloon\Http\Response; + +final class TrainingsData +{ + /** + * @param array<int, TrainingData> $results + */ + public function __construct( + public readonly ?string $previous, + public readonly ?string $next, + public readonly array $results, + ) {} + + public static function fromResponse(Response $response): self + { + $data = $response->json(); + + $previous = $data['previous'] ?? null; + $next = $data['next'] ?? null; + $rawResults = $data['results'] ?? []; + + $results = []; + if (is_array($rawResults)) { + foreach ($rawResults as $item) { + if (is_array($item)) { + $results[] = TrainingData::fromArray($item); + } + } + } + + return new self( + previous: is_string($previous) ? $previous : null, + next: is_string($next) ? $next : null, + results: $results, + ); + } +} diff --git a/src/Data/WebhookSecretData.php b/src/Data/WebhookSecretData.php new file mode 100644 index 0000000..366a96a --- /dev/null +++ b/src/Data/WebhookSecretData.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Data; + +use Saloon\Http\Response; + +final class WebhookSecretData +{ + public function __construct( + public readonly string $key, + ) {} + + public static function fromResponse(Response $response): self + { + $data = $response->json(); + + return new self( + key: (string) ($data['key'] ?? ''), + ); + } +} diff --git a/src/PredictionsResource.php b/src/PredictionsResource.php deleted file mode 100644 index 5e4b173..0000000 --- a/src/PredictionsResource.php +++ /dev/null @@ -1,119 +0,0 @@ -<?php - -namespace MarceloEatWorld\Replicate; - -use MarceloEatWorld\Replicate\Data\PredictionData; -use MarceloEatWorld\Replicate\Data\PredictionsData; -use MarceloEatWorld\Replicate\Requests\GetPrediction; -use MarceloEatWorld\Replicate\Requests\GetPredictions; -use MarceloEatWorld\Replicate\Requests\PostPrediction; -use MarceloEatWorld\Replicate\Requests\PostDeploymentPrediction; -use Exception; - -class PredictionsResource extends Resource -{ - protected ?string $webhookUrl = null; - - /** - * @var array<string> - */ - protected ?array $webhookEvents; - - public function list(?string $cursor = null): PredictionsData - { - $request = new GetPredictions(); - - if ($cursor) { - $request->query()->add('cursor', $cursor); - } - - $response = $this->connector->send($request); - $data = $response->dtoOrFail(); - if (! $data instanceof PredictionsData) { - throw new Exception('Unexpected data type'); - } - - return $data; - } - - public function get(string $id): PredictionData - { - $request = new GetPrediction($id); - $response = $this->connector->send($request); - - $data = $response->dtoOrFail(); - if (! $data instanceof PredictionData) { - throw new Exception('Unexpected data type'); - } - - return $data; - } - - /** - * @param array<string, float|int|string|null> $input - * - * @throws Exception - */ - public function create(string $version, array $input): PredictionData - { - $request = new PostPrediction($version, $input); - if ($this->webhookUrl) { - // https://replicate.com/changelog/2023-02-10-improved-webhook-events-and-event-filtering - $request->body()->merge([ - 'webhook' => $this->webhookUrl, - 'webhook_events_filter' => $this->webhookEvents, - ]); - } - - $response = $this->connector->send($request); - - $data = $response->dtoOrFail(); - if (! $data instanceof PredictionData) { - throw new Exception('Unexpected data type'); - } - - return $data; - } - - /** - * @param array<string> $events - */ - public function withWebhook(string $url, ?array $events = ['completed']): self - { - $this->webhookUrl = $url; - $this->webhookEvents = $events; - - return $this; - } - /** - * Creates a prediction for a specific deployment. - * - * @param string $fullDeploymentName Full name of the deployment. - * @param array<string, mixed> $input The input data for the prediction. - * @return PredictionData The data of the created prediction. - * @throws Exception If the response does not contain the expected data. - */ - public function createForDeployment(string $fullDeploymentName, array $input): PredictionData - { - $request = new PostDeploymentPrediction($input, $fullDeploymentName); - - if ($this->webhookUrl) { - $request->body()->merge([ - 'webhook' => $this->webhookUrl, - 'webhook_events_filter' => $this->webhookEvents, - ]); - } - - $response = $this->connector->send($request); - - $data = $response->dtoOrFail(); - if (! $data instanceof PredictionData) { - throw new Exception('Unexpected data type'); - } - - return $data; - } - - - -} diff --git a/src/Replicate.php b/src/Replicate.php index 2d95269..1261322 100644 --- a/src/Replicate.php +++ b/src/Replicate.php @@ -4,18 +4,23 @@ namespace MarceloEatWorld\Replicate; +use MarceloEatWorld\Replicate\Resources\AccountResource; +use MarceloEatWorld\Replicate\Resources\CollectionsResource; +use MarceloEatWorld\Replicate\Resources\DeploymentsResource; +use MarceloEatWorld\Replicate\Resources\FilesResource; +use MarceloEatWorld\Replicate\Resources\HardwareResource; +use MarceloEatWorld\Replicate\Resources\ModelsResource; +use MarceloEatWorld\Replicate\Resources\PredictionsResource; +use MarceloEatWorld\Replicate\Resources\TrainingsResource; +use MarceloEatWorld\Replicate\Resources\WebhooksResource; +use Saloon\Http\Auth\TokenAuthenticator; use Saloon\Http\Connector; -/** - * @internal - */ final class Replicate extends Connector { public function __construct( - public string $apiToken, - ) { - $this->withTokenAuth($this->apiToken, 'Token'); - } + protected readonly string $apiToken, + ) {} public function resolveBaseUrl(): string { @@ -30,8 +35,53 @@ protected function defaultHeaders(): array ]; } + protected function defaultAuth(): TokenAuthenticator + { + return new TokenAuthenticator($this->apiToken); + } + + public function account(): AccountResource + { + return new AccountResource($this); + } + public function predictions(): PredictionsResource { return new PredictionsResource($this); } + + public function models(): ModelsResource + { + return new ModelsResource($this); + } + + public function collections(): CollectionsResource + { + return new CollectionsResource($this); + } + + public function deployments(): DeploymentsResource + { + return new DeploymentsResource($this); + } + + public function trainings(): TrainingsResource + { + return new TrainingsResource($this); + } + + public function files(): FilesResource + { + return new FilesResource($this); + } + + public function hardware(): HardwareResource + { + return new HardwareResource($this); + } + + public function webhooks(): WebhooksResource + { + return new WebhooksResource($this); + } } diff --git a/src/Requests/Account/GetAccount.php b/src/Requests/Account/GetAccount.php new file mode 100644 index 0000000..c34989b --- /dev/null +++ b/src/Requests/Account/GetAccount.php @@ -0,0 +1,25 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Account; + +use MarceloEatWorld\Replicate\Data\AccountData; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; + +class GetAccount extends Request +{ + protected Method $method = Method::GET; + + public function resolveEndpoint(): string + { + return '/account'; + } + + public function createDtoFromResponse(Response $response): AccountData + { + return AccountData::fromResponse($response); + } +} diff --git a/src/Requests/Collections/GetCollection.php b/src/Requests/Collections/GetCollection.php new file mode 100644 index 0000000..19788a1 --- /dev/null +++ b/src/Requests/Collections/GetCollection.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Collections; + +use MarceloEatWorld\Replicate\Data\CollectionData; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; + +class GetCollection extends Request +{ + protected Method $method = Method::GET; + + public function __construct( + protected readonly string $slug, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/collections/%s', $this->slug); + } + + public function createDtoFromResponse(Response $response): CollectionData + { + return CollectionData::fromResponse($response); + } +} diff --git a/src/Requests/Collections/GetCollections.php b/src/Requests/Collections/GetCollections.php new file mode 100644 index 0000000..6519b89 --- /dev/null +++ b/src/Requests/Collections/GetCollections.php @@ -0,0 +1,25 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Collections; + +use MarceloEatWorld\Replicate\Data\CollectionsData; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; + +class GetCollections extends Request +{ + protected Method $method = Method::GET; + + public function resolveEndpoint(): string + { + return '/collections'; + } + + public function createDtoFromResponse(Response $response): CollectionsData + { + return CollectionsData::fromResponse($response); + } +} diff --git a/src/Requests/Deployments/DeleteDeployment.php b/src/Requests/Deployments/DeleteDeployment.php new file mode 100644 index 0000000..f14666e --- /dev/null +++ b/src/Requests/Deployments/DeleteDeployment.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Deployments; + +use Saloon\Enums\Method; +use Saloon\Http\Request; + +class DeleteDeployment extends Request +{ + protected Method $method = Method::DELETE; + + public function __construct( + protected readonly string $owner, + protected readonly string $name, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/deployments/%s/%s', $this->owner, $this->name); + } +} diff --git a/src/Requests/Deployments/GetDeployment.php b/src/Requests/Deployments/GetDeployment.php new file mode 100644 index 0000000..0371d55 --- /dev/null +++ b/src/Requests/Deployments/GetDeployment.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Deployments; + +use MarceloEatWorld\Replicate\Data\DeploymentData; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; + +class GetDeployment extends Request +{ + protected Method $method = Method::GET; + + public function __construct( + protected readonly string $owner, + protected readonly string $name, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/deployments/%s/%s', $this->owner, $this->name); + } + + public function createDtoFromResponse(Response $response): DeploymentData + { + return DeploymentData::fromResponse($response); + } +} diff --git a/src/Requests/Deployments/GetDeployments.php b/src/Requests/Deployments/GetDeployments.php new file mode 100644 index 0000000..6cba5d4 --- /dev/null +++ b/src/Requests/Deployments/GetDeployments.php @@ -0,0 +1,25 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Deployments; + +use MarceloEatWorld\Replicate\Data\DeploymentsData; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; + +class GetDeployments extends Request +{ + protected Method $method = Method::GET; + + public function resolveEndpoint(): string + { + return '/deployments'; + } + + public function createDtoFromResponse(Response $response): DeploymentsData + { + return DeploymentsData::fromResponse($response); + } +} diff --git a/src/Requests/Deployments/PatchDeployment.php b/src/Requests/Deployments/PatchDeployment.php new file mode 100644 index 0000000..1960971 --- /dev/null +++ b/src/Requests/Deployments/PatchDeployment.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Deployments; + +use MarceloEatWorld\Replicate\Data\DeploymentData; +use Saloon\Contracts\Body\HasBody; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; +use Saloon\Traits\Body\HasJsonBody; + +class PatchDeployment extends Request implements HasBody +{ + use HasJsonBody; + + protected Method $method = Method::PATCH; + + /** + * @param array<string, mixed> $data + */ + public function __construct( + protected readonly string $owner, + protected readonly string $name, + protected readonly array $data, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/deployments/%s/%s', $this->owner, $this->name); + } + + /** + * @return array<string, mixed> + */ + protected function defaultBody(): array + { + return $this->data; + } + + public function createDtoFromResponse(Response $response): DeploymentData + { + return DeploymentData::fromResponse($response); + } +} diff --git a/src/Requests/Deployments/PostDeployment.php b/src/Requests/Deployments/PostDeployment.php new file mode 100644 index 0000000..4ef59ac --- /dev/null +++ b/src/Requests/Deployments/PostDeployment.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Deployments; + +use MarceloEatWorld\Replicate\Data\DeploymentData; +use Saloon\Contracts\Body\HasBody; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; +use Saloon\Traits\Body\HasJsonBody; + +class PostDeployment extends Request implements HasBody +{ + use HasJsonBody; + + protected Method $method = Method::POST; + + public function __construct( + protected readonly string $name, + protected readonly string $model, + protected readonly string $version, + protected readonly string $hardware, + protected readonly int $minInstances, + protected readonly int $maxInstances, + ) {} + + public function resolveEndpoint(): string + { + return '/deployments'; + } + + /** + * @return array<string, mixed> + */ + protected function defaultBody(): array + { + return [ + 'name' => $this->name, + 'model' => $this->model, + 'version' => $this->version, + 'hardware' => $this->hardware, + 'min_instances' => $this->minInstances, + 'max_instances' => $this->maxInstances, + ]; + } + + public function createDtoFromResponse(Response $response): DeploymentData + { + return DeploymentData::fromResponse($response); + } +} diff --git a/src/Requests/Deployments/PostDeploymentPrediction.php b/src/Requests/Deployments/PostDeploymentPrediction.php new file mode 100644 index 0000000..aa89f22 --- /dev/null +++ b/src/Requests/Deployments/PostDeploymentPrediction.php @@ -0,0 +1,75 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Deployments; + +use MarceloEatWorld\Replicate\Data\PredictionData; +use Saloon\Contracts\Body\HasBody; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; +use Saloon\Traits\Body\HasJsonBody; + +class PostDeploymentPrediction extends Request implements HasBody +{ + use HasJsonBody; + + protected Method $method = Method::POST; + + /** + * @param array<string, mixed> $input + * @param array<string>|null $webhookEventsFilter + */ + public function __construct( + protected readonly string $owner, + protected readonly string $name, + protected readonly array $input, + protected readonly ?string $webhook = null, + protected readonly ?array $webhookEventsFilter = null, + protected readonly bool $stream = false, + protected readonly ?int $wait = null, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/deployments/%s/%s/predictions', $this->owner, $this->name); + } + + protected function defaultHeaders(): array + { + $headers = []; + + if ($this->wait !== null) { + $headers['Prefer'] = sprintf('wait=%d', $this->wait); + } + + return $headers; + } + + /** + * @return array<string, mixed> + */ + protected function defaultBody(): array + { + $body = [ + 'input' => $this->input, + ]; + + if ($this->webhook !== null) { + $body['webhook'] = $this->webhook; + $body['webhook_events_filter'] = $this->webhookEventsFilter; + } + + if ($this->stream) { + $body['stream'] = true; + } + + return $body; + } + + public function createDtoFromResponse(Response $response): PredictionData + { + return PredictionData::fromResponse($response); + } +} diff --git a/src/Requests/Files/DeleteFile.php b/src/Requests/Files/DeleteFile.php new file mode 100644 index 0000000..5476591 --- /dev/null +++ b/src/Requests/Files/DeleteFile.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Files; + +use Saloon\Enums\Method; +use Saloon\Http\Request; + +class DeleteFile extends Request +{ + protected Method $method = Method::DELETE; + + public function __construct( + protected readonly string $id, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/files/%s', $this->id); + } +} diff --git a/src/Requests/Files/GetFile.php b/src/Requests/Files/GetFile.php new file mode 100644 index 0000000..8659cd2 --- /dev/null +++ b/src/Requests/Files/GetFile.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Files; + +use MarceloEatWorld\Replicate\Data\FileData; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; + +class GetFile extends Request +{ + protected Method $method = Method::GET; + + public function __construct( + protected readonly string $id, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/files/%s', $this->id); + } + + public function createDtoFromResponse(Response $response): FileData + { + return FileData::fromResponse($response); + } +} diff --git a/src/Requests/Files/GetFiles.php b/src/Requests/Files/GetFiles.php new file mode 100644 index 0000000..27d0657 --- /dev/null +++ b/src/Requests/Files/GetFiles.php @@ -0,0 +1,25 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Files; + +use MarceloEatWorld\Replicate\Data\FilesData; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; + +class GetFiles extends Request +{ + protected Method $method = Method::GET; + + public function resolveEndpoint(): string + { + return '/files'; + } + + public function createDtoFromResponse(Response $response): FilesData + { + return FilesData::fromResponse($response); + } +} diff --git a/src/Requests/Files/PostFile.php b/src/Requests/Files/PostFile.php new file mode 100644 index 0000000..e608904 --- /dev/null +++ b/src/Requests/Files/PostFile.php @@ -0,0 +1,67 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Files; + +use MarceloEatWorld\Replicate\Data\FileData; +use Saloon\Contracts\Body\HasBody; +use Saloon\Data\MultipartValue; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; +use Saloon\Traits\Body\HasMultipartBody; + +class PostFile extends Request implements HasBody +{ + use HasMultipartBody; + + protected Method $method = Method::POST; + + /** + * @param string $content The file contents + * @param string $filename The filename + * @param string|null $contentType The MIME type + * @param array<string, mixed>|null $metadata Optional metadata + */ + public function __construct( + protected readonly string $content, + protected readonly string $filename, + protected readonly ?string $contentType = null, + protected readonly ?array $metadata = null, + ) {} + + public function resolveEndpoint(): string + { + return '/files'; + } + + /** + * @return array<int, MultipartValue> + */ + protected function defaultBody(): array + { + $parts = [ + new MultipartValue( + name: 'content', + value: $this->content, + filename: $this->filename, + headers: $this->contentType ? ['Content-Type' => $this->contentType] : [], + ), + ]; + + if ($this->metadata !== null) { + $parts[] = new MultipartValue( + name: 'metadata', + value: json_encode($this->metadata, JSON_THROW_ON_ERROR), + ); + } + + return $parts; + } + + public function createDtoFromResponse(Response $response): FileData + { + return FileData::fromResponse($response); + } +} diff --git a/src/Requests/Hardware/GetHardware.php b/src/Requests/Hardware/GetHardware.php new file mode 100644 index 0000000..850d901 --- /dev/null +++ b/src/Requests/Hardware/GetHardware.php @@ -0,0 +1,18 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Hardware; + +use Saloon\Enums\Method; +use Saloon\Http\Request; + +class GetHardware extends Request +{ + protected Method $method = Method::GET; + + public function resolveEndpoint(): string + { + return '/hardware'; + } +} diff --git a/src/Requests/Models/DeleteModel.php b/src/Requests/Models/DeleteModel.php new file mode 100644 index 0000000..33420e5 --- /dev/null +++ b/src/Requests/Models/DeleteModel.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Models; + +use Saloon\Enums\Method; +use Saloon\Http\Request; + +class DeleteModel extends Request +{ + protected Method $method = Method::DELETE; + + public function __construct( + protected readonly string $owner, + protected readonly string $name, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/models/%s/%s', $this->owner, $this->name); + } +} diff --git a/src/Requests/Models/DeleteModelVersion.php b/src/Requests/Models/DeleteModelVersion.php new file mode 100644 index 0000000..ab46c25 --- /dev/null +++ b/src/Requests/Models/DeleteModelVersion.php @@ -0,0 +1,24 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Models; + +use Saloon\Enums\Method; +use Saloon\Http\Request; + +class DeleteModelVersion extends Request +{ + protected Method $method = Method::DELETE; + + public function __construct( + protected readonly string $owner, + protected readonly string $name, + protected readonly string $versionId, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/models/%s/%s/versions/%s', $this->owner, $this->name, $this->versionId); + } +} diff --git a/src/Requests/Models/GetModel.php b/src/Requests/Models/GetModel.php new file mode 100644 index 0000000..fdba6f7 --- /dev/null +++ b/src/Requests/Models/GetModel.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Models; + +use MarceloEatWorld\Replicate\Data\ModelData; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; + +class GetModel extends Request +{ + protected Method $method = Method::GET; + + public function __construct( + protected readonly string $owner, + protected readonly string $name, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/models/%s/%s', $this->owner, $this->name); + } + + public function createDtoFromResponse(Response $response): ModelData + { + return ModelData::fromResponse($response); + } +} diff --git a/src/Requests/Models/GetModelVersion.php b/src/Requests/Models/GetModelVersion.php new file mode 100644 index 0000000..56b2a4a --- /dev/null +++ b/src/Requests/Models/GetModelVersion.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Models; + +use MarceloEatWorld\Replicate\Data\ModelVersionData; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; + +class GetModelVersion extends Request +{ + protected Method $method = Method::GET; + + public function __construct( + protected readonly string $owner, + protected readonly string $name, + protected readonly string $versionId, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/models/%s/%s/versions/%s', $this->owner, $this->name, $this->versionId); + } + + public function createDtoFromResponse(Response $response): ModelVersionData + { + return ModelVersionData::fromResponse($response); + } +} diff --git a/src/Requests/Models/GetModelVersions.php b/src/Requests/Models/GetModelVersions.php new file mode 100644 index 0000000..ade0a0d --- /dev/null +++ b/src/Requests/Models/GetModelVersions.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Models; + +use MarceloEatWorld\Replicate\Data\ModelVersionsData; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; + +class GetModelVersions extends Request +{ + protected Method $method = Method::GET; + + public function __construct( + protected readonly string $owner, + protected readonly string $name, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/models/%s/%s/versions', $this->owner, $this->name); + } + + public function createDtoFromResponse(Response $response): ModelVersionsData + { + return ModelVersionsData::fromResponse($response); + } +} diff --git a/src/Requests/Models/GetModels.php b/src/Requests/Models/GetModels.php new file mode 100644 index 0000000..7497ae5 --- /dev/null +++ b/src/Requests/Models/GetModels.php @@ -0,0 +1,25 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Models; + +use MarceloEatWorld\Replicate\Data\ModelsData; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; + +class GetModels extends Request +{ + protected Method $method = Method::GET; + + public function resolveEndpoint(): string + { + return '/models'; + } + + public function createDtoFromResponse(Response $response): ModelsData + { + return ModelsData::fromResponse($response); + } +} diff --git a/src/Requests/Models/PatchModel.php b/src/Requests/Models/PatchModel.php new file mode 100644 index 0000000..48f236f --- /dev/null +++ b/src/Requests/Models/PatchModel.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Models; + +use MarceloEatWorld\Replicate\Data\ModelData; +use Saloon\Contracts\Body\HasBody; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; +use Saloon\Traits\Body\HasJsonBody; + +class PatchModel extends Request implements HasBody +{ + use HasJsonBody; + + protected Method $method = Method::PATCH; + + /** + * @param array<string, mixed> $data + */ + public function __construct( + protected readonly string $owner, + protected readonly string $name, + protected readonly array $data, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/models/%s/%s', $this->owner, $this->name); + } + + /** + * @return array<string, mixed> + */ + protected function defaultBody(): array + { + return $this->data; + } + + public function createDtoFromResponse(Response $response): ModelData + { + return ModelData::fromResponse($response); + } +} diff --git a/src/Requests/Models/PostModel.php b/src/Requests/Models/PostModel.php new file mode 100644 index 0000000..78ca683 --- /dev/null +++ b/src/Requests/Models/PostModel.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Models; + +use MarceloEatWorld\Replicate\Data\ModelData; +use Saloon\Contracts\Body\HasBody; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; +use Saloon\Traits\Body\HasJsonBody; + +class PostModel extends Request implements HasBody +{ + use HasJsonBody; + + protected Method $method = Method::POST; + + /** + * @param array<string, mixed> $optional + */ + public function __construct( + protected readonly string $owner, + protected readonly string $name, + protected readonly string $hardware, + protected readonly string $visibility, + protected readonly array $optional = [], + ) {} + + public function resolveEndpoint(): string + { + return '/models'; + } + + /** + * @return array<string, mixed> + */ + protected function defaultBody(): array + { + return array_merge([ + 'owner' => $this->owner, + 'name' => $this->name, + 'hardware' => $this->hardware, + 'visibility' => $this->visibility, + ], $this->optional); + } + + public function createDtoFromResponse(Response $response): ModelData + { + return ModelData::fromResponse($response); + } +} diff --git a/src/Requests/Models/PostModelPrediction.php b/src/Requests/Models/PostModelPrediction.php new file mode 100644 index 0000000..e3cac33 --- /dev/null +++ b/src/Requests/Models/PostModelPrediction.php @@ -0,0 +1,75 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Models; + +use MarceloEatWorld\Replicate\Data\PredictionData; +use Saloon\Contracts\Body\HasBody; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; +use Saloon\Traits\Body\HasJsonBody; + +class PostModelPrediction extends Request implements HasBody +{ + use HasJsonBody; + + protected Method $method = Method::POST; + + /** + * @param array<string, mixed> $input + * @param array<string>|null $webhookEventsFilter + */ + public function __construct( + protected readonly string $owner, + protected readonly string $name, + protected readonly array $input, + protected readonly ?string $webhook = null, + protected readonly ?array $webhookEventsFilter = null, + protected readonly bool $stream = false, + protected readonly ?int $wait = null, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/models/%s/%s/predictions', $this->owner, $this->name); + } + + protected function defaultHeaders(): array + { + $headers = []; + + if ($this->wait !== null) { + $headers['Prefer'] = sprintf('wait=%d', $this->wait); + } + + return $headers; + } + + /** + * @return array<string, mixed> + */ + protected function defaultBody(): array + { + $body = [ + 'input' => $this->input, + ]; + + if ($this->webhook !== null) { + $body['webhook'] = $this->webhook; + $body['webhook_events_filter'] = $this->webhookEventsFilter; + } + + if ($this->stream) { + $body['stream'] = true; + } + + return $body; + } + + public function createDtoFromResponse(Response $response): PredictionData + { + return PredictionData::fromResponse($response); + } +} diff --git a/src/Requests/PostDeploymentPrediction.php b/src/Requests/PostDeploymentPrediction.php deleted file mode 100644 index 24e411e..0000000 --- a/src/Requests/PostDeploymentPrediction.php +++ /dev/null @@ -1,52 +0,0 @@ -<?php - -namespace MarceloEatWorld\Replicate\Requests; - -use MarceloEatWorld\Replicate\Data\PredictionData; -use Saloon\Contracts\Body\HasBody; -use Saloon\Enums\Method; -use Saloon\Http\Request; -use Saloon\Http\Response; -use Saloon\Traits\Body\HasJsonBody; - -class PostDeploymentPrediction extends Request implements HasBody -{ - use HasJsonBody; - - protected Method $method = Method::POST; - protected string $fullDeploymentName; - - - /** - * @param array<string, mixed> $input - * @param string $fullDeploymentName - */ - public function __construct( - protected array $input, - string $fullDeploymentName, - - ) { - $this->fullDeploymentName = $fullDeploymentName; - - } - - public function resolveEndpoint(): string - { - return sprintf('/deployments/%s/predictions', $this->fullDeploymentName); - } - - /** - * @return array<string, mixed> - */ - protected function defaultBody(): array - { - return [ - 'input' => $this->input, - ]; - } - - public function createDtoFromResponse(Response $response): PredictionData - { - return PredictionData::fromResponse($response); - } -} \ No newline at end of file diff --git a/src/Requests/PostPrediction.php b/src/Requests/PostPrediction.php deleted file mode 100644 index 7577be2..0000000 --- a/src/Requests/PostPrediction.php +++ /dev/null @@ -1,47 +0,0 @@ -<?php - -namespace MarceloEatWorld\Replicate\Requests; - -use MarceloEatWorld\Replicate\Data\PredictionData; -use Saloon\Contracts\Body\HasBody; -use Saloon\Enums\Method; -use Saloon\Http\Request; -use Saloon\Http\Response; -use Saloon\Traits\Body\HasJsonBody; - -class PostPrediction extends Request implements HasBody -{ - use HasJsonBody; - - protected Method $method = Method::POST; - - /** - * @param array<string, float|int|string|null> $input - */ - public function __construct( - protected string $version, - protected array $input, - ) { - } - - public function resolveEndpoint(): string - { - return '/predictions'; - } - - /** - * @return array<string, array<string, float|int|string|null>|string> - */ - protected function defaultBody(): array - { - return [ - 'version' => $this->version, - 'input' => $this->input, - ]; - } - - public function createDtoFromResponse(Response $response): PredictionData - { - return PredictionData::fromResponse($response); - } -} diff --git a/src/Requests/PostPredictionCancel.php b/src/Requests/PostPredictionCancel.php deleted file mode 100644 index 364dd22..0000000 --- a/src/Requests/PostPredictionCancel.php +++ /dev/null @@ -1,33 +0,0 @@ -<?php - -namespace MarceloEatWorld\Replicate\Requests; - -use MarceloEatWorld\Replicate\Data\PredictionData; -use Saloon\Contracts\Body\HasBody; -use Saloon\Enums\Method; -use Saloon\Http\Request; -use Saloon\Http\Response; -use Saloon\Traits\Body\HasJsonBody; - -class PostPredictionCancel extends Request implements HasBody -{ - use HasJsonBody; - - protected Method $method = Method::POST; - - public function __construct( - protected string $id, - ) { - } - - public function resolveEndpoint(): string - { - return sprintf('/predictions/%s/cancel -', $this->id); - } - - public function createDtoFromResponse(Response $response): PredictionData - { - return PredictionData::fromResponse($response); - } -} diff --git a/src/Requests/GetPrediction.php b/src/Requests/Predictions/GetPrediction.php similarity index 79% rename from src/Requests/GetPrediction.php rename to src/Requests/Predictions/GetPrediction.php index eb34d19..9a745e7 100644 --- a/src/Requests/GetPrediction.php +++ b/src/Requests/Predictions/GetPrediction.php @@ -1,6 +1,8 @@ <?php -namespace MarceloEatWorld\Replicate\Requests; +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Predictions; use MarceloEatWorld\Replicate\Data\PredictionData; use Saloon\Enums\Method; @@ -12,9 +14,8 @@ class GetPrediction extends Request protected Method $method = Method::GET; public function __construct( - protected string $id, - ) { - } + protected readonly string $id, + ) {} public function resolveEndpoint(): string { diff --git a/src/Requests/GetPredictions.php b/src/Requests/Predictions/GetPredictions.php similarity index 84% rename from src/Requests/GetPredictions.php rename to src/Requests/Predictions/GetPredictions.php index 0dd7a7e..e684f63 100644 --- a/src/Requests/GetPredictions.php +++ b/src/Requests/Predictions/GetPredictions.php @@ -1,6 +1,8 @@ <?php -namespace MarceloEatWorld\Replicate\Requests; +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Predictions; use MarceloEatWorld\Replicate\Data\PredictionsData; use Saloon\Enums\Method; diff --git a/src/Requests/Predictions/PostPrediction.php b/src/Requests/Predictions/PostPrediction.php new file mode 100644 index 0000000..1e001a9 --- /dev/null +++ b/src/Requests/Predictions/PostPrediction.php @@ -0,0 +1,75 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Predictions; + +use MarceloEatWorld\Replicate\Data\PredictionData; +use Saloon\Contracts\Body\HasBody; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; +use Saloon\Traits\Body\HasJsonBody; + +class PostPrediction extends Request implements HasBody +{ + use HasJsonBody; + + protected Method $method = Method::POST; + + /** + * @param array<string, mixed> $input + * @param array<string>|null $webhookEventsFilter + */ + public function __construct( + protected readonly string $version, + protected readonly array $input, + protected readonly ?string $webhook = null, + protected readonly ?array $webhookEventsFilter = null, + protected readonly bool $stream = false, + protected readonly ?int $wait = null, + ) {} + + public function resolveEndpoint(): string + { + return '/predictions'; + } + + protected function defaultHeaders(): array + { + $headers = []; + + if ($this->wait !== null) { + $headers['Prefer'] = sprintf('wait=%d', $this->wait); + } + + return $headers; + } + + /** + * @return array<string, mixed> + */ + protected function defaultBody(): array + { + $body = [ + 'version' => $this->version, + 'input' => $this->input, + ]; + + if ($this->webhook !== null) { + $body['webhook'] = $this->webhook; + $body['webhook_events_filter'] = $this->webhookEventsFilter; + } + + if ($this->stream) { + $body['stream'] = true; + } + + return $body; + } + + public function createDtoFromResponse(Response $response): PredictionData + { + return PredictionData::fromResponse($response); + } +} diff --git a/src/Requests/Predictions/PostPredictionCancel.php b/src/Requests/Predictions/PostPredictionCancel.php new file mode 100644 index 0000000..2c39940 --- /dev/null +++ b/src/Requests/Predictions/PostPredictionCancel.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Predictions; + +use Saloon\Enums\Method; +use Saloon\Http\Request; + +class PostPredictionCancel extends Request +{ + protected Method $method = Method::POST; + + public function __construct( + protected readonly string $id, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/predictions/%s/cancel', $this->id); + } +} diff --git a/src/Requests/Trainings/GetTraining.php b/src/Requests/Trainings/GetTraining.php new file mode 100644 index 0000000..1b173d0 --- /dev/null +++ b/src/Requests/Trainings/GetTraining.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Trainings; + +use MarceloEatWorld\Replicate\Data\TrainingData; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; + +class GetTraining extends Request +{ + protected Method $method = Method::GET; + + public function __construct( + protected readonly string $id, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/trainings/%s', $this->id); + } + + public function createDtoFromResponse(Response $response): TrainingData + { + return TrainingData::fromResponse($response); + } +} diff --git a/src/Requests/Trainings/GetTrainings.php b/src/Requests/Trainings/GetTrainings.php new file mode 100644 index 0000000..fcbdb2e --- /dev/null +++ b/src/Requests/Trainings/GetTrainings.php @@ -0,0 +1,25 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Trainings; + +use MarceloEatWorld\Replicate\Data\TrainingsData; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; + +class GetTrainings extends Request +{ + protected Method $method = Method::GET; + + public function resolveEndpoint(): string + { + return '/trainings'; + } + + public function createDtoFromResponse(Response $response): TrainingsData + { + return TrainingsData::fromResponse($response); + } +} diff --git a/src/Requests/Trainings/PostTraining.php b/src/Requests/Trainings/PostTraining.php new file mode 100644 index 0000000..83b7417 --- /dev/null +++ b/src/Requests/Trainings/PostTraining.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Trainings; + +use MarceloEatWorld\Replicate\Data\TrainingData; +use Saloon\Contracts\Body\HasBody; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; +use Saloon\Traits\Body\HasJsonBody; + +class PostTraining extends Request implements HasBody +{ + use HasJsonBody; + + protected Method $method = Method::POST; + + /** + * @param array<string, mixed> $input + * @param array<string>|null $webhookEventsFilter + */ + public function __construct( + protected readonly string $owner, + protected readonly string $name, + protected readonly string $versionId, + protected readonly string $destination, + protected readonly array $input, + protected readonly ?string $webhook = null, + protected readonly ?array $webhookEventsFilter = null, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/models/%s/%s/versions/%s/trainings', $this->owner, $this->name, $this->versionId); + } + + /** + * @return array<string, mixed> + */ + protected function defaultBody(): array + { + $body = [ + 'destination' => $this->destination, + 'input' => $this->input, + ]; + + if ($this->webhook !== null) { + $body['webhook'] = $this->webhook; + $body['webhook_events_filter'] = $this->webhookEventsFilter; + } + + return $body; + } + + public function createDtoFromResponse(Response $response): TrainingData + { + return TrainingData::fromResponse($response); + } +} diff --git a/src/Requests/Trainings/PostTrainingCancel.php b/src/Requests/Trainings/PostTrainingCancel.php new file mode 100644 index 0000000..986e0b8 --- /dev/null +++ b/src/Requests/Trainings/PostTrainingCancel.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Trainings; + +use Saloon\Enums\Method; +use Saloon\Http\Request; + +class PostTrainingCancel extends Request +{ + protected Method $method = Method::POST; + + public function __construct( + protected readonly string $id, + ) {} + + public function resolveEndpoint(): string + { + return sprintf('/trainings/%s/cancel', $this->id); + } +} diff --git a/src/Requests/Webhooks/GetWebhookSecret.php b/src/Requests/Webhooks/GetWebhookSecret.php new file mode 100644 index 0000000..31513e3 --- /dev/null +++ b/src/Requests/Webhooks/GetWebhookSecret.php @@ -0,0 +1,25 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Requests\Webhooks; + +use MarceloEatWorld\Replicate\Data\WebhookSecretData; +use Saloon\Enums\Method; +use Saloon\Http\Request; +use Saloon\Http\Response; + +class GetWebhookSecret extends Request +{ + protected Method $method = Method::GET; + + public function resolveEndpoint(): string + { + return '/webhooks/default/secret'; + } + + public function createDtoFromResponse(Response $response): WebhookSecretData + { + return WebhookSecretData::fromResponse($response); + } +} diff --git a/src/Resource.php b/src/Resource.php deleted file mode 100644 index 78dbf8c..0000000 --- a/src/Resource.php +++ /dev/null @@ -1,13 +0,0 @@ -<?php - -namespace MarceloEatWorld\Replicate; - -use Saloon\Http\Connector; - -class Resource -{ - public function __construct(protected Connector $connector) - { - // - } -} diff --git a/src/Resources/AccountResource.php b/src/Resources/AccountResource.php new file mode 100644 index 0000000..720a4be --- /dev/null +++ b/src/Resources/AccountResource.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Resources; + +use MarceloEatWorld\Replicate\Data\AccountData; +use MarceloEatWorld\Replicate\Requests\Account\GetAccount; +use Saloon\Http\BaseResource; + +class AccountResource extends BaseResource +{ + public function get(): AccountData + { + $response = $this->connector->send(new GetAccount); + + return AccountData::fromResponse($response); + } +} diff --git a/src/Resources/CollectionsResource.php b/src/Resources/CollectionsResource.php new file mode 100644 index 0000000..669f589 --- /dev/null +++ b/src/Resources/CollectionsResource.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Resources; + +use MarceloEatWorld\Replicate\Data\CollectionData; +use MarceloEatWorld\Replicate\Data\CollectionsData; +use MarceloEatWorld\Replicate\Requests\Collections\GetCollection; +use MarceloEatWorld\Replicate\Requests\Collections\GetCollections; +use Saloon\Http\BaseResource; + +class CollectionsResource extends BaseResource +{ + public function list(?string $cursor = null): CollectionsData + { + $request = new GetCollections; + + if ($cursor !== null) { + $request->query()->add('cursor', $cursor); + } + + return CollectionsData::fromResponse($this->connector->send($request)); + } + + public function get(string $slug): CollectionData + { + return CollectionData::fromResponse($this->connector->send(new GetCollection($slug))); + } +} diff --git a/src/Resources/DeploymentsResource.php b/src/Resources/DeploymentsResource.php new file mode 100644 index 0000000..2eecb9b --- /dev/null +++ b/src/Resources/DeploymentsResource.php @@ -0,0 +1,91 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Resources; + +use MarceloEatWorld\Replicate\Data\DeploymentData; +use MarceloEatWorld\Replicate\Data\DeploymentsData; +use MarceloEatWorld\Replicate\Data\PredictionData; +use MarceloEatWorld\Replicate\Requests\Deployments\DeleteDeployment; +use MarceloEatWorld\Replicate\Requests\Deployments\GetDeployment; +use MarceloEatWorld\Replicate\Requests\Deployments\GetDeployments; +use MarceloEatWorld\Replicate\Requests\Deployments\PatchDeployment; +use MarceloEatWorld\Replicate\Requests\Deployments\PostDeployment; +use MarceloEatWorld\Replicate\Requests\Deployments\PostDeploymentPrediction; +use Saloon\Http\BaseResource; +use Saloon\Http\Response; + +class DeploymentsResource extends BaseResource +{ + public function list(?string $cursor = null): DeploymentsData + { + $request = new GetDeployments; + + if ($cursor !== null) { + $request->query()->add('cursor', $cursor); + } + + return DeploymentsData::fromResponse($this->connector->send($request)); + } + + public function get(string $owner, string $name): DeploymentData + { + return DeploymentData::fromResponse($this->connector->send(new GetDeployment($owner, $name))); + } + + public function create( + string $name, + string $model, + string $version, + string $hardware, + int $minInstances, + int $maxInstances, + ): DeploymentData { + return DeploymentData::fromResponse($this->connector->send(new PostDeployment( + name: $name, + model: $model, + version: $version, + hardware: $hardware, + minInstances: $minInstances, + maxInstances: $maxInstances, + ))); + } + + /** + * @param array<string, mixed> $data Fields to update: version, hardware, min_instances, max_instances + */ + public function update(string $owner, string $name, array $data): DeploymentData + { + return DeploymentData::fromResponse($this->connector->send(new PatchDeployment($owner, $name, $data))); + } + + public function delete(string $owner, string $name): Response + { + return $this->connector->send(new DeleteDeployment($owner, $name)); + } + + /** + * @param array<string, mixed> $input + * @param array<string>|null $webhookEventsFilter + */ + public function createPrediction( + string $owner, + string $name, + array $input, + ?string $webhook = null, + ?array $webhookEventsFilter = null, + bool $stream = false, + ?int $wait = null, + ): PredictionData { + return PredictionData::fromResponse($this->connector->send(new PostDeploymentPrediction( + owner: $owner, + name: $name, + input: $input, + webhook: $webhook, + webhookEventsFilter: $webhookEventsFilter, + stream: $stream, + wait: $wait, + ))); + } +} diff --git a/src/Resources/FilesResource.php b/src/Resources/FilesResource.php new file mode 100644 index 0000000..31aa412 --- /dev/null +++ b/src/Resources/FilesResource.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Resources; + +use MarceloEatWorld\Replicate\Data\FileData; +use MarceloEatWorld\Replicate\Data\FilesData; +use MarceloEatWorld\Replicate\Requests\Files\DeleteFile; +use MarceloEatWorld\Replicate\Requests\Files\GetFile; +use MarceloEatWorld\Replicate\Requests\Files\GetFiles; +use MarceloEatWorld\Replicate\Requests\Files\PostFile; +use Saloon\Http\BaseResource; +use Saloon\Http\Response; + +class FilesResource extends BaseResource +{ + public function list(?string $cursor = null): FilesData + { + $request = new GetFiles; + + if ($cursor !== null) { + $request->query()->add('cursor', $cursor); + } + + return FilesData::fromResponse($this->connector->send($request)); + } + + public function get(string $id): FileData + { + return FileData::fromResponse($this->connector->send(new GetFile($id))); + } + + /** + * @param array<string, mixed>|null $metadata + */ + public function upload( + string $content, + string $filename, + ?string $contentType = null, + ?array $metadata = null, + ): FileData { + return FileData::fromResponse($this->connector->send(new PostFile( + content: $content, + filename: $filename, + contentType: $contentType, + metadata: $metadata, + ))); + } + + public function delete(string $id): Response + { + return $this->connector->send(new DeleteFile($id)); + } +} diff --git a/src/Resources/HardwareResource.php b/src/Resources/HardwareResource.php new file mode 100644 index 0000000..d7cf4cc --- /dev/null +++ b/src/Resources/HardwareResource.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Resources; + +use MarceloEatWorld\Replicate\Data\HardwareData; +use MarceloEatWorld\Replicate\Requests\Hardware\GetHardware; +use Saloon\Http\BaseResource; + +class HardwareResource extends BaseResource +{ + /** + * @return array<int, HardwareData> + */ + public function list(): array + { + return HardwareData::collectionFromResponse( + $this->connector->send(new GetHardware), + ); + } +} diff --git a/src/Resources/ModelsResource.php b/src/Resources/ModelsResource.php new file mode 100644 index 0000000..1f21380 --- /dev/null +++ b/src/Resources/ModelsResource.php @@ -0,0 +1,118 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Resources; + +use MarceloEatWorld\Replicate\Data\ModelData; +use MarceloEatWorld\Replicate\Data\ModelsData; +use MarceloEatWorld\Replicate\Data\ModelVersionData; +use MarceloEatWorld\Replicate\Data\ModelVersionsData; +use MarceloEatWorld\Replicate\Data\PredictionData; +use MarceloEatWorld\Replicate\Requests\Models\DeleteModel; +use MarceloEatWorld\Replicate\Requests\Models\DeleteModelVersion; +use MarceloEatWorld\Replicate\Requests\Models\GetModel; +use MarceloEatWorld\Replicate\Requests\Models\GetModels; +use MarceloEatWorld\Replicate\Requests\Models\GetModelVersion; +use MarceloEatWorld\Replicate\Requests\Models\GetModelVersions; +use MarceloEatWorld\Replicate\Requests\Models\PatchModel; +use MarceloEatWorld\Replicate\Requests\Models\PostModel; +use MarceloEatWorld\Replicate\Requests\Models\PostModelPrediction; +use Saloon\Http\BaseResource; +use Saloon\Http\Response; + +class ModelsResource extends BaseResource +{ + public function list(?string $cursor = null): ModelsData + { + $request = new GetModels; + + if ($cursor !== null) { + $request->query()->add('cursor', $cursor); + } + + return ModelsData::fromResponse($this->connector->send($request)); + } + + public function get(string $owner, string $name): ModelData + { + return ModelData::fromResponse($this->connector->send(new GetModel($owner, $name))); + } + + /** + * @param array<string, mixed> $optional Optional fields: description, github_url, paper_url, license_url, cover_image_url + */ + public function create( + string $owner, + string $name, + string $hardware, + string $visibility, + array $optional = [], + ): ModelData { + return ModelData::fromResponse($this->connector->send(new PostModel( + owner: $owner, + name: $name, + hardware: $hardware, + visibility: $visibility, + optional: $optional, + ))); + } + + /** + * @param array<string, mixed> $data Fields to update: description, readme, github_url, paper_url, license_url, weights_url + */ + public function update(string $owner, string $name, array $data): ModelData + { + return ModelData::fromResponse($this->connector->send(new PatchModel($owner, $name, $data))); + } + + public function delete(string $owner, string $name): Response + { + return $this->connector->send(new DeleteModel($owner, $name)); + } + + /** + * @param array<string, mixed> $input + * @param array<string>|null $webhookEventsFilter + */ + public function createPrediction( + string $owner, + string $name, + array $input, + ?string $webhook = null, + ?array $webhookEventsFilter = null, + bool $stream = false, + ?int $wait = null, + ): PredictionData { + return PredictionData::fromResponse($this->connector->send(new PostModelPrediction( + owner: $owner, + name: $name, + input: $input, + webhook: $webhook, + webhookEventsFilter: $webhookEventsFilter, + stream: $stream, + wait: $wait, + ))); + } + + public function getVersion(string $owner, string $name, string $versionId): ModelVersionData + { + return ModelVersionData::fromResponse($this->connector->send(new GetModelVersion($owner, $name, $versionId))); + } + + public function listVersions(string $owner, string $name, ?string $cursor = null): ModelVersionsData + { + $request = new GetModelVersions($owner, $name); + + if ($cursor !== null) { + $request->query()->add('cursor', $cursor); + } + + return ModelVersionsData::fromResponse($this->connector->send($request)); + } + + public function deleteVersion(string $owner, string $name, string $versionId): Response + { + return $this->connector->send(new DeleteModelVersion($owner, $name, $versionId)); + } +} diff --git a/src/Resources/PredictionsResource.php b/src/Resources/PredictionsResource.php new file mode 100644 index 0000000..1899078 --- /dev/null +++ b/src/Resources/PredictionsResource.php @@ -0,0 +1,60 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Resources; + +use MarceloEatWorld\Replicate\Data\PredictionData; +use MarceloEatWorld\Replicate\Data\PredictionsData; +use MarceloEatWorld\Replicate\Requests\Predictions\GetPrediction; +use MarceloEatWorld\Replicate\Requests\Predictions\GetPredictions; +use MarceloEatWorld\Replicate\Requests\Predictions\PostPrediction; +use MarceloEatWorld\Replicate\Requests\Predictions\PostPredictionCancel; +use Saloon\Http\BaseResource; +use Saloon\Http\Response; + +class PredictionsResource extends BaseResource +{ + public function list(?string $cursor = null): PredictionsData + { + $request = new GetPredictions; + + if ($cursor !== null) { + $request->query()->add('cursor', $cursor); + } + + return PredictionsData::fromResponse($this->connector->send($request)); + } + + public function get(string $id): PredictionData + { + return PredictionData::fromResponse($this->connector->send(new GetPrediction($id))); + } + + /** + * @param array<string, mixed> $input + * @param array<string>|null $webhookEventsFilter + */ + public function create( + string $version, + array $input, + ?string $webhook = null, + ?array $webhookEventsFilter = null, + bool $stream = false, + ?int $wait = null, + ): PredictionData { + return PredictionData::fromResponse($this->connector->send(new PostPrediction( + version: $version, + input: $input, + webhook: $webhook, + webhookEventsFilter: $webhookEventsFilter, + stream: $stream, + wait: $wait, + ))); + } + + public function cancel(string $id): Response + { + return $this->connector->send(new PostPredictionCancel($id)); + } +} diff --git a/src/Resources/TrainingsResource.php b/src/Resources/TrainingsResource.php new file mode 100644 index 0000000..9c40a58 --- /dev/null +++ b/src/Resources/TrainingsResource.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Resources; + +use MarceloEatWorld\Replicate\Data\TrainingData; +use MarceloEatWorld\Replicate\Data\TrainingsData; +use MarceloEatWorld\Replicate\Requests\Trainings\GetTraining; +use MarceloEatWorld\Replicate\Requests\Trainings\GetTrainings; +use MarceloEatWorld\Replicate\Requests\Trainings\PostTraining; +use MarceloEatWorld\Replicate\Requests\Trainings\PostTrainingCancel; +use Saloon\Http\BaseResource; +use Saloon\Http\Response; + +class TrainingsResource extends BaseResource +{ + public function list(?string $cursor = null): TrainingsData + { + $request = new GetTrainings; + + if ($cursor !== null) { + $request->query()->add('cursor', $cursor); + } + + return TrainingsData::fromResponse($this->connector->send($request)); + } + + public function get(string $id): TrainingData + { + return TrainingData::fromResponse($this->connector->send(new GetTraining($id))); + } + + /** + * @param array<string, mixed> $input + * @param array<string>|null $webhookEventsFilter + */ + public function create( + string $owner, + string $name, + string $versionId, + string $destination, + array $input, + ?string $webhook = null, + ?array $webhookEventsFilter = null, + ): TrainingData { + return TrainingData::fromResponse($this->connector->send(new PostTraining( + owner: $owner, + name: $name, + versionId: $versionId, + destination: $destination, + input: $input, + webhook: $webhook, + webhookEventsFilter: $webhookEventsFilter, + ))); + } + + public function cancel(string $id): Response + { + return $this->connector->send(new PostTrainingCancel($id)); + } +} diff --git a/src/Resources/WebhooksResource.php b/src/Resources/WebhooksResource.php new file mode 100644 index 0000000..b634b1a --- /dev/null +++ b/src/Resources/WebhooksResource.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +namespace MarceloEatWorld\Replicate\Resources; + +use MarceloEatWorld\Replicate\Data\WebhookSecretData; +use MarceloEatWorld\Replicate\Requests\Webhooks\GetWebhookSecret; +use Saloon\Http\BaseResource; + +class WebhooksResource extends BaseResource +{ + public function getSecret(): WebhookSecretData + { + return WebhookSecretData::fromResponse($this->connector->send(new GetWebhookSecret)); + } +} From 2ae2154e45feae637b592cd2b459d7c93dff7957 Mon Sep 17 00:00:00 2001 From: Marcelo <20625497+marceloeatworld@users.noreply.github.com> Date: Fri, 3 Apr 2026 01:36:58 +0100 Subject: [PATCH 7/8] Update README and composer.json to clarify compatibility with Laravel and native PHP --- README.md | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a8f43b6..a06ec63 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/marceloeatworld/replicate-php/tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/marceloeatworld/replicate-php/actions?query=workflow%3Atests+branch%3Amain) [![PHPStan](https://img.shields.io/github/actions/workflow/status/marceloeatworld/replicate-php/formats.yml?branch=main&label=phpstan&style=flat-square)](https://github.com/marceloeatworld/replicate-php/actions?query=workflow%3Aformats+branch%3Amain) -A framework-agnostic PHP client for the [Replicate API](https://replicate.com/) built on [Saloon v4](https://docs.saloon.dev/). +A framework-agnostic PHP client for the [Replicate API](https://replicate.com/) compatible with Laravel and native PHP, built on [Saloon v4](https://docs.saloon.dev/). Full coverage of the Replicate HTTP API: predictions, models, deployments, trainings, files, collections, hardware, webhooks, and account. diff --git a/composer.json b/composer.json index e35aa7d..6a6e4f6 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "marceloeatworld/replicate-php", - "description": "A PHP client for the Replicate API built on Saloon v4", + "description": "PHP client for the Replicate API, compatible with Laravel and native PHP, built on Saloon v4", "keywords": ["replicate", "php", "ai", "machine-learning", "api-client", "saloon"], "license": "MIT", "authors": [ From 448b8eef2dd8fae1943f941371bed4d09bee3f67 Mon Sep 17 00:00:00 2001 From: Marcelo <20625497+marceloeatworld@users.noreply.github.com> Date: Fri, 3 Apr 2026 01:41:23 +0100 Subject: [PATCH 8/8] Update description: add Laravel and native PHP compatibility, add #1 prefix --- README.md | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a06ec63..90cd810 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/marceloeatworld/replicate-php/tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/marceloeatworld/replicate-php/actions?query=workflow%3Atests+branch%3Amain) [![PHPStan](https://img.shields.io/github/actions/workflow/status/marceloeatworld/replicate-php/formats.yml?branch=main&label=phpstan&style=flat-square)](https://github.com/marceloeatworld/replicate-php/actions?query=workflow%3Aformats+branch%3Amain) -A framework-agnostic PHP client for the [Replicate API](https://replicate.com/) compatible with Laravel and native PHP, built on [Saloon v4](https://docs.saloon.dev/). +#1 A framework-agnostic PHP client for the [Replicate API](https://replicate.com/) compatible with Laravel and native PHP, built on [Saloon v4](https://docs.saloon.dev/). Full coverage of the Replicate HTTP API: predictions, models, deployments, trainings, files, collections, hardware, webhooks, and account. diff --git a/composer.json b/composer.json index 6a6e4f6..40cda65 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "marceloeatworld/replicate-php", - "description": "PHP client for the Replicate API, compatible with Laravel and native PHP, built on Saloon v4", + "description": "#1 PHP client for the Replicate API, compatible with Laravel and native PHP, built on Saloon v4", "keywords": ["replicate", "php", "ai", "machine-learning", "api-client", "saloon"], "license": "MIT", "authors": [