From 003986b9d4d0f9d89a6cee7b8d8ccffd8663d5fb Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 23 Apr 2026 14:42:15 +0200 Subject: [PATCH 1/3] feat: generate Literal type aliases instead of StrEnum classes Rewrite every generated `class X(StrEnum)` into a reusable `X: TypeAlias = Literal[...]` so users can pass plain strings and type aliases can be shared across resource-client method signatures. Closes #576. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/postprocess_generated_models.py | 101 +- src/apify_client/_consts.py | 13 +- src/apify_client/_models_generated.py | 959 +++++++++--------- src/apify_client/_resource_clients/actor.py | 4 +- .../_resource_clients/actor_version.py | 16 +- .../actor_version_collection.py | 16 +- src/apify_client/_status_message_watcher.py | 2 +- tests/integration/test_actor_version.py | 5 +- tests/integration/test_build.py | 10 +- tests/integration/test_run.py | 26 +- tests/integration/test_task.py | 4 +- tests/integration/test_webhook.py | 14 +- tests/unit/test_actor_start_params.py | 3 +- tests/unit/test_logging.py | 17 +- .../unit/test_postprocess_generated_models.py | 166 ++- tests/unit/test_storage_collection_listing.py | 13 +- tests/unit/test_utils.py | 10 +- 17 files changed, 792 insertions(+), 587 deletions(-) diff --git a/scripts/postprocess_generated_models.py b/scripts/postprocess_generated_models.py index 0efbfae1..d9094700 100644 --- a/scripts/postprocess_generated_models.py +++ b/scripts/postprocess_generated_models.py @@ -4,6 +4,8 @@ - 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: TypeAlias = Literal[...]` so downstream code can pass + plain strings (and reuse the named alias in resource-client signatures) instead of enum members. - Add `@docs_group('Models')` to every model class (plus the required import). Applied to `_typeddicts_generated.py`: @@ -51,6 +53,27 @@ ) +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 fix_discriminators(content: str) -> str: """Replace camelCase discriminator values with their snake_case equivalents.""" for camel, snake in DISCRIMINATOR_FIXES.items(): @@ -73,8 +96,64 @@ def deduplicate_error_type_enum(content: str) -> str: ) # Replace standalone `Type` references in annotation contexts (`: Type`, `| Type`, `[Type`). 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: TypeAlias = 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 = {b.id for b in node.bases if isinstance(b, ast.Name)} + 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}: TypeAlias = 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 _ensure_typing_import(_collapse_blank_lines('\n'.join(lines)), 'TypeAlias') def add_docs_group_decorators(content: str, group_name: GroupName) -> str: @@ -136,17 +215,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: @@ -237,10 +306,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: @@ -258,6 +324,7 @@ def postprocess_models(path: Path) -> bool: original = 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 diff --git a/src/apify_client/_consts.py b/src/apify_client/_consts.py index f6a18d97..0cf478b2 100644 --- a/src/apify_client/_consts.py +++ b/src/apify_client/_consts.py @@ -1,8 +1,10 @@ from __future__ import annotations from datetime import timedelta +from typing import TYPE_CHECKING -from apify_client._models_generated import ActorJobStatus +if TYPE_CHECKING: + from apify_client._models_generated import ActorJobStatus DEFAULT_API_URL = 'https://api.apify.com' """Default base URL for the Apify API.""" @@ -34,14 +36,7 @@ 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, - } -) +TERMINAL_STATUSES: frozenset[ActorJobStatus] = frozenset({'SUCCEEDED', 'FAILED', 'TIMED-OUT', 'ABORTED'}) """Set of terminal Actor job statuses that indicate the job has finished.""" OVERRIDABLE_DEFAULT_HEADERS = {'Accept', 'Authorization', 'Accept-Encoding', 'User-Agent'} diff --git a/src/apify_client/_models_generated.py b/src/apify_client/_models_generated.py index 284d6780..51854026 100644 --- a/src/apify_client/_models_generated.py +++ b/src/apify_client/_models_generated.py @@ -2,8 +2,7 @@ from __future__ import annotations -from enum import StrEnum -from typing import Annotated, Any, Literal +from typing import Annotated, Any, Literal, TypeAlias from pydantic import AnyUrl, AwareDatetime, BaseModel, ConfigDict, EmailStr, Field, RootModel @@ -143,26 +142,24 @@ 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' +ActorJobStatus: TypeAlias = Literal[ + 'READY', + 'RUNNING', + 'SUCCEEDED', + 'FAILED', + 'TIMING-OUT', + 'TIMED-OUT', + 'ABORTING', + 'ABORTED', +] +"""Status of an Actor job (run or build).""" -@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' +ActorPermissionLevel: TypeAlias = 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).""" @docs_group('Models') @@ -1034,401 +1031,398 @@ 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' +ErrorType: TypeAlias = 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.""" @docs_group('Models') @@ -1489,14 +1483,13 @@ 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' +GeneralAccess: TypeAlias = Literal[ + 'ANYONE_WITH_ID_CAN_READ', + 'ANYONE_WITH_NAME_CAN_READ', + 'FOLLOW_USER_SETTING', + 'RESTRICTED', +] +"""Defines the general access level for the resource.""" @docs_group('Models') @@ -1548,17 +1541,17 @@ 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' +HttpMethod: TypeAlias = Literal[ + 'GET', + 'HEAD', + 'POST', + 'PUT', + 'DELETE', + 'CONNECT', + 'OPTIONS', + 'TRACE', + 'PATCH', +] @docs_group('Models') @@ -2946,17 +2939,17 @@ 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' +RunOrigin: TypeAlias = Literal[ + 'DEVELOPMENT', + 'WEB', + 'API', + 'SCHEDULER', + 'TEST', + 'WEBHOOK', + 'ACTOR', + 'CLI', + 'STANDBY', +] @docs_group('Models') @@ -3272,10 +3265,10 @@ class SourceCodeFile(BaseModel): name: Annotated[str, Field(examples=['src/main.js'])] -@docs_group('Models') -class SourceCodeFileFormat(StrEnum): - BASE64 = 'BASE64' - TEXT = 'TEXT' +SourceCodeFileFormat: TypeAlias = Literal[ + 'BASE64', + 'TEXT', +] @docs_group('Models') @@ -3321,10 +3314,10 @@ class StorageIds(BaseModel): """ -@docs_group('Models') -class StorageOwnership(StrEnum): - OWNED_BY_ME = 'ownedByMe' - SHARED_WITH_ME = 'sharedWithMe' +StorageOwnership: TypeAlias = Literal[ + 'ownedByMe', + 'sharedWithMe', +] @docs_group('Models') @@ -3820,12 +3813,12 @@ 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' +VersionSourceType: TypeAlias = Literal[ + 'SOURCE_FILES', + 'GIT_REPO', + 'TARBALL', + 'GITHUB_GIST', +] @docs_group('Models') @@ -3924,31 +3917,29 @@ 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' +WebhookDispatchStatus: TypeAlias = Literal[ + 'ACTIVE', + 'SUCCEEDED', + 'FAILED', +] +"""Status of the webhook dispatch indicating whether the HTTP request was successful.""" + + +WebhookEventType: TypeAlias = 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.""" @docs_group('Models') diff --git a/src/apify_client/_resource_clients/actor.py b/src/apify_client/_resource_clients/actor.py index c4463427..cb5d5c1a 100644 --- a/src/apify_client/_resource_clients/actor.py +++ b/src/apify_client/_resource_clients/actor.py @@ -276,7 +276,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=WebhookRepresentationList.from_webhooks(webhooks or []).to_base64(), ) @@ -772,7 +772,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=WebhookRepresentationList.from_webhooks(webhooks or []).to_base64(), ) diff --git a/src/apify_client/_resource_clients/actor_version.py b/src/apify_client/_resource_clients/actor_version.py index 825803e4..2383fb34 100644 --- a/src/apify_client/_resource_clients/actor_version.py +++ b/src/apify_client/_resource_clients/actor_version.py @@ -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 aac5e4c3..0d0b5b65 100644 --- a/src/apify_client/_resource_clients/actor_version_collection.py +++ b/src/apify_client/_resource_clients/actor_version_collection.py @@ -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/_status_message_watcher.py b/src/apify_client/_status_message_watcher.py index 5bdac9e7..e841a719 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/tests/integration/test_actor_version.py b/tests/integration/test_actor_version.py index b8ff31c3..f3992ae2 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_generated 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 ef8ac662..e8e0cb00 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 3416fe85..4eb4ca88 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_generated import ActorJobStatus, Run +from apify_client._models_generated 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 322185a2..212ba10a 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 a011aaa7..9e4197e8 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_generated 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 b58f465b..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_generated 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 2c1fed2d..9fb64f75 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_generated 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._models_generated 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..b43fe18a 100644 --- a/tests/unit/test_postprocess_generated_models.py +++ b/tests/unit/test_postprocess_generated_models.py @@ -4,6 +4,7 @@ from scripts.postprocess_generated_models import ( add_docs_group_decorators, + convert_enums_to_literals, deduplicate_error_type_enum, fix_discriminators, ) @@ -229,11 +230,164 @@ def test_add_docs_group_decorators_no_classes() -> None: assert "@docs_group('Models')" not in result +# -- convert_enums_to_literals ------------------------------------------------ + + +def test_convert_enums_to_literals_replaces_single_enum() -> None: + content = textwrap.dedent("""\ + from enum import StrEnum + from typing import Literal, TypeAlias + + class Status(StrEnum): + READY = 'READY' + RUNNING = 'RUNNING' + """) + result = convert_enums_to_literals(content) + assert 'class Status(StrEnum)' not in result + assert 'Status: TypeAlias = Literal[' in result + assert "'READY'," in result + assert "'RUNNING'," in result + + +def test_convert_enums_to_literals_preserves_value_order() -> None: + content = textwrap.dedent("""\ + from typing import Literal, TypeAlias + + 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: + content = textwrap.dedent("""\ + from typing import Literal, TypeAlias + + 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: TypeAlias') + docstring_idx = result.index('"""Describes a status."""') + assert alias_idx < docstring_idx + + +def test_convert_enums_to_literals_preserves_hyphenated_values() -> None: + content = textwrap.dedent("""\ + from typing import Literal, TypeAlias + + 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: + content = textwrap.dedent("""\ + from typing import Literal, TypeAlias + + class Alpha(StrEnum): + A = 'a' + + class Beta(StrEnum): + B = 'b' + """) + result = convert_enums_to_literals(content) + assert 'Alpha: TypeAlias = Literal[' in result + assert 'Beta: TypeAlias = 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: + content = textwrap.dedent("""\ + from typing import Literal, TypeAlias + + 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_injects_typealias_import() -> None: + content = textwrap.dedent("""\ + from typing import Annotated, Literal + + class Status(StrEnum): + A = 'a' + """) + result = convert_enums_to_literals(content) + assert 'TypeAlias' in result + assert 'from typing import Annotated, Literal, TypeAlias' in result + + +def test_convert_enums_to_literals_leaves_typealias_import_alone_when_present() -> None: + content = textwrap.dedent("""\ + from typing import Literal, TypeAlias + + class Status(StrEnum): + A = 'a' + """) + result = convert_enums_to_literals(content) + assert result.count('TypeAlias') == 2 # one in import, one in the new alias + + +def test_convert_enums_to_literals_no_change_when_no_enums() -> None: + content = textwrap.dedent("""\ + from typing import Literal, TypeAlias + + 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: + content = textwrap.dedent("""\ + from typing import Literal, TypeAlias + + 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: TypeAlias = Literal[' in result + + # -- Integration: full pipeline ----------------------------------------------- def test_full_pipeline() -> None: content = textwrap.dedent("""\ + from enum import StrEnum + from typing import Literal + from pydantic import BaseModel class Zebra(BaseModel): @@ -253,15 +407,21 @@ 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: TypeAlias = 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 + + # TypeAlias was imported. + assert 'TypeAlias' in result diff --git a/tests/unit/test_storage_collection_listing.py b/tests/unit/test_storage_collection_listing.py index d698e857..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_generated 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 ecb41699..390cc666 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -10,7 +10,7 @@ import pytest from apify_client._models import WebhookRepresentationList -from apify_client._models_generated import WebhookCondition, WebhookCreate, WebhookEventType +from apify_client._models_generated 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_webhook_representation_list_to_base64() -> None: WebhookRepresentationList.from_webhooks( [ 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_webhook_representation_list_from_dicts() -> None: result_from_models = WebhookRepresentationList.from_webhooks( [ 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"}', From 842c82de2c7ca6c437d6e76b9a57aacc321a3287 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 23 Apr 2026 15:17:46 +0200 Subject: [PATCH 2/3] refactor: move generated literals to _literals_generated.py and snake_case API values Split the 11 generated `Literal` type aliases out of `_models_generated.py` into a new `_literals_generated.py` so downstream code can depend on the literals without pulling in every Pydantic model. Rename the hand-maintained `_types.py` to `_literals.py` to match. Convert camelCase literal values (`'ownedByMe'`, `'sharedWithMe'`) to snake_case and emit a wire-format mapping so the API still receives the camelCase form it expects. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/02_concepts/code/03_nested_async.py | 2 +- docs/02_concepts/code/03_nested_sync.py | 2 +- pyproject.toml | 3 + scripts/postprocess_generated_models.py | 179 +++++- src/apify_client/__init__.py | 2 +- src/apify_client/_consts.py | 4 +- src/apify_client/_http_clients/_base.py | 2 +- src/apify_client/_http_clients/_impit.py | 2 +- src/apify_client/{_types.py => _literals.py} | 5 +- src/apify_client/_literals_generated.py | 504 +++++++++++++++++ src/apify_client/_models.py | 5 +- src/apify_client/_models_generated.py | 508 +----------------- .../_resource_clients/_resource_client.py | 2 +- src/apify_client/_resource_clients/actor.py | 6 +- .../_resource_clients/actor_collection.py | 2 +- .../_resource_clients/actor_env_var.py | 2 +- .../actor_env_var_collection.py | 2 +- .../_resource_clients/actor_version.py | 4 +- .../actor_version_collection.py | 4 +- src/apify_client/_resource_clients/build.py | 2 +- .../_resource_clients/build_collection.py | 2 +- src/apify_client/_resource_clients/dataset.py | 4 +- .../_resource_clients/dataset_collection.py | 26 +- .../_resource_clients/key_value_store.py | 4 +- .../key_value_store_collection.py | 26 +- src/apify_client/_resource_clients/log.py | 2 +- .../_resource_clients/request_queue.py | 4 +- .../request_queue_collection.py | 26 +- src/apify_client/_resource_clients/run.py | 4 +- .../_resource_clients/run_collection.py | 4 +- .../_resource_clients/schedule.py | 2 +- .../_resource_clients/schedule_collection.py | 2 +- .../_resource_clients/store_collection.py | 2 +- src/apify_client/_resource_clients/task.py | 5 +- .../_resource_clients/task_collection.py | 2 +- src/apify_client/_resource_clients/user.py | 2 +- src/apify_client/_resource_clients/webhook.py | 4 +- .../_resource_clients/webhook_collection.py | 5 +- .../_resource_clients/webhook_dispatch.py | 2 +- .../webhook_dispatch_collection.py | 2 +- tests/unit/test_logging.py | 2 +- tests/unit/test_pluggable_http_client.py | 2 +- .../unit/test_postprocess_generated_models.py | 164 ++++++ tests/unit/test_storage_collection_listing.py | 12 +- 44 files changed, 964 insertions(+), 588 deletions(-) rename src/apify_client/{_types.py => _literals.py} (86%) create mode 100644 src/apify_client/_literals_generated.py diff --git a/docs/02_concepts/code/03_nested_async.py b/docs/02_concepts/code/03_nested_async.py index cca76012..f0e2ccb9 100644 --- a/docs/02_concepts/code/03_nested_async.py +++ b/docs/02_concepts/code/03_nested_async.py @@ -1,5 +1,5 @@ from apify_client import ApifyClientAsync -from apify_client._models_generated import ActorJobStatus +from apify_client._literals_generated import ActorJobStatus TOKEN = 'MY-APIFY-TOKEN' diff --git a/docs/02_concepts/code/03_nested_sync.py b/docs/02_concepts/code/03_nested_sync.py index 159b3ce0..1c82fe4e 100644 --- a/docs/02_concepts/code/03_nested_sync.py +++ b/docs/02_concepts/code/03_nested_sync.py @@ -1,5 +1,5 @@ from apify_client import ApifyClient -from apify_client._models_generated import ActorJobStatus +from apify_client._literals_generated import ActorJobStatus TOKEN = 'MY-APIFY-TOKEN' diff --git a/pyproject.toml b/pyproject.toml index f79dfe85..2cb0572e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,6 +123,9 @@ indent-style = "space" "**/__init__.py" = [ "F401", # Unused imports ] +"**/{_models,_models_generated}.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 d9094700..109d3683 100644 --- a/scripts/postprocess_generated_models.py +++ b/scripts/postprocess_generated_models.py @@ -6,6 +6,12 @@ - Deduplicate the inlined `Type(StrEnum)` that comes from ErrorResponse.yaml; rewire to `ErrorType`. - Rewrite every `class X(StrEnum)` as `X: TypeAlias = Literal[...]` so downstream code can pass plain strings (and reuse the named alias in resource-client signatures) instead of enum members. +- Convert camelCase string values in each literal alias to snake_case (Pythonic), and emit a + `__WIRE_VALUES` mapping the Python value back to the original camelCase form so the + resource clients can still produce the exact string the API expects on the wire. +- Move the resulting `TypeAlias = Literal[...]` definitions into `_literals_generated.py`, leaving + `_models_generated.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_generated.py`: @@ -30,6 +36,7 @@ REPO_ROOT = Path(__file__).resolve().parent.parent PACKAGE_DIR = REPO_ROOT / 'src' / 'apify_client' MODELS_PATH = PACKAGE_DIR / '_models_generated.py' +LITERALS_PATH = PACKAGE_DIR / '_literals_generated.py' TYPEDDICTS_PATH = PACKAGE_DIR / '_typeddicts_generated.py' # Map of camelCase discriminator values to their snake_case equivalents. @@ -156,6 +163,141 @@ def convert_enums_to_literals(content: str) -> str: return _ensure_typing_import(_collapse_blank_lines('\n'.join(lines)), 'TypeAlias') +LITERALS_FILE_HEADER = """\ +# generated by postprocess_generated_models + +from __future__ import annotations + +from typing import Literal, TypeAlias + + +""" + +_CAMEL_CASE_VALUE = re.compile(r"^'([a-z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*)',?$") + + +def _camel_to_snake(value: str) -> str: + """Convert a camelCase identifier to snake_case.""" + return re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', value).lower() + + +def _is_literal_alias(node: ast.stmt) -> bool: + """Return True if `node` is a top-level `Name: TypeAlias = Literal[...]` statement.""" + return ( + isinstance(node, ast.AnnAssign) + and isinstance(node.target, ast.Name) + and isinstance(node.annotation, ast.Name) + and node.annotation.id == 'TypeAlias' + and isinstance(node.value, ast.Subscript) + and isinstance(node.value.value, ast.Name) + and node.value.value.id == 'Literal' + ) + + +def snake_case_camelcase_literal_values(content: str) -> str: + """Rewrite camelCase string values in `Literal[...]` aliases into snake_case. + + Scans each `Name: TypeAlias = Literal[...]` block and, for any value matching the camelCase + pattern (lowercase-first followed by an uppercase letter), converts it to snake_case. For each + alias that had at least one conversion, emits a `__WIRE_VALUES: dict[, str] = ...` + mapping right after the alias so consumers can translate back to the API wire format. + + SCREAMING_SNAKE_CASE, dotted, hyphenated, and HTTP-method values pass through unchanged. + """ + tree = ast.parse(content) + lines = content.split('\n') + insertions: list[tuple[int, list[str]]] = [] # (insert-after-line-exclusive, lines to insert) + + for alias_name, node, end_line in _extract_top_level_symbols(tree): + if not _is_literal_alias(node): + continue + + assert node.end_lineno is not None # noqa: S101 + wire_mapping: dict[str, str] = {} + for line_idx in range(node.lineno - 1, node.end_lineno): + match = _CAMEL_CASE_VALUE.match(lines[line_idx].strip()) + if match is None: + continue + original = match.group(1) + snake = _camel_to_snake(original) + wire_mapping[snake] = original + lines[line_idx] = lines[line_idx].replace(f"'{original}'", f"'{snake}'", 1) + + if not wire_mapping: + continue + + constant_name = '_' + _camel_to_snake(alias_name).upper() + '_WIRE_VALUES' + docstring = f'"""Maps snake_case `{alias_name}` values to the camelCase form expected on the API wire."""' + mapping_lines = [ + '', + f'{constant_name}: dict[{alias_name}, str] = {{', + *(f" '{snake}': '{original}'," for snake, original in wire_mapping.items()), + '}', + docstring, + ] + # Insert after the alias's trailing docstring (absorbed into end_line) so the docstring + # stays attached to the alias rather than to the mapping dict. + insertions.append((end_line, mapping_lines)) + + if not insertions: + return content + + for insert_at, new_lines in sorted(insertions, key=lambda r: r[0], reverse=True): + lines[insert_at:insert_at] = new_lines + + return '\n'.join(lines) + + +def split_literals_to_file(content: str) -> tuple[str, str]: + """Move every top-level `Name: TypeAlias = 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_generated.py`, and rebuilds `_literals_generated.py` from the blocks + in original order. The models content gains a `from apify_client._literals_generated 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_generated 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: """Add `@docs_group(group_name)` to every class and inject the required import. @@ -319,17 +461,30 @@ def rename_with_dict_suffix(content: str, names: set[str]) -> str: return content -def postprocess_models(path: Path) -> bool: - """Apply `_models_generated.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_generated.py`-specific fixes and emit `_literals_generated.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) + if literals_content: + literals_content = snake_case_camelcase_literal_values(literals_content) + + 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: @@ -353,12 +508,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_generated.py') + print('No fixes needed for _models_generated.py / _literals_generated.py') if postprocess_typeddicts(TYPEDDICTS_PATH): changed.append(TYPEDDICTS_PATH) diff --git a/src/apify_client/__init__.py b/src/apify_client/__init__.py index 3add76db..43ad0f7a 100644 --- a/src/apify_client/__init__.py +++ b/src/apify_client/__init__.py @@ -8,7 +8,7 @@ ImpitHttpClient, ImpitHttpClientAsync, ) -from ._types import Timeout +from ._literals import Timeout __version__ = metadata.version('apify-client') diff --git a/src/apify_client/_consts.py b/src/apify_client/_consts.py index 0cf478b2..76250340 100644 --- a/src/apify_client/_consts.py +++ b/src/apify_client/_consts.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from apify_client._models_generated import ActorJobStatus + from apify_client._literals import TerminalActorJobStatus DEFAULT_API_URL = 'https://api.apify.com' """Default base URL for the Apify API.""" @@ -36,7 +36,7 @@ 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] = frozenset({'SUCCEEDED', 'FAILED', 'TIMED-OUT', 'ABORTED'}) +TERMINAL_STATUSES: frozenset[TerminalActorJobStatus] = frozenset({'SUCCEEDED', 'FAILED', 'TIMED-OUT', 'ABORTED'}) """Set of terminal Actor job statuses that indicate the job has finished.""" OVERRIDABLE_DEFAULT_HEADERS = {'Accept', 'Authorization', 'Accept-Encoding', 'User-Agent'} diff --git a/src/apify_client/_http_clients/_base.py b/src/apify_client/_http_clients/_base.py index 7683fdfb..b100ca4c 100644 --- a/src/apify_client/_http_clients/_base.py +++ b/src/apify_client/_http_clients/_base.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterator, Mapping - from apify_client._types import JsonSerializable, Timeout + from apify_client._literals import JsonSerializable, Timeout @docs_group('HTTP clients') diff --git a/src/apify_client/_http_clients/_impit.py b/src/apify_client/_http_clients/_impit.py index f56e72c2..2fbb1a8b 100644 --- a/src/apify_client/_http_clients/_impit.py +++ b/src/apify_client/_http_clients/_impit.py @@ -28,8 +28,8 @@ from collections.abc import Awaitable, Callable from apify_client._http_clients import HttpResponse + from apify_client._literals import JsonSerializable, Timeout from apify_client._statistics import ClientStatistics - from apify_client._types import JsonSerializable, Timeout T = TypeVar('T') diff --git a/src/apify_client/_types.py b/src/apify_client/_literals.py similarity index 86% rename from src/apify_client/_types.py rename to src/apify_client/_literals.py index d3a272e8..a7666434 100644 --- a/src/apify_client/_types.py +++ b/src/apify_client/_literals.py @@ -1,7 +1,7 @@ from __future__ import annotations from datetime import timedelta -from typing import Literal +from typing import Literal, TypeAlias from apify_client._models import WebhookRepresentation from apify_client._models_generated import WebhookCreate @@ -19,6 +19,9 @@ `condition`) are ignored at runtime. """ +TerminalActorJobStatus: TypeAlias = 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/src/apify_client/_literals_generated.py b/src/apify_client/_literals_generated.py new file mode 100644 index 00000000..df86459a --- /dev/null +++ b/src/apify_client/_literals_generated.py @@ -0,0 +1,504 @@ +# generated by postprocess_generated_models + +from __future__ import annotations + +from typing import Literal, TypeAlias + +ActorJobStatus: TypeAlias = Literal[ + 'READY', + 'RUNNING', + 'SUCCEEDED', + 'FAILED', + 'TIMING-OUT', + 'TIMED-OUT', + 'ABORTING', + 'ABORTED', +] +"""Status of an Actor job (run or build).""" + + +ActorPermissionLevel: TypeAlias = 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: TypeAlias = 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: TypeAlias = Literal[ + 'ANYONE_WITH_ID_CAN_READ', + 'ANYONE_WITH_NAME_CAN_READ', + 'FOLLOW_USER_SETTING', + 'RESTRICTED', +] +"""Defines the general access level for the resource.""" + + +HttpMethod: TypeAlias = Literal[ + 'GET', + 'HEAD', + 'POST', + 'PUT', + 'DELETE', + 'CONNECT', + 'OPTIONS', + 'TRACE', + 'PATCH', +] + + +RunOrigin: TypeAlias = Literal[ + 'DEVELOPMENT', + 'WEB', + 'API', + 'SCHEDULER', + 'TEST', + 'WEBHOOK', + 'ACTOR', + 'CLI', + 'STANDBY', +] + + +SourceCodeFileFormat: TypeAlias = Literal[ + 'BASE64', + 'TEXT', +] + + +StorageOwnership: TypeAlias = Literal[ + 'owned_by_me', + 'shared_with_me', +] + +_STORAGE_OWNERSHIP_WIRE_VALUES: dict[StorageOwnership, str] = { + 'owned_by_me': 'ownedByMe', + 'shared_with_me': 'sharedWithMe', +} +"""Maps snake_case `StorageOwnership` values to the camelCase form expected on the API wire.""" + + +VersionSourceType: TypeAlias = Literal[ + 'SOURCE_FILES', + 'GIT_REPO', + 'TARBALL', + 'GITHUB_GIST', +] + + +WebhookDispatchStatus: TypeAlias = Literal[ + 'ACTIVE', + 'SUCCEEDED', + 'FAILED', +] +"""Status of the webhook dispatch indicating whether the HTTP request was successful.""" + + +WebhookEventType: TypeAlias = 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 be6fba38..e5af5b99 100644 --- a/src/apify_client/_models.py +++ b/src/apify_client/_models.py @@ -12,10 +12,11 @@ from pydantic import AnyUrl, BaseModel, ConfigDict, Field, RootModel, model_validator from apify_client._docs import docs_group -from apify_client._models_generated import ActorJobStatus, WebhookCreate +from apify_client._literals_generated import ActorJobStatus +from apify_client._models_generated import WebhookCreate if TYPE_CHECKING: - from apify_client._types import WebhooksList + from apify_client._literals import WebhooksList @docs_group('Models') diff --git a/src/apify_client/_models_generated.py b/src/apify_client/_models_generated.py index 51854026..172dfd2c 100644 --- a/src/apify_client/_models_generated.py +++ b/src/apify_client/_models_generated.py @@ -2,11 +2,23 @@ from __future__ import annotations -from typing import Annotated, Any, Literal, TypeAlias +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_generated import ( + ActorJobStatus, + ActorPermissionLevel, + ErrorType, + GeneralAccess, + HttpMethod, + RunOrigin, + SourceCodeFileFormat, + VersionSourceType, + WebhookDispatchStatus, + WebhookEventType, +) @docs_group('Models') @@ -142,26 +154,6 @@ class ActorDefinition(BaseModel): """ -ActorJobStatus: TypeAlias = Literal[ - 'READY', - 'RUNNING', - 'SUCCEEDED', - 'FAILED', - 'TIMING-OUT', - 'TIMED-OUT', - 'ABORTING', - 'ABORTED', -] -"""Status of an Actor job (run or build).""" - - -ActorPermissionLevel: TypeAlias = 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).""" - - @docs_group('Models') class ActorResponse(BaseModel): """Response containing Actor data.""" @@ -1031,400 +1023,6 @@ class ErrorResponse(BaseModel): error: ErrorDetail -ErrorType: TypeAlias = 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.""" - - @docs_group('Models') class EventData(BaseModel): model_config = ConfigDict( @@ -1483,15 +1081,6 @@ class FreeActorPricingInfo(CommonActorPricingInfo): pricing_model: Annotated[Literal['FREE'], Field(alias='pricingModel')] -GeneralAccess: TypeAlias = Literal[ - 'ANYONE_WITH_ID_CAN_READ', - 'ANYONE_WITH_NAME_CAN_READ', - 'FOLLOW_USER_SETTING', - 'RESTRICTED', -] -"""Defines the general access level for the resource.""" - - @docs_group('Models') class HeadAndLockResponse(BaseModel): """Response containing locked requests from the request queue head.""" @@ -1541,19 +1130,6 @@ class HeadResponse(BaseModel): data: RequestQueueHead -HttpMethod: TypeAlias = Literal[ - 'GET', - 'HEAD', - 'POST', - 'PUT', - 'DELETE', - 'CONNECT', - 'OPTIONS', - 'TRACE', - 'PATCH', -] - - @docs_group('Models') class InvalidItem(BaseModel): model_config = ConfigDict( @@ -2939,19 +2515,6 @@ class RunOptions(BaseModel): max_total_charge_usd: Annotated[float | None, Field(alias='maxTotalChargeUsd', examples=[5], ge=0.0)] = None -RunOrigin: TypeAlias = Literal[ - 'DEVELOPMENT', - 'WEB', - 'API', - 'SCHEDULER', - 'TEST', - 'WEBHOOK', - 'ACTOR', - 'CLI', - 'STANDBY', -] - - @docs_group('Models') class RunResponse(BaseModel): model_config = ConfigDict( @@ -3265,12 +2828,6 @@ class SourceCodeFile(BaseModel): name: Annotated[str, Field(examples=['src/main.js'])] -SourceCodeFileFormat: TypeAlias = Literal[ - 'BASE64', - 'TEXT', -] - - @docs_group('Models') class SourceCodeFolder(BaseModel): """Represents a folder in the Actor's source code structure. Distinguished from @@ -3314,12 +2871,6 @@ class StorageIds(BaseModel): """ -StorageOwnership: TypeAlias = Literal[ - 'ownedByMe', - 'sharedWithMe', -] - - @docs_group('Models') class Storages(BaseModel): model_config = ConfigDict( @@ -3813,14 +3364,6 @@ class VersionResponse(BaseModel): data: Version -VersionSourceType: TypeAlias = Literal[ - 'SOURCE_FILES', - 'GIT_REPO', - 'TARBALL', - 'GITHUB_GIST', -] - - @docs_group('Models') class Webhook(BaseModel): model_config = ConfigDict( @@ -3917,31 +3460,6 @@ class WebhookDispatchResponse(BaseModel): data: WebhookDispatch -WebhookDispatchStatus: TypeAlias = Literal[ - 'ACTIVE', - 'SUCCEEDED', - 'FAILED', -] -"""Status of the webhook dispatch indicating whether the HTTP request was successful.""" - - -WebhookEventType: TypeAlias = 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.""" - - @docs_group('Models') class WebhookResponse(BaseModel): """Response containing webhook data.""" diff --git a/src/apify_client/_resource_clients/_resource_client.py b/src/apify_client/_resource_clients/_resource_client.py index 7a2f84bf..36e2ada6 100644 --- a/src/apify_client/_resource_clients/_resource_client.py +++ b/src/apify_client/_resource_clients/_resource_client.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: from apify_client._client_registry import ClientRegistry, ClientRegistryAsync from apify_client._http_clients import HttpClient, HttpClientAsync - from apify_client._types import Timeout + from apify_client._literals import Timeout class ResourceClientBase(metaclass=WithLogDetailsClient): diff --git a/src/apify_client/_resource_clients/actor.py b/src/apify_client/_resource_clients/actor.py index cb5d5c1a..77ea3548 100644 --- a/src/apify_client/_resource_clients/actor.py +++ b/src/apify_client/_resource_clients/actor.py @@ -8,7 +8,6 @@ from apify_client._models import WebhookRepresentationList from apify_client._models_generated import ( Actor, - ActorPermissionLevel, ActorResponse, ActorStandby, Build, @@ -21,7 +20,6 @@ PayPerEventActorPricingInfo, PricePerDatasetItemActorPricingInfo, Run, - RunOrigin, RunResponse, UpdateActorRequest, ) @@ -33,7 +31,8 @@ from decimal import Decimal from logging import Logger - from apify_client._models_generated import ActorJobStatus + from apify_client._literals import Timeout, WebhooksList + from apify_client._literals_generated import ActorJobStatus, ActorPermissionLevel, RunOrigin from apify_client._resource_clients import ( ActorVersionClient, ActorVersionClientAsync, @@ -50,7 +49,6 @@ WebhookCollectionClient, WebhookCollectionClientAsync, ) - from apify_client._types import Timeout, WebhooksList _PricingInfo = ( PayPerEventActorPricingInfo diff --git a/src/apify_client/_resource_clients/actor_collection.py b/src/apify_client/_resource_clients/actor_collection.py index 63a780df..a452776c 100644 --- a/src/apify_client/_resource_clients/actor_collection.py +++ b/src/apify_client/_resource_clients/actor_collection.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: from datetime import timedelta - from apify_client._types import Timeout + from apify_client._literals import Timeout _SORT_BY_TO_API: dict[str, str] = { 'created_at': 'createdAt', diff --git a/src/apify_client/_resource_clients/actor_env_var.py b/src/apify_client/_resource_clients/actor_env_var.py index ffb3b571..42a75e1c 100644 --- a/src/apify_client/_resource_clients/actor_env_var.py +++ b/src/apify_client/_resource_clients/actor_env_var.py @@ -7,7 +7,7 @@ from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync if TYPE_CHECKING: - from apify_client._types import Timeout + from apify_client._literals import Timeout @docs_group('Resource clients') diff --git a/src/apify_client/_resource_clients/actor_env_var_collection.py b/src/apify_client/_resource_clients/actor_env_var_collection.py index d4eb2af5..623a89ba 100644 --- a/src/apify_client/_resource_clients/actor_env_var_collection.py +++ b/src/apify_client/_resource_clients/actor_env_var_collection.py @@ -7,7 +7,7 @@ from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync if TYPE_CHECKING: - from apify_client._types import Timeout + from apify_client._literals import Timeout @docs_group('Resource clients') diff --git a/src/apify_client/_resource_clients/actor_version.py b/src/apify_client/_resource_clients/actor_version.py index 2383fb34..cd28c9f7 100644 --- a/src/apify_client/_resource_clients/actor_version.py +++ b/src/apify_client/_resource_clients/actor_version.py @@ -12,18 +12,18 @@ SourceCodeFolder, Version, VersionResponse, - VersionSourceType, ) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync if TYPE_CHECKING: + from apify_client._literals import Timeout + from apify_client._literals_generated import VersionSourceType from apify_client._resource_clients import ( ActorEnvVarClient, ActorEnvVarClientAsync, ActorEnvVarCollectionClient, ActorEnvVarCollectionClientAsync, ) - from apify_client._types import Timeout _source_file_list_adapter = TypeAdapter(list[SourceCodeFile | SourceCodeFolder]) diff --git a/src/apify_client/_resource_clients/actor_version_collection.py b/src/apify_client/_resource_clients/actor_version_collection.py index 0d0b5b65..375c3a32 100644 --- a/src/apify_client/_resource_clients/actor_version_collection.py +++ b/src/apify_client/_resource_clients/actor_version_collection.py @@ -14,12 +14,12 @@ SourceCodeFolder, Version, VersionResponse, - VersionSourceType, ) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync if TYPE_CHECKING: - from apify_client._types import Timeout + from apify_client._literals import Timeout + from apify_client._literals_generated import VersionSourceType _source_file_list_adapter = TypeAdapter(list[SourceCodeFile | SourceCodeFolder]) diff --git a/src/apify_client/_resource_clients/build.py b/src/apify_client/_resource_clients/build.py index 9b255351..716440a1 100644 --- a/src/apify_client/_resource_clients/build.py +++ b/src/apify_client/_resource_clients/build.py @@ -10,8 +10,8 @@ if TYPE_CHECKING: from datetime import timedelta + from apify_client._literals import Timeout from apify_client._resource_clients import LogClient, LogClientAsync - from apify_client._types import Timeout @docs_group('Resource clients') diff --git a/src/apify_client/_resource_clients/build_collection.py b/src/apify_client/_resource_clients/build_collection.py index 6ead2a67..28ac5692 100644 --- a/src/apify_client/_resource_clients/build_collection.py +++ b/src/apify_client/_resource_clients/build_collection.py @@ -7,7 +7,7 @@ from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync if TYPE_CHECKING: - from apify_client._types import Timeout + from apify_client._literals import Timeout @docs_group('Resource clients') diff --git a/src/apify_client/_resource_clients/dataset.py b/src/apify_client/_resource_clients/dataset.py index 1ae7e945..b4805f40 100644 --- a/src/apify_client/_resource_clients/dataset.py +++ b/src/apify_client/_resource_clients/dataset.py @@ -20,8 +20,8 @@ from datetime import timedelta from apify_client._http_clients import HttpResponse - from apify_client._models_generated import GeneralAccess - from apify_client._types import JsonSerializable, Timeout + from apify_client._literals import JsonSerializable, Timeout + from apify_client._literals_generated import GeneralAccess @docs_group('Other') diff --git a/src/apify_client/_resource_clients/dataset_collection.py b/src/apify_client/_resource_clients/dataset_collection.py index 2ffb71d6..2ca85ed2 100644 --- a/src/apify_client/_resource_clients/dataset_collection.py +++ b/src/apify_client/_resource_clients/dataset_collection.py @@ -3,17 +3,17 @@ from typing import TYPE_CHECKING, Any from apify_client._docs import docs_group +from apify_client._literals_generated import _STORAGE_OWNERSHIP_WIRE_VALUES, StorageOwnership from apify_client._models_generated import ( Dataset, DatasetResponse, ListOfDatasets, ListOfDatasetsResponse, - StorageOwnership, ) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync if TYPE_CHECKING: - from apify_client._types import Timeout + from apify_client._literals import Timeout @docs_group('Resource clients') @@ -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. `'owned_by_me'` returns only user's own datasets, + `'shared_with_me'` 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=_STORAGE_OWNERSHIP_WIRE_VALUES[ownership] if ownership is not None else None, ) 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. `'owned_by_me'` returns only user's own datasets, + `'shared_with_me'` 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=_STORAGE_OWNERSHIP_WIRE_VALUES[ownership] if ownership is not None else None, ) 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 6c388b49..9917ed24 100644 --- a/src/apify_client/_resource_clients/key_value_store.py +++ b/src/apify_client/_resource_clients/key_value_store.py @@ -29,8 +29,8 @@ from datetime import timedelta from apify_client._http_clients import HttpResponse - from apify_client._models_generated import GeneralAccess - from apify_client._types import Timeout + from apify_client._literals import Timeout + from apify_client._literals_generated import GeneralAccess def _parse_get_record_response(response: HttpResponse) -> Any: 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 f221a192..38f329a3 100644 --- a/src/apify_client/_resource_clients/key_value_store_collection.py +++ b/src/apify_client/_resource_clients/key_value_store_collection.py @@ -3,17 +3,17 @@ from typing import TYPE_CHECKING, Any from apify_client._docs import docs_group +from apify_client._literals_generated import _STORAGE_OWNERSHIP_WIRE_VALUES, StorageOwnership from apify_client._models_generated import ( KeyValueStore, KeyValueStoreResponse, ListOfKeyValueStores, ListOfKeyValueStoresResponse, - StorageOwnership, ) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync if TYPE_CHECKING: - from apify_client._types import Timeout + from apify_client._literals import Timeout @docs_group('Resource clients') @@ -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. `'owned_by_me'` returns only user's own key-value stores, + `'shared_with_me'` 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=_STORAGE_OWNERSHIP_WIRE_VALUES[ownership] if ownership is not None else None, ) 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. `'owned_by_me'` returns only user's own key-value stores, + `'shared_with_me'` 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=_STORAGE_OWNERSHIP_WIRE_VALUES[ownership] if ownership is not None else None, ) return ListOfKeyValueStoresResponse.model_validate(result).data diff --git a/src/apify_client/_resource_clients/log.py b/src/apify_client/_resource_clients/log.py index e92bd037..d4322166 100644 --- a/src/apify_client/_resource_clients/log.py +++ b/src/apify_client/_resource_clients/log.py @@ -12,7 +12,7 @@ from collections.abc import AsyncIterator, Iterator from apify_client._http_clients import HttpResponse - from apify_client._types import Timeout + from apify_client._literals import Timeout @docs_group('Resource clients') diff --git a/src/apify_client/_resource_clients/request_queue.py b/src/apify_client/_resource_clients/request_queue.py index 90aa746d..c5c2d874 100644 --- a/src/apify_client/_resource_clients/request_queue.py +++ b/src/apify_client/_resource_clients/request_queue.py @@ -42,10 +42,10 @@ if TYPE_CHECKING: from datetime import timedelta - from apify_client._models_generated import GeneralAccess + from apify_client._literals import Timeout + from apify_client._literals_generated import GeneralAccess from apify_client._typeddicts import RequestDeleteInputDict, RequestInputDict from apify_client._typeddicts_generated import RequestDict - from apify_client._types import Timeout _RQ_MAX_REQUESTS_PER_BATCH = 25 _MAX_PAYLOAD_SIZE_BYTES = 9 * 1024 * 1024 # 9 MB diff --git a/src/apify_client/_resource_clients/request_queue_collection.py b/src/apify_client/_resource_clients/request_queue_collection.py index 1d06fcbc..ffe4a74e 100644 --- a/src/apify_client/_resource_clients/request_queue_collection.py +++ b/src/apify_client/_resource_clients/request_queue_collection.py @@ -3,17 +3,17 @@ from typing import TYPE_CHECKING, Any from apify_client._docs import docs_group +from apify_client._literals_generated import _STORAGE_OWNERSHIP_WIRE_VALUES, StorageOwnership from apify_client._models_generated import ( ListOfRequestQueues, ListOfRequestQueuesResponse, RequestQueue, RequestQueueResponse, - StorageOwnership, ) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync if TYPE_CHECKING: - from apify_client._types import Timeout + from apify_client._literals import Timeout @docs_group('Resource clients') @@ -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. `'owned_by_me'` returns only user's own request queues, + `'shared_with_me'` 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=_STORAGE_OWNERSHIP_WIRE_VALUES[ownership] if ownership is not None else None, ) 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. `'owned_by_me'` returns only user's own request queues, + `'shared_with_me'` 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=_STORAGE_OWNERSHIP_WIRE_VALUES[ownership] if ownership is not None else None, ) 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 1d0dc49d..78ba64ea 100644 --- a/src/apify_client/_resource_clients/run.py +++ b/src/apify_client/_resource_clients/run.py @@ -19,7 +19,8 @@ import logging from decimal import Decimal - from apify_client._models_generated import GeneralAccess + from apify_client._literals import Timeout + from apify_client._literals_generated import GeneralAccess from apify_client._resource_clients import ( DatasetClient, DatasetClientAsync, @@ -30,7 +31,6 @@ RequestQueueClient, RequestQueueClientAsync, ) - from apify_client._types import Timeout @docs_group('Resource clients') diff --git a/src/apify_client/_resource_clients/run_collection.py b/src/apify_client/_resource_clients/run_collection.py index b63b3fc9..7d8b8daa 100644 --- a/src/apify_client/_resource_clients/run_collection.py +++ b/src/apify_client/_resource_clients/run_collection.py @@ -9,8 +9,8 @@ if TYPE_CHECKING: from datetime import datetime - from apify_client._models_generated import ActorJobStatus - from apify_client._types import Timeout + from apify_client._literals import Timeout + from apify_client._literals_generated import ActorJobStatus @docs_group('Resource clients') diff --git a/src/apify_client/_resource_clients/schedule.py b/src/apify_client/_resource_clients/schedule.py index a3cd192f..48eca5db 100644 --- a/src/apify_client/_resource_clients/schedule.py +++ b/src/apify_client/_resource_clients/schedule.py @@ -14,7 +14,7 @@ from apify_client._utils import response_to_dict if TYPE_CHECKING: - from apify_client._types import Timeout + from apify_client._literals import Timeout @docs_group('Resource clients') diff --git a/src/apify_client/_resource_clients/schedule_collection.py b/src/apify_client/_resource_clients/schedule_collection.py index 1421d257..c212bbd5 100644 --- a/src/apify_client/_resource_clients/schedule_collection.py +++ b/src/apify_client/_resource_clients/schedule_collection.py @@ -13,7 +13,7 @@ from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync if TYPE_CHECKING: - from apify_client._types import Timeout + from apify_client._literals import Timeout @docs_group('Resource clients') diff --git a/src/apify_client/_resource_clients/store_collection.py b/src/apify_client/_resource_clients/store_collection.py index 9c80ad31..6d09c922 100644 --- a/src/apify_client/_resource_clients/store_collection.py +++ b/src/apify_client/_resource_clients/store_collection.py @@ -7,7 +7,7 @@ from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync if TYPE_CHECKING: - from apify_client._types import Timeout + from apify_client._literals import Timeout @docs_group('Resource clients') diff --git a/src/apify_client/_resource_clients/task.py b/src/apify_client/_resource_clients/task.py index 77479b10..dabaf097 100644 --- a/src/apify_client/_resource_clients/task.py +++ b/src/apify_client/_resource_clients/task.py @@ -7,7 +7,6 @@ from apify_client._models_generated import ( ActorStandby, Run, - RunOrigin, RunResponse, Task, TaskInput, @@ -21,7 +20,8 @@ if TYPE_CHECKING: from datetime import timedelta - from apify_client._models_generated import ActorJobStatus + from apify_client._literals import Timeout, WebhooksList + from apify_client._literals_generated import ActorJobStatus, RunOrigin from apify_client._resource_clients import ( RunClient, RunClientAsync, @@ -31,7 +31,6 @@ WebhookCollectionClientAsync, ) from apify_client._typeddicts_generated import TaskInputDict - from apify_client._types import Timeout, WebhooksList @docs_group('Resource clients') diff --git a/src/apify_client/_resource_clients/task_collection.py b/src/apify_client/_resource_clients/task_collection.py index 44c46c9b..27adff86 100644 --- a/src/apify_client/_resource_clients/task_collection.py +++ b/src/apify_client/_resource_clients/task_collection.py @@ -19,8 +19,8 @@ if TYPE_CHECKING: from datetime import timedelta + from apify_client._literals import Timeout from apify_client._typeddicts_generated import TaskInputDict - from apify_client._types import Timeout @docs_group('Resource clients') diff --git a/src/apify_client/_resource_clients/user.py b/src/apify_client/_resource_clients/user.py index 18325671..5628bbfd 100644 --- a/src/apify_client/_resource_clients/user.py +++ b/src/apify_client/_resource_clients/user.py @@ -19,7 +19,7 @@ from apify_client._utils import response_to_dict if TYPE_CHECKING: - from apify_client._types import Timeout + from apify_client._literals import Timeout @docs_group('Resource clients') diff --git a/src/apify_client/_resource_clients/webhook.py b/src/apify_client/_resource_clients/webhook.py index a22b0064..5f2795bf 100644 --- a/src/apify_client/_resource_clients/webhook.py +++ b/src/apify_client/_resource_clients/webhook.py @@ -17,9 +17,9 @@ from apify_client._utils import response_to_dict if TYPE_CHECKING: - from apify_client._models_generated import WebhookEventType + from apify_client._literals import Timeout + from apify_client._literals_generated import WebhookEventType from apify_client._resource_clients import WebhookDispatchCollectionClient, WebhookDispatchCollectionClientAsync - from apify_client._types import Timeout @docs_group('Resource clients') diff --git a/src/apify_client/_resource_clients/webhook_collection.py b/src/apify_client/_resource_clients/webhook_collection.py index 12834ce1..00cb2a14 100644 --- a/src/apify_client/_resource_clients/webhook_collection.py +++ b/src/apify_client/_resource_clients/webhook_collection.py @@ -13,8 +13,9 @@ from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync if TYPE_CHECKING: - from apify_client._models_generated import Webhook, WebhookEventType - from apify_client._types import Timeout + from apify_client._literals import Timeout + from apify_client._literals_generated import WebhookEventType + from apify_client._models_generated import Webhook @docs_group('Resource clients') diff --git a/src/apify_client/_resource_clients/webhook_dispatch.py b/src/apify_client/_resource_clients/webhook_dispatch.py index a8405a87..0a5ed9b4 100644 --- a/src/apify_client/_resource_clients/webhook_dispatch.py +++ b/src/apify_client/_resource_clients/webhook_dispatch.py @@ -7,7 +7,7 @@ from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync if TYPE_CHECKING: - from apify_client._types import Timeout + from apify_client._literals import Timeout @docs_group('Resource clients') diff --git a/src/apify_client/_resource_clients/webhook_dispatch_collection.py b/src/apify_client/_resource_clients/webhook_dispatch_collection.py index dbea43aa..6025f542 100644 --- a/src/apify_client/_resource_clients/webhook_dispatch_collection.py +++ b/src/apify_client/_resource_clients/webhook_dispatch_collection.py @@ -7,7 +7,7 @@ from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync if TYPE_CHECKING: - from apify_client._types import Timeout + from apify_client._literals import Timeout @docs_group('Resource clients') diff --git a/tests/unit/test_logging.py b/tests/unit/test_logging.py index 9fb64f75..cbecb746 100644 --- a/tests/unit/test_logging.py +++ b/tests/unit/test_logging.py @@ -22,7 +22,7 @@ from _pytest.logging import LogCaptureFixture from pytest_httpserver import HTTPServer - from apify_client._models_generated import ActorJobStatus + from apify_client._literals_generated import ActorJobStatus _MOCKED_RUN_ID = 'mocked_run_id' _MOCKED_ACTOR_NAME = 'mocked_actor_name' diff --git a/tests/unit/test_pluggable_http_client.py b/tests/unit/test_pluggable_http_client.py index 04711f85..8a0e9549 100644 --- a/tests/unit/test_pluggable_http_client.py +++ b/tests/unit/test_pluggable_http_client.py @@ -21,7 +21,7 @@ from pytest_httpserver import HTTPServer - from apify_client._types import Timeout + from apify_client._literals import Timeout # -- Test response and client implementations -- diff --git a/tests/unit/test_postprocess_generated_models.py b/tests/unit/test_postprocess_generated_models.py index b43fe18a..8bc6b40e 100644 --- a/tests/unit/test_postprocess_generated_models.py +++ b/tests/unit/test_postprocess_generated_models.py @@ -7,6 +7,8 @@ convert_enums_to_literals, deduplicate_error_type_enum, fix_discriminators, + snake_case_camelcase_literal_values, + split_literals_to_file, ) # -- fix_discriminators ------------------------------------------------------- @@ -425,3 +427,165 @@ class Alpha(BaseModel): # TypeAlias was imported. assert 'TypeAlias' in result + + +# -- split_literals_to_file --------------------------------------------------- + + +def test_split_literals_to_file_moves_literal_aliases() -> None: + content = textwrap.dedent("""\ + from __future__ import annotations + + from typing import Literal, TypeAlias + + from pydantic import BaseModel + + from apify_client._docs import docs_group + + + class Alpha(BaseModel): + status: Status + + + Status: TypeAlias = Literal[ + 'READY', + 'RUNNING', + ] + \"\"\"Alpha status docstring.\"\"\" + """) + models, literals = split_literals_to_file(content) + + assert 'Status: TypeAlias = Literal[' not in models + assert 'from apify_client._literals_generated import Status' in models + assert 'status: Status' in models # field annotation still references the name + + assert 'Status: TypeAlias = Literal[' in literals + assert "'READY'" in literals + assert '"""Alpha status docstring."""' in literals + + +def test_split_literals_to_file_handles_multiple_aliases() -> None: + content = textwrap.dedent("""\ + from __future__ import annotations + + from typing import Literal, TypeAlias + + from apify_client._docs import docs_group + + + A: TypeAlias = Literal[ + 'x', + ] + + B: TypeAlias = Literal[ + 'y', + ] + """) + models, literals = split_literals_to_file(content) + + assert 'A: TypeAlias' not in models + assert 'B: TypeAlias' not in models + assert 'from apify_client._literals_generated import A, B' in models + + assert "A: TypeAlias = Literal[\n 'x',\n]" in literals + assert "B: TypeAlias = Literal[\n 'y',\n]" in literals + + +def test_split_literals_to_file_no_literals_returns_original() -> None: + 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: + content = textwrap.dedent("""\ + from __future__ import annotations + + from typing import Literal, TypeAlias + + from apify_client._docs import docs_group + + + A: TypeAlias = Literal['x'] + """) + _, literals = split_literals_to_file(content) + assert 'from __future__ import annotations' in literals + assert 'from typing import Literal, TypeAlias' in literals + + +# -- snake_case_camelcase_literal_values -------------------------------------- + + +def test_snake_case_camelcase_literal_values_converts_values_and_emits_mapping() -> None: + content = textwrap.dedent("""\ + Ownership: TypeAlias = Literal[ + 'ownedByMe', + 'sharedWithMe', + ] + \"\"\"Ownership docstring.\"\"\" + """) + result = snake_case_camelcase_literal_values(content) + + assert "'owned_by_me'" in result + assert "'shared_with_me'" in result + assert "'ownedByMe'" in result # preserved inside the wire mapping + assert '_OWNERSHIP_WIRE_VALUES: dict[Ownership, str]' in result + assert '"""Ownership docstring."""' in result # alias docstring still above the mapping + + # The mapping should come AFTER the alias's trailing docstring. + alias_docstring_idx = result.index('"""Ownership docstring."""') + mapping_idx = result.index('_OWNERSHIP_WIRE_VALUES') + assert alias_docstring_idx < mapping_idx + + +def test_snake_case_camelcase_literal_values_ignores_non_camelcase_values() -> None: + content = textwrap.dedent("""\ + Status: TypeAlias = Literal[ + 'READY', + 'TIMED-OUT', + 'ACTOR.RUN.CREATED', + ] + """) + result = snake_case_camelcase_literal_values(content) + + assert result == content # untouched + assert 'WIRE_VALUES' not in result + + +def test_snake_case_camelcase_literal_values_handles_multiple_aliases() -> None: + content = textwrap.dedent("""\ + A: TypeAlias = Literal[ + 'fooBar', + ] + + B: TypeAlias = Literal[ + 'UPPER_CASE', + ] + + C: TypeAlias = Literal[ + 'bazQux', + ] + """) + result = snake_case_camelcase_literal_values(content) + + assert "'foo_bar'" in result + assert "'baz_qux'" in result + assert '_A_WIRE_VALUES' in result + assert '_C_WIRE_VALUES' in result + assert '_B_WIRE_VALUES' not in result # B had no camelCase values, no mapping emitted + + +def test_snake_case_camelcase_literal_values_no_change_when_no_aliases() -> None: + content = 'x = 1\n' + assert snake_case_camelcase_literal_values(content) == content diff --git a/tests/unit/test_storage_collection_listing.py b/tests/unit/test_storage_collection_listing.py index feaf483f..58423478 100644 --- a/tests/unit/test_storage_collection_listing.py +++ b/tests/unit/test_storage_collection_listing.py @@ -47,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='ownedByMe') + result = client.datasets().list(ownership='owned_by_me') assert result.total == 0 assert captured['args']['ownership'] == 'ownedByMe' @@ -58,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='sharedWithMe') + result = await client.datasets().list(ownership='shared_with_me') assert result.total == 0 assert captured['args']['ownership'] == 'sharedWithMe' @@ -71,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='ownedByMe') + result = client.key_value_stores().list(ownership='owned_by_me') assert result.total == 0 assert captured['args']['ownership'] == 'ownedByMe' @@ -84,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='sharedWithMe') + result = await client.key_value_stores().list(ownership='shared_with_me') assert result.total == 0 assert captured['args']['ownership'] == 'sharedWithMe' @@ -95,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='ownedByMe') + result = client.request_queues().list(ownership='owned_by_me') assert result.total == 0 assert captured['args']['ownership'] == 'ownedByMe' @@ -106,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='sharedWithMe') + result = await client.request_queues().list(ownership='shared_with_me') assert result.total == 0 assert captured['args']['ownership'] == 'sharedWithMe' From 36b6927f9aa2eb96de24f8bc42138954686ff5d5 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 23 Apr 2026 15:32:20 +0200 Subject: [PATCH 3/3] refactor: drop explicit TypeAlias annotation from generated literals `X = Literal[...]` is already recognized as a type alias by every Python type checker, so the `: TypeAlias` annotation adds noise without adding any semantic value. Drop it from the postprocess-generated aliases and from the hand-maintained `TerminalActorJobStatus`. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/postprocess_generated_models.py | 35 +++---- src/apify_client/_literals.py | 4 +- src/apify_client/_literals_generated.py | 24 ++--- .../unit/test_postprocess_generated_models.py | 92 +++++++------------ 4 files changed, 65 insertions(+), 90 deletions(-) diff --git a/scripts/postprocess_generated_models.py b/scripts/postprocess_generated_models.py index 109d3683..f5657e5f 100644 --- a/scripts/postprocess_generated_models.py +++ b/scripts/postprocess_generated_models.py @@ -4,12 +4,12 @@ - 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: TypeAlias = 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 `X = Literal[...]` so downstream code can pass plain strings + (and reuse the named alias in resource-client signatures) instead of enum members. - Convert camelCase string values in each literal alias to snake_case (Pythonic), and emit a `__WIRE_VALUES` mapping the Python value back to the original camelCase form so the resource clients can still produce the exact string the API expects on the wire. -- Move the resulting `TypeAlias = Literal[...]` definitions into `_literals_generated.py`, leaving +- Move the resulting `X = Literal[...]` definitions into `_literals_generated.py`, leaving `_models_generated.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). @@ -107,7 +107,7 @@ def deduplicate_error_type_enum(content: str) -> str: def convert_enums_to_literals(content: str) -> str: - """Rewrite every `class X(StrEnum): ...` into an `X: TypeAlias = Literal[...]` alias. + """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 @@ -139,7 +139,7 @@ def convert_enums_to_literals(content: str) -> str: ] docstring = ast.get_docstring(node) - new_lines: list[str] = [f'{node.name}: TypeAlias = Literal['] + 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: @@ -160,7 +160,7 @@ def convert_enums_to_literals(content: str) -> str: for start, end, new in sorted(replacements, key=lambda r: r[0], reverse=True): lines[start:end] = new - return _ensure_typing_import(_collapse_blank_lines('\n'.join(lines)), 'TypeAlias') + return _collapse_blank_lines('\n'.join(lines)) LITERALS_FILE_HEADER = """\ @@ -168,7 +168,7 @@ def convert_enums_to_literals(content: str) -> str: from __future__ import annotations -from typing import Literal, TypeAlias +from typing import Literal """ @@ -182,12 +182,11 @@ def _camel_to_snake(value: str) -> str: def _is_literal_alias(node: ast.stmt) -> bool: - """Return True if `node` is a top-level `Name: TypeAlias = Literal[...]` statement.""" + """Return True if `node` is a top-level `Name = Literal[...]` statement.""" return ( - isinstance(node, ast.AnnAssign) - and isinstance(node.target, ast.Name) - and isinstance(node.annotation, ast.Name) - and node.annotation.id == 'TypeAlias' + 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' @@ -197,9 +196,9 @@ def _is_literal_alias(node: ast.stmt) -> bool: def snake_case_camelcase_literal_values(content: str) -> str: """Rewrite camelCase string values in `Literal[...]` aliases into snake_case. - Scans each `Name: TypeAlias = Literal[...]` block and, for any value matching the camelCase - pattern (lowercase-first followed by an uppercase letter), converts it to snake_case. For each - alias that had at least one conversion, emits a `__WIRE_VALUES: dict[, str] = ...` + Scans each `Name = Literal[...]` block and, for any value matching the camelCase pattern + (lowercase-first followed by an uppercase letter), converts it to snake_case. For each alias + that had at least one conversion, emits a `__WIRE_VALUES: dict[, str] = ...` mapping right after the alias so consumers can translate back to the API wire format. SCREAMING_SNAKE_CASE, dotted, hyphenated, and HTTP-method values pass through unchanged. @@ -249,7 +248,7 @@ def snake_case_camelcase_literal_values(content: str) -> str: def split_literals_to_file(content: str) -> tuple[str, str]: - """Move every top-level `Name: TypeAlias = Literal[...]` block into a separate literals module. + """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_generated.py`, and rebuilds `_literals_generated.py` from the blocks @@ -370,7 +369,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 @@ -382,6 +381,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 diff --git a/src/apify_client/_literals.py b/src/apify_client/_literals.py index a7666434..fb72e08d 100644 --- a/src/apify_client/_literals.py +++ b/src/apify_client/_literals.py @@ -1,7 +1,7 @@ from __future__ import annotations from datetime import timedelta -from typing import Literal, TypeAlias +from typing import Literal from apify_client._models import WebhookRepresentation from apify_client._models_generated import WebhookCreate @@ -19,7 +19,7 @@ `condition`) are ignored at runtime. """ -TerminalActorJobStatus: TypeAlias = Literal['SUCCEEDED', 'FAILED', 'TIMED-OUT', 'ABORTED'] +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'] diff --git a/src/apify_client/_literals_generated.py b/src/apify_client/_literals_generated.py index df86459a..4079ed3f 100644 --- a/src/apify_client/_literals_generated.py +++ b/src/apify_client/_literals_generated.py @@ -2,9 +2,9 @@ from __future__ import annotations -from typing import Literal, TypeAlias +from typing import Literal -ActorJobStatus: TypeAlias = Literal[ +ActorJobStatus = Literal[ 'READY', 'RUNNING', 'SUCCEEDED', @@ -17,14 +17,14 @@ """Status of an Actor job (run or build).""" -ActorPermissionLevel: TypeAlias = Literal[ +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: TypeAlias = Literal[ +ErrorType = Literal[ '3d-secure-auth-failed', 'access-right-already-exists', 'action-not-found', @@ -418,7 +418,7 @@ """Machine-processable error type identifier.""" -GeneralAccess: TypeAlias = Literal[ +GeneralAccess = Literal[ 'ANYONE_WITH_ID_CAN_READ', 'ANYONE_WITH_NAME_CAN_READ', 'FOLLOW_USER_SETTING', @@ -427,7 +427,7 @@ """Defines the general access level for the resource.""" -HttpMethod: TypeAlias = Literal[ +HttpMethod = Literal[ 'GET', 'HEAD', 'POST', @@ -440,7 +440,7 @@ ] -RunOrigin: TypeAlias = Literal[ +RunOrigin = Literal[ 'DEVELOPMENT', 'WEB', 'API', @@ -453,13 +453,13 @@ ] -SourceCodeFileFormat: TypeAlias = Literal[ +SourceCodeFileFormat = Literal[ 'BASE64', 'TEXT', ] -StorageOwnership: TypeAlias = Literal[ +StorageOwnership = Literal[ 'owned_by_me', 'shared_with_me', ] @@ -471,7 +471,7 @@ """Maps snake_case `StorageOwnership` values to the camelCase form expected on the API wire.""" -VersionSourceType: TypeAlias = Literal[ +VersionSourceType = Literal[ 'SOURCE_FILES', 'GIT_REPO', 'TARBALL', @@ -479,7 +479,7 @@ ] -WebhookDispatchStatus: TypeAlias = Literal[ +WebhookDispatchStatus = Literal[ 'ACTIVE', 'SUCCEEDED', 'FAILED', @@ -487,7 +487,7 @@ """Status of the webhook dispatch indicating whether the HTTP request was successful.""" -WebhookEventType: TypeAlias = Literal[ +WebhookEventType = Literal[ 'ACTOR.BUILD.ABORTED', 'ACTOR.BUILD.CREATED', 'ACTOR.BUILD.FAILED', diff --git a/tests/unit/test_postprocess_generated_models.py b/tests/unit/test_postprocess_generated_models.py index 8bc6b40e..73bd75a7 100644 --- a/tests/unit/test_postprocess_generated_models.py +++ b/tests/unit/test_postprocess_generated_models.py @@ -238,7 +238,7 @@ def test_add_docs_group_decorators_no_classes() -> None: def test_convert_enums_to_literals_replaces_single_enum() -> None: content = textwrap.dedent("""\ from enum import StrEnum - from typing import Literal, TypeAlias + from typing import Literal class Status(StrEnum): READY = 'READY' @@ -246,14 +246,14 @@ class Status(StrEnum): """) result = convert_enums_to_literals(content) assert 'class Status(StrEnum)' not in result - assert 'Status: TypeAlias = Literal[' 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: content = textwrap.dedent("""\ - from typing import Literal, TypeAlias + from typing import Literal class Status(StrEnum): SECOND = 'second' @@ -269,7 +269,7 @@ class Status(StrEnum): def test_convert_enums_to_literals_preserves_docstring() -> None: content = textwrap.dedent("""\ - from typing import Literal, TypeAlias + from typing import Literal class Status(StrEnum): \"\"\"Describes a status.\"\"\" @@ -279,14 +279,14 @@ class Status(StrEnum): 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: TypeAlias') + 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: content = textwrap.dedent("""\ - from typing import Literal, TypeAlias + from typing import Literal class Status(StrEnum): TIMED_OUT = 'TIMED-OUT' @@ -301,7 +301,7 @@ class Status(StrEnum): def test_convert_enums_to_literals_handles_multiple_enums() -> None: content = textwrap.dedent("""\ - from typing import Literal, TypeAlias + from typing import Literal class Alpha(StrEnum): A = 'a' @@ -310,15 +310,15 @@ class Beta(StrEnum): B = 'b' """) result = convert_enums_to_literals(content) - assert 'Alpha: TypeAlias = Literal[' in result - assert 'Beta: TypeAlias = Literal[' in result + 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: content = textwrap.dedent("""\ - from typing import Literal, TypeAlias + from typing import Literal class Foo(BaseModel): name: str @@ -331,32 +331,9 @@ class Status(StrEnum): assert 'class Status(StrEnum)' not in result -def test_convert_enums_to_literals_injects_typealias_import() -> None: - content = textwrap.dedent("""\ - from typing import Annotated, Literal - - class Status(StrEnum): - A = 'a' - """) - result = convert_enums_to_literals(content) - assert 'TypeAlias' in result - assert 'from typing import Annotated, Literal, TypeAlias' in result - - -def test_convert_enums_to_literals_leaves_typealias_import_alone_when_present() -> None: - content = textwrap.dedent("""\ - from typing import Literal, TypeAlias - - class Status(StrEnum): - A = 'a' - """) - result = convert_enums_to_literals(content) - assert result.count('TypeAlias') == 2 # one in import, one in the new alias - - def test_convert_enums_to_literals_no_change_when_no_enums() -> None: content = textwrap.dedent("""\ - from typing import Literal, TypeAlias + from typing import Literal class Foo(BaseModel): name: str @@ -367,7 +344,7 @@ class Foo(BaseModel): def test_convert_enums_to_literals_field_references_still_resolve() -> None: content = textwrap.dedent("""\ - from typing import Literal, TypeAlias + from typing import Literal class Foo(BaseModel): status: Status @@ -379,7 +356,7 @@ class Status(StrEnum): 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: TypeAlias = Literal[' in result + assert 'Status = Literal[' in result # -- Integration: full pipeline ----------------------------------------------- @@ -419,15 +396,12 @@ class Alpha(BaseModel): # 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: TypeAlias = Literal[' in result + assert 'ErrorType = Literal[' in result assert 'error_type: ErrorType' in result # Decorators added to real models but not to the type alias. assert result.count("@docs_group('Models')") == 3 # Zebra, ErrorResponse, Alpha - # TypeAlias was imported. - assert 'TypeAlias' in result - # -- split_literals_to_file --------------------------------------------------- @@ -436,7 +410,7 @@ def test_split_literals_to_file_moves_literal_aliases() -> None: content = textwrap.dedent("""\ from __future__ import annotations - from typing import Literal, TypeAlias + from typing import Literal from pydantic import BaseModel @@ -447,7 +421,7 @@ class Alpha(BaseModel): status: Status - Status: TypeAlias = Literal[ + Status = Literal[ 'READY', 'RUNNING', ] @@ -455,11 +429,11 @@ class Alpha(BaseModel): """) models, literals = split_literals_to_file(content) - assert 'Status: TypeAlias = Literal[' not in models + assert 'Status = Literal[' not in models assert 'from apify_client._literals_generated import Status' in models assert 'status: Status' in models # field annotation still references the name - assert 'Status: TypeAlias = Literal[' in literals + assert 'Status = Literal[' in literals assert "'READY'" in literals assert '"""Alpha status docstring."""' in literals @@ -468,27 +442,27 @@ def test_split_literals_to_file_handles_multiple_aliases() -> None: content = textwrap.dedent("""\ from __future__ import annotations - from typing import Literal, TypeAlias + from typing import Literal from apify_client._docs import docs_group - A: TypeAlias = Literal[ + A = Literal[ 'x', ] - B: TypeAlias = Literal[ + B = Literal[ 'y', ] """) models, literals = split_literals_to_file(content) - assert 'A: TypeAlias' not in models - assert 'B: TypeAlias' not in models + assert 'A = Literal[' not in models + assert 'B = Literal[' not in models assert 'from apify_client._literals_generated import A, B' in models - assert "A: TypeAlias = Literal[\n 'x',\n]" in literals - assert "B: TypeAlias = Literal[\n 'y',\n]" in literals + 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: @@ -512,16 +486,16 @@ def test_split_literals_to_file_output_has_valid_header() -> None: content = textwrap.dedent("""\ from __future__ import annotations - from typing import Literal, TypeAlias + from typing import Literal from apify_client._docs import docs_group - A: TypeAlias = Literal['x'] + A = Literal['x'] """) _, literals = split_literals_to_file(content) assert 'from __future__ import annotations' in literals - assert 'from typing import Literal, TypeAlias' in literals + assert 'from typing import Literal' in literals # -- snake_case_camelcase_literal_values -------------------------------------- @@ -529,7 +503,7 @@ def test_split_literals_to_file_output_has_valid_header() -> None: def test_snake_case_camelcase_literal_values_converts_values_and_emits_mapping() -> None: content = textwrap.dedent("""\ - Ownership: TypeAlias = Literal[ + Ownership = Literal[ 'ownedByMe', 'sharedWithMe', ] @@ -551,7 +525,7 @@ def test_snake_case_camelcase_literal_values_converts_values_and_emits_mapping() def test_snake_case_camelcase_literal_values_ignores_non_camelcase_values() -> None: content = textwrap.dedent("""\ - Status: TypeAlias = Literal[ + Status = Literal[ 'READY', 'TIMED-OUT', 'ACTOR.RUN.CREATED', @@ -565,15 +539,15 @@ def test_snake_case_camelcase_literal_values_ignores_non_camelcase_values() -> N def test_snake_case_camelcase_literal_values_handles_multiple_aliases() -> None: content = textwrap.dedent("""\ - A: TypeAlias = Literal[ + A = Literal[ 'fooBar', ] - B: TypeAlias = Literal[ + B = Literal[ 'UPPER_CASE', ] - C: TypeAlias = Literal[ + C = Literal[ 'bazQux', ] """)