diff --git a/CHANGELOG.md b/CHANGELOG.md index 74f5941..eebdecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.2.0 + +- Bumped `ApifyClientVersion.ApiSpecVersion` to the Apify OpenAPI spec `v2-2026-08-05T133145Z` and the + project version to `0.2.0`. +- Added `TaskClient.PublishAsync()` and `TaskClient.UnpublishAsync()`, plus `ActorTask.IsPublic` and + `ActorTask.PublicConfig` (backed by the new `TaskPublicConfig` model), matching the reference + client's task publish/unpublish support. +- Removed the duplicated AI-disclaimer paragraph from `docs/README.md` and the `ApifyClient` XML + doc comment; it is now stated once, in the top-level `README.md`, per the client requirements. + ## 0.1.4 - Bumped `ApifyClientVersion.ApiSpecVersion` to the Apify OpenAPI spec `v2-2026-07-13T092445Z` and the diff --git a/docs/README.md b/docs/README.md index b261209..6f9dd69 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,8 +1,6 @@ # Apify .NET client documentation -> **Official, but experimental — AI-generated and AI-maintained.** This is an official Apify client, -> but it is experimental: it is generated and maintained by AI. Review the code before relying on it in -> production and report issues on the repository. +See the [top-level README](../README.md) for the client's official-but-experimental status. A resource-oriented .NET client for the [Apify API](https://docs.apify.com/api/v2), mirroring the official [JavaScript](https://github.com/apify/apify-client-js) reference client: start from an diff --git a/docs/models.md b/docs/models.md index 6e3baf6..4615812 100644 --- a/docs/models.md +++ b/docs/models.md @@ -114,6 +114,20 @@ An entry returned when browsing the Apify Store. | `Title` | `string?` | Human-readable title. | | `CreatedAt` | `string?` | ISO 8601 creation timestamp. | | `ModifiedAt` | `string?` | ISO 8601 last-modification timestamp. | +| `IsPublic` | `bool?` | Whether the task is published on its public landing page. Set via `TaskClient.PublishAsync()`/`UnpublishAsync()`, not directly. | +| `PublicConfig` | `TaskPublicConfig?` | The task's public landing page display configuration, or `null` if not set. | + +## `TaskPublicConfig` + +| Property | Type | Description | +|---|---|---| +| `PublishedAt` | `string?` | ISO 8601 timestamp the task was published, or `null` if unpublished. Read-only. | +| `SeoTitle` | `string?` | Name to display for search engines. | +| `SeoDescription` | `string?` | Description to display for search engines. | +| `Categorization` | `string?` | The task's category on its public landing page. | +| `InputSchemaFields` | `IReadOnlyList?` | Input schema fields shown on the public landing page. | +| `DatasetName` | `string?` | Name of the dataset shown on the public landing page. | +| `DatasetView` | `string?` | View of the dataset shown on the public landing page. | ## `Dataset` diff --git a/docs/tasks.md b/docs/tasks.md index e67147d..871a8f1 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -12,6 +12,13 @@ a specific task with `client.Task(id)`. ## Single task — `client.Task(id)` - `GetAsync()` → `ActorTask?`; `UpdateAsync(object newFields)` → `ActorTask`; `DeleteAsync()`. +- `PublishAsync()` → `ActorTask` — publishes the task on its public landing page in Apify Store + (sets `isPublic: true`). The task's Actor must be public and the task must already have + `PublicConfig` set up. Requires write permission to both the task and its Actor. Publishing an + already published task does nothing. +- `UnpublishAsync()` → `ActorTask` — unpublishes the task (sets `isPublic: false`); `PublicConfig` is + preserved so the task can be published again without re-entering it. Requires write permission to + both the task and its Actor. Unpublishing a task that is not published does nothing. - `StartAsync(object? input = null, TaskStartOptions? options = null)` → `ActorRun`. - `CallAsync(object? input = null, TaskStartOptions? options = null, int? waitSecs = null, Action? log = null)` → `ActorRun` (`log`, if set, redirects the run's live log to that sink for the duration of the wait). @@ -42,3 +49,14 @@ await client.Task(task.Id!).UpdateInputAsync(new { message = "updated" }); var run = await client.Task(task.Id!).CallAsync(null, null, 120); Console.WriteLine(run.Status); ``` + +Publishing a task requires its Actor to be public and the task to have `PublicConfig` set up first: + +```csharp +var taskClient = client.Task(task.Id!); +await taskClient.UpdateAsync(new { publicConfig = new { seoTitle = "My task" } }); +var published = await taskClient.PublishAsync(); +Console.WriteLine(published.IsPublic == true); + +await taskClient.UnpublishAsync(); +``` diff --git a/src/Apify.Client/Apify.Client.csproj b/src/Apify.Client/Apify.Client.csproj index 4359176..482579e 100644 --- a/src/Apify.Client/Apify.Client.csproj +++ b/src/Apify.Client/Apify.Client.csproj @@ -6,7 +6,7 @@ Apify.Client - 0.1.4 + 0.2.0 Apify Apify Apify API client for .NET diff --git a/src/Apify.Client/ApifyClient.cs b/src/Apify.Client/ApifyClient.cs index bf5f09c..d1bb83f 100644 --- a/src/Apify.Client/ApifyClient.cs +++ b/src/Apify.Client/ApifyClient.cs @@ -16,9 +16,7 @@ namespace Apify.Client; /// /// /// -/// Official, but experimental — AI-generated and AI-maintained. This is an official Apify client, -/// but it is experimental: it is generated and maintained by AI. Review the code before relying on it in -/// production and report issues on the repository. +/// See the project's top-level README for the client's official-but-experimental status. /// /// /// Construct it with an API token (and optional settings via ), then diff --git a/src/Apify.Client/ApifyClientVersion.cs b/src/Apify.Client/ApifyClientVersion.cs index ebe2df8..dc895d4 100644 --- a/src/Apify.Client/ApifyClientVersion.cs +++ b/src/Apify.Client/ApifyClientVersion.cs @@ -14,11 +14,11 @@ public static class ApifyClientVersion /// The semantic version of this client library (see https://semver.org/). Changes to the public /// interface other than additive ones are considered breaking changes. /// - public const string ClientVersion = "0.1.4"; + public const string ClientVersion = "0.2.0"; /// /// The version of the Apify OpenAPI specification this client was generated and verified against. /// Corresponds to the info.version field of the Apify OpenAPI document. /// - public const string ApiSpecVersion = "v2-2026-07-13T092445Z"; + public const string ApiSpecVersion = "v2-2026-08-05T133145Z"; } diff --git a/src/Apify.Client/Models/ActorTask.cs b/src/Apify.Client/Models/ActorTask.cs index abed185..0493ca4 100644 --- a/src/Apify.Client/Models/ActorTask.cs +++ b/src/Apify.Client/Models/ActorTask.cs @@ -38,4 +38,14 @@ public ActorTask(JsonObject data) /// When the task was last modified (ISO-8601 string). public string? ModifiedAt => GetString("modifiedAt"); + + /// + /// Whether the task is published on its public landing page in Apify Store. Derived from + /// 's publishedAt; set it via + /// or , not by writing this field directly. + /// + public bool? IsPublic => GetBool("isPublic"); + + /// The task's public landing page display configuration, or null if not set. + public TaskPublicConfig? PublicConfig => Get("publicConfig") is JsonObject obj ? new TaskPublicConfig(obj) : null; } diff --git a/src/Apify.Client/Models/TaskPublicConfig.cs b/src/Apify.Client/Models/TaskPublicConfig.cs new file mode 100644 index 0000000..4694738 --- /dev/null +++ b/src/Apify.Client/Models/TaskPublicConfig.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Text.Json.Nodes; + +namespace Apify.Client.Models; + +/// +/// Public-facing display configuration for a task's public landing page in Apify Store. +/// +/// +/// The task is published when is set and unpublished when it is +/// null. is read-only; use +/// and to change the publication state. +/// +public sealed class TaskPublicConfig : ApifyResource +{ + /// Wraps a raw public-config object. + /// The raw decoded object. + public TaskPublicConfig(JsonObject data) + : base(data) + { + } + + /// When the task was published (ISO-8601 string), or null if it is not published. + public string? PublishedAt => GetString("publishedAt"); + + /// Name to display for search engines such as Google. + public string? SeoTitle => GetString("seoTitle"); + + /// Description to display for search engines such as Google. + public string? SeoDescription => GetString("seoDescription"); + + /// The task's category on its public landing page. + public string? Categorization => GetString("categorization"); + + /// The input schema fields shown on the public landing page. + public IReadOnlyList? InputSchemaFields => GetStringList("inputSchemaFields"); + + /// The name of the dataset shown on the public landing page. + public string? DatasetName => GetString("datasetName"); + + /// The view of the dataset shown on the public landing page. + public string? DatasetView => GetString("datasetView"); +} diff --git a/src/Apify.Client/Resources/TaskClient.cs b/src/Apify.Client/Resources/TaskClient.cs index 411eefb..957b1cf 100644 --- a/src/Apify.Client/Resources/TaskClient.cs +++ b/src/Apify.Client/Resources/TaskClient.cs @@ -49,6 +49,32 @@ public async Task UpdateAsync(object newFields, CancellationToken can /// A token to cancel the request. public Task DeleteAsync(CancellationToken cancellationToken = default) => _ctx.DeleteResourceAsync("", cancellationToken); + /// + /// Publishes the task on its public landing page in Apify Store, by setting isPublic + /// through . + /// + /// + /// The task's Actor must be public and the task must already have its public display + /// configuration () set up. Requires write permission to both + /// the task and its Actor. Publishing an already published task does nothing. + /// + /// A token to cancel the request. + public Task PublishAsync(CancellationToken cancellationToken = default) => + UpdateAsync(new { isPublic = true }, cancellationToken); + + /// + /// Unpublishes the task from its public landing page, by setting isPublic through + /// . + /// + /// + /// The public display configuration () is preserved, so the + /// task can be published again without re-entering it. Requires write permission to both the task + /// and its Actor. Unpublishing a task that is not published does nothing. + /// + /// A token to cancel the request. + public Task UnpublishAsync(CancellationToken cancellationToken = default) => + UpdateAsync(new { isPublic = false }, cancellationToken); + /// Starts the task and returns immediately with the created run. /// Optionally overrides the task's stored input (null to use it). /// Optional run-start options. diff --git a/tests/Apify.Client.Tests/Integration/TaskIntegrationTests.cs b/tests/Apify.Client.Tests/Integration/TaskIntegrationTests.cs index b632b22..d45f32d 100644 --- a/tests/Apify.Client.Tests/Integration/TaskIntegrationTests.cs +++ b/tests/Apify.Client.Tests/Integration/TaskIntegrationTests.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Apify.Client.Exceptions; using Apify.Client.Options; using Xunit; @@ -61,4 +62,30 @@ public async Task TaskCrudFlow() await client.Task(task.Id!).DeleteAsync(); } } + + [SkippableFact] + public async Task TaskPublishUnpublish() + { + var client = RequireClient(); + var task = await client.Tasks().CreateAsync(TaskDef(UniqueName("task-publish"))); + try + { + var tc = client.Task(task.Id!); + + // Unpublishing an already-unpublished task is a documented no-op: it succeeds and returns + // the task unchanged (still not public). + var unpublished = await tc.UnpublishAsync(); + Assert.Equal(task.Id, unpublished.Id); + Assert.True(unpublished.IsPublic != true); + + // Publishing requires write permission to both the task and its Actor (apify/hello-world), + // which the test account does not have, so this is expected to fail rather than succeed. + var ex = await Assert.ThrowsAsync(() => tc.PublishAsync()); + Assert.True(ex.StatusCode is 400 or 403); + } + finally + { + await client.Task(task.Id!).DeleteAsync(); + } + } } diff --git a/tests/Apify.Client.Tests/Unit/RequestShapeTests.cs b/tests/Apify.Client.Tests/Unit/RequestShapeTests.cs index 0dbc956..2e8ef0e 100644 --- a/tests/Apify.Client.Tests/Unit/RequestShapeTests.cs +++ b/tests/Apify.Client.Tests/Unit/RequestShapeTests.cs @@ -209,6 +209,36 @@ public async Task UpdateLimitsPutsToMeLimits() Assert.Equal(100, JsonNode.Parse(request.Body)!["maxMonthlyUsageUsd"]!.GetValue()); } + [Fact] + public async Task PublishPutsIsPublicTrue() + { + var transport = new MockTransport().QueueResponse( + 200, + "{\"data\":{\"id\":\"t1\",\"isPublic\":true,\"publicConfig\":{\"publishedAt\":\"2026-01-01T00:00:00.000Z\",\"seoTitle\":\"My task\"}}}"); + var task = await Client(transport).Task("t1").PublishAsync(); + + var request = transport.LastRequest; + Assert.Equal("PUT", request.Method); + Assert.Contains("/actor-tasks/t1", request.Uri, StringComparison.Ordinal); + Assert.True(JsonNode.Parse(request.Body)!["isPublic"]!.GetValue()); + Assert.True(task.IsPublic); + Assert.Equal("2026-01-01T00:00:00.000Z", task.PublicConfig!.PublishedAt); + Assert.Equal("My task", task.PublicConfig!.SeoTitle); + } + + [Fact] + public async Task UnpublishPutsIsPublicFalse() + { + var transport = new MockTransport().QueueResponse(200, "{\"data\":{\"id\":\"t1\",\"isPublic\":false}}"); + var task = await Client(transport).Task("t1").UnpublishAsync(); + + var request = transport.LastRequest; + Assert.Equal("PUT", request.Method); + Assert.Contains("/actor-tasks/t1", request.Uri, StringComparison.Ordinal); + Assert.False(JsonNode.Parse(request.Body)!["isPublic"]!.GetValue()); + Assert.False(task.IsPublic); + } + [Fact] public async Task DatasetListItemsJoinsMultiValueParamsAsCsv() {