diff --git a/pyproject.toml b/pyproject.toml index c827db4e..85da0ac1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,6 +168,7 @@ indent-style = "space" "TC003", # Move standard library import into a type-checking block ] "src/apify_client/_literals.py" = [ + "E501", # Line too long "PYI051", # `Literal[...]` is redundant in a union with `str`; the open `Literal[...] | str` is deliberate — it keeps known members as hints while tolerating unknown API values (see scripts/postprocess_generated_models.py) ] diff --git a/scripts/postprocess_generated_models.py b/scripts/postprocess_generated_models.py index e35a01b1..4591d53f 100644 --- a/scripts/postprocess_generated_models.py +++ b/scripts/postprocess_generated_models.py @@ -114,10 +114,11 @@ def fix_discriminators(content: str) -> str: def convert_enums_to_literals(content: str) -> str: - """Rewrite every `class X(StrEnum): ...` into an open literal `X = Literal[...] | str` alias. + """Rewrite every `class X(StrEnum): ...` (or `Enum`) into an open literal `X = Literal[...] | str` alias. Each member assignment (`NAME = 'value'`) contributes its string value to the literal in - declaration order. The trailing `| str` widens the alias so unknown values (e.g. a new enum + declaration order; a `None` member of a nullable enum is dropped, since the fields using it are already + `X | None`. The trailing `| str` widens the alias so unknown values (e.g. a new enum member the platform introduces before this client is regenerated) still validate against the Pydantic models, while the known members remain as autocomplete hints. The class docstring, if present, is preserved as a trailing bare-string docstring after the alias — matching the @@ -134,7 +135,7 @@ def convert_enums_to_literals(content: str) -> str: if not isinstance(node, ast.ClassDef): continue base_names = _base_names(node) - if 'StrEnum' not in base_names: + if 'StrEnum' not in base_names and 'Enum' not in base_names: continue values: list[str] = [ @@ -148,9 +149,12 @@ def convert_enums_to_literals(content: str) -> str: ] docstring = ast.get_docstring(node) - new_lines: list[str] = [f'{node.name} = Literal['] - new_lines.extend(f' {v!r},' for v in values) - new_lines.append('] | str') + if len(values) == 1: + new_lines: list[str] = [f'{node.name} = Literal[{values[0]!r}] | str'] + else: + new_lines = [f'{node.name} = Literal['] + new_lines.extend(f' {v!r},' for v in values) + new_lines.append('] | str') if docstring is not None: if '\n' in docstring: new_lines.append('"""') diff --git a/src/apify_client/_literals.py b/src/apify_client/_literals.py index de56efd5..e4657ed9 100644 --- a/src/apify_client/_literals.py +++ b/src/apify_client/_literals.py @@ -20,6 +20,17 @@ """Status of an Actor job (run or build).""" +ActorNotice = ( + Literal[ + 'NONE', + 'RESIDENTIAL_PROXY_REQUIRED', + 'UNDER_MAINTENANCE', + ] + | str +) +"""A warning displayed on the Actor's page in Apify Store and Console. Can be set by the Actor's developer or automatically by Apify's quality checks.""" + + ActorPermissionLevel = ( Literal[ 'LIMITED_PERMISSIONS', @@ -495,6 +506,10 @@ ) +MonitoringLevel = Literal['STANDARD'] | str +"""Level of monitoring services included in the plan. `null` or missing means the plan includes no monitoring service.""" + + OAuthProviderType = ( Literal[ 'CUSTOM', @@ -550,6 +565,16 @@ ) +Tenancy = ( + Literal[ + 'SINGLE_TENANT', + 'MULTI_TENANT', + ] + | str +) +"""Whether each user gets their own standby runs (`SINGLE_TENANT`) or runs are shared across users (`MULTI_TENANT`).""" + + VersionSourceType = ( Literal[ 'SOURCE_FILES', diff --git a/src/apify_client/_models.py b/src/apify_client/_models.py index 577e8b9a..9669f14e 100644 --- a/src/apify_client/_models.py +++ b/src/apify_client/_models.py @@ -2,7 +2,6 @@ from __future__ import annotations -from enum import Enum from typing import Annotated, Any, Literal from pydantic import AnyUrl, AwareDatetime, BaseModel, ConfigDict, EmailStr, Field, RootModel @@ -11,16 +10,19 @@ from apify_client._docs import docs_group from apify_client._literals import ( ActorJobStatus, + ActorNotice, ActorPermissionLevel, Deployment, Encoding, ErrorType, GeneralAccess, HttpMethod, + MonitoringLevel, OAuthProviderType, Provider, RunOrigin, SourceCodeFileFormat, + Tenancy, VersionSourceType, WebhookDispatchStatus, WebhookEventType, @@ -250,16 +252,6 @@ class ActorListItem(BaseModel): stats: ActorStats | None = None -@docs_group('Models') -class ActorNotice(Enum): - """A warning displayed on the Actor's page in Apify Store and Console. Can be set by the Actor's developer or automatically by Apify's quality checks.""" - - NONE = 'NONE' - RESIDENTIAL_PROXY_REQUIRED = 'RESIDENTIAL_PROXY_REQUIRED' - UNDER_MAINTENANCE = 'UNDER_MAINTENANCE' - NONE_TYPE_NONE = None - - @docs_group('Models') class ActorOAuthAccount(BaseModel): """An account a user connected through an Actor OAuth connection.""" @@ -2511,14 +2503,6 @@ class LockedRequestQueueHeadResponse(BaseModel): data: LockedRequestQueueHead -@docs_group('Models') -class MonitoringLevel(Enum): - """Level of monitoring services included in the plan. `null` or missing means the plan includes no monitoring service.""" - - STANDARD = 'STANDARD' - NONE_TYPE_NONE = None - - @docs_group('Models') class MonthlyUsage(BaseModel): model_config = ConfigDict( @@ -4417,15 +4401,6 @@ class TaskStats(BaseModel): """ -@docs_group('Models') -class Tenancy(Enum): - """Whether each user gets their own standby runs (`SINGLE_TENANT`) or runs are shared across users (`MULTI_TENANT`).""" - - SINGLE_TENANT = 'SINGLE_TENANT' - MULTI_TENANT = 'MULTI_TENANT' - NONE_TYPE_NONE = None - - @docs_group('Models') class TieredPricing(BaseModel): model_config = ConfigDict( diff --git a/src/apify_client/_resource_clients/actor.py b/src/apify_client/_resource_clients/actor.py index eddb4177..1f1d97d7 100644 --- a/src/apify_client/_resource_clients/actor.py +++ b/src/apify_client/_resource_clients/actor.py @@ -38,7 +38,7 @@ from decimal import Decimal from logging import Logger - from apify_client._literals import ActorJobStatus, ActorPermissionLevel, RunOrigin + from apify_client._literals import ActorJobStatus, ActorPermissionLevel, RunOrigin, Tenancy from apify_client._resource_clients import ( ActorServerClient, ActorServerClientAsync, @@ -134,6 +134,7 @@ def update( server_memory_mbytes: int | None = None, server_disable_fields_override: bool | None = None, server_should_pass_actor_input: bool | None = None, + server_tenancy: Tenancy | None = None, pricing_infos: list[dict[str, Any]] | None = None, actor_permission_level: ActorPermissionLevel | None = None, tagged_builds: dict[str, dict[str, str] | None] | None = None, @@ -146,6 +147,7 @@ def update( actor_standby_memory_mbytes: int | None = None, actor_standby_disable_standby_fields_override: bool | None = None, actor_standby_should_pass_actor_input: bool | None = None, + actor_standby_tenancy: Tenancy | None = None, ) -> ActorResource: """Update the Actor with the specified fields. @@ -186,6 +188,8 @@ def update( elsewhere. server_should_pass_actor_input: Whether to pass the Actor input to the runs of the Server Actor. If false, they start with no input. + server_tenancy: Whether each user gets their own runs of the Server Actor (`SINGLE_TENANT`) or the runs are + shared across users (`MULTI_TENANT`). `MULTI_TENANT` requires pay-per-event pricing. pricing_infos: A list of objects that describes the pricing of the Actor. actor_permission_level: The permission level of the Actor on Apify platform. tagged_builds: A dictionary mapping build tag names to their settings. Use it to create, update, @@ -204,6 +208,7 @@ def update( removed in v4. actor_standby_should_pass_actor_input: Deprecated alias of `server_should_pass_actor_input`. Will be removed in v4. + actor_standby_tenancy: Deprecated alias of `server_tenancy`. Will be removed in v4. Returns: The updated Actor. @@ -238,6 +243,7 @@ def update( 'server_memory_mbytes': server_memory_mbytes, 'server_disable_fields_override': server_disable_fields_override, 'server_should_pass_actor_input': server_should_pass_actor_input, + 'server_tenancy': server_tenancy, }, { 'actor_standby_is_enabled': actor_standby_is_enabled, @@ -248,6 +254,7 @@ def update( 'actor_standby_memory_mbytes': actor_standby_memory_mbytes, 'actor_standby_disable_standby_fields_override': actor_standby_disable_standby_fields_override, 'actor_standby_should_pass_actor_input': actor_standby_should_pass_actor_input, + 'actor_standby_tenancy': actor_standby_tenancy, }, ), example_run_input=ExampleRunInput( @@ -768,6 +775,7 @@ async def update( server_memory_mbytes: int | None = None, server_disable_fields_override: bool | None = None, server_should_pass_actor_input: bool | None = None, + server_tenancy: Tenancy | None = None, pricing_infos: list[dict[str, Any]] | None = None, actor_permission_level: ActorPermissionLevel | None = None, tagged_builds: dict[str, dict[str, str] | None] | None = None, @@ -780,6 +788,7 @@ async def update( actor_standby_memory_mbytes: int | None = None, actor_standby_disable_standby_fields_override: bool | None = None, actor_standby_should_pass_actor_input: bool | None = None, + actor_standby_tenancy: Tenancy | None = None, ) -> ActorResource: """Update the Actor with the specified fields. @@ -820,6 +829,8 @@ async def update( elsewhere. server_should_pass_actor_input: Whether to pass the Actor input to the runs of the Server Actor. If false, they start with no input. + server_tenancy: Whether each user gets their own runs of the Server Actor (`SINGLE_TENANT`) or the runs are + shared across users (`MULTI_TENANT`). `MULTI_TENANT` requires pay-per-event pricing. pricing_infos: A list of objects that describes the pricing of the Actor. actor_permission_level: The permission level of the Actor on Apify platform. tagged_builds: A dictionary mapping build tag names to their settings. Use it to create, update, @@ -838,6 +849,7 @@ async def update( removed in v4. actor_standby_should_pass_actor_input: Deprecated alias of `server_should_pass_actor_input`. Will be removed in v4. + actor_standby_tenancy: Deprecated alias of `server_tenancy`. Will be removed in v4. Returns: The updated Actor. @@ -872,6 +884,7 @@ async def update( 'server_memory_mbytes': server_memory_mbytes, 'server_disable_fields_override': server_disable_fields_override, 'server_should_pass_actor_input': server_should_pass_actor_input, + 'server_tenancy': server_tenancy, }, { 'actor_standby_is_enabled': actor_standby_is_enabled, @@ -882,6 +895,7 @@ async def update( 'actor_standby_memory_mbytes': actor_standby_memory_mbytes, 'actor_standby_disable_standby_fields_override': actor_standby_disable_standby_fields_override, 'actor_standby_should_pass_actor_input': actor_standby_should_pass_actor_input, + 'actor_standby_tenancy': actor_standby_tenancy, }, ), example_run_input=ExampleRunInput( diff --git a/src/apify_client/_resource_clients/actor_collection.py b/src/apify_client/_resource_clients/actor_collection.py index 530a9f46..7838f412 100644 --- a/src/apify_client/_resource_clients/actor_collection.py +++ b/src/apify_client/_resource_clients/actor_collection.py @@ -21,7 +21,7 @@ from collections.abc import AsyncIterator, Iterator from datetime import timedelta - from apify_client._literals import ActorPermissionLevel + from apify_client._literals import ActorPermissionLevel, Tenancy from apify_client._models import ActorListItem from apify_client.types import Timeout @@ -137,6 +137,7 @@ def create( server_memory_mbytes: int | None = None, server_disable_fields_override: bool | None = None, server_should_pass_actor_input: bool | None = None, + server_tenancy: Tenancy | None = None, actor_permission_level: ActorPermissionLevel | None = None, timeout: Timeout = 'medium', actor_standby_is_enabled: bool | None = None, @@ -147,6 +148,7 @@ def create( actor_standby_memory_mbytes: int | None = None, actor_standby_disable_standby_fields_override: bool | None = None, actor_standby_should_pass_actor_input: bool | None = None, + actor_standby_tenancy: Tenancy | None = None, ) -> ActorResource: """Create a new Actor. @@ -187,6 +189,8 @@ def create( elsewhere. server_should_pass_actor_input: Whether to pass the Actor input to the runs of the Server Actor. If false, they start with no input. + server_tenancy: Whether each user gets their own runs of the Server Actor (`SINGLE_TENANT`) or the runs are + shared across users (`MULTI_TENANT`). `MULTI_TENANT` requires pay-per-event pricing. actor_permission_level: The permission level of the Actor on Apify platform. timeout: Timeout for the API HTTP request. actor_standby_is_enabled: Deprecated alias of `server_is_enabled`. Will be removed in v4. @@ -201,6 +205,7 @@ def create( removed in v4. actor_standby_should_pass_actor_input: Deprecated alias of `server_should_pass_actor_input`. Will be removed in v4. + actor_standby_tenancy: Deprecated alias of `server_tenancy`. Will be removed in v4. Returns: The created Actor. @@ -234,6 +239,7 @@ def create( 'server_memory_mbytes': server_memory_mbytes, 'server_disable_fields_override': server_disable_fields_override, 'server_should_pass_actor_input': server_should_pass_actor_input, + 'server_tenancy': server_tenancy, }, { 'actor_standby_is_enabled': actor_standby_is_enabled, @@ -244,6 +250,7 @@ def create( 'actor_standby_memory_mbytes': actor_standby_memory_mbytes, 'actor_standby_disable_standby_fields_override': actor_standby_disable_standby_fields_override, 'actor_standby_should_pass_actor_input': actor_standby_should_pass_actor_input, + 'actor_standby_tenancy': actor_standby_tenancy, }, ), example_run_input=ExampleRunInput( @@ -366,6 +373,7 @@ async def create( server_memory_mbytes: int | None = None, server_disable_fields_override: bool | None = None, server_should_pass_actor_input: bool | None = None, + server_tenancy: Tenancy | None = None, actor_permission_level: ActorPermissionLevel | None = None, timeout: Timeout = 'medium', actor_standby_is_enabled: bool | None = None, @@ -376,6 +384,7 @@ async def create( actor_standby_memory_mbytes: int | None = None, actor_standby_disable_standby_fields_override: bool | None = None, actor_standby_should_pass_actor_input: bool | None = None, + actor_standby_tenancy: Tenancy | None = None, ) -> ActorResource: """Create a new Actor. @@ -416,6 +425,8 @@ async def create( elsewhere. server_should_pass_actor_input: Whether to pass the Actor input to the runs of the Server Actor. If false, they start with no input. + server_tenancy: Whether each user gets their own runs of the Server Actor (`SINGLE_TENANT`) or the runs are + shared across users (`MULTI_TENANT`). `MULTI_TENANT` requires pay-per-event pricing. actor_permission_level: The permission level of the Actor on Apify platform. timeout: Timeout for the API HTTP request. actor_standby_is_enabled: Deprecated alias of `server_is_enabled`. Will be removed in v4. @@ -430,6 +441,7 @@ async def create( removed in v4. actor_standby_should_pass_actor_input: Deprecated alias of `server_should_pass_actor_input`. Will be removed in v4. + actor_standby_tenancy: Deprecated alias of `server_tenancy`. Will be removed in v4. Returns: The created Actor. @@ -463,6 +475,7 @@ async def create( 'server_memory_mbytes': server_memory_mbytes, 'server_disable_fields_override': server_disable_fields_override, 'server_should_pass_actor_input': server_should_pass_actor_input, + 'server_tenancy': server_tenancy, }, { 'actor_standby_is_enabled': actor_standby_is_enabled, @@ -473,6 +486,7 @@ async def create( 'actor_standby_memory_mbytes': actor_standby_memory_mbytes, 'actor_standby_disable_standby_fields_override': actor_standby_disable_standby_fields_override, 'actor_standby_should_pass_actor_input': actor_standby_should_pass_actor_input, + 'actor_standby_tenancy': actor_standby_tenancy, }, ), example_run_input=ExampleRunInput( diff --git a/src/apify_client/_utils/server_args.py b/src/apify_client/_utils/server_args.py index bfdaed8c..aa94e2ba 100644 --- a/src/apify_client/_utils/server_args.py +++ b/src/apify_client/_utils/server_args.py @@ -15,6 +15,7 @@ 'actor_standby_memory_mbytes': 'server_memory_mbytes', 'actor_standby_disable_standby_fields_override': 'server_disable_fields_override', 'actor_standby_should_pass_actor_input': 'server_should_pass_actor_input', + 'actor_standby_tenancy': 'server_tenancy', } """Deprecated `actor_standby_*` arguments, named after Actor Standby (the former name of Server Actors), mapped to the `server_*` arguments that replace them.""" @@ -57,4 +58,5 @@ def build_actor_standby(server_args: dict[str, Any], deprecated_args: dict[str, memory_mbytes=args.get('server_memory_mbytes'), disable_standby_fields_override=args.get('server_disable_fields_override'), should_pass_actor_input=args.get('server_should_pass_actor_input'), + tenancy=args.get('server_tenancy'), ) diff --git a/tests/unit/test_actor_update_fields.py b/tests/unit/test_actor_update_fields.py index 112be869..c2d00389 100644 --- a/tests/unit/test_actor_update_fields.py +++ b/tests/unit/test_actor_update_fields.py @@ -37,15 +37,21 @@ _FIELDS: dict[str, Any] = { 'server_disable_fields_override': True, 'server_should_pass_actor_input': False, + 'server_tenancy': 'SINGLE_TENANT', } _DEPRECATED_FIELDS: dict[str, Any] = { 'actor_standby_disable_standby_fields_override': True, 'actor_standby_should_pass_actor_input': False, + 'actor_standby_tenancy': 'SINGLE_TENANT', } _EXPECTED_BODY = { - 'actorStandby': {'disableStandbyFieldsOverride': True, 'shouldPassActorInput': False}, + 'actorStandby': { + 'disableStandbyFieldsOverride': True, + 'shouldPassActorInput': False, + 'tenancy': 'SINGLE_TENANT', + }, } @@ -114,7 +120,7 @@ def test_update_deprecated_standby_args_are_aliases(httpserver: HTTPServer, sync with pytest.warns(DeprecationWarning, match='argument is deprecated') as record: sync_client.actor(_MOCKED_ACTOR_ID).update(**_DEPRECATED_FIELDS) - assert [warning.filename for warning in record] == [__file__, __file__] + assert [warning.filename for warning in record] == [__file__, __file__, __file__] assert len(captured) == 1 assert body_subset(captured[0]) == _EXPECTED_BODY @@ -129,7 +135,7 @@ async def test_update_deprecated_standby_args_are_aliases_async( with pytest.warns(DeprecationWarning, match='argument is deprecated') as record: await async_client.actor(_MOCKED_ACTOR_ID).update(**_DEPRECATED_FIELDS) - assert [warning.filename for warning in record] == [__file__, __file__] + assert [warning.filename for warning in record] == [__file__, __file__, __file__] assert len(captured) == 1 assert body_subset(captured[0]) == _EXPECTED_BODY @@ -142,7 +148,7 @@ def test_create_deprecated_standby_args_are_aliases(httpserver: HTTPServer, sync with pytest.warns(DeprecationWarning, match='argument is deprecated') as record: sync_client.actors().create(name='test-actor', **_DEPRECATED_FIELDS) - assert [warning.filename for warning in record] == [__file__, __file__] + assert [warning.filename for warning in record] == [__file__, __file__, __file__] assert len(captured) == 1 assert body_subset(captured[0]) == _EXPECTED_BODY @@ -157,7 +163,7 @@ async def test_create_deprecated_standby_args_are_aliases_async( with pytest.warns(DeprecationWarning, match='argument is deprecated') as record: await async_client.actors().create(name='test-actor', **_DEPRECATED_FIELDS) - assert [warning.filename for warning in record] == [__file__, __file__] + assert [warning.filename for warning in record] == [__file__, __file__, __file__] assert len(captured) == 1 assert body_subset(captured[0]) == _EXPECTED_BODY