diff --git a/pyproject.toml b/pyproject.toml index 418eb206..6a8a2f41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -170,6 +170,9 @@ indent-style = "space" "ERA001", # Commented-out code "TC003", # Move standard library import into a type-checking block ] +"src/apify_client/_literals.py" = [ + "PYI051", # `Literal[...]` is redundant in a union with `str`; the open `Literal[...] | str` is deliberate — it keeps known members as hints while tolerating unknown API values (see scripts/postprocess_generated_models.py) +] [tool.ruff.lint.flake8-quotes] docstring-quotes = "double" diff --git a/scripts/postprocess_generated_models.py b/scripts/postprocess_generated_models.py index c1db0688..50a51f56 100644 --- a/scripts/postprocess_generated_models.py +++ b/scripts/postprocess_generated_models.py @@ -3,8 +3,10 @@ Applied to `_models.py`: - Fix discriminator field names that use camelCase instead of snake_case (known issue with discriminators on schemas referenced from array items). -- 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. +- Rewrite every `class X(StrEnum)` as an open literal `X = Literal[...] | str` so downstream code can pass plain + strings (and reuse the named alias in resource-client signatures) instead of enum members. The trailing `| str` + keeps the known members as autocomplete hints while letting Pydantic validate any value the platform sends that + this (possibly older) client doesn't know about yet, instead of raising a `ValidationError` on the whole response. - 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). @@ -112,12 +114,14 @@ def fix_discriminators(content: str) -> str: def convert_enums_to_literals(content: str) -> str: - """Rewrite every `class X(StrEnum): ...` into an `X = Literal[...]` alias. + """Rewrite every `class X(StrEnum): ...` into an open literal `X = Literal[...] | str` 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. + declaration order. The trailing `| str` widens the alias so unknown values (e.g. a new enum + member the platform introduces before this client is regenerated) still validate against the + Pydantic models, while the known members remain as autocomplete hints. The class docstring, if + present, is preserved as a trailing bare-string docstring after the alias — matching the + 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. @@ -146,7 +150,7 @@ def convert_enums_to_literals(content: str) -> str: new_lines: list[str] = [f'{node.name} = Literal['] new_lines.extend(f' {v!r},' for v in values) - new_lines.append(']') + new_lines.append('] | str') if docstring is not None: if '\n' in docstring: new_lines.append('"""') @@ -179,15 +183,26 @@ def convert_enums_to_literals(content: str) -> str: """ +def _is_literal_expr(value: ast.expr) -> bool: + """Return True if `value` is a bare `Literal[...]` or an open `Literal[...] | str` union.""" + if isinstance(value, ast.Subscript) and isinstance(value.value, ast.Name) and value.value.id == 'Literal': + return True + return ( + isinstance(value, ast.BinOp) + and isinstance(value.op, ast.BitOr) + and _is_literal_expr(value.left) + and isinstance(value.right, ast.Name) + and value.right.id == 'str' + ) + + def _is_literal_alias(node: ast.stmt) -> bool: - """Return True if `node` is a top-level `Name = Literal[...]` statement.""" + """Return True if `node` is a top-level `Name = Literal[...]` or `Name = Literal[...] | str` 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' + and _is_literal_expr(node.value) ) diff --git a/src/apify_client/_literals.py b/src/apify_client/_literals.py index b85f92b6..530cee1d 100644 --- a/src/apify_client/_literals.py +++ b/src/apify_client/_literals.py @@ -4,502 +4,535 @@ from typing import Literal -ActorJobStatus = Literal[ - 'READY', - 'RUNNING', - 'SUCCEEDED', - 'FAILED', - 'TIMING-OUT', - 'TIMED-OUT', - 'ABORTING', - 'ABORTED', -] +ActorJobStatus = ( + Literal[ + 'READY', + 'RUNNING', + 'SUCCEEDED', + 'FAILED', + 'TIMING-OUT', + 'TIMED-OUT', + 'ABORTING', + 'ABORTED', + ] + | str +) """Status of an Actor job (run or build).""" -ActorPermissionLevel = Literal[ - 'LIMITED_PERMISSIONS', - 'FULL_PERMISSIONS', -] +ActorPermissionLevel = ( + Literal[ + 'LIMITED_PERMISSIONS', + 'FULL_PERMISSIONS', + ] + | str +) """Determines the permission level that the Actor requires to run. For details, see [Actor permissions](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', - 'full-permission-actor-blocked-for-admin', - 'full-permission-actor-not-approved', - '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-input-body-not-valid-json', - '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', - 'service-worker-registration-not-allowed', - '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', -] +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', + 'full-permission-actor-blocked-for-admin', + 'full-permission-actor-not-approved', + '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-input-body-not-valid-json', + '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', + 'service-worker-registration-not-allowed', + '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', + ] + | str +) """Machine-processable error type identifier.""" -GeneralAccess = Literal[ - 'ANYONE_WITH_ID_CAN_READ', - 'ANYONE_WITH_NAME_CAN_READ', - 'FOLLOW_USER_SETTING', - 'RESTRICTED', -] +GeneralAccess = ( + Literal[ + 'ANYONE_WITH_ID_CAN_READ', + 'ANYONE_WITH_NAME_CAN_READ', + 'FOLLOW_USER_SETTING', + 'RESTRICTED', + ] + | str +) """Defines the general access level for the resource.""" -HttpMethod = Literal[ - 'GET', - 'HEAD', - 'POST', - 'PUT', - 'DELETE', - 'CONNECT', - 'OPTIONS', - 'TRACE', - 'PATCH', -] +HttpMethod = ( + Literal[ + 'GET', + 'HEAD', + 'POST', + 'PUT', + 'DELETE', + 'CONNECT', + 'OPTIONS', + 'TRACE', + 'PATCH', + ] + | str +) -RunOrigin = Literal[ - 'DEVELOPMENT', - 'WEB', - 'API', - 'SCHEDULER', - 'TEST', - 'WEBHOOK', - 'ACTOR', - 'CLI', - 'CI', - 'STANDBY', - 'MCP', -] +RunOrigin = ( + Literal[ + 'DEVELOPMENT', + 'WEB', + 'API', + 'SCHEDULER', + 'TEST', + 'WEBHOOK', + 'ACTOR', + 'CLI', + 'CI', + 'STANDBY', + 'MCP', + ] + | str +) -SourceCodeFileFormat = Literal[ - 'BASE64', - 'TEXT', -] +SourceCodeFileFormat = ( + Literal[ + 'BASE64', + 'TEXT', + ] + | str +) -StorageOwnership = Literal[ - 'ownedByMe', - 'sharedWithMe', -] +StorageOwnership = ( + Literal[ + 'ownedByMe', + 'sharedWithMe', + ] + | str +) -VersionSourceType = Literal[ - 'SOURCE_FILES', - 'GIT_REPO', - 'TARBALL', - 'GITHUB_GIST', - 'SOURCE_CODE', -] +VersionSourceType = ( + Literal[ + 'SOURCE_FILES', + 'GIT_REPO', + 'TARBALL', + 'GITHUB_GIST', + 'SOURCE_CODE', + ] + | str +) -WebhookDispatchStatus = Literal[ - 'ACTIVE', - 'SUCCEEDED', - 'FAILED', -] +WebhookDispatchStatus = ( + Literal[ + 'ACTIVE', + 'SUCCEEDED', + 'FAILED', + ] + | str +) """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', -] +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', + ] + | str +) """Type of event that triggers the webhook.""" diff --git a/tests/unit/test_postprocess_generated_models.py b/tests/unit/test_postprocess_generated_models.py index 75568d59..c652ba5a 100644 --- a/tests/unit/test_postprocess_generated_models.py +++ b/tests/unit/test_postprocess_generated_models.py @@ -164,6 +164,21 @@ class Status(StrEnum): assert "'RUNNING'," in result +def test_convert_enums_to_literals_emits_open_union() -> None: + """The alias is widened with a trailing `| str` so unknown API values still validate against the models.""" + content = textwrap.dedent("""\ + from typing import Literal + + class Status(StrEnum): + READY = 'READY' + RUNNING = 'RUNNING' + """) + result = convert_enums_to_literals(content) + assert '] | str' in result + # The `| str` widens the alias itself, right after its closing bracket. + assert result.index('Status = Literal[') < result.index('] | str') + + 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("""\ @@ -355,6 +370,75 @@ class Alpha(BaseModel): assert '"""Alpha status docstring."""' in literals +def test_split_literals_to_file_moves_open_literal_union() -> None: + """An open `Literal[...] | str` alias (what `convert_enums_to_literals` emits) is moved to the literals file.""" + content = textwrap.dedent("""\ + from __future__ import annotations + + from typing import Literal + + from apify_client._docs import docs_group + + + class Alpha(BaseModel): + status: Status + + + Status = Literal[ + 'READY', + 'RUNNING', + ] | str + \"\"\"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 + + assert 'Status = Literal[' in literals + assert '] | str' in literals + assert '"""Status docstring."""' in literals + + +def test_convert_then_split_preserves_open_union() -> None: + """Composing convert + split moves the enum out as an open `Literal[...] | str` alias in the literals file.""" + content = textwrap.dedent("""\ + from enum import StrEnum + from typing import Literal + + from apify_client._docs import docs_group + + + class Status(StrEnum): + READY = 'READY' + RUNNING = 'RUNNING' + """) + _, literals = split_literals_to_file(convert_enums_to_literals(content)) + assert 'Status = Literal[' in literals + assert '] | str' in literals + + +def test_split_literals_to_file_ignores_non_str_open_union() -> None: + """Only `Literal[...]` and `Literal[...] | str` count as shared aliases; `Literal[...] | None` stays in models.""" + content = textwrap.dedent("""\ + from __future__ import annotations + + from typing import Literal + + from apify_client._docs import docs_group + + + Status = Literal[ + 'READY', + 'RUNNING', + ] | None + """) + models, literals = split_literals_to_file(content) + assert models == content + assert 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("""\