From a3cf9cf74ae8369000fe96d4833d3655307dde4c Mon Sep 17 00:00:00 2001 From: CodeMash Date: Mon, 12 Oct 2026 13:52:54 +0300 Subject: [PATCH 1/2] =?UTF-8?q?chore(types):=20regenerate=20from=20gateway?= =?UTF-8?q?=20audit/code=20=E2=80=94=20Code=20functions,=20envelope=20type?= =?UTF-8?q?s,=20Fake=20provider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit references/hub_dtos.py refreshed in place with x python (BaseUrl swapped to the campaign gateway's ephemeral port, header restored): the five code/functions requests, the CodeFunction* envelope types, FunctionRefDto, TriggerActionCodeDto.function_ref, CodeProvider.Fake, FakeCodeIntegration*. AiScopeDto and OAuthAuthorizeDecisionRequest drop out — the gateway no longer has them. references/api_dtos.py kept: the Api metadata no longer exports the 18 JsonSchemaFieldDto subclasses. hub/code.py has no methods for the new routes: scripts/generate_endpoints.py is not on disk. Co-Authored-By: Claude Fable 5.1 --- references/hub_dtos.py | 1420 ++++++++++++++++++++++++++++++++++------ 1 file changed, 1219 insertions(+), 201 deletions(-) diff --git a/references/hub_dtos.py b/references/hub_dtos.py index 8f10383..2cf6fc0 100644 --- a/references/hub_dtos.py +++ b/references/hub_dtos.py @@ -1,5 +1,5 @@ """ Options: -Date: 2026-10-07 10:03:25 +Date: 2026-10-12 13:41:44 Version: 10.20 Tip: To override a DTO option, remove "#" prefix before updating BaseUrl: http://localhost:5001 @@ -240,6 +240,8 @@ class SaveTriggerRequest: description: Optional[str] = None is_enabled: bool = False pre_execute_code: Optional[str] = None + order: Optional[int] = None + break_on_error: bool = False action: Optional[TriggerActionDto] = None @@ -897,19 +899,57 @@ class PushIntegrationRequest: @dataclass class EdgeWebPushIntegrationRequest(PushIntegrationRequest): provider: Optional[PushProvider] = None + # @ApiMember(Description="VAPID public key (URL-safe Base-64, 65 bytes), generated together with the private key — e.g. `npx web-push generate-vapid-keys`. The browser subscribes with it as applicationServerKey.", IsRequired=true) vapid_public_key: Optional[str] = None + """ + VAPID public key (URL-safe Base-64, 65 bytes), generated together with the private key — e.g. `npx web-push generate-vapid-keys`. The browser subscribes with it as applicationServerKey. + """ + + + # @ApiMember(Description="VAPID private key (URL-safe Base-64, 32 bytes). Stored as a secret.", IsRequired=true) vapid_private_key: Optional[str] = None + """ + VAPID private key (URL-safe Base-64, 32 bytes). Stored as a secret. + """ + + + # @ApiMember(Description="Contact e-mail the browser push services can reach you at, e.g. admin@example.com. An e-mail address only (no URL); it is sent as mailto:.") subject: Optional[str] = None + """ + Contact e-mail the browser push services can reach you at, e.g. admin@example.com. An e-mail address only (no URL); it is sent as mailto:. + """ @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class ChromePluginPushIntegrationRequest(PushIntegrationRequest): provider: Optional[PushProvider] = None + # @ApiMember(Description="The extension's Chrome id: 32 lower-case letters a–p, as shown on chrome://extensions.", IsRequired=true) extension_id: Optional[str] = None + """ + The extension's Chrome id: 32 lower-case letters a–p, as shown on chrome://extensions. + """ + + + # @ApiMember(Description="VAPID public key (URL-safe Base-64, 65 bytes), generated together with the private key — e.g. `npx web-push generate-vapid-keys`. The browser subscribes with it as applicationServerKey.", IsRequired=true) vapid_public_key: Optional[str] = None + """ + VAPID public key (URL-safe Base-64, 65 bytes), generated together with the private key — e.g. `npx web-push generate-vapid-keys`. The browser subscribes with it as applicationServerKey. + """ + + + # @ApiMember(Description="VAPID private key (URL-safe Base-64, 32 bytes). Stored as a secret.", IsRequired=true) vapid_private_key: Optional[str] = None + """ + VAPID private key (URL-safe Base-64, 32 bytes). Stored as a secret. + """ + + + # @ApiMember(Description="Contact e-mail the browser push services can reach you at, e.g. admin@example.com. An e-mail address only (no URL); it is sent as mailto:.") subject: Optional[str] = None + """ + Contact e-mail the browser push services can reach you at, e.g. admin@example.com. An e-mail address only (no URL); it is sent as mailto:. + """ @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -925,18 +965,50 @@ class SafariPushIntegrationRequest(PushIntegrationRequest): @dataclass class ChromeWebPushIntegrationRequest(PushIntegrationRequest): provider: Optional[PushProvider] = None + # @ApiMember(Description="VAPID public key (URL-safe Base-64, 65 bytes), generated together with the private key — e.g. `npx web-push generate-vapid-keys`. The browser subscribes with it as applicationServerKey.", IsRequired=true) vapid_public_key: Optional[str] = None + """ + VAPID public key (URL-safe Base-64, 65 bytes), generated together with the private key — e.g. `npx web-push generate-vapid-keys`. The browser subscribes with it as applicationServerKey. + """ + + + # @ApiMember(Description="VAPID private key (URL-safe Base-64, 32 bytes). Stored as a secret.", IsRequired=true) vapid_private_key: Optional[str] = None + """ + VAPID private key (URL-safe Base-64, 32 bytes). Stored as a secret. + """ + + + # @ApiMember(Description="Contact e-mail the browser push services can reach you at, e.g. admin@example.com. An e-mail address only (no URL); it is sent as mailto:.") subject: Optional[str] = None + """ + Contact e-mail the browser push services can reach you at, e.g. admin@example.com. An e-mail address only (no URL); it is sent as mailto:. + """ @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class FirefoxWebPushIntegrationRequest(PushIntegrationRequest): provider: Optional[PushProvider] = None + # @ApiMember(Description="VAPID public key (URL-safe Base-64, 65 bytes), generated together with the private key — e.g. `npx web-push generate-vapid-keys`. The browser subscribes with it as applicationServerKey.", IsRequired=true) vapid_public_key: Optional[str] = None + """ + VAPID public key (URL-safe Base-64, 65 bytes), generated together with the private key — e.g. `npx web-push generate-vapid-keys`. The browser subscribes with it as applicationServerKey. + """ + + + # @ApiMember(Description="VAPID private key (URL-safe Base-64, 32 bytes). Stored as a secret.", IsRequired=true) vapid_private_key: Optional[str] = None + """ + VAPID private key (URL-safe Base-64, 32 bytes). Stored as a secret. + """ + + + # @ApiMember(Description="Contact e-mail the browser push services can reach you at, e.g. admin@example.com. An e-mail address only (no URL); it is sent as mailto:.") subject: Optional[str] = None + """ + Contact e-mail the browser push services can reach you at, e.g. admin@example.com. An e-mail address only (no URL); it is sent as mailto:. + """ @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -970,6 +1042,7 @@ class CodeProvider(str, Enum): NETLIFY = 'Netlify' SUPABASE_EDGE = 'SupabaseEdge' MODAL = 'Modal' + FAKE = 'Fake' @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -1393,6 +1466,21 @@ class TriggerActionPushDto(TriggerActionDto): initiator_id: Optional[str] = None +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class FunctionRefDto: + integration_id: Optional[str] = None + provider: Optional[CodeProvider] = None + function_id: Optional[str] = None + qualifier: Optional[str] = None + display_name: Optional[str] = None + + +class CodeInvocationType(str, Enum): + SYNC = 'Sync' + ASYNC = 'Async' + + @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class CodeDeliverySettingsDto: @@ -1402,8 +1490,14 @@ class CodeDeliverySettingsDto: @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class TriggerActionCodeDto(TriggerActionDto): - function_id: Optional[str] = None + function_ref: Optional[FunctionRefDto] = None + run_as_user_id: Optional[str] = None + invocation_type: Optional[CodeInvocationType] = None + config: Optional[str] = None delivery_settings: Optional[CodeDeliverySettingsDto] = None + function_id: Optional[str] = None + activation_code: Optional[str] = None + meta: Optional[Dict[str, str]] = None @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -2234,6 +2328,7 @@ class LocalFilesIntegrationDto(FilesIntegrationDto): class DatabaseIntegrationDto(IntegrationDto): provider: Optional[DatabaseProvider] = None is_system_owned: bool = False + can_manage_indexes: Optional[bool] = None @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -2353,6 +2448,8 @@ class AndroidFirebasePushIntegrationDto(PushIntegrationDto): class AppleApnsPushIntegrationDto(PushIntegrationDto): team_id: Optional[str] = None app_bundle_id: Optional[str] = None + key_id: Optional[str] = None + is_production: bool = False @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -2494,6 +2591,9 @@ class SchedulerTaskDto: env: Optional[str] = None created_at_unix: Optional[int] = None updated_at_unix: Optional[int] = None + is_failing: bool = False + last_failure_reason: Optional[str] = None + last_failed_at_unix: Optional[int] = None @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -3035,6 +3135,343 @@ class ConversationSnapshotEntryWireDto(AiChatEntryWireDto): covers_up_to_seq: int = 0 +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class JsonSchemaFieldDto: + field_name: Optional[str] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class StringFieldDto(JsonSchemaFieldDto): + format: Optional[str] = None + pattern: Optional[str] = None + min_length: Optional[int] = None + max_length: Optional[int] = None + translate_options: Optional[IReadOnlyDictionary[str, str]] = None + default: Optional[str] = None + unique: Optional[bool] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class DecimalFieldDto(JsonSchemaFieldDto): + minimum: Optional[Decimal] = None + maximum: Optional[Decimal] = None + multiple_of: Optional[Decimal] = None + default: Optional[Decimal] = None + unique: Optional[bool] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class CurrencyDefaultDto: + value: Decimal = decimal.Decimal(0) + currency: Optional[str] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class CurrencyFieldDto(JsonSchemaFieldDto): + allowed_currencies: Optional[IReadOnlyList[str]] = None + multiple_of: Optional[Decimal] = None + minimum: Optional[Decimal] = None + maximum: Optional[Decimal] = None + default: Optional[CurrencyDefaultDto] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class BooleanFieldDto(JsonSchemaFieldDto): + default: Optional[bool] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class DateFieldDto(JsonSchemaFieldDto): + minimum: Optional[int] = None + maximum: Optional[int] = None + default: Optional[int] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class IntegerFieldDto(JsonSchemaFieldDto): + minimum: Optional[int] = None + maximum: Optional[int] = None + default: Optional[int] = None + unique: Optional[bool] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class GeolocationFieldDto(JsonSchemaFieldDto): + allowed_types: Optional[IReadOnlyList[str]] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class TagsFieldDto(JsonSchemaFieldDto): + min_items: Optional[int] = None + max_items: Optional[int] = None + default: Optional[IReadOnlyList[str]] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class FileFieldDto(JsonSchemaFieldDto): + storages: Optional[IReadOnlyList[str]] = None + min_items: Optional[int] = None + max_items: Optional[int] = None + allowed_file_type: Optional[str] = None + max_size_mb: Optional[Decimal] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class TaxonomySelectionFieldDto(JsonSchemaFieldDto): + taxonomy_id: Optional[str] = None + multiple: bool = False + display_field: Optional[str] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class CollectionSelectionFieldDto(JsonSchemaFieldDto): + collection_id: Optional[str] = None + display_field: Optional[str] = None + multiple: bool = False + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class UserSelectionFieldDto(JsonSchemaFieldDto): + multiple: bool = False + display_field: Optional[str] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class RoleSelectionFieldDto(JsonSchemaFieldDto): + multiple: bool = False + display_field: Optional[str] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class EnumSelectionFieldDto(JsonSchemaFieldDto): + values: Optional[IReadOnlyList[str]] = None + multiple: bool = False + default: Optional[IReadOnlyList[str]] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class ObjectFieldDto(JsonSchemaFieldDto): + properties: Optional[IReadOnlyList[JsonSchemaFieldDto]] = None + required: Optional[IReadOnlyList[str]] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class ArrayFieldDto(JsonSchemaFieldDto): + items: Optional[JsonSchemaFieldDto] = None + min_items: Optional[int] = None + max_items: Optional[int] = None + unique_items: Optional[bool] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class JsonFieldDto(JsonSchemaFieldDto): + max_bytes: Optional[int] = None + + +class CodeFunctionSource(str, Enum): + DATABASE = 'Database' + MEMBERSHIP = 'Membership' + FILES = 'Files' + PAYMENTS = 'Payments' + AI = 'Ai' + SCHEDULER = 'Scheduler' + MANUAL = 'Manual' + + +class CodeFunctionCallerKind(str, Enum): + USER = 'User' + API_KEY = 'ApiKey' + SYSTEM = 'System' + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class CodeFunctionCallerDto: + user_id: Optional[str] = None + email: Optional[str] = None + kind: Optional[CodeFunctionCallerKind] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class CodeFunctionRunAsDto: + user_id: Optional[str] = None + access_token: Optional[str] = None + expires_at: Optional[datetime.datetime] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class CodeFunctionContextDto: + account_id: Optional[str] = None + project_id: Optional[str] = None + environment: Optional[str] = None + region: Optional[str] = None + hub_url: Optional[str] = None + api_url: Optional[str] = None + echo_url: Optional[str] = None + trigger_id: Optional[str] = None + task_id: Optional[str] = None + correlation_id: Optional[str] = None + caller: Optional[CodeFunctionCallerDto] = None + run_as: Optional[CodeFunctionRunAsDto] = None + + +class ResourceKindDto(str, Enum): + CONTACT = 'contact' + DOCUMENT = 'document' + FILE = 'file' + PAYMENT_CUSTOMER = 'paymentCustomer' + ORDER = 'order' + PAYMENT = 'payment' + PRODUCT = 'product' + INTEGRATION = 'integration' + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class ResourceRefDto: + project_id: Optional[str] = None + integration_id: Optional[str] = None + kind: Optional[ResourceKindDto] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class DocumentRefDto(ResourceRefDto): + kind: Optional[ResourceKindDto] = None + database_integration_id: Optional[str] = None + schema_id: Optional[str] = None + external_id: Optional[str] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class DatabaseRecordChangeDto: + collection: Optional[str] = None + schema_id: Optional[str] = None + schema_version: Optional[int] = None + record_id: Optional[str] = None + old: Optional[Dict[str, Object]] = None + new: Optional[Dict[str, Object]] = None + truncated: bool = False + old_ref: Optional[DocumentRefDto] = None + new_ref: Optional[DocumentRefDto] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class UserChangeDto: + user_id: Optional[str] = None + old: Optional[Dict[str, Object]] = None + new: Optional[Dict[str, Object]] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class FileChangeDto: + old: Optional[Dict[str, Object]] = None + new: Optional[Dict[str, Object]] = None + path: Optional[str] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class PaymentFactDto: + integration_id: Optional[str] = None + provider: Optional[str] = None + provider_event_id: Optional[str] = None + event_name: Optional[str] = None + status: Optional[str] = None + amount_minor: Optional[int] = None + currency: Optional[str] = None + external_ids: Optional[Dict[str, str]] = None + occurred_on: Optional[datetime.datetime] = None + received_on: datetime.datetime = datetime.datetime(1, 1, 1) + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class AiUsageEventDto: + payload: Dict[str, Object] = field(default_factory=dict) + target_user_id: Optional[str] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class ScheduledTaskInfoDto: + task_id: Optional[str] = None + name: Optional[str] = None + fired_at: datetime.datetime = datetime.datetime(1, 1, 1) + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class CodeFunctionDataDto: + database: Optional[DatabaseRecordChangeDto] = None + user: Optional[UserChangeDto] = None + file: Optional[FileChangeDto] = None + payment: Optional[PaymentFactDto] = None + ai: Optional[AiUsageEventDto] = None + task: Optional[ScheduledTaskInfoDto] = None + payload: Optional[Dict[str, Object]] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class CodeFunctionEventDto: + spec_version: Optional[str] = None + id: Optional[str] = None + event_id: Optional[str] = None + event: Optional[str] = None + source: Optional[CodeFunctionSource] = None + created_on: datetime.datetime = datetime.datetime(1, 1, 1) + context: Optional[CodeFunctionContextDto] = None + data: Optional[CodeFunctionDataDto] = None + config: Optional[Dict[str, Object]] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class FakeCodeIntegrationDto(CodeIntegrationDto): + pass + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class FakeCodeIntegrationRequest(CodeIntegrationRequest): + provider: Optional[CodeProvider] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class CodeFunctionSchedulerTaskRequest(SchedulerTaskRequest): + type: Optional[SchedulerTaskType] = None + function_ref: Optional[FunctionRefDto] = None + invocation_type: Optional[CodeInvocationType] = None + config: Optional[str] = None + mapped_tokens: Optional[List[TokenMappingDto]] = None + + class ICultureBasedRequest: culture_code: Optional[str] = None @@ -3230,11 +3667,9 @@ class ProjectAiAssistantRequestBase(CodeMashRequestBase): @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass -class AiScopeDto: - reach: Optional[str] = None +class AiProjectRoleIdsDto: project_id: Optional[str] = None - rights: Optional[str] = None - envs: List[str] = field(default_factory=list) + role_ids: List[str] = field(default_factory=list) @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -3545,25 +3980,6 @@ class EmbeddingIntegrationRequest: api_key: Optional[str] = None -class ResourceKindDto(str, Enum): - CONTACT = 'contact' - DOCUMENT = 'document' - FILE = 'file' - PAYMENT_CUSTOMER = 'paymentCustomer' - ORDER = 'order' - PAYMENT = 'payment' - PRODUCT = 'product' - INTEGRATION = 'integration' - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class ResourceRefDto: - project_id: Optional[str] = None - integration_id: Optional[str] = None - kind: Optional[ResourceKindDto] = None - - class CaseResolutionFixKind(str, Enum): CODE_FIX = 'CodeFix' CONFIG_CHANGE = 'ConfigChange' @@ -3618,6 +4034,16 @@ class EchoRegionDto: hub_url: Optional[str] = None +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class EchoAgentSnippetDto: + client: Optional[str] = None + kind: Optional[str] = None + recommended: bool = False + auth: Optional[str] = None + config: Optional[str] = None + + @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class EchoAgentDto: @@ -3626,6 +4052,18 @@ class EchoAgentDto: installation_type: Optional[str] = None onboarding_docs_url: Optional[str] = None tools_url: Optional[str] = None + cli_package: Optional[str] = None + cli_install_command: Optional[str] = None + device_authorization_url: Optional[str] = None + device_token_url: Optional[str] = None + snippets: List[EchoAgentSnippetDto] = field(default_factory=list) + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class EchoHostingPlatformDto: + provider: Optional[str] = None + is_aws: bool = False @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -3717,6 +4155,8 @@ class AccountStatusDto: roles: List[str] = field(default_factory=list) allowed_projects: Optional[List[str]] = None trial_was_issued: bool = False + email_verified: bool = False + joined_by_invitation: bool = False @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -3900,7 +4340,7 @@ class ProjectAiSettingsDto: @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass -class TriggerDto(IHasViewId): +class TriggerDto(IHasViewId, IQueuedTrigger): type: Optional[TriggerType] = None view_id: Optional[str] = None name: Optional[str] = None @@ -3908,6 +4348,8 @@ class TriggerDto(IHasViewId): description: Optional[str] = None is_enabled: bool = False activation_code: Optional[str] = None + order: Optional[int] = None + break_on_error: bool = False saved_by_auth_id: Optional[str] = None @@ -3981,6 +4423,7 @@ class PolicyItemDto: name: Optional[str] = None description: Optional[str] = None is_system: bool = False + version: Optional[str] = None permissions: Optional[List[PermissionDto]] = None @@ -4035,6 +4478,7 @@ class SchedulerDto: @dataclass class CodeDto: is_enabled: bool = False + default_integration_view_id: Optional[str] = None @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -4248,12 +4692,20 @@ class ApplicationModule(IntEnum): COMPLIANCE = 8192 CONTACTS = 16384 MARKETPLACE = 32768 + DIAGNOSTICS = 65536 + + +class LanguageGapKind(str, Enum): + TEMPLATE = 'Template' + FOOTER = 'Footer' + SIGNATURE = 'Signature' @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class TemplateLanguageGapDto: module: Optional[ApplicationModule] = None + kind: Optional[LanguageGapKind] = None template_id: Optional[str] = None template_name: Optional[str] = None missing_languages: List[str] = field(default_factory=list) @@ -4501,6 +4953,23 @@ class InstallationLicenseStatusDto: message: Optional[str] = None +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class AiRoleDto: + id: Optional[str] = None + name: Optional[str] = None + description: Optional[str] = None + is_system: bool = False + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class AiProjectRolesDto: + id: Optional[str] = None + name: Optional[str] = None + roles: List[AiRoleDto] = field(default_factory=list) + + @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class AiServiceUserKeyDto: @@ -4514,7 +4983,8 @@ class AiServiceUserKeyDto: class AiServiceUserDto: id: Optional[str] = None name: Optional[str] = None - scope: Optional[AiScopeDto] = None + account_roles: List[AiRoleDto] = field(default_factory=list) + projects: List[AiProjectRolesDto] = field(default_factory=list) created_at: datetime.datetime = datetime.datetime(1, 1, 1) keys: List[AiServiceUserKeyDto] = field(default_factory=list) @@ -4540,12 +5010,14 @@ class GetTriggerResponse(ResponseBase): @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass -class TriggerProjectionList(IHasViewId): +class TriggerProjectionList(IHasViewId, IQueuedTrigger): view_id: Optional[str] = None name: Optional[str] = None action_type: Optional[TriggerActionType] = None has_pre_execute_code: bool = False is_enabled: bool = False + order: Optional[int] = None + break_on_error: bool = False @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -4683,12 +5155,6 @@ class SchemaTriggerProjectionList(TriggerProjectionList): env: Optional[str] = None -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class JsonSchemaFieldDto: - field_name: Optional[str] = None - - @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class DataSchemaDto: @@ -4850,6 +5316,40 @@ class SchemaDraftDto: updated_at: datetime.datetime = datetime.datetime(1, 1, 1) +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class SchemaIndexDto: + name: Optional[str] = None + keys: List[str] = field(default_factory=list) + unique: bool = False + reason: Optional[str] = None + field: Optional[str] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class SchemaIndexIntegrationStatusDto: + integration_id: Optional[str] = None + state: Optional[str] = None + wanted: List[SchemaIndexDto] = field(default_factory=list) + created: List[str] = field(default_factory=list) + dropped: List[str] = field(default_factory=list) + over_cap: List[SchemaIndexDto] = field(default_factory=list) + refused_reason: Optional[str] = None + last_run_utc: datetime.datetime = datetime.datetime(1, 1, 1) + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class SchemaIndexStatusDto: + schema_id: Optional[str] = None + env: Optional[str] = None + collection: Optional[str] = None + state: Optional[str] = None + integrations: List[SchemaIndexIntegrationStatusDto] = field(default_factory=list) + last_run_utc: datetime.datetime = datetime.datetime(1, 1, 1) + + @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class SchemaDiffDto: @@ -4881,6 +5381,7 @@ class CollectionIndexKeyDto: class CollectionIndexDto: name: Optional[str] = None keys: List[CollectionIndexKeyDto] = field(default_factory=list) + created_by_norbix: bool = False @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -5070,7 +5571,10 @@ class HtmlFromMjmlResponse: @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class SystemEmailTemplateListProjection(EmailTemplateListProjection): - pass + image_preview: Optional[str] = None + description: Optional[str] = None + theme: Optional[SystemEmailTemplateTheme] = None + system_tags: Optional[List[str]] = None @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -5397,6 +5901,46 @@ class CodeIntegrationListProjection(IntegrationListProjection): provider: Optional[CodeProvider] = None +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class CodeFunctionDescriptorDto: + function_id: Optional[str] = None + display_name: Optional[str] = None + runtime: Optional[str] = None + region: Optional[str] = None + description: Optional[str] = None + last_modified: Optional[datetime.datetime] = None + tags: Optional[Dict[str, str]] = None + arn: Optional[str] = None + + +class CodeFunctionQualifierKindDto(str, Enum): + ALIAS = 'Alias' + VERSION = 'Version' + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class CodeFunctionQualifierDto: + name: Optional[str] = None + kind: Optional[CodeFunctionQualifierKindDto] = None + description: Optional[str] = None + target_version: Optional[str] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class CodeFunctionRunDto: + fired_at: datetime.datetime = datetime.datetime(1, 1, 1) + event: Optional[str] = None + source: Optional[str] = None + succeeded: bool = False + status_code: Optional[int] = None + duration_ms: Optional[int] = None + error: Optional[str] = None + request_id: Optional[str] = None + + @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class PushTemplateListProjection(TemplateListProjection): @@ -5538,6 +6082,8 @@ class AgentOnboardingSnippet: client: Optional[str] = None config: Optional[str] = None auth: Optional[str] = None + kind: Optional[str] = None + recommended: bool = False @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -6040,9 +6586,30 @@ class SchedulerTaskListProjection(IHasViewId): cron: Optional[str] = None type: Optional[SchedulerTaskType] = None is_enabled: bool = False + is_failing: bool = False + last_failure_reason: Optional[str] = None + last_failed_at_unix: Optional[int] = None view_id: Optional[str] = None +class SchedulerTaskRunOutcome(IntEnum): + FIRED = 1 + FAILED = 2 + SKIPPED = 3 + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class SchedulerTaskRunDto: + log_id: Optional[str] = None + at_utc: datetime.datetime = datetime.datetime(1, 1, 1) + at_unix: int = 0 + outcome: Optional[SchedulerTaskRunOutcome] = None + reason: Optional[str] = None + detail: Optional[str] = None + correlation_id: Optional[str] = None + + class ResolvedRefStatus(str, Enum): OK = 'ok' NOT_FOUND = 'notFound' @@ -6414,6 +6981,13 @@ class IHasRazorTemplateCode: pass +class IQueuedTrigger: + view_id: Optional[str] = None + name: Optional[str] = None + order: Optional[int] = None + break_on_error: bool = False + + class IHasResponsibleUserId: user_id: Optional[str] = None @@ -6423,152 +6997,6 @@ class ICursorArgs: order: int = 0 -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class StringFieldDto(JsonSchemaFieldDto): - format: Optional[str] = None - pattern: Optional[str] = None - min_length: Optional[int] = None - max_length: Optional[int] = None - translate_options: Optional[IReadOnlyDictionary[str, str]] = None - default: Optional[str] = None - unique: Optional[bool] = None - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class DecimalFieldDto(JsonSchemaFieldDto): - minimum: Optional[Decimal] = None - maximum: Optional[Decimal] = None - multiple_of: Optional[Decimal] = None - default: Optional[Decimal] = None - unique: Optional[bool] = None - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class CurrencyDefaultDto: - value: Decimal = decimal.Decimal(0) - currency: Optional[str] = None - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class CurrencyFieldDto(JsonSchemaFieldDto): - allowed_currencies: Optional[IReadOnlyList[str]] = None - multiple_of: Optional[Decimal] = None - minimum: Optional[Decimal] = None - maximum: Optional[Decimal] = None - default: Optional[CurrencyDefaultDto] = None - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class BooleanFieldDto(JsonSchemaFieldDto): - default: Optional[bool] = None - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class DateFieldDto(JsonSchemaFieldDto): - minimum: Optional[int] = None - maximum: Optional[int] = None - default: Optional[int] = None - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class IntegerFieldDto(JsonSchemaFieldDto): - minimum: Optional[int] = None - maximum: Optional[int] = None - default: Optional[int] = None - unique: Optional[bool] = None - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class GeolocationFieldDto(JsonSchemaFieldDto): - allowed_types: Optional[IReadOnlyList[str]] = None - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class TagsFieldDto(JsonSchemaFieldDto): - min_items: Optional[int] = None - max_items: Optional[int] = None - default: Optional[IReadOnlyList[str]] = None - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class FileFieldDto(JsonSchemaFieldDto): - storages: Optional[IReadOnlyList[str]] = None - min_items: Optional[int] = None - max_items: Optional[int] = None - allowed_file_type: Optional[str] = None - max_size_mb: Optional[Decimal] = None - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class TaxonomySelectionFieldDto(JsonSchemaFieldDto): - taxonomy_id: Optional[str] = None - multiple: bool = False - display_field: Optional[str] = None - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class CollectionSelectionFieldDto(JsonSchemaFieldDto): - collection_id: Optional[str] = None - display_field: Optional[str] = None - multiple: bool = False - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class UserSelectionFieldDto(JsonSchemaFieldDto): - multiple: bool = False - display_field: Optional[str] = None - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class RoleSelectionFieldDto(JsonSchemaFieldDto): - multiple: bool = False - display_field: Optional[str] = None - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class EnumSelectionFieldDto(JsonSchemaFieldDto): - values: Optional[IReadOnlyList[str]] = None - multiple: bool = False - default: Optional[IReadOnlyList[str]] = None - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class ObjectFieldDto(JsonSchemaFieldDto): - properties: Optional[IReadOnlyList[JsonSchemaFieldDto]] = None - required: Optional[IReadOnlyList[str]] = None - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class ArrayFieldDto(JsonSchemaFieldDto): - items: Optional[JsonSchemaFieldDto] = None - min_items: Optional[int] = None - max_items: Optional[int] = None - unique_items: Optional[bool] = None - - -@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) -@dataclass -class JsonFieldDto(JsonSchemaFieldDto): - max_bytes: Optional[int] = None - - @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class EchoResponse: @@ -6595,6 +7023,7 @@ class EchoResponse: installation_domain: Optional[str] = None licensing_docs_url: Optional[str] = None agent: Optional[EchoAgentDto] = None + hosting_platform: Optional[EchoHostingPlatformDto] = None @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -6637,7 +7066,8 @@ class GetAccountStatusResponse(ResponseBase): @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class CreateStripeCheckoutSessionResponse(IdResponse): - pass + trial_included: bool = False + no_trial_reason: Optional[str] = None @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -7049,6 +7479,12 @@ class GetDatabaseSchemaDraftResponse(ResponseBase): item: Optional[SchemaDraftDto] = None +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class GetDatabaseSchemaIndexStatusResponse(ResponseBase): + status: Optional[SchemaIndexStatusDto] = None + + @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class GetDatabaseSchemaListSettingsResponse(ResponseBase): @@ -7586,6 +8022,7 @@ class GetCodeIntegrationResponse(ResponseBase): @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class GetCodeIntegrationsResponse(ResponseBase): + default_integration_id: Optional[str] = None list: Optional[PaginatedResponse[CodeIntegrationListProjection]] = None @@ -7595,6 +8032,40 @@ class TestCodeIntegrationResponse(ResponseBase): items: Optional[IReadOnlyList[IntegrationTestResultItemDto]] = None +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class GetCodeFunctionsResponse(ResponseBase): + list: Optional[PaginatedResponse[CodeFunctionDescriptorDto]] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class GetCodeFunctionEventSampleResponse(ResponseBase): + sample: Optional[CodeFunctionEventDto] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class GetCodeFunctionQualifiersResponse(ResponseBase): + qualifiers: Optional[IReadOnlyList[CodeFunctionQualifierDto]] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class GetCodeFunctionRunsResponse(ResponseBase): + runs: List[CodeFunctionRunDto] = field(default_factory=list) + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class InvokeCodeFunctionResponse(ResponseBase): + status_code: int = 0 + output: Optional[Object] = None + duration_ms: int = 0 + request_id: Optional[str] = None + logs: Optional[str] = None + + @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class GetPushTemplateResponse(ResponseBase): @@ -7940,6 +8411,70 @@ class GetAiTriggerResponse(GetTriggerResponse): trigger: Optional[AiTriggerDto] = None +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class AuthDeviceStartResponse(ResponseBase): + device_code: Optional[str] = None + user_code: Optional[str] = None + verification_uri: Optional[str] = None + verification_uri_complete: Optional[str] = None + expires_in: int = 0 + interval: int = 0 + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class AuthDeviceTokenResponse(ResponseBase): + error: Optional[str] = None + error_description: Optional[str] = None + error_code: Optional[str] = None + reason: Optional[str] = None + bearer_token: Optional[str] = None + refresh_token: Optional[str] = None + expires_in: Optional[int] = None + client_id: Optional[str] = None + user_id: Optional[str] = None + user_name: Optional[str] = None + display_name: Optional[str] = None + account_id: Optional[str] = None + project_id: Optional[str] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class AuthDeviceConsentResponse(ResponseBase): + client_name: Optional[str] = None + device_name: Optional[str] = None + user_code: Optional[str] = None + project_id: Optional[str] = None + account_roles: List[AiRoleDto] = field(default_factory=list) + projects: List[AiProjectRolesDto] = field(default_factory=list) + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class AuthDeviceConsentDecisionResponse(ResponseBase): + pass + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class OAuthConsentResponse(ResponseBase): + client_name: Optional[str] = None + redirect_host: Optional[str] = None + client_host: Optional[str] = None + redirect_is_loopback: bool = False + consent_ticket: Optional[str] = None + account_roles: List[AiRoleDto] = field(default_factory=list) + projects: List[AiProjectRolesDto] = field(default_factory=list) + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class OAuthConsentDecisionResponse(ResponseBase): + redirect_url: Optional[str] = None + + @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class GetEmbeddingIntegrationResponse(ResponseBase): @@ -8059,6 +8594,13 @@ class GetSchedulerTasksResponse(ResponseBase): list: Optional[PaginatedResponse[SchedulerTaskListProjection]] = None +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class GetSchedulerTaskRunsResponse(ResponseBase): + logs_enabled: bool = False + runs: List[SchedulerTaskRunDto] = field(default_factory=list) + + @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class ResolveResourcesResponse(ResponseBase): @@ -8793,6 +9335,38 @@ class InternalsTypeGen: typegen_243__action_pending_entry_wire_dto: Optional[ActionPendingEntryWireDto] = None typegen_244__notice_entry_wire_dto: Optional[NoticeEntryWireDto] = None typegen_245__conversation_snapshot_entry_wire_dto: Optional[ConversationSnapshotEntryWireDto] = None + typegen_252__string_field_dto: Optional[StringFieldDto] = None + typegen_253__decimal_field_dto: Optional[DecimalFieldDto] = None + typegen_254__currency_field_dto: Optional[CurrencyFieldDto] = None + typegen_255__boolean_field_dto: Optional[BooleanFieldDto] = None + typegen_256__date_field_dto: Optional[DateFieldDto] = None + typegen_257__integer_field_dto: Optional[IntegerFieldDto] = None + typegen_258__geolocation_field_dto: Optional[GeolocationFieldDto] = None + typegen_259__tags_field_dto: Optional[TagsFieldDto] = None + typegen_260__file_field_dto: Optional[FileFieldDto] = None + typegen_261__taxonomy_selection_field_dto: Optional[TaxonomySelectionFieldDto] = None + typegen_262__collection_selection_field_dto: Optional[CollectionSelectionFieldDto] = None + typegen_263__user_selection_field_dto: Optional[UserSelectionFieldDto] = None + typegen_264__role_selection_field_dto: Optional[RoleSelectionFieldDto] = None + typegen_265__enum_selection_field_dto: Optional[EnumSelectionFieldDto] = None + typegen_266__object_field_dto: Optional[ObjectFieldDto] = None + typegen_267__array_field_dto: Optional[ArrayFieldDto] = None + typegen_268__json_field_dto: Optional[JsonFieldDto] = None + typegen_269__function_ref_dto: Optional[FunctionRefDto] = None + typegen_270__code_function_event_dto: Optional[CodeFunctionEventDto] = None + typegen_271__code_function_context_dto: Optional[CodeFunctionContextDto] = None + typegen_272__code_function_caller_dto: Optional[CodeFunctionCallerDto] = None + typegen_273__code_function_run_as_dto: Optional[CodeFunctionRunAsDto] = None + typegen_274__code_function_data_dto: Optional[CodeFunctionDataDto] = None + typegen_275__database_record_change_dto: Optional[DatabaseRecordChangeDto] = None + typegen_276__user_change_dto: Optional[UserChangeDto] = None + typegen_277__file_change_dto: Optional[FileChangeDto] = None + typegen_278__payment_fact_dto: Optional[PaymentFactDto] = None + typegen_279__ai_usage_event_dto: Optional[AiUsageEventDto] = None + typegen_280__scheduled_task_info_dto: Optional[ScheduledTaskInfoDto] = None + typegen_281__fake_code_integration_dto: Optional[FakeCodeIntegrationDto] = None + typegen_282__fake_code_integration_request: Optional[FakeCodeIntegrationRequest] = None + typegen_283__code_function_scheduler_task_request: Optional[CodeFunctionSchedulerTaskRequest] = None # @Route("/{version}/echo", "GET") @@ -8881,7 +9455,7 @@ class UpdateAccountProfile(RequestBase, IReturn[EmptyResponse]): """ -# @Route("/{version}/account/verify/resend", "GET") +# @Route("/{version}/account/verify/resend", "POST") @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class ResendAccountVerificationToken(RequestBase, IReturn[EmptyResponse]): @@ -8966,7 +9540,7 @@ class GetAccountUsageBilling(RequestBase, IReturn[GetAccountUsageBillingResponse pass -# @Route("/{version}/account/verify", "GET") +# @Route("/{version}/account/verify", "POST") @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class VerifyAccount(RequestBase, IReturn[EmptyResponse], IHasAccountId): @@ -10293,10 +10867,17 @@ class CreateAiServiceUserRequest(RequestBase, IReturn[CreateAiServiceUserRespons """ - # @ApiMember(Description="Reach (account | project + projectId), rights (read | admin), envs ([\"TEST\"] or [\"TEST\",\"PROD\"]).", IsRequired=true) - scope: Optional[AiScopeDto] = None + # @ApiMember(Description="Account role ids (acc_…_nr_…) to give, built-in or custom. At least one role in total (account or project).") + account_role_ids: Optional[List[str]] = None + """ + Account role ids (acc_…_nr_…) to give, built-in or custom. At least one role in total (account or project). + """ + + + # @ApiMember(Description="Project roles to give: [{ projectId, roleIds: [pr_…_nr_…] }]. Without an account role the service user is pinned to these projects.") + project_roles: Optional[List[AiProjectRoleIdsDto]] = None """ - Reach (account | project + projectId), rights (read | admin), envs (["TEST"] or ["TEST","PROD"]). + Project roles to give: [{ projectId, roleIds: [pr_…_nr_…] }]. Without an account role the service user is pinned to these projects. """ @@ -11574,6 +12155,22 @@ class GetDatabaseSchemaDraft(CodeMashRequestBase, IReturn[GetDatabaseSchemaDraft """ +# @Route("/{version}/database/schemas/{Id}/index-status", "GET") +# @Api(Description="Gets the Norbix index status of a database schema") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class GetDatabaseSchemaIndexStatus(CodeMashRequestBase, IReturn[GetDatabaseSchemaIndexStatusResponse]): + """ + Gets the Norbix index status of a database schema + """ + + # @ApiMember(Description="Schema id from get_database_schemas.", IsRequired=true) + id: Optional[str] = None + """ + Schema id from get_database_schemas. + """ + + # @Route("/{version}/database/schemas/{Id}/list-settings", "GET") # @Api(Description="Gets database schema records-list display settings") @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -12968,14 +13565,32 @@ class GetFolderFiles(CodeMashListPaginationRequestBase, IReturn[GetFolderFilesRe # @ApiMember(Description="The files integration id to list from, from get_files_integrations.", IsRequired=true) files_integration_id: Optional[str] = None """ - The files integration id to list from, from get_files_integrations. + The files integration id to list from, from get_files_integrations. + """ + + + # @ApiMember(Description="Path prefix to list. Empty / null lists the root.") + path: Optional[str] = None + """ + Path prefix to list. Empty / null lists the root. + """ + + +# @Route("/{version}/files/item/use", "POST") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class UseFileRequest(CodeMashRequestBase, IReturn[IdResponse]): + # @ApiMember(Description="The files integration the file lives on.", IsRequired=true) + files_integration_id: Optional[str] = None + """ + The files integration the file lives on. """ - # @ApiMember(Description="Path prefix to list. Empty / null lists the root.") + # @ApiMember(Description="Path of the file, relative to the integration.", IsRequired=true) path: Optional[str] = None """ - Path prefix to list. Empty / null lists the root. + Path of the file, relative to the integration. """ @@ -14657,6 +15272,132 @@ class GetMarketplaceFunctionCatalog(CodeMashRequestBase, IReturn[GetMarketplaceF """ +# @Route("/{version}/code/functions", "GET") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class GetCodeFunctions(CodeMashListPaginationRequestBase, IReturn[GetCodeFunctionsResponse]): + # @ApiMember(Description="Code integration id (int_…), from get_code_integrations.", IsRequired=true) + integration_id: Optional[str] = None + """ + Code integration id (int_…), from get_code_integrations. + """ + + + # @ApiMember(Description="Only functions whose id starts with this.") + prefix: Optional[str] = None + """ + Only functions whose id starts with this. + """ + + + # @ApiMember(Description="Only functions carrying this tag: \"key\" or \"key=value\".") + tag: Optional[str] = None + """ + Only functions carrying this tag: "key" or "key=value". + """ + + +# @Route("/{version}/code/functions/event-sample", "GET") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class GetCodeFunctionEventSample(CodeMashRequestBase, IReturn[GetCodeFunctionEventSampleResponse]): + # @ApiMember(Description="Database | Membership | Files | Payments | Ai | Scheduler | Manual.", IsRequired=true) + source: Optional[CodeFunctionSource] = None + """ + Database | Membership | Files | Payments | Ai | Scheduler | Manual. + """ + + + # @ApiMember(Description="The event name, e.g. database.record.inserted; omitted = the source's usual one.") + event: Optional[str] = None + """ + The event name, e.g. database.record.inserted; omitted = the source's usual one. + """ + + + # @ApiMember(Description="The schema id to put into data.database.schemaId (Database source).") + schema_id: Optional[str] = None + """ + The schema id to put into data.database.schemaId (Database source). + """ + + +# @Route("/{version}/code/functions/qualifiers", "GET") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class GetCodeFunctionQualifiers(CodeMashRequestBase, IReturn[GetCodeFunctionQualifiersResponse]): + # @ApiMember(Description="Code integration id (int_…), from get_code_integrations.", IsRequired=true) + integration_id: Optional[str] = None + """ + Code integration id (int_…), from get_code_integrations. + """ + + + # @ApiMember(Description="Provider-native function id, from get_code_functions.", IsRequired=true) + function_id: Optional[str] = None + """ + Provider-native function id, from get_code_functions. + """ + + +# @Route("/{version}/code/functions/runs", "GET") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class GetCodeFunctionRuns(CodeMashRequestBase, IReturn[GetCodeFunctionRunsResponse]): + # @ApiMember(Description="The trigger whose function runs to list (trg_…). One of triggerId / taskId.") + trigger_id: Optional[str] = None + """ + The trigger whose function runs to list (trg_…). One of triggerId / taskId. + """ + + + # @ApiMember(Description="The scheduler task whose function runs to list (tsk_…). One of triggerId / taskId.") + task_id: Optional[str] = None + """ + The scheduler task whose function runs to list (tsk_…). One of triggerId / taskId. + """ + + + # @ApiMember(DataType="integer", Description="How many runs, newest first: 1-50. Default 20.") + limit: Optional[int] = None + """ + How many runs, newest first: 1-50. Default 20. + """ + + +# @Route("/{version}/code/functions/invoke", "POST") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class InvokeCodeFunction(CodeMashRequestBase, IReturn[InvokeCodeFunctionResponse]): + # @ApiMember(Description="Which function: { integrationId, functionId, qualifier? }.", IsRequired=true) + function_ref: Optional[FunctionRefDto] = None + """ + Which function: { integrationId, functionId, qualifier? }. + """ + + + payload: Optional[Dict[str, Object]] = None + # @ApiMember(Description="Sync (wait for the result, default) or Async (fire and forget).") + invocation_type: Optional[CodeInvocationType] = None + """ + Sync (wait for the result, default) or Async (fire and forget). + """ + + + # @ApiMember(Description="The service user the function acts as (usr_…); omitted = you.") + run_as_user_id: Optional[str] = None + """ + The service user the function acts as (usr_…); omitted = you. + """ + + + # @ApiMember(Description="A JSON object → envelope config. No bindings: a manual call has nothing to bind them with.") + config: Optional[str] = None + """ + A JSON object → envelope config. No bindings: a manual call has nothing to bind them with. + """ + + # @Route("/{version}/notifications/push/disable", "PUT") # @Api(Description="Disable push service") @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -15017,17 +15758,17 @@ class TestPushIntegration(CodeMashRequestBase, IReturn[TestEmailIntegrationRespo """ - # @ApiMember(Description="Optional device token to send the test notification to. Requires DeliveryFamily when set.") + # @ApiMember(Description="The device token the test is sent to — a device registered for the person receiving the test (get it from get_push_devices). Required for every real provider (only the Fake integration tests without one). For a web device it is the browser subscription JSON. Requires DeliveryFamily.") test_token: Optional[str] = None """ - Optional device token to send the test notification to. Requires DeliveryFamily when set. + The device token the test is sent to — a device registered for the person receiving the test (get it from get_push_devices). Required for every real provider (only the Fake integration tests without one). For a web device it is the browser subscription JSON. Requires DeliveryFamily. """ - # @ApiMember(Description="Optional delivery family for the test token (ios, android, chrome or safari). Requires TestToken when set.") + # @ApiMember(Description="The delivery family of TestToken: ios, android, chrome (any browser web push) or safari. Requires TestToken.") delivery_family: Optional[str] = None """ - Optional delivery family for the test token (ios, android, chrome or safari). Requires TestToken when set. + The delivery family of TestToken: ios, android, chrome (any browser web push) or safari. Requires TestToken. """ @@ -15896,6 +16637,20 @@ class GetLogs(CodeMashListPaginationRequestBase, IReturn[GetLogsResponse]): """ + # @ApiMember(DataType="string", Description="Meta key to filter on (e.g. taskId). Letters, digits and _ only. Must come with metaValue.", Name="metaKey", ParameterType="query") + meta_key: Optional[str] = None + """ + Meta key to filter on (e.g. taskId). Letters, digits and _ only. Must come with metaValue. + """ + + + # @ApiMember(DataType="string", Description="Exact Meta value for metaKey (e.g. a task id tsk_...). Must come with metaKey.", Name="metaValue", ParameterType="query") + meta_value: Optional[str] = None + """ + Exact Meta value for metaKey (e.g. a task id tsk_...). Must come with metaKey. + """ + + # @ApiMember(Description="Start of the timestamp range (inclusive, UTC). Optional.") from_utc: Optional[datetime.datetime] = None """ @@ -16454,6 +17209,105 @@ class DeleteAiProjectTrigger(DeleteTrigger, IReturn[EmptyResponse]): pass +# @Route("/{version}/auth/device/start", "POST") +# @Api(Description="Starts a browser sign-in for the Norbix CLI (OAuth device authorization, RFC 8628): a device code for polling and a user code the person confirms on the dashboard.") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class AuthDeviceStartRequest(RequestBase, IReturn[AuthDeviceStartResponse]): + """ + Starts a browser sign-in for the Norbix CLI (OAuth device authorization, RFC 8628): a device code for polling and a user code the person confirms on the dashboard. + """ + + # @ApiMember(Description="Who asks, shown on the dashboard, e.g. \"norbix-cli\". 1–80 printable characters.", IsRequired=true) + client_name: Optional[str] = None + """ + Who asks, shown on the dashboard, e.g. "norbix-cli". 1–80 printable characters. + """ + + + # @ApiMember(Description="The computer name, shown on the dashboard and part of the AI service user's name (cleaned, at most 40 characters).") + device_name: Optional[str] = None + """ + The computer name, shown on the dashboard and part of the AI service user's name (cleaned, at most 40 characters). + """ + + + # @ApiMember(Description="The project the CLI wants to work in. It must be a project of the account of the person who allows the sign-in.") + project_id: Optional[str] = None + """ + The project the CLI wants to work in. It must be a project of the account of the person who allows the sign-in. + """ + + +# @Route("/{version}/auth/device/token", "POST") +# @Api(Description="Polls a CLI browser sign-in (RFC 8628). Always HTTP 200: an error name (authorization_pending, slow_down, access_denied, expired_token, invalid_grant, invalid_request) or the tokens, once.") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class AuthDeviceTokenRequest(RequestBase, IReturn[AuthDeviceTokenResponse]): + """ + Polls a CLI browser sign-in (RFC 8628). Always HTTP 200: an error name (authorization_pending, slow_down, access_denied, expired_token, invalid_grant, invalid_request) or the tokens, once. + """ + + # @ApiMember(Description="The deviceCode from POST /auth/device/start.", IsRequired=true) + device_code: Optional[str] = None + """ + The deviceCode from POST /auth/device/start. + """ + + +# @Route("/{version}/auth/device/consent", "GET") +# @Api(Description="The dashboard screen for a CLI browser sign-in: the client and computer that asked, and the roles the signed-in person may give.") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class AuthDeviceConsentRequest(RequestBase, IReturn[AuthDeviceConsentResponse]): + """ + The dashboard screen for a CLI browser sign-in: the client and computer that asked, and the roles the signed-in person may give. + """ + + # @ApiMember(Description="The code the terminal shows, XXXX-XXXX (case, spaces and the dash do not matter).", IsRequired=true) + user_code: Optional[str] = None + """ + The code the terminal shows, XXXX-XXXX (case, spaces and the dash do not matter). + """ + + +# @Route("/{version}/auth/device/consent", "POST") +# @Api(Description="The signed-in person's decision on a CLI browser sign-in: allow with Membership roles, or deny.") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class AuthDeviceConsentDecisionRequest(RequestBase, IReturn[AuthDeviceConsentDecisionResponse]): + """ + The signed-in person's decision on a CLI browser sign-in: allow with Membership roles, or deny. + """ + + # @ApiMember(Description="The code the terminal shows, XXXX-XXXX.", IsRequired=true) + user_code: Optional[str] = None + """ + The code the terminal shows, XXXX-XXXX. + """ + + + # @ApiMember(Description="allow | deny.", IsRequired=true) + decision: Optional[str] = None + """ + allow | deny. + """ + + + # @ApiMember(Description="For allow: account role ids (acc_…_nr_…) to give.") + account_role_ids: Optional[List[str]] = None + """ + For allow: account role ids (acc_…_nr_…) to give. + """ + + + # @ApiMember(Description="For allow: project roles to give, [{ projectId, roleIds: [pr_…_nr_…] }].") + project_roles: Optional[List[AiProjectRoleIdsDto]] = None + """ + For allow: project roles to give, [{ projectId, roleIds: [pr_…_nr_…] }]. + """ + + # @Route("/.well-known/oauth-protected-resource", "GET") # @Route("/.well-known/oauth-protected-resource/{Path*}", "GET") # @Api(Description="OAuth protected-resource metadata (RFC 9728) for the Hub MCP endpoint.") @@ -16494,27 +17348,173 @@ class OAuthRegisterRequest(IReturn[str]): # @Route("/{version}/oauth/authorize", "GET") -# @Api(Description="OAuth authorization endpoint: sign-in hint or the consent page (HTML).") +# @Api(Description="OAuth authorization endpoint: checks the request and redirects to the dashboard consent screen.") @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class OAuthAuthorizeRequest(IReturn[str]): """ - OAuth authorization endpoint: sign-in hint or the consent page (HTML). + OAuth authorization endpoint: checks the request and redirects to the dashboard consent screen. """ version: Optional[str] = None -# @Route("/{version}/oauth/authorize", "POST") -# @Api(Description="OAuth authorization endpoint: the person's decision from the consent page.") +# @Route("/{version}/oauth/consent", "GET") +# @Api(Description="The dashboard consent screen for an AI tool: the client, where it returns to, and the roles the signed-in person may give.") @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass -class OAuthAuthorizeDecisionRequest(IReturn[str]): +class OAuthConsentRequest(RequestBase, IReturn[OAuthConsentResponse]): """ - OAuth authorization endpoint: the person's decision from the consent page. + The dashboard consent screen for an AI tool: the client, where it returns to, and the roles the signed-in person may give. """ - version: Optional[str] = None + # @ApiMember(Description="OAuth response_type from the authorize request (must be \"code\").") + response_type: Optional[str] = None + """ + OAuth response_type from the authorize request (must be "code"). + """ + + + # @ApiMember(Description="OAuth client_id from the authorize request.") + client_id: Optional[str] = None + """ + OAuth client_id from the authorize request. + """ + + + # @ApiMember(Description="OAuth redirect_uri from the authorize request.") + redirect_uri: Optional[str] = None + """ + OAuth redirect_uri from the authorize request. + """ + + + # @ApiMember(Description="PKCE code_challenge from the authorize request.") + code_challenge: Optional[str] = None + """ + PKCE code_challenge from the authorize request. + """ + + + # @ApiMember(Description="PKCE code_challenge_method from the authorize request (must be \"S256\").") + code_challenge_method: Optional[str] = None + """ + PKCE code_challenge_method from the authorize request (must be "S256"). + """ + + + # @ApiMember(Description="OAuth state from the authorize request; returned to the client unchanged.") + state: Optional[str] = None + """ + OAuth state from the authorize request; returned to the client unchanged. + """ + + + # @ApiMember(Description="OAuth resource (RFC 8707) from the authorize request.") + resource: Optional[str] = None + """ + OAuth resource (RFC 8707) from the authorize request. + """ + + + # @ApiMember(Description="OAuth scope from the authorize request.") + scope: Optional[str] = None + """ + OAuth scope from the authorize request. + """ + + +# @Route("/{version}/oauth/consent", "POST") +# @Api(Description="The signed-in person's decision on the consent screen: allow with roles, or deny. Returns the address to send the browser to.") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class OAuthConsentDecisionRequest(RequestBase, IReturn[OAuthConsentDecisionResponse]): + """ + The signed-in person's decision on the consent screen: allow with roles, or deny. Returns the address to send the browser to. + """ + + # @ApiMember(Description="OAuth response_type from the authorize request (must be \"code\").") + response_type: Optional[str] = None + """ + OAuth response_type from the authorize request (must be "code"). + """ + + + # @ApiMember(Description="OAuth client_id from the authorize request.") + client_id: Optional[str] = None + """ + OAuth client_id from the authorize request. + """ + + + # @ApiMember(Description="OAuth redirect_uri from the authorize request.") + redirect_uri: Optional[str] = None + """ + OAuth redirect_uri from the authorize request. + """ + + + # @ApiMember(Description="PKCE code_challenge from the authorize request.") + code_challenge: Optional[str] = None + """ + PKCE code_challenge from the authorize request. + """ + + + # @ApiMember(Description="PKCE code_challenge_method from the authorize request (must be \"S256\").") + code_challenge_method: Optional[str] = None + """ + PKCE code_challenge_method from the authorize request (must be "S256"). + """ + + + # @ApiMember(Description="OAuth state from the authorize request; returned to the client unchanged.") + state: Optional[str] = None + """ + OAuth state from the authorize request; returned to the client unchanged. + """ + + + # @ApiMember(Description="OAuth resource (RFC 8707) from the authorize request.") + resource: Optional[str] = None + """ + OAuth resource (RFC 8707) from the authorize request. + """ + + + # @ApiMember(Description="OAuth scope from the authorize request.") + scope: Optional[str] = None + """ + OAuth scope from the authorize request. + """ + + + # @ApiMember(Description="The consentTicket from GET /oauth/consent.", IsRequired=true) + consent_ticket: Optional[str] = None + """ + The consentTicket from GET /oauth/consent. + """ + + + # @ApiMember(Description="allow | deny.", IsRequired=true) + decision: Optional[str] = None + """ + allow | deny. + """ + + + # @ApiMember(Description="For allow: account role ids (acc_…_nr_…) to give.") + account_role_ids: Optional[List[str]] = None + """ + For allow: account role ids (acc_…_nr_…) to give. + """ + + + # @ApiMember(Description="For allow: project roles to give, [{ projectId, roleIds: [pr_…_nr_…] }].") + project_roles: Optional[List[AiProjectRoleIdsDto]] = None + """ + For allow: project roles to give, [{ projectId, roleIds: [pr_…_nr_…] }]. + """ # @Route("/{version}/oauth/token", "POST") @@ -16778,6 +17778,7 @@ class UpdateWebhookIntegrationExtraHeadersRequest(CodeMashRequestBase, IReturn[E @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class ReceiveWebhook(IReturnVoid): + version: Optional[str] = None source: Optional[str] = None integration_instance_id: Optional[str] = None request_stream: Optional[bytes] = None @@ -16932,6 +17933,23 @@ class GetSchedulerTasks(CodeMashListPaginationRequestBase, IReturn[GetSchedulerT """ +# @Route("/{version}/scheduler/tasks/{id}/runs", "GET") +# @Api(Description="Gets the latest runs of a scheduled task") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class GetSchedulerTaskRuns(CodeMashRequestBase, IReturn[GetSchedulerTaskRunsResponse]): + """ + Gets the latest runs of a scheduled task + """ + + id: Optional[str] = None + # @ApiMember(DataType="integer", Description="How many runs to return, newest first: 1-50. Default 10.", Name="take", ParameterType="query") + take: Optional[int] = None + """ + How many runs to return, newest first: 1-50. Default 10. + """ + + # @Route("/{version}/scheduler/tasks", "POST") # @Api(Description="Save scheduled task") @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) From 39ec467f2500c462a1fd90a9dc56780bd41072c6 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Mon, 12 Oct 2026 14:12:10 +0300 Subject: [PATCH 2/2] =?UTF-8?q?feat(code):=20cloud=20functions=20=E2=80=94?= =?UTF-8?q?=20list=20/=20qualifiers=20/=20invoke=20/=20event-sample=20/=20?= =?UTF-8?q?runs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The five Code-function routes of the gateway contract (Code.Functions.md §3.3, §3.4, §5.1) on `hub.code`, sync and async: `get_code_functions`, `get_code_function_qualifiers`, `invoke_code_function`, `get_code_function_event_sample`, `get_code_function_runs`. `norbix_python.functions`: `parse_function_event(raw)` reads the envelope a cloud function receives (`CodeFunctionEventDto`, specVersion "1") into a `FunctionEvent` — a wrapper over the decoded dict with a typed `context` (`run_as.access_token`, `hub_url`, `api_url`, …), the views `as_database_record_change()`, `as_user_change()`, `as_file_change()`, `as_payment()`, `as_scheduled_task()`, `as_ai_usage()`, `payload()`, and `client_options()` → the `Norbix` constructor kwargs for the run-as user. No DTO is re-declared; the generated types stay in `references/hub_dtos.py`. Tests: the five routes (verb, path, query, body; sync + async), one envelope sample per source, bad specVersion / non-object refusals, a client built from an event calling the envelope's deployment. Docs: `docs/hub/code.md`, README "Cloud functions". Co-Authored-By: Claude Fable 5.1 --- README.md | 29 +++ docs/hub/_index.md | 1 + docs/hub/code.md | 118 +++++++++ src/norbix_python/__init__.py | 4 + src/norbix_python/functions.py | 459 +++++++++++++++++++++++++++++++++ src/norbix_python/hub/code.py | 130 ++++++++++ tests/hub/test_code.py | 165 ++++++++++++ tests/test_functions.py | 300 +++++++++++++++++++++ 8 files changed, 1206 insertions(+) create mode 100644 docs/hub/code.md create mode 100644 src/norbix_python/functions.py create mode 100644 tests/test_functions.py diff --git a/README.md b/README.md index 31fdbee..b7ffd76 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,35 @@ Details: [docs/hub/triggers.md](./docs/hub/triggers.md), [docs/hub/sms.md](./docs/hub/sms.md), [docs/hub/push.md](./docs/hub/push.md), [docs/hub/account.md](./docs/hub/account.md). +## Cloud functions + +Norbix does not host code: a trigger, a scheduler task or a manual call runs a +function in **your** cloud (AWS Lambda, Azure Functions, Google Cloud Functions) +through a Code integration, and the function receives one JSON envelope — the +event, the data, a token to call Norbix back as a service user, and your +config. `parse_function_event` reads it; `client_options()` turns it into a +client for that user: + +```python +from norbix_python import Norbix, parse_function_event + + +def handler(raw, _context): + event = parse_function_event(raw) # raises on an unknown shape / specVersion + if event.event == "database.record.updated": + change = event.as_database_record_change() # .old / .new as dicts, None when absent + norbix = Norbix(**event.client_options()) # acts as the run-as user, 15 minutes + ... + return {"ok": True} +``` + +From your backend: `norbix.hub.code.get_code_functions(integrationId=...)`, +`get_code_function_qualifiers(...)`, `invoke_code_function(functionRef=..., payload=..., invocationType="Sync")`, +`get_code_function_event_sample(source="database")` and +`get_code_function_runs(triggerId=...)`. Details and the typed views +(`as_user_change`, `as_file_change`, `as_payment`, `as_scheduled_task`, `payload()`): +[docs/hub/code.md](./docs/hub/code.md). + ## Errors ```python diff --git a/docs/hub/_index.md b/docs/hub/_index.md index ed0ab34..3064919 100644 --- a/docs/hub/_index.md +++ b/docs/hub/_index.md @@ -4,6 +4,7 @@ | --- | ---: | | [`account`](./account.md) | 61 | | [`ai`](./ai.md) | 14 | +| [`code`](./code.md) | 33 | | [`database`](./database.md) | 67 | | [`echo`](./echo.md) | 1 | | [`email`](./email.md) | 1 | diff --git a/docs/hub/code.md b/docs/hub/code.md new file mode 100644 index 0000000..609c65f --- /dev/null +++ b/docs/hub/code.md @@ -0,0 +1,118 @@ +# HUB · Code + +Access with `norbix.hub.code` (and the same methods, awaited, on +`AsyncNorbix`). The Code module holds the credentials of your own cloud +(AWS Lambda, Azure Functions, Google Cloud Functions) as **Code +integrations**, lists the functions it may call, invokes them by hand, and +is what a trigger's `Code` action and a scheduler `CodeFunctionalCall` task +point at. Norbix does not host code: the function runs in your cloud and +receives one event envelope — see [Cloud functions](#cloud-functions). + +Keyword arguments other than the path ids, `timeout` and `bearer_token` are +the request: the query string for `GET` / `DELETE`, the JSON body otherwise. +Integration ids look like `int_…`. + +## Module and integrations + +| Method | Verb | Path | Scope | Parameters go in | +| --- | --- | --- | --- | --- | +| `enable_code` | `PUT` | `/{version}/code/enable` | `project` | — | +| `disable_code` | `PUT` | `/{version}/code/disable` | `project` | — | +| `get_code_integrations` | `GET` | `/{version}/code/integrations` | `project` | query | +| `get_code_integration` | `GET` | `/{version}/code/integrations/{id}` | `project` | path: `id` | +| `save_code_integration` | `POST` | `/{version}/code/integrations` | `project` | JSON body | +| `test_code_integration` | `POST` | `/{version}/code/integrations/test` | `project` | JSON body | +| `confirm_code_integration_human_delivery` | `POST` | `/{version}/code/integrations/confirm-human-delivery` | `project` | JSON body | +| `set_code_integration_as_default` | `PUT` | `/{version}/code/integrations/{Id}/default` | `project` | path: `id` | +| `enable_code_integration` | `PUT` | `/{version}/code/integrations/{Id}/enable` | `project` | path: `id` | +| `disable_code_integration` | `PUT` | `/{version}/code/integrations/{Id}/disable` | `project` | path: `id` | +| `delete_code_integration` | `DELETE` | `/{version}/code/integrations/{Id}` | `project` | path: `id` | + +The 17 `*_marketplace_*` methods of the module are from an earlier +generation of the gateway contract (the `…/bindings` routes); the marketplace +is documented when its own campaign step lands. + +## Cloud functions + +The five routes of the gateway contract `Code.Functions.md` (§3.3, §3.4, +§5.1). Every method, its verb, path, query and body are asserted in +`tests/hub/test_code.py`, for the sync and the async client. + +| Method | Verb | Path | Scope | Parameters go in | +| --- | --- | --- | --- | --- | +| `get_code_functions` | `GET` | `/{version}/code/functions` | `project` | query: `integrationId` (required), `prefix`, `tag` (`key` or `key=value`), `pageSize`, `startingAfter`, `endingBefore` | +| `get_code_function_qualifiers` | `GET` | `/{version}/code/functions/qualifiers` | `project` | query: `integrationId`, `functionId` | +| `invoke_code_function` | `POST` | `/{version}/code/functions/invoke` | `project` | JSON body: `functionRef` `{integrationId, functionId, qualifier?}`, `payload?`, `invocationType` (`Sync` / `Async`), `runAsUserId?`, `config?` | +| `get_code_function_event_sample` | `GET` | `/{version}/code/functions/event-sample` | `project` | query: `source` (`database` · `membership` · `files` · `payments` · `ai` · `scheduler` · `manual`), `event?`, `schemaId?` | +| `get_code_function_runs` | `GET` | `/{version}/code/functions/runs` | `project` | query: exactly one of `triggerId` / `taskId`, `limit` (1–50, default 20) | + +```python +integration_id = "int_6rBACnEaxQ0x1jTiHtAuWV" +functions = norbix.hub.code.get_code_functions(integrationId=integration_id, prefix="orders-") +qualifiers = norbix.hub.code.get_code_function_qualifiers(integrationId=integration_id, functionId="orders-on-update") +result = norbix.hub.code.invoke_code_function( + functionRef={"integrationId": integration_id, "functionId": "orders-on-update", "qualifier": "prod"}, + payload={"orderId": "6ac54a59212c29912eaddd81", "dryRun": True}, + invocationType="Sync", +) +sample = norbix.hub.code.get_code_function_event_sample(source="database", event="database.record.updated") +runs = norbix.hub.code.get_code_function_runs(triggerId="trg_…", limit=5) +``` + +`get_code_functions` is a live listing from the provider (no Norbix-side +state), paged in memory on the function id. An Azure integration cannot list +(`CM-ERRORS-CODE-010`) — type the function name instead. `invoke_code_function` +answers `{statusCode, output, durationMs, requestId?, logs?}`; `output` is the +function's body as JSON when it parses, else the raw text. `runs` is the last +20 (up to 50) runs of one trigger or task, newest first: +`{firedAt, event, source, succeeded, statusCode?, durationMs?, error?, requestId?}`. + +### The event envelope — `parse_function_event` + +Inside the function, the SDK reads the envelope Norbix sent +(`CodeFunctionEventDto`, one shape for every source) and gives you a client +that acts as the run-as user: + +```python +import json + +from norbix_python import Norbix, parse_function_event + + +def handler(raw, _context): # AWS Lambda; Azure / Google pass the body the same way + event = parse_function_event(raw) # str, bytes or the decoded dict; raises on specVersion != "1" + if event.event != "database.record.updated": + return {"ok": True} + + change = event.as_database_record_change() # .old / .new as dicts, None when absent + old, new = change.old or {}, change.new or {} + if old.get("status") != new.get("status") and new.get("status") == "processing": + norbix = Norbix(**event.client_options()) # project, env, region, hub / api urls, the run-as token + norbix.api.database.insert_one( + "order_events", + document=json.dumps({"orderId": new["_id"], "status": "processing", "eventId": event.event_id}), + ) + return {"ok": True} +``` + +| On the event | What it is | +| --- | --- | +| `event`, `event_id`, `id`, `source`, `created_on`, `spec_version` | the envelope header; `event` is the handler key (`database.record.updated`, `scheduler.task.fired`, `manual.invoke`, …), `source` one of `database` · `membership` · `files` · `payments` · `ai` · `scheduler` · `manual` | +| `context` | `project_id`, `account_id`, `environment`, `region`, `hub_url`, `api_url`, `echo_url`, `trigger_id` / `task_id`, `correlation_id`, `caller` (`user_id`, `email`, `kind` — data, never authority), `run_as` (`user_id`, `access_token` valid 15 minutes, `expires_at`) | +| `as_database_record_change()` | `collection`, `schema_id`, `schema_version`, `record_id`, `old`, `new`, `truncated`, `old_ref` / `new_ref` (over 200 KB the documents are left out and the refs say where to fetch them) | +| `as_user_change()` | `user_id`, `old`, `new` | +| `as_file_change()` | `old` (delete), `new` (upload), `path` | +| `as_payment()` | `integration_id`, `provider`, `provider_event_id`, `event_name`, `status`, `amount_minor`, `currency`, `external_ids`, `occurred_on`, `received_on` | +| `as_scheduled_task()` | `task_id`, `name`, `fired_at` | +| `as_ai_usage()` | `payload`, `target_user_id` | +| `payload()` | the caller's JSON on a manual invoke, or a scheduler task's static payload; `None` when there is none | +| `config` | the developer's JSON bound at fire time; `None` when the action has none | +| `client_options()` | the `Norbix` / `AsyncNorbix` keyword arguments: `project_id`, `account_id`, `env`, `region`, `bearer_token`, `base_url_hub`, `base_url_api` | +| `raw` | the decoded envelope, for anything else | + +A view of a slot the event does not carry (a file event asked for a record +change) raises `NorbixFunctionEventError`, as does a payload that is not an +object or not `specVersion` `"1"`. Timestamps stay ISO-8601 strings. Every +sample source, the refusals and the client built from an event are asserted +in `tests/test_functions.py`. A sample envelope for a local run comes from +`get_code_function_event_sample`. diff --git a/src/norbix_python/__init__.py b/src/norbix_python/__init__.py index 9b623e5..7039136 100644 --- a/src/norbix_python/__init__.py +++ b/src/norbix_python/__init__.py @@ -9,6 +9,7 @@ RateLimitError, ValidationError, ) +from .functions import FunctionEvent, NorbixFunctionEventError, parse_function_event from .models import AuthLoginResult, DatabaseFindResult, ReferenceDisplay from .webhooks import ( AsyncNorbixWebhookReceiver, @@ -22,15 +23,18 @@ "AuthenticationError", "DatabaseFindResult", "ErrorItem", + "FunctionEvent", "LoginCredentials", "Norbix", "NorbixApi", "NorbixHub", "NorbixError", + "NorbixFunctionEventError", "NorbixWebhookReceiver", "NotFoundError", "RateLimitError", "ReferenceDisplay", "ValidationError", + "parse_function_event", ] __version__ = "1.1.1" diff --git a/src/norbix_python/functions.py b/src/norbix_python/functions.py new file mode 100644 index 0000000..8e2e719 --- /dev/null +++ b/src/norbix_python/functions.py @@ -0,0 +1,459 @@ +"""The event envelope a Norbix cloud function receives. + +When a trigger, a scheduler task or a manual call (`hub.code.invoke_code_function`) +runs one of your functions on AWS Lambda, Azure Functions or Google Cloud +Functions, the function gets one JSON envelope (`CodeFunctionEventDto` in +`references/hub_dtos.py`). :func:`parse_function_event` turns it into a +:class:`FunctionEvent`: a thin wrapper over the decoded dict with typed views +of the slots in `data` and the constructor options of a client that acts as +the run-as user. Nothing here re-declares the DTO; every field is read from +the raw envelope on access, and `raw` is always there for the rest. +""" + +from __future__ import annotations + +import json +from typing import Any + +from .errors import NorbixError + +#: The envelope version this helper reads. +FUNCTION_EVENT_SPEC_VERSION = "1" + + +class NorbixFunctionEventError(NorbixError): + """The payload was not a Norbix function event envelope (or a slot is missing).""" + + def __init__(self, message: str) -> None: + super().__init__(message, code="FUNCTION_EVENT_INVALID") + + +def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + +class _View: + """A read-only view over one object of the envelope. `raw` is the dict itself.""" + + __slots__ = ("raw",) + + def __init__(self, raw: Any) -> None: + self.raw: dict[str, Any] = _as_dict(raw) + + def __repr__(self) -> str: # pragma: no cover - debugging aid + return f"{type(self).__name__}({self.raw!r})" + + +class RunAs(_View): + """`context.runAs` — the service user the function acts as, and its token.""" + + @property + def user_id(self) -> str | None: + return self.raw.get("userId") + + @property + def access_token(self) -> str | None: + """A short-lived bearer token (15 minutes) for the run-as user; Hub and Api.""" + return self.raw.get("accessToken") + + @property + def expires_at(self) -> str | None: + return self.raw.get("expiresAt") + + +class Caller(_View): + """`context.caller` — who caused the source event, as data, never authority.""" + + @property + def user_id(self) -> str | None: + return self.raw.get("userId") + + @property + def email(self) -> str | None: + return self.raw.get("email") + + @property + def kind(self) -> str | None: + """`User`, `ApiKey` or `System`.""" + return self.raw.get("kind") + + +class FunctionContext(_View): + """`context` — where the event happened and how to call Norbix back.""" + + @property + def account_id(self) -> str | None: + return self.raw.get("accountId") + + @property + def project_id(self) -> str | None: + return self.raw.get("projectId") + + @property + def environment(self) -> str | None: + """The project environment the event happened in (`PROD`, `TEST`, …).""" + return self.raw.get("environment") + + @property + def region(self) -> str | None: + return self.raw.get("region") + + @property + def hub_url(self) -> str | None: + return self.raw.get("hubUrl") + + @property + def api_url(self) -> str | None: + return self.raw.get("apiUrl") + + @property + def echo_url(self) -> str | None: + return self.raw.get("echoUrl") + + @property + def trigger_id(self) -> str | None: + return self.raw.get("triggerId") + + @property + def task_id(self) -> str | None: + return self.raw.get("taskId") + + @property + def correlation_id(self) -> str | None: + return self.raw.get("correlationId") + + @property + def caller(self) -> Caller | None: + value = self.raw.get("caller") + return Caller(value) if isinstance(value, dict) else None + + @property + def run_as(self) -> RunAs: + return RunAs(self.raw.get("runAs")) + + +class DatabaseRecordChange(_View): + """`data.database` — one record before and after (Database triggers).""" + + @property + def collection(self) -> str | None: + return self.raw.get("collection") + + @property + def schema_id(self) -> str | None: + return self.raw.get("schemaId") + + @property + def schema_version(self) -> int | None: + return self.raw.get("schemaVersion") + + @property + def record_id(self) -> str | None: + return self.raw.get("recordId") + + @property + def old(self) -> dict[str, Any] | None: + """The document before the change; None on an insert, or when truncated.""" + value = self.raw.get("old") + return value if isinstance(value, dict) else None + + @property + def new(self) -> dict[str, Any] | None: + """The document after the change; None on a delete, or when truncated.""" + value = self.raw.get("new") + return value if isinstance(value, dict) else None + + @property + def truncated(self) -> bool: + """True when `old` / `new` were left out (over 200 KB): fetch them by the refs.""" + return bool(self.raw.get("truncated", False)) + + @property + def old_ref(self) -> dict[str, Any] | None: + value = self.raw.get("oldRef") + return value if isinstance(value, dict) else None + + @property + def new_ref(self) -> dict[str, Any] | None: + value = self.raw.get("newRef") + return value if isinstance(value, dict) else None + + +class UserChange(_View): + """`data.user` — a membership user before and after (Membership triggers).""" + + @property + def user_id(self) -> str | None: + return self.raw.get("userId") + + @property + def old(self) -> dict[str, Any] | None: + value = self.raw.get("old") + return value if isinstance(value, dict) else None + + @property + def new(self) -> dict[str, Any] | None: + value = self.raw.get("new") + return value if isinstance(value, dict) else None + + +class FileChange(_View): + """`data.file` — an upload (`new`) or a delete (`old`), Files triggers.""" + + @property + def old(self) -> dict[str, Any] | None: + value = self.raw.get("old") + return value if isinstance(value, dict) else None + + @property + def new(self) -> dict[str, Any] | None: + value = self.raw.get("new") + return value if isinstance(value, dict) else None + + @property + def path(self) -> str | None: + return self.raw.get("path") + + +class PaymentFact(_View): + """`data.payment` — the webhook fact a Payments trigger fired on (no "before").""" + + @property + def integration_id(self) -> str | None: + return self.raw.get("integrationId") + + @property + def provider(self) -> str | None: + return self.raw.get("provider") + + @property + def provider_event_id(self) -> str | None: + return self.raw.get("providerEventId") + + @property + def event_name(self) -> str | None: + return self.raw.get("eventName") + + @property + def status(self) -> str | None: + return self.raw.get("status") + + @property + def amount_minor(self) -> int | None: + return self.raw.get("amountMinor") + + @property + def currency(self) -> str | None: + return self.raw.get("currency") + + @property + def external_ids(self) -> dict[str, str]: + return _as_dict(self.raw.get("externalIds")) + + @property + def occurred_on(self) -> str | None: + return self.raw.get("occurredOn") + + @property + def received_on(self) -> str | None: + return self.raw.get("receivedOn") + + +class ScheduledTaskInfo(_View): + """`data.task` — the scheduler task that fired (`scheduler.task.fired`).""" + + @property + def task_id(self) -> str | None: + return self.raw.get("taskId") + + @property + def name(self) -> str | None: + return self.raw.get("name") + + @property + def fired_at(self) -> str | None: + return self.raw.get("firedAt") + + +class AiUsageEvent(_View): + """`data.ai` — an AI trigger's payload.""" + + @property + def payload(self) -> dict[str, Any]: + return _as_dict(self.raw.get("payload")) + + @property + def target_user_id(self) -> str | None: + return self.raw.get("targetUserId") + + +class FunctionEvent: + """One delivery to a cloud function: the envelope of `Code.Functions.md` §4. + + Built by :func:`parse_function_event`. `raw` is the decoded envelope; the + properties read it on access. The `as_*` views return the typed slot of + `data` for the source, and raise :class:`NorbixFunctionEventError` when the + event carries no such slot (a file event asked for a record change). + """ + + __slots__ = ("raw",) + + def __init__(self, raw: dict[str, Any]) -> None: + self.raw = raw + + def __repr__(self) -> str: # pragma: no cover - debugging aid + return f"FunctionEvent(id={self.id!r}, event={self.event!r}, source={self.source!r})" + + # -- envelope fields --------------------------------------------------- + + @property + def spec_version(self) -> str: + return str(self.raw.get("specVersion")) + + @property + def id(self) -> str | None: + """This delivery.""" + return self.raw.get("id") + + @property + def event_id(self) -> str | None: + """The change; the same for a webhook delivery of the same change.""" + return self.raw.get("eventId") + + @property + def event(self) -> str | None: + """The handler key: `database.record.updated`, `scheduler.task.fired`, `manual.invoke`, …""" + return self.raw.get("event") + + @property + def source(self) -> str | None: + """`database`, `membership`, `files`, `payments`, `ai`, `scheduler` or `manual`.""" + return self.raw.get("source") + + @property + def created_on(self) -> str | None: + return self.raw.get("createdOn") + + @property + def context(self) -> FunctionContext: + return FunctionContext(self.raw.get("context")) + + @property + def data(self) -> dict[str, Any]: + """The raw `data` object: one optional slot per source.""" + return _as_dict(self.raw.get("data")) + + @property + def config(self) -> dict[str, Any] | None: + """The developer's JSON config, bound at fire time; None when the action has none.""" + value = self.raw.get("config") + return value if isinstance(value, dict) else None + + # -- typed views of `data` ---------------------------------------------- + + def _slot(self, name: str) -> dict[str, Any]: + value = self.data.get(name) + if not isinstance(value, dict): + raise NorbixFunctionEventError( + f"The function event {self.id!r} ({self.event!r}, source {self.source!r}) " + f"carries no data.{name}." + ) + return value + + def as_database_record_change(self) -> DatabaseRecordChange: + """`data.database` — Database triggers.""" + return DatabaseRecordChange(self._slot("database")) + + def as_user_change(self) -> UserChange: + """`data.user` — Membership triggers.""" + return UserChange(self._slot("user")) + + def as_file_change(self) -> FileChange: + """`data.file` — Files triggers.""" + return FileChange(self._slot("file")) + + def as_payment(self) -> PaymentFact: + """`data.payment` — Payments triggers.""" + return PaymentFact(self._slot("payment")) + + def as_scheduled_task(self) -> ScheduledTaskInfo: + """`data.task` — a scheduler task fire.""" + return ScheduledTaskInfo(self._slot("task")) + + def as_ai_usage(self) -> AiUsageEvent: + """`data.ai` — AI triggers.""" + return AiUsageEvent(self._slot("ai")) + + def payload(self) -> dict[str, Any] | None: + """`data.payload` — the caller's JSON (manual invoke) or a task's static payload.""" + value = self.data.get("payload") + return value if isinstance(value, dict) else None + + # -- calling Norbix back ------------------------------------------------- + + def client_options(self) -> dict[str, Any]: + """Keyword arguments for :class:`norbix_python.Norbix` / `AsyncNorbix` as the run-as user. + + `project_id`, `account_id`, `env`, `region`, `bearer_token` (the run-as + token) and `base_url_hub` / `base_url_api` (the deployment the event came + from). Keys without a value in the envelope are left out, so + `Norbix(**event.client_options())` is enough. + """ + context = self.context + options: dict[str, Any] = { + "project_id": context.project_id, + "account_id": context.account_id, + "env": context.environment, + "region": context.region, + "bearer_token": context.run_as.access_token, + "base_url_hub": context.hub_url, + "base_url_api": context.api_url, + } + return {key: value for key, value in options.items() if value} + + +def parse_function_event(raw: str | bytes | bytearray | dict[str, Any]) -> FunctionEvent: + """Read the envelope a cloud function received. + + `raw` is the JSON text (or bytes) of the request body, or the already + decoded object your platform hands you (a Lambda `event`). Raises + :class:`NorbixFunctionEventError` when it is not an object, not JSON, or + not `specVersion` "1". + """ + if isinstance(raw, (bytes, bytearray)): + raw = raw.decode("utf-8") + if isinstance(raw, str): + try: + decoded: Any = json.loads(raw) + except ValueError as exc: + raise NorbixFunctionEventError(f"The function event is not JSON: {exc}") from exc + else: + decoded = raw + if not isinstance(decoded, dict): + raise NorbixFunctionEventError( + f"The function event must be a JSON object, got {type(decoded).__name__}." + ) + spec_version = decoded.get("specVersion") + if spec_version != FUNCTION_EVENT_SPEC_VERSION: + raise NorbixFunctionEventError( + f"Unknown function event specVersion {spec_version!r}; " + f"this SDK reads {FUNCTION_EVENT_SPEC_VERSION!r}. Upgrade the SDK or check the sender." + ) + return FunctionEvent(decoded) + + +__all__ = [ + "FUNCTION_EVENT_SPEC_VERSION", + "AiUsageEvent", + "Caller", + "DatabaseRecordChange", + "FileChange", + "FunctionContext", + "FunctionEvent", + "NorbixFunctionEventError", + "PaymentFact", + "RunAs", + "ScheduledTaskInfo", + "UserChange", + "parse_function_event", +] diff --git a/src/norbix_python/hub/code.py b/src/norbix_python/hub/code.py index 42d7090..3231098 100644 --- a/src/norbix_python/hub/code.py +++ b/src/norbix_python/hub/code.py @@ -373,6 +373,71 @@ def get_marketplace_listing_function_tokens(self, listing_view_id: str, function bearer_token=bearer_token, ) + def get_code_functions(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/code/functions""" + return self._transport.send( + target="hub", + path="/{version}/code/functions", + method="GET", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def get_code_function_qualifiers(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/code/functions/qualifiers""" + return self._transport.send( + target="hub", + path="/{version}/code/functions/qualifiers", + method="GET", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def invoke_code_function(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/code/functions/invoke""" + return self._transport.send( + target="hub", + path="/{version}/code/functions/invoke", + method="POST", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def get_code_function_event_sample(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/code/functions/event-sample""" + return self._transport.send( + target="hub", + path="/{version}/code/functions/event-sample", + method="GET", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def get_code_function_runs(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/code/functions/runs""" + return self._transport.send( + target="hub", + path="/{version}/code/functions/runs", + method="GET", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + class AsyncCodeModule: def __init__(self, transport: AsyncTransport) -> None: @@ -741,3 +806,68 @@ async def get_marketplace_listing_function_tokens(self, listing_view_id: str, fu timeout=timeout, bearer_token=bearer_token, ) + + async def get_code_functions(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/code/functions""" + return await self._transport.send( + target="hub", + path="/{version}/code/functions", + method="GET", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def get_code_function_qualifiers(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/code/functions/qualifiers""" + return await self._transport.send( + target="hub", + path="/{version}/code/functions/qualifiers", + method="GET", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def invoke_code_function(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/code/functions/invoke""" + return await self._transport.send( + target="hub", + path="/{version}/code/functions/invoke", + method="POST", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def get_code_function_event_sample(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/code/functions/event-sample""" + return await self._transport.send( + target="hub", + path="/{version}/code/functions/event-sample", + method="GET", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def get_code_function_runs(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/code/functions/runs""" + return await self._transport.send( + target="hub", + path="/{version}/code/functions/runs", + method="GET", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) diff --git a/tests/hub/test_code.py b/tests/hub/test_code.py index 8b29c7e..b49df3a 100644 --- a/tests/hub/test_code.py +++ b/tests/hub/test_code.py @@ -1,5 +1,16 @@ from __future__ import annotations +import asyncio +import json +from collections.abc import Callable +from typing import Any +from urllib.parse import parse_qs, urlparse + +import httpx +import pytest + +from norbix_python import AsyncNorbix + from ..helpers import make_client @@ -34,6 +45,12 @@ def test_hub_code_module_surface() -> None: assert callable(module.get_marketplace_function_catalog) assert callable(module.get_marketplace_listings) assert callable(module.get_marketplace_listing_function_tokens) + # cloud functions (gateway audit/code, Code.Functions.md §3.3, §3.4, §5.1) + assert callable(module.get_code_functions) + assert callable(module.get_code_function_qualifiers) + assert callable(module.invoke_code_function) + assert callable(module.get_code_function_event_sample) + assert callable(module.get_code_function_runs) def test_hub_code_disable_code_request_shape() -> None: @@ -258,3 +275,151 @@ def test_hub_code_get_marketplace_listing_function_tokens_request_shape() -> Non assert transport.last_request['method'] == 'GET' assert transport.last_request is not None assert transport.last_request['url'].startswith('https://') + + +# --- cloud functions --------------------------------------------------------- +# +# The five routes of Code.Functions.md: the verb, the resolved path, and where +# each parameter goes — GET filters in the query string, the invoke request in +# the JSON body. Sync and async against a capture transport, never a gateway. + +INTEGRATION_ID = "int_1" +FUNCTION_REF = {"integrationId": INTEGRATION_ID, "functionId": "orders-on-update", "qualifier": "prod"} +INVOKE_REQUEST: dict[str, Any] = { + "functionRef": FUNCTION_REF, + "payload": {"orderId": "6ac54a59212c29912eaddd81", "dryRun": True}, + "invocationType": "Sync", + "runAsUserId": "usr_svc", +} + +# name, verb, expected path, expected query (None: none), expected body (None: none), call +FunctionCase = tuple[str, str, str, dict[str, list[str]] | None, dict[str, Any] | None, Callable[[Any], Any]] + +FUNCTION_CASES: list[FunctionCase] = [ + ( + "get_code_functions", + "GET", + "/v3/code/functions", + {"integrationId": [INTEGRATION_ID], "prefix": ["orders-"], "tag": ["team=orders"], "pageSize": ["50"]}, + None, + lambda m: m.get_code_functions(integrationId=INTEGRATION_ID, prefix="orders-", tag="team=orders", pageSize=50), + ), + ( + "get_code_function_qualifiers", + "GET", + "/v3/code/functions/qualifiers", + {"integrationId": [INTEGRATION_ID], "functionId": ["orders-on-update"]}, + None, + lambda m: m.get_code_function_qualifiers(integrationId=INTEGRATION_ID, functionId="orders-on-update"), + ), + ( + "invoke_code_function", + "POST", + "/v3/code/functions/invoke", + None, + INVOKE_REQUEST, + lambda m: m.invoke_code_function(**INVOKE_REQUEST), + ), + ( + "get_code_function_event_sample", + "GET", + "/v3/code/functions/event-sample", + {"source": ["database"], "event": ["database.record.updated"], "schemaId": ["sch_1"]}, + None, + lambda m: m.get_code_function_event_sample(source="database", event="database.record.updated", schemaId="sch_1"), + ), + ( + "get_code_function_runs", + "GET", + "/v3/code/functions/runs", + {"triggerId": ["trg_1"], "limit": ["5"]}, + None, + lambda m: m.get_code_function_runs(triggerId="trg_1", limit=5), + ), +] + + +def _assert_function_request( + name: str, + method: str, + url: str, + body: str, + verb: str, + path: str, + query: dict[str, list[str]] | None, + expected_body: dict[str, Any] | None, +) -> None: + parsed = urlparse(url) + assert method == verb, f"{name}: verb {method}, expected {verb}" + assert parsed.path == path, f"{name}: got {url}, expected the path {path}" + assert parse_qs(parsed.query) == (query or {}), f"{name}: query {parsed.query!r}" + if expected_body is None: + assert body == "", f"{name}: expected no body, got {body!r}" + else: + assert json.loads(body) == expected_body, f"{name}: body {body}" + + +@pytest.mark.parametrize(("name", "verb", "path", "query", "body", "call"), FUNCTION_CASES, ids=[c[0] for c in FUNCTION_CASES]) +def test_code_function_request_shape( + name: str, + verb: str, + path: str, + query: dict[str, list[str]] | None, + body: dict[str, Any] | None, + call: Callable[[Any], Any], +) -> None: + client, transport = make_client() + call(client.hub.code) + assert transport.last_request is not None + _assert_function_request( + name, + transport.last_request["method"], + transport.last_request["url"], + transport.last_request["body"], + verb, + path, + query, + body, + ) + assert transport.last_request["headers"]["authorization"] == "Bearer test-token" + + +def test_async_code_function_endpoints_hit_the_same_routes() -> None: + seen: list[httpx.Request] = [] + + def handler(request: httpx.Request) -> httpx.Response: + seen.append(request) + return httpx.Response(200, json={}) + + async def run() -> None: + client = AsyncNorbix( + project_id="test-project", + bearer_token="test-token", + http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)), + ) + try: + for _, _, _, _, _, call in FUNCTION_CASES: + await call(client.hub.code) + finally: + await client.aclose() + + asyncio.run(run()) + + assert len(seen) == len(FUNCTION_CASES) + for request, (name, verb, path, query, body, _) in zip(seen, FUNCTION_CASES, strict=True): + _assert_function_request(name, request.method, str(request.url), request.content.decode("utf-8"), verb, path, query, body) + + +def test_code_function_surface_size() -> None: + """Five cloud-function routes in the gateway (Code.Functions.md). A new one changes this count.""" + assert len(FUNCTION_CASES) == 5 + assert len({case[0] for case in FUNCTION_CASES}) == 5 + + +def test_invoke_code_function_sends_the_function_ref_in_the_body() -> None: + client, transport = make_client() + client.hub.code.invoke_code_function(functionRef=FUNCTION_REF, invocationType="Async") + + assert transport.last_request is not None + sent = json.loads(transport.last_request["body"]) + assert sent == {"functionRef": FUNCTION_REF, "invocationType": "Async"} diff --git a/tests/test_functions.py b/tests/test_functions.py new file mode 100644 index 0000000..1afe91b --- /dev/null +++ b/tests/test_functions.py @@ -0,0 +1,300 @@ +"""The cloud-function event envelope (Code.Functions.md §4). + +`parse_function_event` reads the JSON a function receives, one sample per +source, and the typed views read the slot of `data` for that source. A bad +`specVersion` or a non-object is refused. `client_options()` must build a +`Norbix` that calls the deployment of the envelope as the run-as user — checked +against a capture transport, never a gateway. +""" + +from __future__ import annotations + +import json +from typing import Any + +import httpx +import pytest + +from norbix_python import ( + FunctionEvent, + Norbix, + NorbixError, + NorbixFunctionEventError, + parse_function_event, +) +from norbix_python.functions import FUNCTION_EVENT_SPEC_VERSION + +from .helpers import CaptureTransport + +CONTEXT: dict[str, Any] = { + "accountId": "acc_1", + "projectId": "prj_1", + "environment": "TEST", + "region": "nb-eu-germany", + "hubUrl": "https://hub.example.test", + "apiUrl": "https://api.example.test", + "echoUrl": "https://api.example.test/v3/echo", + "triggerId": "trg_1", + "correlationId": "corr_1", + "caller": {"userId": "usr_caller", "email": "ada@example.test", "kind": "User"}, + "runAs": {"userId": "usr_svc", "accessToken": "run-as-token", "expiresAt": "2026-10-12T08:15:00Z"}, +} + + +def envelope(source: str, event: str, data: dict[str, Any], **extra: Any) -> dict[str, Any]: + return { + "specVersion": "1", + "id": "dlv_1", + "eventId": "evt_1", + "event": event, + "source": source, + "createdOn": "2026-10-12T08:00:00Z", + "context": CONTEXT, + "data": data, + **extra, + } + + +DATABASE = envelope( + "database", + "database.record.updated", + { + "database": { + "collection": "orders", + "schemaId": "sch_1", + "schemaVersion": 3, + "recordId": "6ac54a59212c29912eaddd81", + "old": {"_id": "6ac54a59212c29912eaddd81", "status": "new"}, + "new": {"_id": "6ac54a59212c29912eaddd81", "status": "processing"}, + "truncated": False, + } + }, + config={"acceptStatus": "processing", "imageWidth": 568}, +) + + +# --- parsing ------------------------------------------------------------------ + + +def test_parse_reads_json_text_bytes_and_a_decoded_object() -> None: + text = json.dumps(DATABASE) + for raw in (text, text.encode("utf-8"), DATABASE): + event = parse_function_event(raw) + assert isinstance(event, FunctionEvent) + assert event.spec_version == FUNCTION_EVENT_SPEC_VERSION == "1" + assert event.id == "dlv_1" + assert event.event_id == "evt_1" + assert event.event == "database.record.updated" + assert event.source == "database" + assert event.created_on == "2026-10-12T08:00:00Z" + assert event.config == {"acceptStatus": "processing", "imageWidth": 568} + assert parse_function_event(DATABASE).raw is DATABASE + + +def test_context_is_typed() -> None: + context = parse_function_event(DATABASE).context + assert context.account_id == "acc_1" + assert context.project_id == "prj_1" + assert context.environment == "TEST" + assert context.region == "nb-eu-germany" + assert context.hub_url == "https://hub.example.test" + assert context.api_url == "https://api.example.test" + assert context.echo_url == "https://api.example.test/v3/echo" + assert context.trigger_id == "trg_1" + assert context.task_id is None + assert context.correlation_id == "corr_1" + assert context.caller is not None + assert (context.caller.user_id, context.caller.email, context.caller.kind) == ("usr_caller", "ada@example.test", "User") + assert context.run_as.user_id == "usr_svc" + assert context.run_as.access_token == "run-as-token" + assert context.run_as.expires_at == "2026-10-12T08:15:00Z" + assert context.raw is CONTEXT + + +@pytest.mark.parametrize( + "raw", + [ + {**DATABASE, "specVersion": "2"}, + {**DATABASE, "specVersion": 1}, + {k: v for k, v in DATABASE.items() if k != "specVersion"}, + ], + ids=["other-version", "number-not-text", "missing"], +) +def test_bad_spec_version_is_refused(raw: dict[str, Any]) -> None: + with pytest.raises(NorbixFunctionEventError) as exc: + parse_function_event(raw) + assert "specVersion" in str(exc.value) + assert exc.value.code == "FUNCTION_EVENT_INVALID" + assert isinstance(exc.value, NorbixError) + + +@pytest.mark.parametrize("raw", ["not json", "[1, 2]", "42"], ids=["not-json", "list", "number"]) +def test_non_object_is_refused(raw: str) -> None: + with pytest.raises(NorbixFunctionEventError): + parse_function_event(raw) + + +# --- one sample per source ---------------------------------------------------- + + +def test_database_record_change() -> None: + change = parse_function_event(DATABASE).as_database_record_change() + assert change.collection == "orders" + assert change.schema_id == "sch_1" + assert change.schema_version == 3 + assert change.record_id == "6ac54a59212c29912eaddd81" + assert change.old == {"_id": "6ac54a59212c29912eaddd81", "status": "new"} + assert change.new == {"_id": "6ac54a59212c29912eaddd81", "status": "processing"} + assert change.truncated is False + assert change.old_ref is None and change.new_ref is None + + +def test_database_insert_has_no_old_and_a_truncated_change_carries_refs() -> None: + inserted = envelope("database", "database.record.inserted", {"database": {"collection": "orders", "new": {"a": 1}}}) + change = parse_function_event(inserted).as_database_record_change() + assert change.old is None + assert change.new == {"a": 1} + + ref = {"kind": "document", "projectId": "prj_1", "databaseIntegrationId": "int_db", "schemaId": "sch_1", "externalId": "rec_1"} + big = envelope("database", "database.record.updated", {"database": {"collection": "orders", "truncated": True, "oldRef": ref, "newRef": ref}}) + change = parse_function_event(big).as_database_record_change() + assert change.truncated is True + assert change.old is None and change.new is None + assert change.old_ref == ref and change.new_ref == ref + + +def test_user_change() -> None: + raw = envelope( + "membership", + "membership.user.updated", + {"user": {"userId": "usr_1", "old": {"email": "a@x.test"}, "new": {"email": "b@x.test"}}}, + ) + change = parse_function_event(raw).as_user_change() + assert change.user_id == "usr_1" + assert change.old == {"email": "a@x.test"} + assert change.new == {"email": "b@x.test"} + + +def test_file_change() -> None: + uploaded = envelope("files", "files.file.uploaded", {"file": {"new": {"path": "img/a.png", "size": 10}, "path": "img/a.png"}}) + change = parse_function_event(uploaded).as_file_change() + assert change.old is None + assert change.new == {"path": "img/a.png", "size": 10} + assert change.path == "img/a.png" + + +def test_payment_fact() -> None: + raw = envelope( + "payments", + "payments.webhook.received", + { + "payment": { + "integrationId": "int_pay", + "provider": "Stripe", + "providerEventId": "evt_stripe", + "eventName": "payment_intent.succeeded", + "status": "succeeded", + "amountMinor": 1999, + "currency": "EUR", + "externalIds": {"paymentIntentId": "pi_1"}, + "occurredOn": "2026-10-12T07:59:00Z", + "receivedOn": "2026-10-12T08:00:00Z", + } + }, + ) + event = parse_function_event({**raw, "context": {**CONTEXT, "caller": None}}) + assert event.context.caller is None + payment = event.as_payment() + assert payment.integration_id == "int_pay" + assert payment.provider == "Stripe" + assert payment.provider_event_id == "evt_stripe" + assert payment.event_name == "payment_intent.succeeded" + assert payment.status == "succeeded" + assert payment.amount_minor == 1999 + assert payment.currency == "EUR" + assert payment.external_ids == {"paymentIntentId": "pi_1"} + assert payment.occurred_on == "2026-10-12T07:59:00Z" + assert payment.received_on == "2026-10-12T08:00:00Z" + + +def test_ai_usage() -> None: + raw = envelope("ai", "ai.credits.warning", {"ai": {"payload": {"used": 90, "limit": 100}, "targetUserId": "usr_9"}}) + usage = parse_function_event(raw).as_ai_usage() + assert usage.payload == {"used": 90, "limit": 100} + assert usage.target_user_id == "usr_9" + + +def test_scheduled_task_and_its_static_payload() -> None: + raw = envelope( + "scheduler", + "scheduler.task.fired", + {"task": {"taskId": "tsk_1", "name": "nightly", "firedAt": "2026-10-12T02:00:00Z"}, "payload": {"mode": "full"}}, + ) + event = parse_function_event({**raw, "context": {**CONTEXT, "triggerId": None, "taskId": "tsk_1"}}) + assert event.context.trigger_id is None + assert event.context.task_id == "tsk_1" + task = event.as_scheduled_task() + assert (task.task_id, task.name, task.fired_at) == ("tsk_1", "nightly", "2026-10-12T02:00:00Z") + assert event.payload() == {"mode": "full"} + + +def test_manual_invoke_payload_and_no_config() -> None: + raw = envelope("manual", "manual.invoke", {"payload": {"orderId": "o1", "dryRun": True}}) + event = parse_function_event(raw) + assert event.payload() == {"orderId": "o1", "dryRun": True} + assert event.config is None + assert event.data == {"payload": {"orderId": "o1", "dryRun": True}} + + +def test_a_view_of_a_missing_slot_is_refused() -> None: + event = parse_function_event(envelope("files", "files.file.deleted", {"file": {"old": {"path": "a"}}})) + with pytest.raises(NorbixFunctionEventError) as exc: + event.as_database_record_change() + assert "data.database" in str(exc.value) + assert event.payload() is None + + +# --- calling Norbix back -------------------------------------------------------- + + +def test_client_options_are_the_norbix_constructor_kwargs() -> None: + options = parse_function_event(DATABASE).client_options() + assert options == { + "project_id": "prj_1", + "account_id": "acc_1", + "env": "TEST", + "region": "nb-eu-germany", + "bearer_token": "run-as-token", + "base_url_hub": "https://hub.example.test", + "base_url_api": "https://api.example.test", + } + + +def test_client_options_leave_out_what_the_envelope_has_not() -> None: + bare = {**DATABASE, "context": {"projectId": "prj_1", "hubUrl": "https://hub.example.test", "apiUrl": "https://api.example.test", "runAs": {"accessToken": "t"}}} + assert parse_function_event(bare).client_options() == { + "project_id": "prj_1", + "bearer_token": "t", + "base_url_hub": "https://hub.example.test", + "base_url_api": "https://api.example.test", + } + + +def test_a_client_built_from_the_event_calls_the_deployment_as_the_run_as_user() -> None: + event = parse_function_event(DATABASE) + transport = CaptureTransport() + norbix = Norbix(**event.client_options(), http_client=httpx.Client(transport=transport)) + + norbix.hub.code.get_code_function_runs(triggerId=event.context.trigger_id) + assert transport.last_request is not None + hub = httpx.URL(transport.last_request["url"]) + assert f"{hub.scheme}://{hub.host}" == "https://hub.example.test" + assert hub.path == "/v3/code/functions/runs" + assert transport.last_request["headers"]["authorization"] == "Bearer run-as-token" + assert transport.last_request["headers"]["norbix-env"] == "TEST" + assert transport.last_request["headers"]["nb-region"] == "nb-eu-germany" + + norbix.api.database.find("orders") + api = httpx.URL(transport.last_request["url"]) + assert f"{api.scheme}://{api.host}" == "https://api.example.test" + assert norbix.get_env() == "TEST"