diff --git a/docs/02_concepts/code/03_nested_async.py b/docs/02_concepts/code/03_nested_async.py
index b7843df1..f1d4c27f 100644
--- a/docs/02_concepts/code/03_nested_async.py
+++ b/docs/02_concepts/code/03_nested_async.py
@@ -1,5 +1,4 @@
from apify_client import ApifyClientAsync
-from apify_client._models import ActorJobStatus
TOKEN = 'MY-APIFY-TOKEN'
@@ -14,7 +13,7 @@ async def main() -> None:
actor_runs = (await runs_client.list(limit=10, desc=True)).items
# Select the last run of the Actor that finished with a SUCCEEDED status.
- last_succeeded_run_client = actor_client.last_run(status=ActorJobStatus.SUCCEEDED)
+ last_succeeded_run_client = actor_client.last_run(status='SUCCEEDED')
# Get dataset
actor_run_dataset_client = last_succeeded_run_client.dataset()
diff --git a/docs/02_concepts/code/03_nested_sync.py b/docs/02_concepts/code/03_nested_sync.py
index 4759f28f..65ae8dd2 100644
--- a/docs/02_concepts/code/03_nested_sync.py
+++ b/docs/02_concepts/code/03_nested_sync.py
@@ -1,5 +1,4 @@
from apify_client import ApifyClient
-from apify_client._models import ActorJobStatus
TOKEN = 'MY-APIFY-TOKEN'
@@ -14,7 +13,7 @@ def main() -> None:
actor_runs = runs_client.list(limit=10, desc=True).items
# Select the last run of the Actor that finished with a SUCCEEDED status.
- last_succeeded_run_client = actor_client.last_run(status=ActorJobStatus.SUCCEEDED)
+ last_succeeded_run_client = actor_client.last_run(status='SUCCEEDED')
# Get dataset
actor_run_dataset_client = last_succeeded_run_client.dataset()
diff --git a/docs/04_upgrading/upgrading_to_v3.mdx b/docs/04_upgrading/upgrading_to_v3.mdx
index 7c9e0062..7e1ae66e 100644
--- a/docs/04_upgrading/upgrading_to_v3.mdx
+++ b/docs/04_upgrading/upgrading_to_v3.mdx
@@ -287,22 +287,36 @@ client.key_value_store('my-store').set_record('my-key', {'data': 1}, content_typ
client.run('my-run').charge('my-event', count=5, idempotency_key='my-idempotency-key')
```
-## Snake_case `sort_by` values on `actors().list()`
+## `Literal` type aliases instead of `StrEnum` classes
-The `sort_by` parameter of `ActorCollectionClient.list()` and `ActorCollectionClientAsync.list()` now accepts pythonic snake_case values instead of the raw camelCase values used by the API.
+Generated enum-like types are now [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal) type aliases instead of `StrEnum` classes. Pass plain strings instead of enum members; type checkers will validate them against the allowed set.
+
+Affected types: `ActorJobStatus`, `ActorPermissionLevel`, `ErrorType`, `GeneralAccess`, `HttpMethod`, `RunOrigin`, `SourceCodeFileFormat`, `StorageOwnership`, `VersionSourceType`, `WebhookDispatchStatus`, `WebhookEventType`.
Before (v2):
```python
-client.actors().list(sort_by='createdAt')
-client.actors().list(sort_by='stats.lastRunStartedAt')
+from apify_client._models import WebhookEventType
+
+client.actor('apify/hello-world').webhooks().create(
+ event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED],
+ request_url='https://example.com/webhook',
+)
```
After (v3):
```python
-client.actors().list(sort_by='created_at')
-client.actors().list(sort_by='last_run_started_at')
+client.actor('apify/hello-world').webhooks().create(
+ event_types=['ACTOR.RUN.SUCCEEDED'],
+ request_url='https://example.com/webhook',
+)
```
-The default value also changed from `'createdAt'` to `'created_at'` (behavior is unchanged). The client translates the snake_case value to the form expected by the API internally.
+The aliases now live in `apify_client._literals` (previously `apify_client._models`) and can be imported for use in type annotations:
+
+```python
+from apify_client._literals import WebhookEventType
+
+events: list[WebhookEventType] = ['ACTOR.RUN.SUCCEEDED', 'ACTOR.RUN.FAILED']
+```
diff --git a/pyproject.toml b/pyproject.toml
index fcdb3e3d..1a1939ba 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -123,6 +123,9 @@ indent-style = "space"
"**/__init__.py" = [
"F401", # Unused imports
]
+"**/_models.py" = [
+ "TC001", # Pydantic needs the literal aliases importable at runtime to resolve forward references
+]
"**/{scripts}/*" = [
"D", # Everything from the pydocstyle
"INP001", # File {filename} is part of an implicit namespace package, add an __init__.py
diff --git a/scripts/postprocess_generated_models.py b/scripts/postprocess_generated_models.py
index 6774102e..fdabfe81 100644
--- a/scripts/postprocess_generated_models.py
+++ b/scripts/postprocess_generated_models.py
@@ -4,6 +4,11 @@
- Fix discriminator field names that use camelCase instead of snake_case (known issue with
discriminators on schemas referenced from array items).
- Deduplicate the inlined `Type(StrEnum)` that comes from ErrorResponse.yaml; rewire to `ErrorType`.
+- Rewrite every `class X(StrEnum)` as `X = Literal[...]` so downstream code can pass plain strings
+ (and reuse the named alias in resource-client signatures) instead of enum members.
+- Move the resulting `X = Literal[...]` definitions into `_literals.py`, leaving
+ `_models.py` importing them — so consumers can depend on a dedicated literals module
+ without pulling in every Pydantic model.
- Add `@docs_group('Models')` to every model class (plus the required import).
Applied to `_typeddicts.py`:
@@ -28,6 +33,7 @@
REPO_ROOT = Path(__file__).resolve().parent.parent
PACKAGE_DIR = REPO_ROOT / 'src' / 'apify_client'
MODELS_PATH = PACKAGE_DIR / '_models.py'
+LITERALS_PATH = PACKAGE_DIR / '_literals.py'
TYPEDDICTS_PATH = PACKAGE_DIR / '_typeddicts.py'
# Map of camelCase discriminator values to their snake_case equivalents.
@@ -54,6 +60,32 @@
)
+def _collapse_blank_lines(content: str) -> str:
+ """Collapse runs of 3+ blank lines down to exactly 3, leaving at most 2 blank lines between symbols."""
+ return re.sub(r'\n{3,}', '\n\n\n', content)
+
+
+def _ensure_typing_import(content: str, name: str) -> str:
+ """Append `name` to the `from typing import ...` line if not already imported.
+
+ Assumes the single-line import form datamodel-codegen emits; ruff re-wraps afterwards.
+ """
+ typing_import = re.search(r'from typing import[^\n]+', content)
+ if typing_import is None or name in typing_import.group(0):
+ return content
+ return re.sub(
+ r'(from typing import )([^\n]+)',
+ lambda m: f'{m.group(1)}{m.group(2)}, {name}',
+ content,
+ count=1,
+ )
+
+
+def _base_names(node: ast.ClassDef) -> set[str]:
+ """Return the set of unsubscripted base-class names of `node`."""
+ return {b.id for b in node.bases if isinstance(b, ast.Name)}
+
+
def fix_discriminators(content: str) -> str:
"""Replace camelCase discriminator values with their snake_case equivalents."""
for camel, snake in DISCRIMINATOR_FIXES.items():
@@ -66,18 +98,159 @@ def fix_discriminators(content: str) -> str:
def deduplicate_error_type_enum(content: str) -> str:
- """Remove the duplicate `Type` enum and rewire references to `ErrorType`."""
- # Remove the entire `class Type(StrEnum): ...` block up to the next class definition.
- content = re.sub(
- r'\nclass Type\(StrEnum\):.*?(?=\nclass )',
- '\n',
- content,
- flags=re.DOTALL,
+ """Remove the duplicate `Type` enum and rewire references to `ErrorType`.
+
+ The `type` property on `ErrorResponse` discriminator subtypes (`RunFailedErrorDetail` etc.)
+ re-emits the value list of the named `ErrorType` enum as a separate `class Type(StrEnum)` —
+ upstream issue https://github.com/koxudaxi/datamodel-code-generator/issues/3104.
+ """
+ tree = ast.parse(content)
+ type_node = next(
+ (n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == 'Type' and 'StrEnum' in _base_names(n)),
+ None,
)
- # Replace standalone `Type` references in annotation contexts (`: Type`, `| Type`, `[Type`).
+ if type_node is None:
+ return content
+
+ assert type_node.end_lineno is not None # noqa: S101
+ lines = content.split('\n')
+ del lines[type_node.lineno - 1 : type_node.end_lineno]
+ content = '\n'.join(lines)
+
+ # Lookbehinds are deliberately narrow: matching bare `\bType\b` would also rewrite `Type` in
+ # docstrings (`Content-Type`, `Type of event`), which broke an earlier version.
content = re.sub(r'(?<=: )Type\b|(?<=\| )Type\b|(?<=\[)Type\b', 'ErrorType', content)
- # Collapse triple+ blank lines left by the removal.
- return re.sub(r'\n{3,}', '\n\n\n', content)
+ return _collapse_blank_lines(content)
+
+
+def convert_enums_to_literals(content: str) -> str:
+ """Rewrite every `class X(StrEnum): ...` into an `X = Literal[...]` alias.
+
+ Each member assignment (`NAME = 'value'`) contributes its string value to the literal in
+ declaration order. The class docstring, if present, is preserved as a trailing bare-string
+ docstring after the alias — matching the field-doc convention datamodel-codegen already uses
+ elsewhere in the generated file.
+
+ Runs before `add_docs_group_decorators`, so the enum classes have no `@docs_group` decorator
+ to strip. The `from enum import StrEnum` import is left alone and removed by ruff's F401 fix.
+ """
+ tree = ast.parse(content)
+ lines = content.split('\n')
+ replacements: list[tuple[int, int, list[str]]] = []
+
+ for node in tree.body:
+ if not isinstance(node, ast.ClassDef):
+ continue
+ base_names = _base_names(node)
+ if 'StrEnum' not in base_names:
+ continue
+
+ values: list[str] = [
+ stmt.value.value
+ for stmt in node.body
+ if isinstance(stmt, ast.Assign)
+ and len(stmt.targets) == 1
+ and isinstance(stmt.targets[0], ast.Name)
+ and isinstance(stmt.value, ast.Constant)
+ and isinstance(stmt.value.value, 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(']')
+ if docstring is not None:
+ if '\n' in docstring:
+ new_lines.append('"""')
+ new_lines.extend(docstring.splitlines())
+ new_lines.append('"""')
+ else:
+ new_lines.append(f'"""{docstring}"""')
+
+ assert node.end_lineno is not None # noqa: S101
+ replacements.append((node.lineno - 1, node.end_lineno, new_lines))
+
+ if not replacements:
+ return content
+
+ # Replace in reverse order so earlier slice indices stay valid after each splice.
+ for start, end, new in sorted(replacements, key=lambda r: r[0], reverse=True):
+ lines[start:end] = new
+
+ return _collapse_blank_lines('\n'.join(lines))
+
+
+LITERALS_FILE_HEADER = """\
+# generated by postprocess_generated_models
+
+from __future__ import annotations
+
+from typing import Literal
+
+
+"""
+
+
+def _is_literal_alias(node: ast.stmt) -> bool:
+ """Return True if `node` is a top-level `Name = Literal[...]` statement."""
+ return (
+ isinstance(node, ast.Assign)
+ and len(node.targets) == 1
+ and isinstance(node.targets[0], ast.Name)
+ and isinstance(node.value, ast.Subscript)
+ and isinstance(node.value.value, ast.Name)
+ and node.value.value.id == 'Literal'
+ )
+
+
+def split_literals_to_file(content: str) -> tuple[str, str]:
+ """Move every top-level `Name = Literal[...]` block into a separate literals module.
+
+ Walks the top-level AST, collects each literal alias plus its trailing bare-string docstring,
+ deletes them from `_models.py`, and rebuilds `_literals.py` from the blocks
+ in original order. The models content gains a `from apify_client._literals import ...`
+ line so Pydantic can still resolve the forward references in field annotations.
+
+ Returns `(new_models_content, literals_file_content)`. If no literal aliases are found, the
+ models content is returned unchanged and the literals content is empty.
+ """
+ tree = ast.parse(content)
+ lines = content.split('\n')
+
+ blocks: list[tuple[int, int, str]] = [
+ (node.lineno - 1, end_line, name)
+ for name, node, end_line in _extract_top_level_symbols(tree)
+ if _is_literal_alias(node)
+ ]
+
+ if not blocks:
+ return content, ''
+
+ literal_lines: list[str] = []
+ for start, end, _ in blocks:
+ literal_lines.extend(lines[start:end])
+ literal_lines.append('')
+ literal_lines.append('')
+
+ new_lines = lines[:]
+ for start, end, _ in sorted(blocks, key=lambda b: b[0], reverse=True):
+ del new_lines[start:end]
+
+ # Inject the import right after the last existing `from apify_client.` import so ruff/isort
+ # keep the final ordering stable.
+ names = sorted(name for _, _, name in blocks)
+ import_line = f'from apify_client._literals import {", ".join(names)}'
+ insert_at = next(
+ (idx + 1 for idx in range(len(new_lines) - 1, -1, -1) if new_lines[idx].startswith('from apify_client.')),
+ None,
+ )
+ if insert_at is None:
+ raise RuntimeError('No `from apify_client.` import found in generated models to anchor literals import')
+ new_lines.insert(insert_at, import_line)
+
+ models_content = _collapse_blank_lines('\n'.join(new_lines))
+ literals_content = _collapse_blank_lines(LITERALS_FILE_HEADER + '\n'.join(literal_lines))
+ return models_content, literals_content
def add_docs_group_decorators(content: str, group_name: GroupName) -> str:
@@ -123,7 +296,7 @@ def flatten_empty_typeddicts(content: str) -> str:
if has_fields:
continue
# Only flatten TypedDict-based classes (e.g. skip Enum subclasses).
- base_names = {b.id for b in node.bases if isinstance(b, ast.Name)}
+ base_names = _base_names(node)
if 'TypedDict' not in base_names:
continue
assert node.end_lineno is not None # noqa: S101
@@ -139,17 +312,7 @@ def flatten_empty_typeddicts(content: str) -> str:
if not replaced:
return content
- output = re.sub(r'\n{3,}', '\n\n\n', '\n'.join(lines))
- # Flattening introduces new `TypeAlias` uses; make sure it's imported from typing.
- typing_import = re.search(r'from typing import[^\n]+', output)
- if typing_import is not None and 'TypeAlias' not in typing_import.group(0):
- output = re.sub(
- r'(from typing import )([^\n]+)',
- lambda m: f'{m.group(1)}{m.group(2)}, TypeAlias',
- output,
- count=1,
- )
- return output
+ return _ensure_typing_import(_collapse_blank_lines('\n'.join(lines)), 'TypeAlias')
def _is_string_expr(node: ast.stmt) -> bool:
@@ -162,7 +325,7 @@ def _extract_top_level_symbols(tree: ast.Module) -> list[tuple[str, ast.stmt, in
If a top-level string expression immediately follows a symbol, it is absorbed into that
symbol's `end_line` so they get pruned together (datamodel-codegen emits the schema description
- for TypeAlias statements as a bare string right after the alias).
+ for type-alias statements as a bare string right after the alias).
"""
symbols: list[tuple[str, ast.stmt, int]] = []
body = tree.body
@@ -174,6 +337,8 @@ def _extract_top_level_symbols(tree: ast.Module) -> list[tuple[str, ast.stmt, in
name = node.name
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
name = node.target.id
+ elif isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
+ name = node.targets[0].id
if name is not None:
assert node.end_lineno is not None # noqa: S101
@@ -240,10 +405,7 @@ def prune_typeddicts(content: str, seeds: frozenset[str]) -> tuple[str, set[str]
drop_line_indices.add(line_no)
pruned = [line for i, line in enumerate(lines) if i not in drop_line_indices]
- output = '\n'.join(pruned)
- # Collapse runs of blank lines left behind by deletions.
- output = re.sub(r'\n{3,}', '\n\n\n', output)
- return output, kept
+ return _collapse_blank_lines('\n'.join(pruned)), kept
def rename_with_dict_suffix(content: str, names: set[str]) -> str:
@@ -256,16 +418,28 @@ def rename_with_dict_suffix(content: str, names: set[str]) -> str:
return content
-def postprocess_models(path: Path) -> bool:
- """Apply `_models.py`-specific fixes. Returns True if the file changed."""
- original = path.read_text()
+def postprocess_models(models_path: Path, literals_path: Path) -> list[Path]:
+ """Apply `_models.py`-specific fixes and emit `_literals.py`.
+
+ Returns the list of paths that were (re)written.
+ """
+ original = models_path.read_text()
fixed = fix_discriminators(original)
fixed = deduplicate_error_type_enum(fixed)
+ fixed = convert_enums_to_literals(fixed)
fixed = add_docs_group_decorators(fixed, 'Models')
- if fixed == original:
- return False
- path.write_text(fixed)
- return True
+ models_content, literals_content = split_literals_to_file(fixed)
+
+ changed: list[Path] = []
+ if models_content != original:
+ models_path.write_text(models_content)
+ changed.append(models_path)
+ if literals_content:
+ previous = literals_path.read_text() if literals_path.exists() else ''
+ if literals_content != previous:
+ literals_path.write_text(literals_content)
+ changed.append(literals_path)
+ return changed
def postprocess_typeddicts(path: Path) -> bool:
@@ -289,12 +463,12 @@ def run_ruff(paths: list[Path]) -> None:
def main() -> None:
- changed: list[Path] = []
- if postprocess_models(MODELS_PATH):
- changed.append(MODELS_PATH)
- print(f'Fixed generated models in {MODELS_PATH}')
+ changed = postprocess_models(MODELS_PATH, LITERALS_PATH)
+ if changed:
+ for path in changed:
+ print(f'Wrote {path}')
else:
- print('No fixes needed for _models.py')
+ print('No fixes needed for _models.py / _literals.py')
if postprocess_typeddicts(TYPEDDICTS_PATH):
changed.append(TYPEDDICTS_PATH)
diff --git a/src/apify_client/_consts.py b/src/apify_client/_consts.py
index 91f8fd3e..134e0e7b 100644
--- a/src/apify_client/_consts.py
+++ b/src/apify_client/_consts.py
@@ -2,8 +2,6 @@
from datetime import timedelta
-from apify_client._models import ActorJobStatus
-
DEFAULT_API_URL = 'https://api.apify.com'
"""Default base URL for the Apify API."""
@@ -34,15 +32,5 @@
DEFAULT_WAIT_WHEN_JOB_NOT_EXIST = timedelta(seconds=3)
"""How long to wait for a job to exist before giving up."""
-TERMINAL_STATUSES = frozenset(
- {
- ActorJobStatus.SUCCEEDED,
- ActorJobStatus.FAILED,
- ActorJobStatus.TIMED_OUT,
- ActorJobStatus.ABORTED,
- }
-)
-"""Set of terminal Actor job statuses that indicate the job has finished."""
-
OVERRIDABLE_DEFAULT_HEADERS = {'Accept', 'Authorization', 'Accept-Encoding', 'User-Agent'}
"""Headers that can be overridden by users, but will trigger a warning if they do so, as it may lead to API errors."""
diff --git a/src/apify_client/_literals.py b/src/apify_client/_literals.py
new file mode 100644
index 00000000..28462a67
--- /dev/null
+++ b/src/apify_client/_literals.py
@@ -0,0 +1,498 @@
+# generated by postprocess_generated_models
+
+from __future__ import annotations
+
+from typing import Literal
+
+ActorJobStatus = Literal[
+ 'READY',
+ 'RUNNING',
+ 'SUCCEEDED',
+ 'FAILED',
+ 'TIMING-OUT',
+ 'TIMED-OUT',
+ 'ABORTING',
+ 'ABORTED',
+]
+"""Status of an Actor job (run or build)."""
+
+
+ActorPermissionLevel = Literal[
+ 'LIMITED_PERMISSIONS',
+ 'FULL_PERMISSIONS',
+]
+"""Determines permissions that the Actor requires to run. For more information, see the [Actor permissions documentation](https://docs.apify.com/platform/actors/development/permissions)."""
+
+
+ErrorType = Literal[
+ '3d-secure-auth-failed',
+ 'access-right-already-exists',
+ 'action-not-found',
+ 'actor-already-rented',
+ 'actor-can-not-be-rented',
+ 'actor-disabled',
+ 'actor-is-not-rented',
+ 'actor-memory-limit-exceeded',
+ 'actor-name-exists-new-owner',
+ 'actor-name-not-unique',
+ 'actor-not-found',
+ 'actor-not-github-actor',
+ 'actor-not-public',
+ 'actor-permission-level-not-supported-for-agentic-payments',
+ 'actor-review-already-exists',
+ 'actor-run-failed',
+ 'actor-standby-not-supported-for-agentic-payments',
+ 'actor-task-name-not-unique',
+ 'agentic-payment-info-retrieval-error',
+ 'agentic-payment-information-missing',
+ 'agentic-payment-insufficient-amount',
+ 'agentic-payment-provider-internal-error',
+ 'agentic-payment-provider-unauthorized',
+ 'airtable-webhook-deprecated',
+ 'already-subscribed-to-paid-actor',
+ 'apify-plan-required-to-use-paid-actor',
+ 'apify-signup-not-allowed',
+ 'auth-method-not-supported',
+ 'authorization-server-not-found',
+ 'auto-issue-date-invalid',
+ 'background-check-required',
+ 'billing-system-error',
+ 'black-friday-plan-expired',
+ 'braintree-error',
+ 'braintree-not-linked',
+ 'braintree-operation-timed-out',
+ 'braintree-unsupported-currency',
+ 'build-not-found',
+ 'build-outdated',
+ 'cannot-add-apify-events-to-ppe-actor',
+ 'cannot-add-multiple-pricing-infos',
+ 'cannot-add-pricing-info-that-alters-past',
+ 'cannot-add-second-future-pricing-info',
+ 'cannot-build-actor-from-webhook',
+ 'cannot-change-billing-interval',
+ 'cannot-change-owner',
+ 'cannot-charge-apify-event',
+ 'cannot-charge-non-pay-per-event-actor',
+ 'cannot-comment-as-other-user',
+ 'cannot-copy-actor-task',
+ 'cannot-create-payout',
+ 'cannot-create-public-actor',
+ 'cannot-create-tax-transaction',
+ 'cannot-delete-critical-actor',
+ 'cannot-delete-invoice',
+ 'cannot-delete-paid-actor',
+ 'cannot-disable-one-time-event-for-apify-start-event',
+ 'cannot-disable-organization-with-enabled-members',
+ 'cannot-disable-user-with-subscription',
+ 'cannot-link-oauth-to-unverified-email',
+ 'cannot-metamorph-to-pay-per-result-actor',
+ 'cannot-modify-actor-pricing-too-frequently',
+ 'cannot-modify-actor-pricing-with-immediate-effect',
+ 'cannot-override-paid-actor-trial',
+ 'cannot-permanently-delete-subscription',
+ 'cannot-publish-actor',
+ 'cannot-reduce-last-full-token',
+ 'cannot-reimburse-more-than-original-charge',
+ 'cannot-reimburse-non-rental-charge',
+ 'cannot-remove-own-actor-from-recently-used',
+ 'cannot-remove-payment-method',
+ 'cannot-remove-pricing-info',
+ 'cannot-remove-running-run',
+ 'cannot-remove-user-with-public-actors',
+ 'cannot-remove-user-with-subscription',
+ 'cannot-remove-user-with-unpaid-invoice',
+ 'cannot-rename-env-var',
+ 'cannot-rent-paid-actor',
+ 'cannot-review-own-actor',
+ 'cannot-set-access-rights-for-owner',
+ 'cannot-set-is-status-message-terminal',
+ 'cannot-unpublish-critical-actor',
+ 'cannot-unpublish-paid-actor',
+ 'cannot-unpublish-profile',
+ 'cannot-update-invoice-field',
+ 'concurrent-runs-limit-exceeded',
+ 'concurrent-update-detected',
+ 'conference-token-not-found',
+ 'content-encoding-forbidden-for-html',
+ 'coupon-already-redeemed',
+ 'coupon-expired',
+ 'coupon-for-new-customers',
+ 'coupon-for-subscribed-users',
+ 'coupon-limits-are-in-conflict-with-current-limits',
+ 'coupon-max-number-of-redemptions-reached',
+ 'coupon-not-found',
+ 'coupon-not-unique',
+ 'coupons-disabled',
+ 'create-github-issue-not-allowed',
+ 'creator-plan-not-available',
+ 'cron-expression-invalid',
+ 'daily-ai-token-limit-exceeded',
+ 'daily-publication-limit-exceeded',
+ 'dataset-does-not-have-fields-schema',
+ 'dataset-does-not-have-schema',
+ 'dataset-locked',
+ 'dataset-schema-invalid',
+ 'dcr-not-supported',
+ 'default-dataset-not-found',
+ 'deleting-default-build',
+ 'deleting-unfinished-build',
+ 'email-already-taken',
+ 'email-already-taken-removed-user',
+ 'email-domain-not-allowed-for-coupon',
+ 'email-invalid',
+ 'email-not-allowed',
+ 'email-not-valid',
+ 'email-update-too-soon',
+ 'elevated-permissions-needed',
+ 'env-var-already-exists',
+ 'exchange-rate-fetch-failed',
+ 'expired-conference-token',
+ 'failed-to-charge-user',
+ 'final-invoice-negative',
+ 'github-branch-empty',
+ 'github-issue-already-exists',
+ 'github-public-key-not-found',
+ 'github-repository-not-found',
+ 'github-signature-does-not-match-payload',
+ 'github-user-not-authorized-for-issues',
+ 'gmail-not-allowed',
+ 'id-does-not-match',
+ 'incompatible-billing-interval',
+ 'incomplete-payout-billing-info',
+ 'inconsistent-currencies',
+ 'incorrect-pricing-modifier-prefix',
+ 'input-json-invalid-characters',
+ 'input-json-not-object',
+ 'input-json-too-long',
+ 'input-update-collision',
+ 'insufficient-permissions',
+ 'insufficient-permissions-to-change-field',
+ 'insufficient-security-measures',
+ 'insufficient-tax-country-evidence',
+ 'integration-auth-error',
+ 'internal-server-error',
+ 'invalid-billing-info',
+ 'invalid-billing-period-for-payout',
+ 'invalid-build',
+ 'invalid-client-key',
+ 'invalid-collection',
+ 'invalid-conference-login-password',
+ 'invalid-content-type-header',
+ 'invalid-credentials',
+ 'invalid-git-auth-token',
+ 'invalid-github-issue-url',
+ 'invalid-header',
+ 'invalid-id',
+ 'invalid-idempotency-key',
+ 'invalid-input',
+ 'invalid-input-schema',
+ 'invalid-invoice',
+ 'invalid-invoice-type',
+ 'invalid-issue-date',
+ 'invalid-label-params',
+ 'invalid-main-account-user-id',
+ 'invalid-oauth-app',
+ 'invalid-oauth-scope',
+ 'invalid-one-time-invoice',
+ 'invalid-parameter',
+ 'invalid-payout-status',
+ 'invalid-picture-url',
+ 'invalid-record-key',
+ 'invalid-request',
+ 'invalid-resource-type',
+ 'invalid-signature',
+ 'invalid-subscription-plan',
+ 'invalid-tax-number',
+ 'invalid-tax-number-format',
+ 'invalid-token',
+ 'invalid-token-type',
+ 'invalid-two-factor-code',
+ 'invalid-two-factor-code-or-recovery-code',
+ 'invalid-two-factor-recovery-code',
+ 'invalid-username',
+ 'invalid-value',
+ 'invitation-invalid-resource-type',
+ 'invitation-no-longer-valid',
+ 'invoice-canceled',
+ 'invoice-cannot-be-refunded-due-to-too-high-amount',
+ 'invoice-incomplete',
+ 'invoice-is-draft',
+ 'invoice-locked',
+ 'invoice-must-be-buffer',
+ 'invoice-not-canceled',
+ 'invoice-not-draft',
+ 'invoice-not-found',
+ 'invoice-outdated',
+ 'invoice-paid-already',
+ 'issue-already-connected-to-github',
+ 'issue-not-found',
+ 'issues-bad-request',
+ 'issuer-not-registered',
+ 'job-finished',
+ 'label-already-linked',
+ 'last-api-token',
+ 'limit-reached',
+ 'max-items-must-be-greater-than-zero',
+ 'max-metamorphs-exceeded',
+ 'max-total-charge-usd-below-minimum',
+ 'max-total-charge-usd-must-be-greater-than-zero',
+ 'method-not-allowed',
+ 'migration-disabled',
+ 'missing-actor-rights',
+ 'missing-api-token',
+ 'missing-billing-info',
+ 'missing-line-items',
+ 'missing-payment-date',
+ 'missing-payout-billing-info',
+ 'missing-proxy-password',
+ 'missing-reporting-fields',
+ 'missing-resource-name',
+ 'missing-settings',
+ 'missing-username',
+ 'monthly-usage-limit-too-low',
+ 'more-than-one-update-not-allowed',
+ 'multiple-records-found',
+ 'must-be-admin',
+ 'name-not-unique',
+ 'next-runtime-computation-failed',
+ 'no-columns-in-exported-dataset',
+ 'no-payment-attempt-for-refund-found',
+ 'no-payment-method-available',
+ 'no-team-account-seats-available',
+ 'non-temporary-email',
+ 'not-enough-usage-to-run-paid-actor',
+ 'not-implemented',
+ 'not-supported-currencies',
+ 'o-auth-service-already-connected',
+ 'o-auth-service-not-connected',
+ 'oauth-resource-access-failed',
+ 'one-time-invoice-already-marked-paid',
+ 'only-drafts-can-be-deleted',
+ 'operation-canceled',
+ 'operation-not-allowed',
+ 'operation-timed-out',
+ 'organization-cannot-own-itself',
+ 'organization-role-not-found',
+ 'overlapping-payout-billing-periods',
+ 'own-token-required',
+ 'page-not-found',
+ 'param-not-one-of',
+ 'parameter-required',
+ 'parameters-mismatched',
+ 'password-reset-email-already-sent',
+ 'password-reset-token-expired',
+ 'pay-as-you-go-without-monthly-interval',
+ 'payment-attempt-status-message-required',
+ 'payout-already-paid',
+ 'payout-canceled',
+ 'payout-invalid-state',
+ 'payout-must-be-approved-to-be-marked-paid',
+ 'payout-not-found',
+ 'payout-number-already-exists',
+ 'phone-number-invalid',
+ 'phone-number-landline',
+ 'phone-number-opted-out',
+ 'phone-verification-disabled',
+ 'platform-feature-disabled',
+ 'price-overrides-validation-failed',
+ 'pricing-model-not-supported',
+ 'promotional-plan-not-available',
+ 'proxy-auth-ip-not-unique',
+ 'public-actor-disabled',
+ 'query-timeout',
+ 'quoted-price-outdated',
+ 'rate-limit-exceeded',
+ 'recaptcha-invalid',
+ 'recaptcha-required',
+ 'record-not-found',
+ 'record-not-public',
+ 'record-or-token-not-found',
+ 'record-too-large',
+ 'redirect-uri-mismatch',
+ 'reduced-plan-not-available',
+ 'rental-charge-already-reimbursed',
+ 'rental-not-allowed',
+ 'request-aborted-prematurely',
+ 'request-handled-or-locked',
+ 'request-id-invalid',
+ 'request-queue-duplicate-requests',
+ 'request-too-large',
+ 'requested-dataset-view-does-not-exist',
+ 'resume-token-expired',
+ 'run-failed',
+ 'run-timeout-exceeded',
+ 'russia-is-evil',
+ 'same-user',
+ 'schedule-actor-not-found',
+ 'schedule-actor-task-not-found',
+ 'schedule-name-not-unique',
+ 'schema-validation',
+ 'schema-validation-error',
+ 'schema-validation-failed',
+ 'sign-up-method-not-allowed',
+ 'slack-integration-not-custom',
+ 'socket-closed',
+ 'socket-destroyed',
+ 'store-schema-invalid',
+ 'store-terms-not-accepted',
+ 'stripe-enabled',
+ 'stripe-generic-decline',
+ 'stripe-not-enabled',
+ 'stripe-not-enabled-for-user',
+ 'tagged-build-required',
+ 'tax-country-invalid',
+ 'tax-number-invalid',
+ 'tax-number-validation-failed',
+ 'taxamo-call-failed',
+ 'taxamo-request-failed',
+ 'testing-error',
+ 'token-not-provided',
+ 'too-few-versions',
+ 'too-many-actor-tasks',
+ 'too-many-actors',
+ 'too-many-labels-on-resource',
+ 'too-many-mcp-connectors',
+ 'too-many-o-auth-apps',
+ 'too-many-organizations',
+ 'too-many-requests',
+ 'too-many-schedules',
+ 'too-many-ui-access-keys',
+ 'too-many-user-labels',
+ 'too-many-values',
+ 'too-many-versions',
+ 'too-many-webhooks',
+ 'unexpected-route',
+ 'unknown-build-tag',
+ 'unknown-payment-provider',
+ 'unsubscribe-token-invalid',
+ 'unsupported-actor-pricing-model-for-agentic-payments',
+ 'unsupported-content-encoding',
+ 'unsupported-file-type-for-issue',
+ 'unsupported-file-type-image-expected',
+ 'unsupported-file-type-text-or-json-expected',
+ 'unsupported-permission',
+ 'upcoming-subscription-bill-not-up-to-date',
+ 'user-already-exists',
+ 'user-already-verified',
+ 'user-creates-organizations-too-fast',
+ 'user-disabled',
+ 'user-email-is-disposable',
+ 'user-email-not-set',
+ 'user-email-not-verified',
+ 'user-has-no-subscription',
+ 'user-integration-not-found',
+ 'user-is-already-invited',
+ 'user-is-already-organization-member',
+ 'user-is-not-member-of-organization',
+ 'user-is-not-organization',
+ 'user-is-organization',
+ 'user-is-organization-owner',
+ 'user-is-removed',
+ 'user-not-found',
+ 'user-not-logged-in',
+ 'user-not-verified',
+ 'user-or-token-not-found',
+ 'user-plan-not-allowed-for-coupon',
+ 'user-problem-with-card',
+ 'user-record-not-found',
+ 'username-already-taken',
+ 'username-missing',
+ 'username-not-allowed',
+ 'username-removal-forbidden',
+ 'username-required',
+ 'verification-email-already-sent',
+ 'verification-token-expired',
+ 'version-already-exists',
+ 'versions-size-exceeded',
+ 'weak-password',
+ 'x402-agentic-payment-already-finalized',
+ 'x402-agentic-payment-insufficient-amount',
+ 'x402-agentic-payment-malformed-token',
+ 'x402-agentic-payment-settlement-failed',
+ 'x402-agentic-payment-settlement-in-progress',
+ 'x402-agentic-payment-settlement-stuck',
+ 'x402-agentic-payment-unauthorized',
+ 'x402-payment-required',
+ 'zero-invoice',
+]
+"""Machine-processable error type identifier."""
+
+
+GeneralAccess = Literal[
+ 'ANYONE_WITH_ID_CAN_READ',
+ 'ANYONE_WITH_NAME_CAN_READ',
+ 'FOLLOW_USER_SETTING',
+ 'RESTRICTED',
+]
+"""Defines the general access level for the resource."""
+
+
+HttpMethod = Literal[
+ 'GET',
+ 'HEAD',
+ 'POST',
+ 'PUT',
+ 'DELETE',
+ 'CONNECT',
+ 'OPTIONS',
+ 'TRACE',
+ 'PATCH',
+]
+
+
+RunOrigin = Literal[
+ 'DEVELOPMENT',
+ 'WEB',
+ 'API',
+ 'SCHEDULER',
+ 'TEST',
+ 'WEBHOOK',
+ 'ACTOR',
+ 'CLI',
+ 'STANDBY',
+]
+
+
+SourceCodeFileFormat = Literal[
+ 'BASE64',
+ 'TEXT',
+]
+
+
+StorageOwnership = Literal[
+ 'ownedByMe',
+ 'sharedWithMe',
+]
+
+
+VersionSourceType = Literal[
+ 'SOURCE_FILES',
+ 'GIT_REPO',
+ 'TARBALL',
+ 'GITHUB_GIST',
+]
+
+
+WebhookDispatchStatus = Literal[
+ 'ACTIVE',
+ 'SUCCEEDED',
+ 'FAILED',
+]
+"""Status of the webhook dispatch indicating whether the HTTP request was successful."""
+
+
+WebhookEventType = Literal[
+ 'ACTOR.BUILD.ABORTED',
+ 'ACTOR.BUILD.CREATED',
+ 'ACTOR.BUILD.FAILED',
+ 'ACTOR.BUILD.SUCCEEDED',
+ 'ACTOR.BUILD.TIMED_OUT',
+ 'ACTOR.RUN.ABORTED',
+ 'ACTOR.RUN.CREATED',
+ 'ACTOR.RUN.FAILED',
+ 'ACTOR.RUN.RESURRECTED',
+ 'ACTOR.RUN.SUCCEEDED',
+ 'ACTOR.RUN.TIMED_OUT',
+ 'TEST',
+]
+"""Type of event that triggers the webhook."""
diff --git a/src/apify_client/_models.py b/src/apify_client/_models.py
index 6616881a..a8b4ee69 100644
--- a/src/apify_client/_models.py
+++ b/src/apify_client/_models.py
@@ -2,12 +2,23 @@
from __future__ import annotations
-from enum import StrEnum
from typing import Annotated, Any, Literal
from pydantic import AnyUrl, AwareDatetime, BaseModel, ConfigDict, EmailStr, Field, RootModel
from apify_client._docs import docs_group
+from apify_client._literals import (
+ ActorJobStatus,
+ ActorPermissionLevel,
+ ErrorType,
+ GeneralAccess,
+ HttpMethod,
+ RunOrigin,
+ SourceCodeFileFormat,
+ VersionSourceType,
+ WebhookDispatchStatus,
+ WebhookEventType,
+)
@docs_group('Models')
@@ -143,28 +154,6 @@ class ActorDefinition(BaseModel):
"""
-@docs_group('Models')
-class ActorJobStatus(StrEnum):
- """Status of an Actor job (run or build)."""
-
- READY = 'READY'
- RUNNING = 'RUNNING'
- SUCCEEDED = 'SUCCEEDED'
- FAILED = 'FAILED'
- TIMING_OUT = 'TIMING-OUT'
- TIMED_OUT = 'TIMED-OUT'
- ABORTING = 'ABORTING'
- ABORTED = 'ABORTED'
-
-
-@docs_group('Models')
-class ActorPermissionLevel(StrEnum):
- """Determines permissions that the Actor requires to run. For more information, see the [Actor permissions documentation](https://docs.apify.com/platform/actors/development/permissions)."""
-
- LIMITED_PERMISSIONS = 'LIMITED_PERMISSIONS'
- FULL_PERMISSIONS = 'FULL_PERMISSIONS'
-
-
@docs_group('Models')
class ActorResponse(BaseModel):
"""Response containing Actor data."""
@@ -1034,403 +1023,6 @@ class ErrorResponse(BaseModel):
error: ErrorDetail
-@docs_group('Models')
-class ErrorType(StrEnum):
- """Machine-processable error type identifier."""
-
- FIELD_3D_SECURE_AUTH_FAILED = '3d-secure-auth-failed'
- ACCESS_RIGHT_ALREADY_EXISTS = 'access-right-already-exists'
- ACTION_NOT_FOUND = 'action-not-found'
- ACTOR_ALREADY_RENTED = 'actor-already-rented'
- ACTOR_CAN_NOT_BE_RENTED = 'actor-can-not-be-rented'
- ACTOR_DISABLED = 'actor-disabled'
- ACTOR_IS_NOT_RENTED = 'actor-is-not-rented'
- ACTOR_MEMORY_LIMIT_EXCEEDED = 'actor-memory-limit-exceeded'
- ACTOR_NAME_EXISTS_NEW_OWNER = 'actor-name-exists-new-owner'
- ACTOR_NAME_NOT_UNIQUE = 'actor-name-not-unique'
- ACTOR_NOT_FOUND = 'actor-not-found'
- ACTOR_NOT_GITHUB_ACTOR = 'actor-not-github-actor'
- ACTOR_NOT_PUBLIC = 'actor-not-public'
- ACTOR_PERMISSION_LEVEL_NOT_SUPPORTED_FOR_AGENTIC_PAYMENTS = (
- 'actor-permission-level-not-supported-for-agentic-payments'
- )
- ACTOR_REVIEW_ALREADY_EXISTS = 'actor-review-already-exists'
- ACTOR_RUN_FAILED = 'actor-run-failed'
- ACTOR_STANDBY_NOT_SUPPORTED_FOR_AGENTIC_PAYMENTS = 'actor-standby-not-supported-for-agentic-payments'
- ACTOR_TASK_NAME_NOT_UNIQUE = 'actor-task-name-not-unique'
- AGENTIC_PAYMENT_INFO_RETRIEVAL_ERROR = 'agentic-payment-info-retrieval-error'
- AGENTIC_PAYMENT_INFORMATION_MISSING = 'agentic-payment-information-missing'
- AGENTIC_PAYMENT_INSUFFICIENT_AMOUNT = 'agentic-payment-insufficient-amount'
- AGENTIC_PAYMENT_PROVIDER_INTERNAL_ERROR = 'agentic-payment-provider-internal-error'
- AGENTIC_PAYMENT_PROVIDER_UNAUTHORIZED = 'agentic-payment-provider-unauthorized'
- AIRTABLE_WEBHOOK_DEPRECATED = 'airtable-webhook-deprecated'
- ALREADY_SUBSCRIBED_TO_PAID_ACTOR = 'already-subscribed-to-paid-actor'
- APIFY_PLAN_REQUIRED_TO_USE_PAID_ACTOR = 'apify-plan-required-to-use-paid-actor'
- APIFY_SIGNUP_NOT_ALLOWED = 'apify-signup-not-allowed'
- AUTH_METHOD_NOT_SUPPORTED = 'auth-method-not-supported'
- AUTHORIZATION_SERVER_NOT_FOUND = 'authorization-server-not-found'
- AUTO_ISSUE_DATE_INVALID = 'auto-issue-date-invalid'
- BACKGROUND_CHECK_REQUIRED = 'background-check-required'
- BILLING_SYSTEM_ERROR = 'billing-system-error'
- BLACK_FRIDAY_PLAN_EXPIRED = 'black-friday-plan-expired'
- BRAINTREE_ERROR = 'braintree-error'
- BRAINTREE_NOT_LINKED = 'braintree-not-linked'
- BRAINTREE_OPERATION_TIMED_OUT = 'braintree-operation-timed-out'
- BRAINTREE_UNSUPPORTED_CURRENCY = 'braintree-unsupported-currency'
- BUILD_NOT_FOUND = 'build-not-found'
- BUILD_OUTDATED = 'build-outdated'
- CANNOT_ADD_APIFY_EVENTS_TO_PPE_ACTOR = 'cannot-add-apify-events-to-ppe-actor'
- CANNOT_ADD_MULTIPLE_PRICING_INFOS = 'cannot-add-multiple-pricing-infos'
- CANNOT_ADD_PRICING_INFO_THAT_ALTERS_PAST = 'cannot-add-pricing-info-that-alters-past'
- CANNOT_ADD_SECOND_FUTURE_PRICING_INFO = 'cannot-add-second-future-pricing-info'
- CANNOT_BUILD_ACTOR_FROM_WEBHOOK = 'cannot-build-actor-from-webhook'
- CANNOT_CHANGE_BILLING_INTERVAL = 'cannot-change-billing-interval'
- CANNOT_CHANGE_OWNER = 'cannot-change-owner'
- CANNOT_CHARGE_APIFY_EVENT = 'cannot-charge-apify-event'
- CANNOT_CHARGE_NON_PAY_PER_EVENT_ACTOR = 'cannot-charge-non-pay-per-event-actor'
- CANNOT_COMMENT_AS_OTHER_USER = 'cannot-comment-as-other-user'
- CANNOT_COPY_ACTOR_TASK = 'cannot-copy-actor-task'
- CANNOT_CREATE_PAYOUT = 'cannot-create-payout'
- CANNOT_CREATE_PUBLIC_ACTOR = 'cannot-create-public-actor'
- CANNOT_CREATE_TAX_TRANSACTION = 'cannot-create-tax-transaction'
- CANNOT_DELETE_CRITICAL_ACTOR = 'cannot-delete-critical-actor'
- CANNOT_DELETE_INVOICE = 'cannot-delete-invoice'
- CANNOT_DELETE_PAID_ACTOR = 'cannot-delete-paid-actor'
- CANNOT_DISABLE_ONE_TIME_EVENT_FOR_APIFY_START_EVENT = 'cannot-disable-one-time-event-for-apify-start-event'
- CANNOT_DISABLE_ORGANIZATION_WITH_ENABLED_MEMBERS = 'cannot-disable-organization-with-enabled-members'
- CANNOT_DISABLE_USER_WITH_SUBSCRIPTION = 'cannot-disable-user-with-subscription'
- CANNOT_LINK_OAUTH_TO_UNVERIFIED_EMAIL = 'cannot-link-oauth-to-unverified-email'
- CANNOT_METAMORPH_TO_PAY_PER_RESULT_ACTOR = 'cannot-metamorph-to-pay-per-result-actor'
- CANNOT_MODIFY_ACTOR_PRICING_TOO_FREQUENTLY = 'cannot-modify-actor-pricing-too-frequently'
- CANNOT_MODIFY_ACTOR_PRICING_WITH_IMMEDIATE_EFFECT = 'cannot-modify-actor-pricing-with-immediate-effect'
- CANNOT_OVERRIDE_PAID_ACTOR_TRIAL = 'cannot-override-paid-actor-trial'
- CANNOT_PERMANENTLY_DELETE_SUBSCRIPTION = 'cannot-permanently-delete-subscription'
- CANNOT_PUBLISH_ACTOR = 'cannot-publish-actor'
- CANNOT_REDUCE_LAST_FULL_TOKEN = 'cannot-reduce-last-full-token'
- CANNOT_REIMBURSE_MORE_THAN_ORIGINAL_CHARGE = 'cannot-reimburse-more-than-original-charge'
- CANNOT_REIMBURSE_NON_RENTAL_CHARGE = 'cannot-reimburse-non-rental-charge'
- CANNOT_REMOVE_OWN_ACTOR_FROM_RECENTLY_USED = 'cannot-remove-own-actor-from-recently-used'
- CANNOT_REMOVE_PAYMENT_METHOD = 'cannot-remove-payment-method'
- CANNOT_REMOVE_PRICING_INFO = 'cannot-remove-pricing-info'
- CANNOT_REMOVE_RUNNING_RUN = 'cannot-remove-running-run'
- CANNOT_REMOVE_USER_WITH_PUBLIC_ACTORS = 'cannot-remove-user-with-public-actors'
- CANNOT_REMOVE_USER_WITH_SUBSCRIPTION = 'cannot-remove-user-with-subscription'
- CANNOT_REMOVE_USER_WITH_UNPAID_INVOICE = 'cannot-remove-user-with-unpaid-invoice'
- CANNOT_RENAME_ENV_VAR = 'cannot-rename-env-var'
- CANNOT_RENT_PAID_ACTOR = 'cannot-rent-paid-actor'
- CANNOT_REVIEW_OWN_ACTOR = 'cannot-review-own-actor'
- CANNOT_SET_ACCESS_RIGHTS_FOR_OWNER = 'cannot-set-access-rights-for-owner'
- CANNOT_SET_IS_STATUS_MESSAGE_TERMINAL = 'cannot-set-is-status-message-terminal'
- CANNOT_UNPUBLISH_CRITICAL_ACTOR = 'cannot-unpublish-critical-actor'
- CANNOT_UNPUBLISH_PAID_ACTOR = 'cannot-unpublish-paid-actor'
- CANNOT_UNPUBLISH_PROFILE = 'cannot-unpublish-profile'
- CANNOT_UPDATE_INVOICE_FIELD = 'cannot-update-invoice-field'
- CONCURRENT_RUNS_LIMIT_EXCEEDED = 'concurrent-runs-limit-exceeded'
- CONCURRENT_UPDATE_DETECTED = 'concurrent-update-detected'
- CONFERENCE_TOKEN_NOT_FOUND = 'conference-token-not-found'
- CONTENT_ENCODING_FORBIDDEN_FOR_HTML = 'content-encoding-forbidden-for-html'
- COUPON_ALREADY_REDEEMED = 'coupon-already-redeemed'
- COUPON_EXPIRED = 'coupon-expired'
- COUPON_FOR_NEW_CUSTOMERS = 'coupon-for-new-customers'
- COUPON_FOR_SUBSCRIBED_USERS = 'coupon-for-subscribed-users'
- COUPON_LIMITS_ARE_IN_CONFLICT_WITH_CURRENT_LIMITS = 'coupon-limits-are-in-conflict-with-current-limits'
- COUPON_MAX_NUMBER_OF_REDEMPTIONS_REACHED = 'coupon-max-number-of-redemptions-reached'
- COUPON_NOT_FOUND = 'coupon-not-found'
- COUPON_NOT_UNIQUE = 'coupon-not-unique'
- COUPONS_DISABLED = 'coupons-disabled'
- CREATE_GITHUB_ISSUE_NOT_ALLOWED = 'create-github-issue-not-allowed'
- CREATOR_PLAN_NOT_AVAILABLE = 'creator-plan-not-available'
- CRON_EXPRESSION_INVALID = 'cron-expression-invalid'
- DAILY_AI_TOKEN_LIMIT_EXCEEDED = 'daily-ai-token-limit-exceeded'
- DAILY_PUBLICATION_LIMIT_EXCEEDED = 'daily-publication-limit-exceeded'
- DATASET_DOES_NOT_HAVE_FIELDS_SCHEMA = 'dataset-does-not-have-fields-schema'
- DATASET_DOES_NOT_HAVE_SCHEMA = 'dataset-does-not-have-schema'
- DATASET_LOCKED = 'dataset-locked'
- DATASET_SCHEMA_INVALID = 'dataset-schema-invalid'
- DCR_NOT_SUPPORTED = 'dcr-not-supported'
- DEFAULT_DATASET_NOT_FOUND = 'default-dataset-not-found'
- DELETING_DEFAULT_BUILD = 'deleting-default-build'
- DELETING_UNFINISHED_BUILD = 'deleting-unfinished-build'
- EMAIL_ALREADY_TAKEN = 'email-already-taken'
- EMAIL_ALREADY_TAKEN_REMOVED_USER = 'email-already-taken-removed-user'
- EMAIL_DOMAIN_NOT_ALLOWED_FOR_COUPON = 'email-domain-not-allowed-for-coupon'
- EMAIL_INVALID = 'email-invalid'
- EMAIL_NOT_ALLOWED = 'email-not-allowed'
- EMAIL_NOT_VALID = 'email-not-valid'
- EMAIL_UPDATE_TOO_SOON = 'email-update-too-soon'
- ELEVATED_PERMISSIONS_NEEDED = 'elevated-permissions-needed'
- ENV_VAR_ALREADY_EXISTS = 'env-var-already-exists'
- EXCHANGE_RATE_FETCH_FAILED = 'exchange-rate-fetch-failed'
- EXPIRED_CONFERENCE_TOKEN = 'expired-conference-token'
- FAILED_TO_CHARGE_USER = 'failed-to-charge-user'
- FINAL_INVOICE_NEGATIVE = 'final-invoice-negative'
- GITHUB_BRANCH_EMPTY = 'github-branch-empty'
- GITHUB_ISSUE_ALREADY_EXISTS = 'github-issue-already-exists'
- GITHUB_PUBLIC_KEY_NOT_FOUND = 'github-public-key-not-found'
- GITHUB_REPOSITORY_NOT_FOUND = 'github-repository-not-found'
- GITHUB_SIGNATURE_DOES_NOT_MATCH_PAYLOAD = 'github-signature-does-not-match-payload'
- GITHUB_USER_NOT_AUTHORIZED_FOR_ISSUES = 'github-user-not-authorized-for-issues'
- GMAIL_NOT_ALLOWED = 'gmail-not-allowed'
- ID_DOES_NOT_MATCH = 'id-does-not-match'
- INCOMPATIBLE_BILLING_INTERVAL = 'incompatible-billing-interval'
- INCOMPLETE_PAYOUT_BILLING_INFO = 'incomplete-payout-billing-info'
- INCONSISTENT_CURRENCIES = 'inconsistent-currencies'
- INCORRECT_PRICING_MODIFIER_PREFIX = 'incorrect-pricing-modifier-prefix'
- INPUT_JSON_INVALID_CHARACTERS = 'input-json-invalid-characters'
- INPUT_JSON_NOT_OBJECT = 'input-json-not-object'
- INPUT_JSON_TOO_LONG = 'input-json-too-long'
- INPUT_UPDATE_COLLISION = 'input-update-collision'
- INSUFFICIENT_PERMISSIONS = 'insufficient-permissions'
- INSUFFICIENT_PERMISSIONS_TO_CHANGE_FIELD = 'insufficient-permissions-to-change-field'
- INSUFFICIENT_SECURITY_MEASURES = 'insufficient-security-measures'
- INSUFFICIENT_TAX_COUNTRY_EVIDENCE = 'insufficient-tax-country-evidence'
- INTEGRATION_AUTH_ERROR = 'integration-auth-error'
- INTERNAL_SERVER_ERROR = 'internal-server-error'
- INVALID_BILLING_INFO = 'invalid-billing-info'
- INVALID_BILLING_PERIOD_FOR_PAYOUT = 'invalid-billing-period-for-payout'
- INVALID_BUILD = 'invalid-build'
- INVALID_CLIENT_KEY = 'invalid-client-key'
- INVALID_COLLECTION = 'invalid-collection'
- INVALID_CONFERENCE_LOGIN_PASSWORD = 'invalid-conference-login-password'
- INVALID_CONTENT_TYPE_HEADER = 'invalid-content-type-header'
- INVALID_CREDENTIALS = 'invalid-credentials'
- INVALID_GIT_AUTH_TOKEN = 'invalid-git-auth-token'
- INVALID_GITHUB_ISSUE_URL = 'invalid-github-issue-url'
- INVALID_HEADER = 'invalid-header'
- INVALID_ID = 'invalid-id'
- INVALID_IDEMPOTENCY_KEY = 'invalid-idempotency-key'
- INVALID_INPUT = 'invalid-input'
- INVALID_INPUT_SCHEMA = 'invalid-input-schema'
- INVALID_INVOICE = 'invalid-invoice'
- INVALID_INVOICE_TYPE = 'invalid-invoice-type'
- INVALID_ISSUE_DATE = 'invalid-issue-date'
- INVALID_LABEL_PARAMS = 'invalid-label-params'
- INVALID_MAIN_ACCOUNT_USER_ID = 'invalid-main-account-user-id'
- INVALID_OAUTH_APP = 'invalid-oauth-app'
- INVALID_OAUTH_SCOPE = 'invalid-oauth-scope'
- INVALID_ONE_TIME_INVOICE = 'invalid-one-time-invoice'
- INVALID_PARAMETER = 'invalid-parameter'
- INVALID_PAYOUT_STATUS = 'invalid-payout-status'
- INVALID_PICTURE_URL = 'invalid-picture-url'
- INVALID_RECORD_KEY = 'invalid-record-key'
- INVALID_REQUEST = 'invalid-request'
- INVALID_RESOURCE_TYPE = 'invalid-resource-type'
- INVALID_SIGNATURE = 'invalid-signature'
- INVALID_SUBSCRIPTION_PLAN = 'invalid-subscription-plan'
- INVALID_TAX_NUMBER = 'invalid-tax-number'
- INVALID_TAX_NUMBER_FORMAT = 'invalid-tax-number-format'
- INVALID_TOKEN = 'invalid-token'
- INVALID_TOKEN_TYPE = 'invalid-token-type'
- INVALID_TWO_FACTOR_CODE = 'invalid-two-factor-code'
- INVALID_TWO_FACTOR_CODE_OR_RECOVERY_CODE = 'invalid-two-factor-code-or-recovery-code'
- INVALID_TWO_FACTOR_RECOVERY_CODE = 'invalid-two-factor-recovery-code'
- INVALID_USERNAME = 'invalid-username'
- INVALID_VALUE = 'invalid-value'
- INVITATION_INVALID_RESOURCE_TYPE = 'invitation-invalid-resource-type'
- INVITATION_NO_LONGER_VALID = 'invitation-no-longer-valid'
- INVOICE_CANCELED = 'invoice-canceled'
- INVOICE_CANNOT_BE_REFUNDED_DUE_TO_TOO_HIGH_AMOUNT = 'invoice-cannot-be-refunded-due-to-too-high-amount'
- INVOICE_INCOMPLETE = 'invoice-incomplete'
- INVOICE_IS_DRAFT = 'invoice-is-draft'
- INVOICE_LOCKED = 'invoice-locked'
- INVOICE_MUST_BE_BUFFER = 'invoice-must-be-buffer'
- INVOICE_NOT_CANCELED = 'invoice-not-canceled'
- INVOICE_NOT_DRAFT = 'invoice-not-draft'
- INVOICE_NOT_FOUND = 'invoice-not-found'
- INVOICE_OUTDATED = 'invoice-outdated'
- INVOICE_PAID_ALREADY = 'invoice-paid-already'
- ISSUE_ALREADY_CONNECTED_TO_GITHUB = 'issue-already-connected-to-github'
- ISSUE_NOT_FOUND = 'issue-not-found'
- ISSUES_BAD_REQUEST = 'issues-bad-request'
- ISSUER_NOT_REGISTERED = 'issuer-not-registered'
- JOB_FINISHED = 'job-finished'
- LABEL_ALREADY_LINKED = 'label-already-linked'
- LAST_API_TOKEN = 'last-api-token'
- LIMIT_REACHED = 'limit-reached'
- MAX_ITEMS_MUST_BE_GREATER_THAN_ZERO = 'max-items-must-be-greater-than-zero'
- MAX_METAMORPHS_EXCEEDED = 'max-metamorphs-exceeded'
- MAX_TOTAL_CHARGE_USD_BELOW_MINIMUM = 'max-total-charge-usd-below-minimum'
- MAX_TOTAL_CHARGE_USD_MUST_BE_GREATER_THAN_ZERO = 'max-total-charge-usd-must-be-greater-than-zero'
- METHOD_NOT_ALLOWED = 'method-not-allowed'
- MIGRATION_DISABLED = 'migration-disabled'
- MISSING_ACTOR_RIGHTS = 'missing-actor-rights'
- MISSING_API_TOKEN = 'missing-api-token'
- MISSING_BILLING_INFO = 'missing-billing-info'
- MISSING_LINE_ITEMS = 'missing-line-items'
- MISSING_PAYMENT_DATE = 'missing-payment-date'
- MISSING_PAYOUT_BILLING_INFO = 'missing-payout-billing-info'
- MISSING_PROXY_PASSWORD = 'missing-proxy-password'
- MISSING_REPORTING_FIELDS = 'missing-reporting-fields'
- MISSING_RESOURCE_NAME = 'missing-resource-name'
- MISSING_SETTINGS = 'missing-settings'
- MISSING_USERNAME = 'missing-username'
- MONTHLY_USAGE_LIMIT_TOO_LOW = 'monthly-usage-limit-too-low'
- MORE_THAN_ONE_UPDATE_NOT_ALLOWED = 'more-than-one-update-not-allowed'
- MULTIPLE_RECORDS_FOUND = 'multiple-records-found'
- MUST_BE_ADMIN = 'must-be-admin'
- NAME_NOT_UNIQUE = 'name-not-unique'
- NEXT_RUNTIME_COMPUTATION_FAILED = 'next-runtime-computation-failed'
- NO_COLUMNS_IN_EXPORTED_DATASET = 'no-columns-in-exported-dataset'
- NO_PAYMENT_ATTEMPT_FOR_REFUND_FOUND = 'no-payment-attempt-for-refund-found'
- NO_PAYMENT_METHOD_AVAILABLE = 'no-payment-method-available'
- NO_TEAM_ACCOUNT_SEATS_AVAILABLE = 'no-team-account-seats-available'
- NON_TEMPORARY_EMAIL = 'non-temporary-email'
- NOT_ENOUGH_USAGE_TO_RUN_PAID_ACTOR = 'not-enough-usage-to-run-paid-actor'
- NOT_IMPLEMENTED = 'not-implemented'
- NOT_SUPPORTED_CURRENCIES = 'not-supported-currencies'
- O_AUTH_SERVICE_ALREADY_CONNECTED = 'o-auth-service-already-connected'
- O_AUTH_SERVICE_NOT_CONNECTED = 'o-auth-service-not-connected'
- OAUTH_RESOURCE_ACCESS_FAILED = 'oauth-resource-access-failed'
- ONE_TIME_INVOICE_ALREADY_MARKED_PAID = 'one-time-invoice-already-marked-paid'
- ONLY_DRAFTS_CAN_BE_DELETED = 'only-drafts-can-be-deleted'
- OPERATION_CANCELED = 'operation-canceled'
- OPERATION_NOT_ALLOWED = 'operation-not-allowed'
- OPERATION_TIMED_OUT = 'operation-timed-out'
- ORGANIZATION_CANNOT_OWN_ITSELF = 'organization-cannot-own-itself'
- ORGANIZATION_ROLE_NOT_FOUND = 'organization-role-not-found'
- OVERLAPPING_PAYOUT_BILLING_PERIODS = 'overlapping-payout-billing-periods'
- OWN_TOKEN_REQUIRED = 'own-token-required'
- PAGE_NOT_FOUND = 'page-not-found'
- PARAM_NOT_ONE_OF = 'param-not-one-of'
- PARAMETER_REQUIRED = 'parameter-required'
- PARAMETERS_MISMATCHED = 'parameters-mismatched'
- PASSWORD_RESET_EMAIL_ALREADY_SENT = 'password-reset-email-already-sent'
- PASSWORD_RESET_TOKEN_EXPIRED = 'password-reset-token-expired'
- PAY_AS_YOU_GO_WITHOUT_MONTHLY_INTERVAL = 'pay-as-you-go-without-monthly-interval'
- PAYMENT_ATTEMPT_STATUS_MESSAGE_REQUIRED = 'payment-attempt-status-message-required'
- PAYOUT_ALREADY_PAID = 'payout-already-paid'
- PAYOUT_CANCELED = 'payout-canceled'
- PAYOUT_INVALID_STATE = 'payout-invalid-state'
- PAYOUT_MUST_BE_APPROVED_TO_BE_MARKED_PAID = 'payout-must-be-approved-to-be-marked-paid'
- PAYOUT_NOT_FOUND = 'payout-not-found'
- PAYOUT_NUMBER_ALREADY_EXISTS = 'payout-number-already-exists'
- PHONE_NUMBER_INVALID = 'phone-number-invalid'
- PHONE_NUMBER_LANDLINE = 'phone-number-landline'
- PHONE_NUMBER_OPTED_OUT = 'phone-number-opted-out'
- PHONE_VERIFICATION_DISABLED = 'phone-verification-disabled'
- PLATFORM_FEATURE_DISABLED = 'platform-feature-disabled'
- PRICE_OVERRIDES_VALIDATION_FAILED = 'price-overrides-validation-failed'
- PRICING_MODEL_NOT_SUPPORTED = 'pricing-model-not-supported'
- PROMOTIONAL_PLAN_NOT_AVAILABLE = 'promotional-plan-not-available'
- PROXY_AUTH_IP_NOT_UNIQUE = 'proxy-auth-ip-not-unique'
- PUBLIC_ACTOR_DISABLED = 'public-actor-disabled'
- QUERY_TIMEOUT = 'query-timeout'
- QUOTED_PRICE_OUTDATED = 'quoted-price-outdated'
- RATE_LIMIT_EXCEEDED = 'rate-limit-exceeded'
- RECAPTCHA_INVALID = 'recaptcha-invalid'
- RECAPTCHA_REQUIRED = 'recaptcha-required'
- RECORD_NOT_FOUND = 'record-not-found'
- RECORD_NOT_PUBLIC = 'record-not-public'
- RECORD_OR_TOKEN_NOT_FOUND = 'record-or-token-not-found'
- RECORD_TOO_LARGE = 'record-too-large'
- REDIRECT_URI_MISMATCH = 'redirect-uri-mismatch'
- REDUCED_PLAN_NOT_AVAILABLE = 'reduced-plan-not-available'
- RENTAL_CHARGE_ALREADY_REIMBURSED = 'rental-charge-already-reimbursed'
- RENTAL_NOT_ALLOWED = 'rental-not-allowed'
- REQUEST_ABORTED_PREMATURELY = 'request-aborted-prematurely'
- REQUEST_HANDLED_OR_LOCKED = 'request-handled-or-locked'
- REQUEST_ID_INVALID = 'request-id-invalid'
- REQUEST_QUEUE_DUPLICATE_REQUESTS = 'request-queue-duplicate-requests'
- REQUEST_TOO_LARGE = 'request-too-large'
- REQUESTED_DATASET_VIEW_DOES_NOT_EXIST = 'requested-dataset-view-does-not-exist'
- RESUME_TOKEN_EXPIRED = 'resume-token-expired'
- RUN_FAILED = 'run-failed'
- RUN_TIMEOUT_EXCEEDED = 'run-timeout-exceeded'
- RUSSIA_IS_EVIL = 'russia-is-evil'
- SAME_USER = 'same-user'
- SCHEDULE_ACTOR_NOT_FOUND = 'schedule-actor-not-found'
- SCHEDULE_ACTOR_TASK_NOT_FOUND = 'schedule-actor-task-not-found'
- SCHEDULE_NAME_NOT_UNIQUE = 'schedule-name-not-unique'
- SCHEMA_VALIDATION = 'schema-validation'
- SCHEMA_VALIDATION_ERROR = 'schema-validation-error'
- SCHEMA_VALIDATION_FAILED = 'schema-validation-failed'
- SIGN_UP_METHOD_NOT_ALLOWED = 'sign-up-method-not-allowed'
- SLACK_INTEGRATION_NOT_CUSTOM = 'slack-integration-not-custom'
- SOCKET_CLOSED = 'socket-closed'
- SOCKET_DESTROYED = 'socket-destroyed'
- STORE_SCHEMA_INVALID = 'store-schema-invalid'
- STORE_TERMS_NOT_ACCEPTED = 'store-terms-not-accepted'
- STRIPE_ENABLED = 'stripe-enabled'
- STRIPE_GENERIC_DECLINE = 'stripe-generic-decline'
- STRIPE_NOT_ENABLED = 'stripe-not-enabled'
- STRIPE_NOT_ENABLED_FOR_USER = 'stripe-not-enabled-for-user'
- TAGGED_BUILD_REQUIRED = 'tagged-build-required'
- TAX_COUNTRY_INVALID = 'tax-country-invalid'
- TAX_NUMBER_INVALID = 'tax-number-invalid'
- TAX_NUMBER_VALIDATION_FAILED = 'tax-number-validation-failed'
- TAXAMO_CALL_FAILED = 'taxamo-call-failed'
- TAXAMO_REQUEST_FAILED = 'taxamo-request-failed'
- TESTING_ERROR = 'testing-error'
- TOKEN_NOT_PROVIDED = 'token-not-provided'
- TOO_FEW_VERSIONS = 'too-few-versions'
- TOO_MANY_ACTOR_TASKS = 'too-many-actor-tasks'
- TOO_MANY_ACTORS = 'too-many-actors'
- TOO_MANY_LABELS_ON_RESOURCE = 'too-many-labels-on-resource'
- TOO_MANY_MCP_CONNECTORS = 'too-many-mcp-connectors'
- TOO_MANY_O_AUTH_APPS = 'too-many-o-auth-apps'
- TOO_MANY_ORGANIZATIONS = 'too-many-organizations'
- TOO_MANY_REQUESTS = 'too-many-requests'
- TOO_MANY_SCHEDULES = 'too-many-schedules'
- TOO_MANY_UI_ACCESS_KEYS = 'too-many-ui-access-keys'
- TOO_MANY_USER_LABELS = 'too-many-user-labels'
- TOO_MANY_VALUES = 'too-many-values'
- TOO_MANY_VERSIONS = 'too-many-versions'
- TOO_MANY_WEBHOOKS = 'too-many-webhooks'
- UNEXPECTED_ROUTE = 'unexpected-route'
- UNKNOWN_BUILD_TAG = 'unknown-build-tag'
- UNKNOWN_PAYMENT_PROVIDER = 'unknown-payment-provider'
- UNSUBSCRIBE_TOKEN_INVALID = 'unsubscribe-token-invalid'
- UNSUPPORTED_ACTOR_PRICING_MODEL_FOR_AGENTIC_PAYMENTS = 'unsupported-actor-pricing-model-for-agentic-payments'
- UNSUPPORTED_CONTENT_ENCODING = 'unsupported-content-encoding'
- UNSUPPORTED_FILE_TYPE_FOR_ISSUE = 'unsupported-file-type-for-issue'
- UNSUPPORTED_FILE_TYPE_IMAGE_EXPECTED = 'unsupported-file-type-image-expected'
- UNSUPPORTED_FILE_TYPE_TEXT_OR_JSON_EXPECTED = 'unsupported-file-type-text-or-json-expected'
- UNSUPPORTED_PERMISSION = 'unsupported-permission'
- UPCOMING_SUBSCRIPTION_BILL_NOT_UP_TO_DATE = 'upcoming-subscription-bill-not-up-to-date'
- USER_ALREADY_EXISTS = 'user-already-exists'
- USER_ALREADY_VERIFIED = 'user-already-verified'
- USER_CREATES_ORGANIZATIONS_TOO_FAST = 'user-creates-organizations-too-fast'
- USER_DISABLED = 'user-disabled'
- USER_EMAIL_IS_DISPOSABLE = 'user-email-is-disposable'
- USER_EMAIL_NOT_SET = 'user-email-not-set'
- USER_EMAIL_NOT_VERIFIED = 'user-email-not-verified'
- USER_HAS_NO_SUBSCRIPTION = 'user-has-no-subscription'
- USER_INTEGRATION_NOT_FOUND = 'user-integration-not-found'
- USER_IS_ALREADY_INVITED = 'user-is-already-invited'
- USER_IS_ALREADY_ORGANIZATION_MEMBER = 'user-is-already-organization-member'
- USER_IS_NOT_MEMBER_OF_ORGANIZATION = 'user-is-not-member-of-organization'
- USER_IS_NOT_ORGANIZATION = 'user-is-not-organization'
- USER_IS_ORGANIZATION = 'user-is-organization'
- USER_IS_ORGANIZATION_OWNER = 'user-is-organization-owner'
- USER_IS_REMOVED = 'user-is-removed'
- USER_NOT_FOUND = 'user-not-found'
- USER_NOT_LOGGED_IN = 'user-not-logged-in'
- USER_NOT_VERIFIED = 'user-not-verified'
- USER_OR_TOKEN_NOT_FOUND = 'user-or-token-not-found'
- USER_PLAN_NOT_ALLOWED_FOR_COUPON = 'user-plan-not-allowed-for-coupon'
- USER_PROBLEM_WITH_CARD = 'user-problem-with-card'
- USER_RECORD_NOT_FOUND = 'user-record-not-found'
- USERNAME_ALREADY_TAKEN = 'username-already-taken'
- USERNAME_MISSING = 'username-missing'
- USERNAME_NOT_ALLOWED = 'username-not-allowed'
- USERNAME_REMOVAL_FORBIDDEN = 'username-removal-forbidden'
- USERNAME_REQUIRED = 'username-required'
- VERIFICATION_EMAIL_ALREADY_SENT = 'verification-email-already-sent'
- VERIFICATION_TOKEN_EXPIRED = 'verification-token-expired'
- VERSION_ALREADY_EXISTS = 'version-already-exists'
- VERSIONS_SIZE_EXCEEDED = 'versions-size-exceeded'
- WEAK_PASSWORD = 'weak-password'
- X402_AGENTIC_PAYMENT_ALREADY_FINALIZED = 'x402-agentic-payment-already-finalized'
- X402_AGENTIC_PAYMENT_INSUFFICIENT_AMOUNT = 'x402-agentic-payment-insufficient-amount'
- X402_AGENTIC_PAYMENT_MALFORMED_TOKEN = 'x402-agentic-payment-malformed-token'
- X402_AGENTIC_PAYMENT_SETTLEMENT_FAILED = 'x402-agentic-payment-settlement-failed'
- X402_AGENTIC_PAYMENT_SETTLEMENT_IN_PROGRESS = 'x402-agentic-payment-settlement-in-progress'
- X402_AGENTIC_PAYMENT_SETTLEMENT_STUCK = 'x402-agentic-payment-settlement-stuck'
- X402_AGENTIC_PAYMENT_UNAUTHORIZED = 'x402-agentic-payment-unauthorized'
- X402_PAYMENT_REQUIRED = 'x402-payment-required'
- ZERO_INVOICE = 'zero-invoice'
-
-
@docs_group('Models')
class EventData(BaseModel):
model_config = ConfigDict(
@@ -1489,16 +1081,6 @@ class FreeActorPricingInfo(CommonActorPricingInfo):
pricing_model: Annotated[Literal['FREE'], Field(alias='pricingModel')]
-@docs_group('Models')
-class GeneralAccess(StrEnum):
- """Defines the general access level for the resource."""
-
- ANYONE_WITH_ID_CAN_READ = 'ANYONE_WITH_ID_CAN_READ'
- ANYONE_WITH_NAME_CAN_READ = 'ANYONE_WITH_NAME_CAN_READ'
- FOLLOW_USER_SETTING = 'FOLLOW_USER_SETTING'
- RESTRICTED = 'RESTRICTED'
-
-
@docs_group('Models')
class HeadAndLockResponse(BaseModel):
"""Response containing locked requests from the request queue head."""
@@ -1548,19 +1130,6 @@ class HeadResponse(BaseModel):
data: RequestQueueHead
-@docs_group('Models')
-class HttpMethod(StrEnum):
- GET = 'GET'
- HEAD = 'HEAD'
- POST = 'POST'
- PUT = 'PUT'
- DELETE = 'DELETE'
- CONNECT = 'CONNECT'
- OPTIONS = 'OPTIONS'
- TRACE = 'TRACE'
- PATCH = 'PATCH'
-
-
@docs_group('Models')
class InvalidItem(BaseModel):
model_config = ConfigDict(
@@ -2951,19 +2520,6 @@ class RunOptions(BaseModel):
max_total_charge_usd: Annotated[float | None, Field(alias='maxTotalChargeUsd', examples=[5], ge=0.0)] = None
-@docs_group('Models')
-class RunOrigin(StrEnum):
- DEVELOPMENT = 'DEVELOPMENT'
- WEB = 'WEB'
- API = 'API'
- SCHEDULER = 'SCHEDULER'
- TEST = 'TEST'
- WEBHOOK = 'WEBHOOK'
- ACTOR = 'ACTOR'
- CLI = 'CLI'
- STANDBY = 'STANDBY'
-
-
@docs_group('Models')
class RunResponse(BaseModel):
model_config = ConfigDict(
@@ -3277,12 +2833,6 @@ class SourceCodeFile(BaseModel):
name: Annotated[str, Field(examples=['src/main.js'])]
-@docs_group('Models')
-class SourceCodeFileFormat(StrEnum):
- BASE64 = 'BASE64'
- TEXT = 'TEXT'
-
-
@docs_group('Models')
class SourceCodeFolder(BaseModel):
"""Represents a folder in the Actor's source code structure. Distinguished from
@@ -3326,12 +2876,6 @@ class StorageIds(BaseModel):
"""
-@docs_group('Models')
-class StorageOwnership(StrEnum):
- OWNED_BY_ME = 'ownedByMe'
- SHARED_WITH_ME = 'sharedWithMe'
-
-
@docs_group('Models')
class Storages(BaseModel):
model_config = ConfigDict(
@@ -3825,14 +3369,6 @@ class VersionResponse(BaseModel):
data: Version
-@docs_group('Models')
-class VersionSourceType(StrEnum):
- SOURCE_FILES = 'SOURCE_FILES'
- GIT_REPO = 'GIT_REPO'
- TARBALL = 'TARBALL'
- GITHUB_GIST = 'GITHUB_GIST'
-
-
@docs_group('Models')
class Webhook(BaseModel):
model_config = ConfigDict(
@@ -3920,33 +3456,6 @@ class WebhookDispatchResponse(BaseModel):
data: WebhookDispatch
-@docs_group('Models')
-class WebhookDispatchStatus(StrEnum):
- """Status of the webhook dispatch indicating whether the HTTP request was successful."""
-
- ACTIVE = 'ACTIVE'
- SUCCEEDED = 'SUCCEEDED'
- FAILED = 'FAILED'
-
-
-@docs_group('Models')
-class WebhookEventType(StrEnum):
- """Type of event that triggers the webhook."""
-
- ACTOR_BUILD_ABORTED = 'ACTOR.BUILD.ABORTED'
- ACTOR_BUILD_CREATED = 'ACTOR.BUILD.CREATED'
- ACTOR_BUILD_FAILED = 'ACTOR.BUILD.FAILED'
- ACTOR_BUILD_SUCCEEDED = 'ACTOR.BUILD.SUCCEEDED'
- ACTOR_BUILD_TIMED_OUT = 'ACTOR.BUILD.TIMED_OUT'
- ACTOR_RUN_ABORTED = 'ACTOR.RUN.ABORTED'
- ACTOR_RUN_CREATED = 'ACTOR.RUN.CREATED'
- ACTOR_RUN_FAILED = 'ACTOR.RUN.FAILED'
- ACTOR_RUN_RESURRECTED = 'ACTOR.RUN.RESURRECTED'
- ACTOR_RUN_SUCCEEDED = 'ACTOR.RUN.SUCCEEDED'
- ACTOR_RUN_TIMED_OUT = 'ACTOR.RUN.TIMED_OUT'
- TEST = 'TEST'
-
-
@docs_group('Models')
class WebhookRepresentation(BaseModel):
"""Minimal representation of an ad-hoc webhook attached to a single Actor run or build via the
diff --git a/src/apify_client/_resource_clients/_resource_client.py b/src/apify_client/_resource_clients/_resource_client.py
index e83d3399..f2e34104 100644
--- a/src/apify_client/_resource_clients/_resource_client.py
+++ b/src/apify_client/_resource_clients/_resource_client.py
@@ -4,11 +4,12 @@
import time
from datetime import UTC, datetime, timedelta
from functools import cached_property
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, get_args
-from apify_client._consts import DEFAULT_WAIT_FOR_FINISH, DEFAULT_WAIT_WHEN_JOB_NOT_EXIST, TERMINAL_STATUSES
+from apify_client._consts import DEFAULT_WAIT_FOR_FINISH, DEFAULT_WAIT_WHEN_JOB_NOT_EXIST
from apify_client._docs import docs_group
from apify_client._logging import WithLogDetailsClient
+from apify_client._types import TerminalActorJobStatus
from apify_client._utils import (
catch_not_found_for_resource_or_throw,
catch_not_found_or_throw,
@@ -23,6 +24,8 @@
from apify_client._http_clients import HttpClient, HttpClientAsync
from apify_client._types import Timeout
+_TERMINAL_STATUSES: frozenset[TerminalActorJobStatus] = frozenset(get_args(TerminalActorJobStatus))
+
class ResourceClientBase(metaclass=WithLogDetailsClient):
"""Base class with shared implementation for sync and async resource clients.
@@ -331,7 +334,7 @@ def _wait_for_finish(
# Reset the not-found streak so a later transient 404 gets its own grace window.
not_found_deadline = None
- is_terminal = actor_job.get('status') in TERMINAL_STATUSES
+ is_terminal = actor_job.get('status') in _TERMINAL_STATUSES
is_timed_out = deadline is not None and datetime.now(UTC) >= deadline
if is_terminal or is_timed_out:
@@ -523,7 +526,7 @@ async def _wait_for_finish(
# Reset the not-found streak so a later transient 404 gets its own grace window.
not_found_deadline = None
- is_terminal = actor_job.get('status') in TERMINAL_STATUSES
+ is_terminal = actor_job.get('status') in _TERMINAL_STATUSES
is_timed_out = deadline is not None and datetime.now(UTC) >= deadline
if is_terminal or is_timed_out:
diff --git a/src/apify_client/_resource_clients/actor.py b/src/apify_client/_resource_clients/actor.py
index 2c7b298d..e0ba33fe 100644
--- a/src/apify_client/_resource_clients/actor.py
+++ b/src/apify_client/_resource_clients/actor.py
@@ -7,7 +7,6 @@
from apify_client._docs import docs_group
from apify_client._models import (
Actor,
- ActorPermissionLevel,
ActorResponse,
ActorStandby,
Build,
@@ -20,7 +19,6 @@
PayPerEventActorPricingInfo,
PricePerDatasetItemActorPricingInfo,
Run,
- RunOrigin,
RunResponse,
UpdateActorRequest,
)
@@ -37,7 +35,7 @@
from decimal import Decimal
from logging import Logger
- from apify_client._models import ActorJobStatus
+ from apify_client._literals import ActorJobStatus, ActorPermissionLevel, RunOrigin
from apify_client._resource_clients import (
ActorVersionClient,
ActorVersionClientAsync,
@@ -280,7 +278,7 @@ def start(
memory=memory_mbytes,
timeout=to_seconds(run_timeout, as_int=True),
waitForFinish=wait_for_finish,
- forcePermissionLevel=force_permission_level.value if force_permission_level is not None else None,
+ forcePermissionLevel=force_permission_level,
webhooks=encode_webhooks_to_base64(webhooks),
)
@@ -776,7 +774,7 @@ async def start(
memory=memory_mbytes,
timeout=to_seconds(run_timeout, as_int=True),
waitForFinish=wait_for_finish,
- forcePermissionLevel=force_permission_level.value if force_permission_level is not None else None,
+ forcePermissionLevel=force_permission_level,
webhooks=encode_webhooks_to_base64(webhooks),
)
diff --git a/src/apify_client/_resource_clients/actor_collection.py b/src/apify_client/_resource_clients/actor_collection.py
index 21f8b19b..f9e6ac16 100644
--- a/src/apify_client/_resource_clients/actor_collection.py
+++ b/src/apify_client/_resource_clients/actor_collection.py
@@ -21,11 +21,6 @@
from apify_client._types import Timeout
-_SORT_BY_TO_API: dict[str, str] = {
- 'created_at': 'createdAt',
- 'last_run_started_at': 'stats.lastRunStartedAt',
-}
-
@docs_group('Resource clients')
class ActorCollectionClient(ResourceClient):
@@ -53,7 +48,7 @@ def list(
limit: int | None = None,
offset: int | None = None,
desc: bool | None = None,
- sort_by: Literal['created_at', 'last_run_started_at'] | None = 'created_at',
+ sort_by: Literal['createdAt', 'stats.lastRunStartedAt'] | None = 'createdAt',
timeout: Timeout = 'medium',
) -> ListOfActors:
"""List the Actors the user has created or used.
@@ -71,8 +66,7 @@ def list(
Returns:
The list of available Actors matching the specified filters.
"""
- api_sort_by = _SORT_BY_TO_API[sort_by] if sort_by is not None else None
- result = self._list(timeout=timeout, my=my, limit=limit, offset=offset, desc=desc, sortBy=api_sort_by)
+ result = self._list(timeout=timeout, my=my, limit=limit, offset=offset, desc=desc, sortBy=sort_by)
return ListOfActorsResponse.model_validate(result).data
def create(
@@ -199,7 +193,7 @@ async def list(
limit: int | None = None,
offset: int | None = None,
desc: bool | None = None,
- sort_by: Literal['created_at', 'last_run_started_at'] | None = 'created_at',
+ sort_by: Literal['createdAt', 'stats.lastRunStartedAt'] | None = 'createdAt',
timeout: Timeout = 'medium',
) -> ListOfActors:
"""List the Actors the user has created or used.
@@ -217,8 +211,7 @@ async def list(
Returns:
The list of available Actors matching the specified filters.
"""
- api_sort_by = _SORT_BY_TO_API[sort_by] if sort_by is not None else None
- result = await self._list(timeout=timeout, my=my, limit=limit, offset=offset, desc=desc, sortBy=api_sort_by)
+ result = await self._list(timeout=timeout, my=my, limit=limit, offset=offset, desc=desc, sortBy=sort_by)
return ListOfActorsResponse.model_validate(result).data
async def create(
diff --git a/src/apify_client/_resource_clients/actor_version.py b/src/apify_client/_resource_clients/actor_version.py
index ba34454d..7a4a3acf 100644
--- a/src/apify_client/_resource_clients/actor_version.py
+++ b/src/apify_client/_resource_clients/actor_version.py
@@ -12,11 +12,11 @@
SourceCodeFolder,
Version,
VersionResponse,
- VersionSourceType,
)
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
if TYPE_CHECKING:
+ from apify_client._literals import VersionSourceType
from apify_client._resource_clients import (
ActorEnvVarClient,
ActorEnvVarClientAsync,
@@ -90,13 +90,13 @@ def update(
be set to the Actor build process.
source_type: What source type is the Actor version using.
source_files: Source code comprised of multiple files, each an item of the array. Required when
- `source_type` is `VersionSourceType.SOURCE_FILES`. See the API docs for the exact structure.
+ `source_type` is `'SOURCE_FILES'`. See the API docs for the exact structure.
git_repo_url: The URL of a Git repository from which the source code will be cloned.
- Required when `source_type` is `VersionSourceType.GIT_REPO`.
+ Required when `source_type` is `'GIT_REPO'`.
tarball_url: The URL of a tarball or a zip archive from which the source code will be downloaded.
- Required when `source_type` is `VersionSourceType.TARBALL`.
+ Required when `source_type` is `'TARBALL'`.
github_gist_url: The URL of a GitHub Gist from which the source will be downloaded.
- Required when `source_type` is `VersionSourceType.GITHUB_GIST`.
+ Required when `source_type` is `'GITHUB_GIST'`.
timeout: Timeout for the API HTTP request.
Returns:
@@ -206,13 +206,13 @@ async def update(
be set to the Actor build process.
source_type: What source type is the Actor version using.
source_files: Source code comprised of multiple files, each an item of the array. Required when
- `source_type` is `VersionSourceType.SOURCE_FILES`. See the API docs for the exact structure.
+ `source_type` is `'SOURCE_FILES'`. See the API docs for the exact structure.
git_repo_url: The URL of a Git repository from which the source code will be cloned.
- Required when `source_type` is `VersionSourceType.GIT_REPO`.
+ Required when `source_type` is `'GIT_REPO'`.
tarball_url: The URL of a tarball or a zip archive from which the source code will be downloaded.
- Required when `source_type` is `VersionSourceType.TARBALL`.
+ Required when `source_type` is `'TARBALL'`.
github_gist_url: The URL of a GitHub Gist from which the source will be downloaded.
- Required when `source_type` is `VersionSourceType.GITHUB_GIST`.
+ Required when `source_type` is `'GITHUB_GIST'`.
timeout: Timeout for the API HTTP request.
Returns:
diff --git a/src/apify_client/_resource_clients/actor_version_collection.py b/src/apify_client/_resource_clients/actor_version_collection.py
index a6239f26..b1aa21a6 100644
--- a/src/apify_client/_resource_clients/actor_version_collection.py
+++ b/src/apify_client/_resource_clients/actor_version_collection.py
@@ -14,11 +14,11 @@
SourceCodeFolder,
Version,
VersionResponse,
- VersionSourceType,
)
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
if TYPE_CHECKING:
+ from apify_client._literals import VersionSourceType
from apify_client._types import Timeout
@@ -85,13 +85,13 @@ def create(
be set to the Actor build process.
source_type: What source type is the Actor version using.
source_files: Source code comprised of multiple files, each an item of the array. Required
- when `source_type` is `VersionSourceType.SOURCE_FILES`. See the API docs for the exact structure.
+ when `source_type` is `'SOURCE_FILES'`. See the API docs for the exact structure.
git_repo_url: The URL of a Git repository from which the source code will be cloned.
- Required when `source_type` is `VersionSourceType.GIT_REPO`.
+ Required when `source_type` is `'GIT_REPO'`.
tarball_url: The URL of a tarball or a zip archive from which the source code will be downloaded.
- Required when `source_type` is `VersionSourceType.TARBALL`.
+ Required when `source_type` is `'TARBALL'`.
github_gist_url: The URL of a GitHub Gist from which the source will be downloaded.
- Required when `source_type` is `VersionSourceType.GITHUB_GIST`.
+ Required when `source_type` is `'GITHUB_GIST'`.
timeout: Timeout for the API HTTP request.
Returns:
@@ -172,13 +172,13 @@ async def create(
be set to the Actor build process.
source_type: What source type is the Actor version using.
source_files: Source code comprised of multiple files, each an item of the array. Required
- when `source_type` is `VersionSourceType.SOURCE_FILES`. See the API docs for the exact structure.
+ when `source_type` is `'SOURCE_FILES'`. See the API docs for the exact structure.
git_repo_url: The URL of a Git repository from which the source code will be cloned.
- Required when `source_type` is `VersionSourceType.GIT_REPO`.
+ Required when `source_type` is `'GIT_REPO'`.
tarball_url: The URL of a tarball or a zip archive from which the source code will be downloaded.
- Required when `source_type` is `VersionSourceType.TARBALL`.
+ Required when `source_type` is `'TARBALL'`.
github_gist_url: The URL of a GitHub Gist from which the source will be downloaded.
- Required when `source_type` is `VersionSourceType.GITHUB_GIST`.
+ Required when `source_type` is `'GITHUB_GIST'`.
timeout: Timeout for the API HTTP request.
Returns:
diff --git a/src/apify_client/_resource_clients/dataset.py b/src/apify_client/_resource_clients/dataset.py
index 3d4783e7..95855bde 100644
--- a/src/apify_client/_resource_clients/dataset.py
+++ b/src/apify_client/_resource_clients/dataset.py
@@ -20,7 +20,7 @@
from datetime import timedelta
from apify_client._http_clients import HttpResponse
- from apify_client._models import GeneralAccess
+ from apify_client._literals import GeneralAccess
from apify_client._types import JsonSerializable, Timeout
diff --git a/src/apify_client/_resource_clients/dataset_collection.py b/src/apify_client/_resource_clients/dataset_collection.py
index eb6d66ca..d004e7c8 100644
--- a/src/apify_client/_resource_clients/dataset_collection.py
+++ b/src/apify_client/_resource_clients/dataset_collection.py
@@ -8,11 +8,11 @@
DatasetResponse,
ListOfDatasets,
ListOfDatasetsResponse,
- StorageOwnership,
)
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
if TYPE_CHECKING:
+ from apify_client._literals import StorageOwnership
from apify_client._types import Timeout
@@ -54,15 +54,20 @@ def list(
limit: How many datasets to retrieve.
offset: What dataset to include as first when retrieving the list.
desc: Whether to sort the datasets in descending order based on their modification date.
- ownership: Filter by ownership. 'ownedByMe' returns only user's own datasets,
- 'sharedWithMe' returns only datasets shared with the user.
+ ownership: Filter by ownership. `'ownedByMe'` returns only user's own datasets,
+ `'sharedWithMe'` returns only datasets shared with the user.
timeout: Timeout for the API HTTP request.
Returns:
The list of available datasets matching the specified filters.
"""
result = self._list(
- timeout=timeout, unnamed=unnamed, limit=limit, offset=offset, desc=desc, ownership=ownership
+ timeout=timeout,
+ unnamed=unnamed,
+ limit=limit,
+ offset=offset,
+ desc=desc,
+ ownership=ownership,
)
return ListOfDatasetsResponse.model_validate(result).data
@@ -127,15 +132,20 @@ async def list(
limit: How many datasets to retrieve.
offset: What dataset to include as first when retrieving the list.
desc: Whether to sort the datasets in descending order based on their modification date.
- ownership: Filter by ownership. 'ownedByMe' returns only user's own datasets,
- 'sharedWithMe' returns only datasets shared with the user.
+ ownership: Filter by ownership. `'ownedByMe'` returns only user's own datasets,
+ `'sharedWithMe'` returns only datasets shared with the user.
timeout: Timeout for the API HTTP request.
Returns:
The list of available datasets matching the specified filters.
"""
result = await self._list(
- timeout=timeout, unnamed=unnamed, limit=limit, offset=offset, desc=desc, ownership=ownership
+ timeout=timeout,
+ unnamed=unnamed,
+ limit=limit,
+ offset=offset,
+ desc=desc,
+ ownership=ownership,
)
return ListOfDatasetsResponse.model_validate(result).data
diff --git a/src/apify_client/_resource_clients/key_value_store.py b/src/apify_client/_resource_clients/key_value_store.py
index 073e8305..3ec8b8f7 100644
--- a/src/apify_client/_resource_clients/key_value_store.py
+++ b/src/apify_client/_resource_clients/key_value_store.py
@@ -29,7 +29,7 @@
from datetime import timedelta
from apify_client._http_clients import HttpResponse
- from apify_client._models import GeneralAccess
+ from apify_client._literals import GeneralAccess
from apify_client._types import Timeout
diff --git a/src/apify_client/_resource_clients/key_value_store_collection.py b/src/apify_client/_resource_clients/key_value_store_collection.py
index 0b792360..6c7add4e 100644
--- a/src/apify_client/_resource_clients/key_value_store_collection.py
+++ b/src/apify_client/_resource_clients/key_value_store_collection.py
@@ -8,11 +8,11 @@
KeyValueStoreResponse,
ListOfKeyValueStores,
ListOfKeyValueStoresResponse,
- StorageOwnership,
)
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
if TYPE_CHECKING:
+ from apify_client._literals import StorageOwnership
from apify_client._types import Timeout
@@ -54,15 +54,20 @@ def list(
limit: How many key-value stores to retrieve.
offset: What key-value store to include as first when retrieving the list.
desc: Whether to sort the key-value stores in descending order based on their modification date.
- ownership: Filter by ownership. 'ownedByMe' returns only user's own key-value stores,
- 'sharedWithMe' returns only key-value stores shared with the user.
+ ownership: Filter by ownership. `'ownedByMe'` returns only user's own key-value stores,
+ `'sharedWithMe'` returns only key-value stores shared with the user.
timeout: Timeout for the API HTTP request.
Returns:
The list of available key-value stores matching the specified filters.
"""
result = self._list(
- timeout=timeout, unnamed=unnamed, limit=limit, offset=offset, desc=desc, ownership=ownership
+ timeout=timeout,
+ unnamed=unnamed,
+ limit=limit,
+ offset=offset,
+ desc=desc,
+ ownership=ownership,
)
return ListOfKeyValueStoresResponse.model_validate(result).data
@@ -127,15 +132,20 @@ async def list(
limit: How many key-value stores to retrieve.
offset: What key-value store to include as first when retrieving the list.
desc: Whether to sort the key-value stores in descending order based on their modification date.
- ownership: Filter by ownership. 'ownedByMe' returns only user's own key-value stores,
- 'sharedWithMe' returns only key-value stores shared with the user.
+ ownership: Filter by ownership. `'ownedByMe'` returns only user's own key-value stores,
+ `'sharedWithMe'` returns only key-value stores shared with the user.
timeout: Timeout for the API HTTP request.
Returns:
The list of available key-value stores matching the specified filters.
"""
result = await self._list(
- timeout=timeout, unnamed=unnamed, limit=limit, offset=offset, desc=desc, ownership=ownership
+ timeout=timeout,
+ unnamed=unnamed,
+ limit=limit,
+ offset=offset,
+ desc=desc,
+ ownership=ownership,
)
return ListOfKeyValueStoresResponse.model_validate(result).data
diff --git a/src/apify_client/_resource_clients/request_queue.py b/src/apify_client/_resource_clients/request_queue.py
index 281b1642..c866944d 100644
--- a/src/apify_client/_resource_clients/request_queue.py
+++ b/src/apify_client/_resource_clients/request_queue.py
@@ -42,7 +42,7 @@
if TYPE_CHECKING:
from datetime import timedelta
- from apify_client._models import GeneralAccess
+ from apify_client._literals import GeneralAccess
from apify_client._typeddicts import RequestDict, RequestDraftDeleteDict, RequestDraftDict
from apify_client._types import Timeout
diff --git a/src/apify_client/_resource_clients/request_queue_collection.py b/src/apify_client/_resource_clients/request_queue_collection.py
index c328303a..ffd3aa2b 100644
--- a/src/apify_client/_resource_clients/request_queue_collection.py
+++ b/src/apify_client/_resource_clients/request_queue_collection.py
@@ -8,11 +8,11 @@
ListOfRequestQueuesResponse,
RequestQueue,
RequestQueueResponse,
- StorageOwnership,
)
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
if TYPE_CHECKING:
+ from apify_client._literals import StorageOwnership
from apify_client._types import Timeout
@@ -54,15 +54,20 @@ def list(
limit: How many request queues to retrieve.
offset: What request queue to include as first when retrieving the list.
desc: Whether to sort the request queues in descending order based on their modification date.
- ownership: Filter by ownership. 'ownedByMe' returns only user's own request queues,
- 'sharedWithMe' returns only request queues shared with the user.
+ ownership: Filter by ownership. `'ownedByMe'` returns only user's own request queues,
+ `'sharedWithMe'` returns only request queues shared with the user.
timeout: Timeout for the API HTTP request.
Returns:
The list of available request queues matching the specified filters.
"""
result = self._list(
- timeout=timeout, unnamed=unnamed, limit=limit, offset=offset, desc=desc, ownership=ownership
+ timeout=timeout,
+ unnamed=unnamed,
+ limit=limit,
+ offset=offset,
+ desc=desc,
+ ownership=ownership,
)
return ListOfRequestQueuesResponse.model_validate(result).data
@@ -125,15 +130,20 @@ async def list(
limit: How many request queues to retrieve.
offset: What request queue to include as first when retrieving the list.
desc: Whether to sort the request queues in descending order based on their modification date.
- ownership: Filter by ownership. 'ownedByMe' returns only user's own request queues,
- 'sharedWithMe' returns only request queues shared with the user.
+ ownership: Filter by ownership. `'ownedByMe'` returns only user's own request queues,
+ `'sharedWithMe'` returns only request queues shared with the user.
timeout: Timeout for the API HTTP request.
Returns:
The list of available request queues matching the specified filters.
"""
result = await self._list(
- timeout=timeout, unnamed=unnamed, limit=limit, offset=offset, desc=desc, ownership=ownership
+ timeout=timeout,
+ unnamed=unnamed,
+ limit=limit,
+ offset=offset,
+ desc=desc,
+ ownership=ownership,
)
return ListOfRequestQueuesResponse.model_validate(result).data
diff --git a/src/apify_client/_resource_clients/run.py b/src/apify_client/_resource_clients/run.py
index 17a50fdc..ec0c0e8d 100644
--- a/src/apify_client/_resource_clients/run.py
+++ b/src/apify_client/_resource_clients/run.py
@@ -19,7 +19,7 @@
import logging
from decimal import Decimal
- from apify_client._models import GeneralAccess
+ from apify_client._literals import GeneralAccess
from apify_client._resource_clients import (
DatasetClient,
DatasetClientAsync,
diff --git a/src/apify_client/_resource_clients/run_collection.py b/src/apify_client/_resource_clients/run_collection.py
index 21a70e44..db2c2180 100644
--- a/src/apify_client/_resource_clients/run_collection.py
+++ b/src/apify_client/_resource_clients/run_collection.py
@@ -9,7 +9,7 @@
if TYPE_CHECKING:
from datetime import datetime
- from apify_client._models import ActorJobStatus
+ from apify_client._literals import ActorJobStatus
from apify_client._types import Timeout
diff --git a/src/apify_client/_resource_clients/task.py b/src/apify_client/_resource_clients/task.py
index 01593a8f..819bc464 100644
--- a/src/apify_client/_resource_clients/task.py
+++ b/src/apify_client/_resource_clients/task.py
@@ -6,7 +6,6 @@
from apify_client._models import (
ActorStandby,
Run,
- RunOrigin,
RunResponse,
Task,
TaskInput,
@@ -20,7 +19,7 @@
if TYPE_CHECKING:
from datetime import timedelta
- from apify_client._models import ActorJobStatus
+ from apify_client._literals import ActorJobStatus, RunOrigin
from apify_client._resource_clients import (
RunClient,
RunClientAsync,
diff --git a/src/apify_client/_resource_clients/webhook.py b/src/apify_client/_resource_clients/webhook.py
index 171364b6..10b6a14b 100644
--- a/src/apify_client/_resource_clients/webhook.py
+++ b/src/apify_client/_resource_clients/webhook.py
@@ -17,7 +17,7 @@
from apify_client._utils import response_to_dict
if TYPE_CHECKING:
- from apify_client._models import WebhookEventType
+ from apify_client._literals import WebhookEventType
from apify_client._resource_clients import WebhookDispatchCollectionClient, WebhookDispatchCollectionClientAsync
from apify_client._types import Timeout
diff --git a/src/apify_client/_resource_clients/webhook_collection.py b/src/apify_client/_resource_clients/webhook_collection.py
index d1c579f9..b8bd71bb 100644
--- a/src/apify_client/_resource_clients/webhook_collection.py
+++ b/src/apify_client/_resource_clients/webhook_collection.py
@@ -13,7 +13,8 @@
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
if TYPE_CHECKING:
- from apify_client._models import Webhook, WebhookEventType
+ from apify_client._literals import WebhookEventType
+ from apify_client._models import Webhook
from apify_client._types import Timeout
diff --git a/src/apify_client/_status_message_watcher.py b/src/apify_client/_status_message_watcher.py
index d1ccd8d2..61febdca 100644
--- a/src/apify_client/_status_message_watcher.py
+++ b/src/apify_client/_status_message_watcher.py
@@ -45,7 +45,7 @@ def _log_run_data(self, run_data: Run | None) -> bool:
`True` if more data is expected, `False` otherwise.
"""
if run_data is not None:
- status = run_data.status.value if run_data.status else 'Unknown status'
+ status = run_data.status or 'Unknown status'
status_message = run_data.status_message or ''
new_status_message = f'Status: {status}, Message: {status_message}'
diff --git a/src/apify_client/_types.py b/src/apify_client/_types.py
index d9588d7e..8d285dce 100644
--- a/src/apify_client/_types.py
+++ b/src/apify_client/_types.py
@@ -17,6 +17,9 @@
`condition`) are ignored at runtime.
"""
+TerminalActorJobStatus = Literal['SUCCEEDED', 'FAILED', 'TIMED-OUT', 'ABORTED']
+"""Subset of `ActorJobStatus` values that indicate the job has finished and will not change again."""
+
Timeout = timedelta | Literal['no_timeout', 'short', 'medium', 'long']
"""Type for the `timeout` parameter on resource client methods.
diff --git a/tests/integration/test_actor.py b/tests/integration/test_actor.py
index 480e4477..0ab3ede7 100644
--- a/tests/integration/test_actor.py
+++ b/tests/integration/test_actor.py
@@ -60,7 +60,7 @@ async def test_list_actors_pagination(client: ApifyClient | ApifyClientAsync) ->
async def test_list_actors_sorting(client: ApifyClient | ApifyClientAsync) -> None:
"""Test listing Actors with sorting."""
- result = await maybe_await(client.actors().list(limit=10, desc=True, sort_by='created_at'))
+ result = await maybe_await(client.actors().list(limit=10, desc=True, sort_by='stats.lastRunStartedAt'))
actors_page = cast('ListOfActors', result)
assert actors_page is not None
diff --git a/tests/integration/test_actor_version.py b/tests/integration/test_actor_version.py
index 9fb09023..e0da2754 100644
--- a/tests/integration/test_actor_version.py
+++ b/tests/integration/test_actor_version.py
@@ -10,7 +10,6 @@
from ._utils import get_random_resource_name, maybe_await
-from apify_client._models import VersionSourceType
async def test_actor_version_list(client: ApifyClient | ApifyClientAsync) -> None:
@@ -72,7 +71,7 @@ async def test_actor_version_create_and_get(client: ApifyClient | ApifyClientAsy
result = await maybe_await(
actor_client.versions().create(
version_number='1.0',
- source_type=VersionSourceType.SOURCE_FILES,
+ source_type='SOURCE_FILES',
build_tag='test',
source_files=[
{
@@ -88,7 +87,7 @@ async def test_actor_version_create_and_get(client: ApifyClient | ApifyClientAsy
assert created_version is not None
assert created_version.version_number == '1.0'
assert created_version.build_tag == 'test'
- assert created_version.source_type == VersionSourceType.SOURCE_FILES
+ assert created_version.source_type == 'SOURCE_FILES'
# Get the same version
version_client = actor_client.version('1.0')
diff --git a/tests/integration/test_build.py b/tests/integration/test_build.py
index bcf5cfe1..0f10b84c 100644
--- a/tests/integration/test_build.py
+++ b/tests/integration/test_build.py
@@ -76,7 +76,7 @@ async def test_build_log(client: ApifyClient | ApifyClientAsync) -> None:
# Find a completed build (SUCCEEDED status)
completed_build = None
for build in builds_page.items:
- if build.status and build.status.value == 'SUCCEEDED':
+ if build.status and build.status == 'SUCCEEDED':
completed_build = build
break
@@ -103,14 +103,14 @@ async def test_build_wait_for_finish(client: ApifyClient | ApifyClientAsync) ->
# Find a completed build (SUCCEEDED status)
completed_build = None
for build in builds_page.items:
- if build.status and build.status.value == 'SUCCEEDED':
+ if build.status and build.status == 'SUCCEEDED':
completed_build = build
break
if completed_build is None:
# If no succeeded build found, use any finished build
for build in builds_page.items:
- if build.status and build.status.value in ('SUCCEEDED', 'FAILED', 'ABORTED', 'TIMED_OUT'):
+ if build.status and build.status in ('SUCCEEDED', 'FAILED', 'ABORTED', 'TIMED-OUT'):
completed_build = build
break
@@ -183,13 +183,13 @@ async def test_build_delete_and_abort(client: ApifyClient | ApifyClientAsync) ->
result = await maybe_await(second_build_client.wait_for_finish())
finished_build = cast('Build | None', result)
assert finished_build is not None
- assert finished_build.status.value in ('SUCCEEDED', 'FAILED')
+ assert finished_build.status in ('SUCCEEDED', 'FAILED')
# Test abort on already finished build (should return the build in its current state)
result = await maybe_await(second_build_client.abort())
aborted_build = cast('Build', result)
assert aborted_build is not None
- assert aborted_build.status.value in ('SUCCEEDED', 'FAILED')
+ assert aborted_build.status in ('SUCCEEDED', 'FAILED')
# Delete the first build (not the default/latest)
await maybe_await(first_build_client.delete())
diff --git a/tests/integration/test_run.py b/tests/integration/test_run.py
index 6dd1afaf..20b3045e 100644
--- a/tests/integration/test_run.py
+++ b/tests/integration/test_run.py
@@ -12,7 +12,7 @@
from datetime import UTC, datetime, timedelta
from ._utils import maybe_await, maybe_sleep
-from apify_client._models import ActorJobStatus, Run
+from apify_client._models import Run
from apify_client.errors import ApifyApiError
HELLO_WORLD_ACTOR = 'apify/hello-world'
@@ -35,19 +35,17 @@ async def test_run_collection_list_multiple_statuses(client: ApifyClient | Apify
try:
run_collection = client.actor(HELLO_WORLD_ACTOR).runs()
- result = await maybe_await(run_collection.list(status=[ActorJobStatus.SUCCEEDED, ActorJobStatus.TIMED_OUT]))
+ result = await maybe_await(run_collection.list(status=['SUCCEEDED', 'TIMED-OUT']))
multiple_status_runs = cast('ListOfRuns', result)
- result = await maybe_await(run_collection.list(status=ActorJobStatus.SUCCEEDED))
+ result = await maybe_await(run_collection.list(status='SUCCEEDED'))
single_status_runs = cast('ListOfRuns', result)
assert multiple_status_runs is not None
assert single_status_runs is not None
- assert all(
- run.status in [ActorJobStatus.SUCCEEDED, ActorJobStatus.TIMED_OUT] for run in multiple_status_runs.items
- )
- assert all(run.status == ActorJobStatus.SUCCEEDED for run in single_status_runs.items)
+ assert all(run.status in ['SUCCEEDED', 'TIMED-OUT'] for run in multiple_status_runs.items)
+ assert all(run.status == 'SUCCEEDED' for run in single_status_runs.items)
finally:
for run in created_runs:
run_id = run.id
@@ -100,7 +98,7 @@ async def test_run_get_and_delete(client: ApifyClient | ApifyClientAsync) -> Non
retrieved_run = cast('Run', result)
assert retrieved_run is not None
assert retrieved_run.id == run.id
- assert retrieved_run.status.value == 'SUCCEEDED'
+ assert retrieved_run.status == 'SUCCEEDED'
# Delete the run
await maybe_await(run_client.delete())
@@ -197,13 +195,13 @@ async def test_run_abort(client: ApifyClient | ApifyClientAsync) -> None:
assert aborted_run is not None
# Status should be ABORTING or ABORTED (or SUCCEEDED if too fast)
- assert aborted_run.status.value in ['ABORTING', 'ABORTED', 'SUCCEEDED']
+ assert aborted_run.status in ['ABORTING', 'ABORTED', 'SUCCEEDED']
# Wait for abort to complete
result = await maybe_await(run_client.wait_for_finish())
final_run = cast('Run', result)
assert final_run is not None
- assert final_run.status.value in ['ABORTED', 'SUCCEEDED']
+ assert final_run.status in ['ABORTED', 'SUCCEEDED']
finally:
await maybe_await(run_client.wait_for_finish())
await maybe_await(run_client.delete())
@@ -242,7 +240,7 @@ async def test_run_resurrect(client: ApifyClient | ApifyClientAsync) -> None:
result = await maybe_await(actor.call())
run = cast('Run', result)
assert run is not None
- assert run.status.value == 'SUCCEEDED'
+ assert run.status == 'SUCCEEDED'
run_client = client.run(run.id)
@@ -252,13 +250,13 @@ async def test_run_resurrect(client: ApifyClient | ApifyClientAsync) -> None:
resurrected_run = cast('Run', result)
assert resurrected_run is not None
# Status should be READY, RUNNING or already finished (if fast)
- assert resurrected_run.status.value in ['READY', 'RUNNING', 'SUCCEEDED']
+ assert resurrected_run.status in ['READY', 'RUNNING', 'SUCCEEDED']
# Wait for it to finish before deleting
result = await maybe_await(run_client.wait_for_finish())
final_run = cast('Run', result)
assert final_run is not None
- assert final_run.status.value == 'SUCCEEDED'
+ assert final_run.status == 'SUCCEEDED'
finally:
# Wait for run to finish before cleanup (resurrected run might still be running)
@@ -367,7 +365,7 @@ async def test_run_reboot(client: ApifyClient | ApifyClientAsync, *, is_async: b
# Only try to reboot if the run is still running
# Note: There's a race condition - run may finish between check and reboot call
- if current_run and current_run.status.value == 'RUNNING':
+ if current_run and current_run.status == 'RUNNING':
try:
result = await maybe_await(run_client.reboot())
rebooted_run = cast('Run', result)
diff --git a/tests/integration/test_task.py b/tests/integration/test_task.py
index 055d4a01..d6dadb39 100644
--- a/tests/integration/test_task.py
+++ b/tests/integration/test_task.py
@@ -194,7 +194,7 @@ async def test_task_start(client: ApifyClient | ApifyClientAsync) -> None:
result = await maybe_await(client.run(run.id).wait_for_finish())
finished_run = cast('Run', result)
assert finished_run is not None
- assert finished_run.status.value == 'SUCCEEDED'
+ assert finished_run.status == 'SUCCEEDED'
# Cleanup run
await maybe_await(client.run(run.id).delete())
@@ -227,7 +227,7 @@ async def test_task_call(client: ApifyClient | ApifyClientAsync) -> None:
run = cast('Run', result)
assert run is not None
assert run.id is not None
- assert run.status.value == 'SUCCEEDED'
+ assert run.status == 'SUCCEEDED'
# Cleanup run
await maybe_await(client.run(run.id).delete())
diff --git a/tests/integration/test_webhook.py b/tests/integration/test_webhook.py
index b5fae924..31e99a30 100644
--- a/tests/integration/test_webhook.py
+++ b/tests/integration/test_webhook.py
@@ -10,14 +10,12 @@
from ._utils import maybe_await
from apify_client._models import (
- ActorJobStatus,
ListOfRuns,
ListOfWebhookDispatches,
ListOfWebhooks,
Run,
Webhook,
WebhookDispatch,
- WebhookEventType,
)
HELLO_WORLD_ACTOR = 'apify/hello-world'
@@ -30,7 +28,7 @@ async def _get_finished_run_id(client: ApifyClient | ApifyClientAsync) -> str:
since a completed run won't emit new events. If no completed runs exist, starts a new run and
waits for it to finish.
"""
- runs_page = await maybe_await(client.actor(HELLO_WORLD_ACTOR).runs().list(limit=1, status=ActorJobStatus.SUCCEEDED))
+ runs_page = await maybe_await(client.actor(HELLO_WORLD_ACTOR).runs().list(limit=1, status='SUCCEEDED'))
assert isinstance(runs_page, ListOfRuns)
@@ -68,7 +66,7 @@ async def test_webhook_create_and_get(client: ApifyClient | ApifyClientAsync) ->
# Create webhook bound to a finished run (will never fire)
created_webhook = await maybe_await(
client.webhooks().create(
- event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED],
+ event_types=['ACTOR.RUN.SUCCEEDED'],
request_url='https://httpbin.org/post',
actor_run_id=run_id,
is_ad_hoc=True,
@@ -95,7 +93,7 @@ async def test_webhook_update(client: ApifyClient | ApifyClientAsync) -> None:
# Create webhook bound to a finished run
created_webhook = await maybe_await(
client.webhooks().create(
- event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED],
+ event_types=['ACTOR.RUN.SUCCEEDED'],
request_url='https://httpbin.org/post',
actor_run_id=run_id,
is_ad_hoc=True,
@@ -125,7 +123,7 @@ async def test_webhook_test(client: ApifyClient | ApifyClientAsync) -> None:
# Create webhook bound to a finished run
created_webhook = await maybe_await(
client.webhooks().create(
- event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED],
+ event_types=['ACTOR.RUN.SUCCEEDED'],
request_url='https://httpbin.org/post',
actor_run_id=run_id,
is_ad_hoc=True,
@@ -150,7 +148,7 @@ async def test_webhook_dispatches(client: ApifyClient | ApifyClientAsync) -> Non
# Create webhook bound to a finished run
created_webhook = await maybe_await(
client.webhooks().create(
- event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED],
+ event_types=['ACTOR.RUN.SUCCEEDED'],
request_url='https://httpbin.org/post',
actor_run_id=run_id,
is_ad_hoc=True,
@@ -180,7 +178,7 @@ async def test_webhook_delete(client: ApifyClient | ApifyClientAsync) -> None:
# Create webhook bound to a finished run
created_webhook = await maybe_await(
client.webhooks().create(
- event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED],
+ event_types=['ACTOR.RUN.SUCCEEDED'],
request_url='https://httpbin.org/post',
actor_run_id=run_id,
is_ad_hoc=True,
diff --git a/tests/unit/test_actor_start_params.py b/tests/unit/test_actor_start_params.py
index 87d65fb2..6aa447cc 100644
--- a/tests/unit/test_actor_start_params.py
+++ b/tests/unit/test_actor_start_params.py
@@ -8,7 +8,6 @@
from werkzeug import Request, Response
from apify_client import ApifyClient, ApifyClientAsync
-from apify_client._models import ActorJobStatus
if TYPE_CHECKING:
from pytest_httpserver import HTTPServer
@@ -26,7 +25,7 @@ def _create_minimal_run_response() -> dict:
'userId': 'test_user_id',
'startedAt': '2019-11-30T07:34:24.202Z',
'finishedAt': '2019-12-12T09:30:12.202Z',
- 'status': ActorJobStatus.RUNNING.value,
+ 'status': 'RUNNING',
'statusMessage': 'Running',
'isStatusMessageTerminal': False,
'meta': {'origin': 'WEB'},
diff --git a/tests/unit/test_logging.py b/tests/unit/test_logging.py
index cf537795..62139e67 100644
--- a/tests/unit/test_logging.py
+++ b/tests/unit/test_logging.py
@@ -13,7 +13,6 @@
from apify_client import ApifyClient, ApifyClientAsync
from apify_client._logging import RedirectLogFormatter
-from apify_client._models import ActorJobStatus
from apify_client._status_message_watcher import StatusMessageWatcherBase
from apify_client._streamed_log import StreamedLogBase
@@ -23,6 +22,8 @@
from _pytest.logging import LogCaptureFixture
from pytest_httpserver import HTTPServer
+ from apify_client._literals import ActorJobStatus
+
_MOCKED_RUN_ID = 'mocked_run_id'
_MOCKED_ACTOR_NAME = 'mocked_actor_name'
_MOCKED_ACTOR_ID = 'mocked_actor_id'
@@ -81,10 +82,10 @@ def __init__(self) -> None:
self.requests_for_current_status = 0
self.min_requests_per_status = 5
- self.statuses = [
- ('Initial message', ActorJobStatus.RUNNING, False),
- ('Another message', ActorJobStatus.RUNNING, False),
- ('Final message', ActorJobStatus.SUCCEEDED, True),
+ self.statuses: list[tuple[str, ActorJobStatus, bool]] = [
+ ('Initial message', 'RUNNING', False),
+ ('Another message', 'RUNNING', False),
+ ('Final message', 'SUCCEEDED', True),
]
def _create_minimal_run_data(self, message: str, status: ActorJobStatus, *, is_terminal: bool) -> dict:
@@ -95,7 +96,7 @@ def _create_minimal_run_data(self, message: str, status: ActorJobStatus, *, is_t
'userId': 'test_user_id',
'startedAt': '2019-11-30T07:34:24.202Z',
'finishedAt': '2019-12-12T09:30:12.202Z',
- 'status': status.value,
+ 'status': status,
'statusMessage': message,
'isStatusMessageTerminal': is_terminal,
'meta': {'origin': 'WEB'},
@@ -203,9 +204,7 @@ def mock_api(httpserver: HTTPServer) -> None:
# Add actor run creation endpoint
httpserver.expect_request(f'/v2/acts/{_MOCKED_ACTOR_ID}/runs', method='POST').respond_with_json(
{
- 'data': status_generator._create_minimal_run_data(
- 'Initial message', ActorJobStatus.RUNNING, is_terminal=False
- ),
+ 'data': status_generator._create_minimal_run_data('Initial message', 'RUNNING', is_terminal=False),
}
)
diff --git a/tests/unit/test_postprocess_generated_models.py b/tests/unit/test_postprocess_generated_models.py
index 5ce35fb1..0c9b7e08 100644
--- a/tests/unit/test_postprocess_generated_models.py
+++ b/tests/unit/test_postprocess_generated_models.py
@@ -4,20 +4,24 @@
from scripts.postprocess_generated_models import (
add_docs_group_decorators,
+ convert_enums_to_literals,
deduplicate_error_type_enum,
fix_discriminators,
+ split_literals_to_file,
)
# -- fix_discriminators -------------------------------------------------------
def test_fix_discriminators_replaces_camel_case() -> None:
+ """A camelCase discriminator value is rewritten to its snake_case form."""
content = "items: list[Pricing] = Field(discriminator='pricingModel')"
result = fix_discriminators(content)
assert result == "items: list[Pricing] = Field(discriminator='pricing_model')"
def test_fix_discriminators_replaces_multiple_occurrences() -> None:
+ """Every occurrence of a known camelCase discriminator is rewritten, not just the first."""
content = "a: list[X] = Field(discriminator='pricingModel')\nb: list[Y] = Field(discriminator='pricingModel')\n"
result = fix_discriminators(content)
assert "discriminator='pricingModel'" not in result
@@ -25,18 +29,21 @@ def test_fix_discriminators_replaces_multiple_occurrences() -> None:
def test_fix_discriminators_leaves_already_snake_case() -> None:
+ """A discriminator already in snake_case is left untouched."""
content = "items: list[Pricing] = Field(discriminator='pricing_model')"
result = fix_discriminators(content)
assert result == content
def test_fix_discriminators_no_change_when_no_discriminators() -> None:
+ """Source with no discriminators at all passes through unchanged."""
content = 'class Foo(BaseModel):\n name: str\n'
result = fix_discriminators(content)
assert result == content
def test_fix_discriminators_does_not_touch_unrelated() -> None:
+ """A discriminator value that is not in the known map is not rewritten."""
content = "items: list[X] = Field(discriminator='event_type')"
result = fix_discriminators(content)
assert result == content
@@ -46,6 +53,7 @@ def test_fix_discriminators_does_not_touch_unrelated() -> None:
def test_deduplicate_error_type_enum_removes_duplicate() -> None:
+ """The duplicate `Type(StrEnum)` is dropped while the original `ErrorType` is preserved."""
content = textwrap.dedent("""\
class ErrorType(StrEnum):
SOME_ERROR = 'some-error'
@@ -63,6 +71,7 @@ class ErrorResponse(BaseModel):
def test_deduplicate_error_type_enum_rewires_colon_annotation() -> None:
+ """A `: Type` annotation is rewired to `: ErrorType` after the duplicate is dropped."""
content = textwrap.dedent("""\
class ErrorType(StrEnum):
SOME_ERROR = 'some-error'
@@ -78,6 +87,7 @@ class ErrorResponse(BaseModel):
def test_deduplicate_error_type_enum_rewires_union_annotation() -> None:
+ """A `| Type` arm in a union annotation is rewired to `| ErrorType`."""
content = textwrap.dedent("""\
class ErrorType(StrEnum):
X = 'x'
@@ -94,6 +104,7 @@ class Foo(BaseModel):
def test_deduplicate_error_type_enum_rewires_bracket_annotation() -> None:
+ """A `[Type]` subscript inside a generic annotation is rewired to `[ErrorType]`."""
content = textwrap.dedent("""\
class ErrorType(StrEnum):
X = 'x'
@@ -110,12 +121,14 @@ class Foo(BaseModel):
def test_deduplicate_error_type_enum_collapses_extra_blank_lines() -> None:
+ """Removing the duplicate enum collapses any resulting run of 4+ blank lines."""
content = "\nclass Type(StrEnum):\n X = 'x'\n\n\n\n\nclass Next(BaseModel):\n pass\n"
result = deduplicate_error_type_enum(content)
assert '\n\n\n\n' not in result
def test_deduplicate_error_type_enum_no_change_when_no_duplicate() -> None:
+ """Source without a `Type(StrEnum)` duplicate passes through unchanged."""
content = textwrap.dedent("""\
class ErrorType(StrEnum):
SOME_ERROR = 'some-error'
@@ -128,7 +141,7 @@ class Foo(BaseModel):
def test_deduplicate_error_type_enum_does_not_touch_type_in_class_names() -> None:
- """Ensure `Type` in class names like `ContentType` is not replaced."""
+ """`Type` inside other class names (e.g. `ContentType`) is not rewritten."""
content = textwrap.dedent("""\
class ErrorType(StrEnum):
X = 'x'
@@ -143,10 +156,41 @@ class ContentType(BaseModel):
assert 'class ContentType(BaseModel)' in result
+def test_deduplicate_error_type_enum_handles_type_as_last_class() -> None:
+ """The `Type` enum is removed even when it's the last top-level definition."""
+ content = textwrap.dedent("""\
+ class ErrorType(StrEnum):
+ X = 'x'
+
+ class Foo(BaseModel):
+ field: Type
+
+ class Type(StrEnum):
+ X = 'x'
+ """)
+ result = deduplicate_error_type_enum(content)
+ assert 'class Type(StrEnum)' not in result
+ assert 'field: ErrorType' in result
+
+
+def test_deduplicate_error_type_enum_skips_non_strenum_type() -> None:
+ """A `Type` class that is not a `StrEnum` (e.g. a Pydantic model) is left in place."""
+ content = textwrap.dedent("""\
+ class ErrorType(StrEnum):
+ X = 'x'
+
+ class Type(BaseModel):
+ value: str
+ """)
+ result = deduplicate_error_type_enum(content)
+ assert 'class Type(BaseModel)' in result
+
+
# -- add_docs_group_decorators ------------------------------------------------
def test_add_docs_group_decorators_adds_import_and_decorators() -> None:
+ """Both the `docs_group` import and a decorator on every class are inserted."""
content = textwrap.dedent("""\
from pydantic import BaseModel
@@ -162,6 +206,7 @@ class Bar(BaseModel):
def test_add_docs_group_decorators_places_import_after_pydantic() -> None:
+ """The injected `docs_group` import lands directly after the existing `from pydantic` line."""
content = textwrap.dedent("""\
from pydantic import BaseModel
@@ -176,6 +221,7 @@ class Foo(BaseModel):
def test_add_docs_group_decorators_idempotent_import() -> None:
+ """Re-running the step doesn't duplicate the `docs_group` import."""
content = textwrap.dedent("""\
from pydantic import BaseModel
@@ -189,6 +235,7 @@ class Foo(BaseModel):
def test_add_docs_group_decorators_idempotent_decorators() -> None:
+ """Re-running the step doesn't add a second decorator above an already-decorated class."""
content = textwrap.dedent("""\
from pydantic import BaseModel
@@ -207,6 +254,7 @@ class Bar(BaseModel):
def test_add_docs_group_decorators_placed_before_each_class() -> None:
+ """Every `class` line is immediately preceded by the decorator line."""
content = textwrap.dedent("""\
from pydantic import BaseModel
@@ -224,16 +272,156 @@ class Beta(BaseModel):
def test_add_docs_group_decorators_no_classes() -> None:
+ """Source with no classes gets no decorators inserted."""
content = 'from pydantic import BaseModel\n\nx = 1\n'
result = add_docs_group_decorators(content, 'Models')
assert "@docs_group('Models')" not in result
+# -- convert_enums_to_literals ------------------------------------------------
+
+
+def test_convert_enums_to_literals_replaces_single_enum() -> None:
+ """A `StrEnum` class becomes a `Name = Literal[...]` alias carrying the same string values."""
+ content = textwrap.dedent("""\
+ from enum import StrEnum
+ from typing import Literal
+
+ class Status(StrEnum):
+ READY = 'READY'
+ RUNNING = 'RUNNING'
+ """)
+ result = convert_enums_to_literals(content)
+ assert 'class Status(StrEnum)' not in result
+ assert 'Status = Literal[' in result
+ assert "'READY'," in result
+ assert "'RUNNING'," in result
+
+
+def test_convert_enums_to_literals_preserves_value_order() -> None:
+ """Literal values appear in the alias in the same order as the enum's member declarations."""
+ content = textwrap.dedent("""\
+ from typing import Literal
+
+ class Status(StrEnum):
+ SECOND = 'second'
+ FIRST = 'first'
+ THIRD = 'third'
+ """)
+ result = convert_enums_to_literals(content)
+ second_idx = result.index("'second'")
+ first_idx = result.index("'first'")
+ third_idx = result.index("'third'")
+ assert second_idx < first_idx < third_idx
+
+
+def test_convert_enums_to_literals_preserves_docstring() -> None:
+ """The class docstring is kept as a trailing bare-string after the alias."""
+ content = textwrap.dedent("""\
+ from typing import Literal
+
+ class Status(StrEnum):
+ \"\"\"Describes a status.\"\"\"
+
+ READY = 'READY'
+ """)
+ result = convert_enums_to_literals(content)
+ assert '"""Describes a status."""' in result
+ # Docstring must appear AFTER the type alias, not before.
+ alias_idx = result.index('Status = Literal[')
+ docstring_idx = result.index('"""Describes a status."""')
+ assert alias_idx < docstring_idx
+
+
+def test_convert_enums_to_literals_preserves_hyphenated_values() -> None:
+ """The string values are taken verbatim — hyphenated values like `'TIMED-OUT'` survive intact."""
+ content = textwrap.dedent("""\
+ from typing import Literal
+
+ class Status(StrEnum):
+ TIMED_OUT = 'TIMED-OUT'
+ TIMING_OUT = 'TIMING-OUT'
+ """)
+ result = convert_enums_to_literals(content)
+ assert "'TIMED-OUT'," in result
+ assert "'TIMING-OUT'," in result
+ # The enum-member name (TIMED_OUT) should not appear in the output.
+ assert 'TIMED_OUT' not in result
+
+
+def test_convert_enums_to_literals_handles_multiple_enums() -> None:
+ """Multiple `StrEnum` classes in the same file are each converted independently."""
+ content = textwrap.dedent("""\
+ from typing import Literal
+
+ class Alpha(StrEnum):
+ A = 'a'
+
+ class Beta(StrEnum):
+ B = 'b'
+ """)
+ result = convert_enums_to_literals(content)
+ assert 'Alpha = Literal[' in result
+ assert 'Beta = Literal[' in result
+ assert 'class Alpha(StrEnum)' not in result
+ assert 'class Beta(StrEnum)' not in result
+
+
+def test_convert_enums_to_literals_skips_non_strenum_classes() -> None:
+ """Classes whose bases don't include `StrEnum` (e.g. Pydantic models) are not touched."""
+ content = textwrap.dedent("""\
+ from typing import Literal
+
+ class Foo(BaseModel):
+ name: str
+
+ class Status(StrEnum):
+ A = 'a'
+ """)
+ result = convert_enums_to_literals(content)
+ assert 'class Foo(BaseModel)' in result
+ assert 'class Status(StrEnum)' not in result
+
+
+def test_convert_enums_to_literals_no_change_when_no_enums() -> None:
+ """Source without any `StrEnum` class is returned byte-for-byte unchanged."""
+ content = textwrap.dedent("""\
+ from typing import Literal
+
+ class Foo(BaseModel):
+ name: str
+ """)
+ result = convert_enums_to_literals(content)
+ assert result == content
+
+
+def test_convert_enums_to_literals_field_references_still_resolve() -> None:
+ """Field annotations referencing the enum name still resolve since the alias keeps that name."""
+ content = textwrap.dedent("""\
+ from typing import Literal
+
+ class Foo(BaseModel):
+ status: Status
+
+ class Status(StrEnum):
+ A = 'a'
+ B = 'b'
+ """)
+ result = convert_enums_to_literals(content)
+ # Field still references the name; the name is now a type alias below.
+ assert 'status: Status' in result
+ assert 'Status = Literal[' in result
+
+
# -- Integration: full pipeline -----------------------------------------------
def test_full_pipeline() -> None:
+ """All steps composed: discriminator fix, `Type` dedup, enum-to-literal, docs decorators."""
content = textwrap.dedent("""\
+ from enum import StrEnum
+ from typing import Literal
+
from pydantic import BaseModel
class Zebra(BaseModel):
@@ -253,15 +441,117 @@ class Alpha(BaseModel):
""")
result = fix_discriminators(content)
result = deduplicate_error_type_enum(result)
+ result = convert_enums_to_literals(result)
result = add_docs_group_decorators(result, 'Models')
# Discriminator fixed.
assert "discriminator='pricing_model'" in result
assert "discriminator='pricingModel'" not in result
- # Duplicate Type enum removed and references rewired.
+ # Duplicate Type enum removed and references rewired, then the remaining enum converted.
assert 'class Type(StrEnum)' not in result
+ assert 'class ErrorType(StrEnum)' not in result
+ assert 'ErrorType = Literal[' in result
assert 'error_type: ErrorType' in result
- # Decorators added.
- assert "@docs_group('Models')" in result
+ # Decorators added to real models but not to the type alias.
+ assert result.count("@docs_group('Models')") == 3 # Zebra, ErrorResponse, Alpha
+
+
+# -- split_literals_to_file ---------------------------------------------------
+
+
+def test_split_literals_to_file_moves_literal_aliases() -> None:
+ """The `Literal[...]` block plus its docstring move to the literals file; the models file imports it back."""
+ content = textwrap.dedent("""\
+ from __future__ import annotations
+
+ from typing import Literal
+
+ from pydantic import BaseModel
+
+ from apify_client._docs import docs_group
+
+
+ class Alpha(BaseModel):
+ status: Status
+
+
+ Status = Literal[
+ 'READY',
+ 'RUNNING',
+ ]
+ \"\"\"Alpha status docstring.\"\"\"
+ """)
+ models, literals = split_literals_to_file(content)
+
+ assert 'Status = Literal[' not in models
+ assert 'from apify_client._literals import Status' in models
+ assert 'status: Status' in models # field annotation still references the name
+
+ assert 'Status = Literal[' in literals
+ assert "'READY'" in literals
+ assert '"""Alpha status docstring."""' in literals
+
+
+def test_split_literals_to_file_handles_multiple_aliases() -> None:
+ """Several aliases all move out and are re-imported together in a single import line."""
+ content = textwrap.dedent("""\
+ from __future__ import annotations
+
+ from typing import Literal
+
+ from apify_client._docs import docs_group
+
+
+ A = Literal[
+ 'x',
+ ]
+
+ B = Literal[
+ 'y',
+ ]
+ """)
+ models, literals = split_literals_to_file(content)
+
+ assert 'A = Literal[' not in models
+ assert 'B = Literal[' not in models
+ assert 'from apify_client._literals import A, B' in models
+
+ assert "A = Literal[\n 'x',\n]" in literals
+ assert "B = Literal[\n 'y',\n]" in literals
+
+
+def test_split_literals_to_file_no_literals_returns_original() -> None:
+ """Source with no `Literal[...]` aliases yields the original models content and an empty literals string."""
+ content = textwrap.dedent("""\
+ from __future__ import annotations
+
+ from pydantic import BaseModel
+
+ from apify_client._docs import docs_group
+
+
+ class Alpha(BaseModel):
+ name: str
+ """)
+ models, literals = split_literals_to_file(content)
+ assert models == content
+ assert literals == ''
+
+
+def test_split_literals_to_file_output_has_valid_header() -> None:
+ """The generated literals file starts with the standard `from __future__` and `from typing` imports."""
+ content = textwrap.dedent("""\
+ from __future__ import annotations
+
+ from typing import Literal
+
+ from apify_client._docs import docs_group
+
+
+ A = Literal['x']
+ """)
+ _, literals = split_literals_to_file(content)
+ assert 'from __future__ import annotations' in literals
+ assert 'from typing import Literal' in literals
diff --git a/tests/unit/test_storage_collection_listing.py b/tests/unit/test_storage_collection_listing.py
index 5f19978a..feaf483f 100644
--- a/tests/unit/test_storage_collection_listing.py
+++ b/tests/unit/test_storage_collection_listing.py
@@ -7,7 +7,6 @@
from werkzeug.wrappers import Response
from apify_client import ApifyClient, ApifyClientAsync
-from apify_client._models import StorageOwnership
if TYPE_CHECKING:
from collections.abc import Callable
@@ -48,7 +47,7 @@ def test_dataset_collection_list_ownership_sync(httpserver: HTTPServer, client_u
httpserver.expect_oneshot_request('/v2/datasets', method='GET').respond_with_handler(_make_handler(captured))
client = ApifyClient(token='placeholder_token', **client_urls)
- result = client.datasets().list(ownership=StorageOwnership.OWNED_BY_ME)
+ result = client.datasets().list(ownership='ownedByMe')
assert result.total == 0
assert captured['args']['ownership'] == 'ownedByMe'
@@ -59,7 +58,7 @@ async def test_dataset_collection_list_ownership_async(httpserver: HTTPServer, c
httpserver.expect_oneshot_request('/v2/datasets', method='GET').respond_with_handler(_make_handler(captured))
client = ApifyClientAsync(token='placeholder_token', **client_urls)
- result = await client.datasets().list(ownership=StorageOwnership.SHARED_WITH_ME)
+ result = await client.datasets().list(ownership='sharedWithMe')
assert result.total == 0
assert captured['args']['ownership'] == 'sharedWithMe'
@@ -72,7 +71,7 @@ def test_key_value_store_collection_list_ownership_sync(httpserver: HTTPServer,
)
client = ApifyClient(token='placeholder_token', **client_urls)
- result = client.key_value_stores().list(ownership=StorageOwnership.OWNED_BY_ME)
+ result = client.key_value_stores().list(ownership='ownedByMe')
assert result.total == 0
assert captured['args']['ownership'] == 'ownedByMe'
@@ -85,7 +84,7 @@ async def test_key_value_store_collection_list_ownership_async(httpserver: HTTPS
)
client = ApifyClientAsync(token='placeholder_token', **client_urls)
- result = await client.key_value_stores().list(ownership=StorageOwnership.SHARED_WITH_ME)
+ result = await client.key_value_stores().list(ownership='sharedWithMe')
assert result.total == 0
assert captured['args']['ownership'] == 'sharedWithMe'
@@ -96,7 +95,7 @@ def test_request_queue_collection_list_ownership_sync(httpserver: HTTPServer, cl
httpserver.expect_oneshot_request('/v2/request-queues', method='GET').respond_with_handler(_make_handler(captured))
client = ApifyClient(token='placeholder_token', **client_urls)
- result = client.request_queues().list(ownership=StorageOwnership.OWNED_BY_ME)
+ result = client.request_queues().list(ownership='ownedByMe')
assert result.total == 0
assert captured['args']['ownership'] == 'ownedByMe'
@@ -107,7 +106,7 @@ async def test_request_queue_collection_list_ownership_async(httpserver: HTTPSer
httpserver.expect_oneshot_request('/v2/request-queues', method='GET').respond_with_handler(_make_handler(captured))
client = ApifyClientAsync(token='placeholder_token', **client_urls)
- result = await client.request_queues().list(ownership=StorageOwnership.SHARED_WITH_ME)
+ result = await client.request_queues().list(ownership='sharedWithMe')
assert result.total == 0
assert captured['args']['ownership'] == 'sharedWithMe'
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 64d3d0c4..7ecf037c 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -9,7 +9,7 @@
import impit
import pytest
-from apify_client._models import WebhookCondition, WebhookCreate, WebhookEventType
+from apify_client._models import WebhookCondition, WebhookCreate
from apify_client._resource_clients._resource_client import ResourceClientBase
from apify_client._utils import (
catch_not_found_or_throw,
@@ -42,12 +42,12 @@ def test_encode_webhooks_to_base64() -> None:
encode_webhooks_to_base64(
[
WebhookCreate(
- event_types=[WebhookEventType.ACTOR_RUN_CREATED],
+ event_types=['ACTOR.RUN.CREATED'],
condition=WebhookCondition(),
request_url='https://example.com/run-created',
),
WebhookCreate(
- event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED],
+ event_types=['ACTOR.RUN.SUCCEEDED'],
condition=WebhookCondition(),
request_url='https://example.com/run-succeeded',
payload_template='{"hello": "world", "resource":{{resource}}}',
@@ -79,12 +79,12 @@ def test_encode_webhooks_to_base64_from_dicts() -> None:
result_from_models = encode_webhooks_to_base64(
[
WebhookCreate(
- event_types=[WebhookEventType.ACTOR_RUN_CREATED],
+ event_types=['ACTOR.RUN.CREATED'],
condition=WebhookCondition(),
request_url='https://example.com/run-created',
),
WebhookCreate(
- event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED],
+ event_types=['ACTOR.RUN.SUCCEEDED'],
condition=WebhookCondition(),
request_url='https://example.com/run-succeeded',
payload_template='{"hello": "world"}',