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 Copyright (c) Ben Bjurstrom Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/README.md b/README.md index d16b009..90cd810 100644 --- a/README.md +++ b/README.md @@ -1,172 +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) +#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/). -## 🚀 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 BenBjurstrom\Replicate\Replicate; -... +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'], +); -$data = $api->predictions()->create($version, $input); -$data->id; // yfv4cakjzvh2lexxv7o5qzymqy +$prediction->id; // "xyz123" +$prediction->status; // "starting" ``` -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. -### -## Using with Laravel -Begin by adding your credentials to your services config file. +### Create a prediction using an official model + ```php -// config/services.php -'replicate' => [ - 'api_token' => env('REPLICATE_API_TOKEN'), -], +$prediction = $replicate->models()->createPrediction( + owner: 'meta', + name: 'meta-llama-3-70b-instruct', + input: ['prompt' => 'Write a haiku about PHP'], +); ``` -### -Bind the `Replicate` class in a service provider. +### Synchronous predictions (wait for result) + ```php -// app/Providers/AppServiceProvider.php -public function register() -{ - $this->app->bind(Replicate::class, function () { - return new Replicate( - apiToken: config('services.replicate.api_token'), - ); - }); +$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 +); + +if ($prediction->status === 'succeeded') { + $prediction->output; // result is ready } -```` -### +``` + +### Get prediction status -And use anywhere in your application. ```php -$data = app(Replicate::class)->predictions()->get($id); +$prediction = $replicate->predictions()->get('xyz123'); +$prediction->status; // "succeeded" +$prediction->output; // ["https://replicate.delivery/..."] ``` -### -Test your integration using Saloon's amazing [response recording](https://docs.saloon.dev/testing/recording-requests#fixture-path). +### List predictions + ```php -use Saloon\Laravel\Saloon; // composer require sammyjo20/saloon-laravel "^2.0" -... -Saloon::fake([ - MockResponse::fixture('getPrediction'), -]); +$list = $replicate->predictions()->list(); +$list->results; // array of PredictionData +$list->next; // cursor for next page -$id = 'yfv4cakjzvh2lexxv7o5qzymqy'; +// Paginate +$nextPage = $replicate->predictions()->list(cursor: $list->next); +``` -// 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); +### Cancel a prediction -// 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); +```php +$replicate->predictions()->cancel('xyz123'); ``` -## Response Data -All responses are returned as data objects. Detailed information can be found by inspecting the following class properties: +## Webhooks -* [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) +Pass webhook parameters directly to creation methods: -## 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: +```php +$prediction = $replicate->predictions()->create( + version: 'owner/model:version', + input: ['prompt' => 'hello'], + webhook: 'https://example.com/webhook', + webhookEventsFilter: ['completed'], +); +``` + +Get the webhook signing secret for verification: ```php -$api->predictions()->withWebhook('https://www.example.com/webhook')->create($version, $input); -$data->id; // la5xlbbrfzg57ip5jlx6obmm5y +$secret = $replicate->webhooks()->getSecret(); +$secret->key; // "whsec_..." ``` -## 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. +## Streaming + ```php -use BenBjurstrom\Replicate\Data\PredictionData; -... -$id = 'la5xlbbrfzg57ip5jlx6obmm5y' -/* @var PredictionData $data */ -$data = $api->predictions()->get($id); -$data->output[0]; // https://replicate.delivery/pbxt/6UFOVtl1xCJPAFFiTB2tfveYBNRLhLmJz8yMQAYCOeZSFhOhA/out-0.png +$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 ``` -### list() -Use to get a cursor paginated list of predictions. Returns an PredictionsData object. +## Models + ```php -use BenBjurstrom\Replicate\Data\PredictionsData -... +// List public models +$models = $replicate->models()->list(); + +// Get a model +$model = $replicate->models()->get('stability-ai', 'sdxl'); -/* @var PredictionsData $data */ -$data = $api->predictions()->list( - cursor: '123', // optional +// Create a model +$model = $replicate->models()->create( + owner: 'your-username', + name: 'my-model', + hardware: 'gpu-a40-large', + visibility: 'private', ); -$data->results[0]->id; // la5xlbbrfzg57ip5jlx6obmm5y +// Update a model +$model = $replicate->models()->update('your-username', 'my-model', [ + 'description' => 'Updated description', +]); +// Delete a model (must be private, no versions) +$replicate->models()->delete('your-username', 'my-model'); ``` -### create() -Use to create a new prediction (invoke a model). Returns an PredictionData object. + +### Model Versions + +```php +$versions = $replicate->models()->listVersions('stability-ai', 'sdxl'); +$version = $replicate->models()->getVersion('stability-ai', 'sdxl', 'abc123'); +$replicate->models()->deleteVersion('your-username', 'my-model', 'abc123'); +``` + +## Deployments + ```php -use BenBjurstrom\Replicate\Data\PredictionData; -... -$version = '5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa'; -$input = [ - 'text' => 'Alice' -]; +// List deployments +$deployments = $replicate->deployments()->list(); -/* @var PredictionData $data */ -$data = $api->predictions() - ->withWebhook('https://www.example.com/webhook') // optional - ->create($version, $input); -$data->id; // la5xlbbrfzg57ip5jlx6obmm5y +// 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'); ``` +## Trainings + +```php +// 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 + +```php +// Upload a file +$file = $replicate->files()->upload( + content: file_get_contents('/path/to/image.jpg'), + filename: 'image.jpg', + contentType: 'image/jpeg', +); + +// 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); +``` + +## Testing + +Use Saloon's built-in mocking: + +```php +use Saloon\Http\Faking\MockClient; +use Saloon\Http\Faking\MockResponse; +use MarceloEatWorld\Replicate\Requests\Predictions\GetPrediction; + +$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 3a7c47b..40cda65 100644 --- a/composer.json +++ b/composer.json @@ -1,28 +1,28 @@ { - "name": "benbjurstrom/replicate-php", - "description": "A PHP client for the Replicate API", - "keywords": ["replicate", "php", "package"], + "name": "marceloeatworld/replicate-php", + "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": [ { - "name": "Ben Bjurstrom", - "email": "bbjurstrom@gmail.com" + "name": "Marcelo Pereira", + "email": "diagngo@gmail.com" } ], "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": { - "BenBjurstrom\\Replicate\\": "src/" + "MarceloEatWorld\\Replicate\\": "src/" } }, "autoload-dev": { 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 @@ +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 @@ + $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 $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 @@ + $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 @@ +|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 $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 @@ + $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 @@ +|null $checksums + * @param array|null $metadata + * @param array|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 $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 @@ + $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 @@ + + */ + 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 @@ +|null $defaultExample + * @param array|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 $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 @@ +|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 $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 @@ + $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 @@ + $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 bd56cb9..24cee3e 100644 --- a/src/Data/PredictionData.php +++ b/src/Data/PredictionData.php @@ -1,53 +1,69 @@ $input - * @param array $metrics + * @param array $input + * @param array|null $metrics * @param array $urls - * @param string|array $output - * @param null|array $error */ public function __construct( - public string $id, - 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 $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'], - 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 24197ee..28a4911 100644 --- a/src/Data/PredictionsData.php +++ b/src/Data/PredictionsData.php @@ -1,8 +1,9 @@ $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'], - 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 @@ + $input + * @param array|null $output + * @param array|null $metrics + * @param array $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 $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 @@ + $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 @@ +json(); + + return new self( + key: (string) ($data['key'] ?? ''), + ); + } +} diff --git a/src/PredictionsResource.php b/src/PredictionsResource.php deleted file mode 100644 index 93e8738..0000000 --- a/src/PredictionsResource.php +++ /dev/null @@ -1,87 +0,0 @@ - - */ - 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 $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 $events - */ - public function withWebhook(string $url, ?array $events = ['completed']): self - { - $this->webhookUrl = $url; - $this->webhookEvents = $events; - - return $this; - } -} diff --git a/src/Replicate.php b/src/Replicate.php index e55d985..1261322 100644 --- a/src/Replicate.php +++ b/src/Replicate.php @@ -2,20 +2,25 @@ declare(strict_types=1); -namespace BenBjurstrom\Replicate; +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 @@ +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 @@ +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 @@ +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 @@ + $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 + */ + 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 @@ + + */ + 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 @@ + $input + * @param array|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 + */ + 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 @@ +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 @@ +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 @@ +|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 + */ + 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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ + $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 + */ + 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 @@ + $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 + */ + 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 @@ + $input + * @param array|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 + */ + 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/PostPrediction.php b/src/Requests/PostPrediction.php deleted file mode 100644 index b07614c..0000000 --- a/src/Requests/PostPrediction.php +++ /dev/null @@ -1,47 +0,0 @@ - $input - */ - public function __construct( - protected string $version, - protected array $input, - ) { - } - - public function resolveEndpoint(): string - { - return '/predictions'; - } - - /** - * @return array|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 a61cf4f..0000000 --- a/src/Requests/PostPredictionCancel.php +++ /dev/null @@ -1,33 +0,0 @@ -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 71% rename from src/Requests/GetPrediction.php rename to src/Requests/Predictions/GetPrediction.php index 14670fa..9a745e7 100644 --- a/src/Requests/GetPrediction.php +++ b/src/Requests/Predictions/GetPrediction.php @@ -1,8 +1,10 @@ $input + * @param array|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 + */ + 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 @@ +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 @@ +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 @@ + $input + * @param array|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 + */ + 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 @@ +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 @@ +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 @@ +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 @@ +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 $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 $input + * @param array|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 @@ +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|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 @@ + + */ + 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 @@ +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 $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 $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 $input + * @param array|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 @@ +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 $input + * @param array|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 @@ +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 $input + * @param array|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 @@ +connector->send(new GetWebhookSecret)); + } +} 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'); -});