"""Schemas for the data-access domain HTTP surface.""" from __future__ import annotations import uuid from datetime import datetime from typing import Literal from pydantic import BaseModel, ConfigDict, Field, model_validator class DataAccessProviderEffect(BaseModel): model_config = ConfigDict(extra="forbid") member_prefix: str role: str class DataAccessV1Contract(BaseModel): model_config = ConfigDict(extra="forbid") domain: str providers: tuple[str, ...] resource_types: tuple[str, ...] principal_types: tuple[str, ...] entitlements: tuple[str, ...] approval_flows: tuple[str, ...] provider_effect: DataAccessProviderEffect class DataAccessAuthorityBoundary(BaseModel): model_config = ConfigDict(extra="forbid") semantic_principal_field: str provider_member_email_field: str runtime_owner_authority_field: str approval_transport_owner: str provider_mutation_owner: str request_handlers_mutate_provider: bool feishu_callbacks_mutate_provider: bool class DataAccessCapabilityResponse(BaseModel): model_config = ConfigDict(extra="forbid") status: str provider_mutation_enabled: bool contract: DataAccessV1Contract authority_boundary: DataAccessAuthorityBoundary non_goals: tuple[str, ...] class DataAccessRequestableTableMatchResponse(BaseModel): model_config = ConfigDict(extra="forbid") table_id: str table_type: str description: str | None catalog_tier: Literal["lineage", "managed"] class DataAccessRequestableResourceResponse(BaseModel): model_config = ConfigDict(extra="forbid") id: uuid.UUID provider: str resource_type: str status: str requestable: bool availability: str notice_code: str | None project_id: str dataset_id: str display_name: str | None description: str | None owner_person_id: str | None owner_label: str | None owner_resolution_status: str owner_authority_enabled: bool caller_can_manage_owner: bool owner_source: str source_version: str source_etag: str updated_at: datetime | None matched_table_count: int = Field(default=0, ge=0) matched_tables: tuple[DataAccessRequestableTableMatchResponse, ...] = () class DataAccessDatasetTableSummaryResponse(BaseModel): model_config = ConfigDict(extra="forbid") table_id: str table_type: str resource_id: uuid.UUID | None owner_person_id: str | None owner_label: str | None owner_source: str owner_inherited: bool caller_can_manage_owner: bool metadata_source: Literal["provider", "lineage_catalog"] = "provider" catalog_tier: Literal["lineage", "managed", "discovered"] = "discovered" lineage_tracked: bool = False downstream_reference_count: int = Field(0, ge=0) lineage_direction: Literal["upstream", "downstream"] | None = None selectable: bool = True schema_available: bool = True class DataAccessOwnerTransferRequest(BaseModel): model_config = ConfigDict(extra="forbid") owner_person_id: uuid.UUID class DataAccessTableSelectionRequest(BaseModel): model_config = ConfigDict(extra="forbid") dataset_resource_id: uuid.UUID table_id: str = Field(..., min_length=1, max_length=1024) def _validate_submit_resource_selection( resource_ids: list[uuid.UUID], table_selections: list[DataAccessTableSelectionRequest], ) -> None: if not resource_ids and not table_selections: raise ValueError("Provide at least one resource_id or table_selection") if len(resource_ids) + len(table_selections) > 50: raise ValueError("Provide at most 50 dataset and table selections in total") dataset_ids = set(resource_ids) if any( selection.dataset_resource_id in dataset_ids for selection in table_selections ): raise ValueError( "A dataset and one of its tables cannot be selected together", ) class DataAccessDatasetFieldSchemaResponse(BaseModel): model_config = ConfigDict(extra="forbid") name: str field_path: str field_type: str mode: str description: str | None depth: int ordinal_position: int class DataAccessDatasetTableSchemaResponse(BaseModel): model_config = ConfigDict(extra="forbid") project_id: str dataset_id: str table_id: str table_type: str description: str | None fields: tuple[DataAccessDatasetFieldSchemaResponse, ...] class DataAccessTableLineageNodeResponse(BaseModel): model_config = ConfigDict(extra="forbid") node_id: str project_id: str dataset_id: str table_id: str node_type: str materialization: str | None description: str | None layer: str level: int is_root: bool is_target: bool dataset_cataloged: bool dataset_requestable: bool dataset_owner_person_id: str | None dataset_owner_label: str | None dataset_owner_source: str | None class DataAccessTableLineageEdgeResponse(BaseModel): model_config = ConfigDict(extra="forbid") upstream_node_id: str downstream_node_id: str relationship: str class DataAccessTableLineageResponse(BaseModel): model_config = ConfigDict(extra="forbid") status: str direction: Literal["upstream", "downstream"] catalog_id: str | None source_version: str | None generated_at: str | None project_id: str dataset_id: str table_id: str nodes: tuple[DataAccessTableLineageNodeResponse, ...] edges: tuple[DataAccessTableLineageEdgeResponse, ...] truncated: bool class DataAccessRequestablePrincipalResponse(BaseModel): model_config = ConfigDict(extra="forbid") id: uuid.UUID email: str name: str description: str | None status: str google_group_id: str aliases: tuple[str, ...] direct_members_count: int | None owner_count: int caller_is_owner: bool caller_is_member: bool class DataAccessSubmitServiceAccountGrantRequest(BaseModel): """Client supplies a typed GCP SA email and selected DAX resources.""" model_config = ConfigDict(extra="forbid") service_account_email: str = Field(..., min_length=3, max_length=255) resource_ids: list[uuid.UUID] = Field(default_factory=list, max_length=50) table_selections: list[DataAccessTableSelectionRequest] = Field( default_factory=list, max_length=50, ) business_reason: str = Field(..., min_length=4, max_length=1000) detail_url: str | None = Field(None, max_length=1024) @model_validator(mode="after") def validate_resource_selection(self): _validate_submit_resource_selection(self.resource_ids, self.table_selections) return self class DataAccessSubmitPersonalGrantRequest(BaseModel): """Request exact dataset/table read access for the authenticated employee.""" model_config = ConfigDict(extra="forbid") resource_ids: list[uuid.UUID] = Field(default_factory=list, max_length=50) table_selections: list[DataAccessTableSelectionRequest] = Field( default_factory=list, max_length=50, ) business_reason: str = Field(..., min_length=4, max_length=1000) detail_url: str | None = Field(None, max_length=1024) @model_validator(mode="after") def validate_resource_selection(self): _validate_submit_resource_selection(self.resource_ids, self.table_selections) return self class DataAccessSubmitGrantResponse(BaseModel): model_config = ConfigDict(extra="forbid") request_id: uuid.UUID approval_request_id: uuid.UUID | None feishu_instance_code: str | None feishu_instance_created: bool reused_existing: bool status: str dataset_count: int class DataAccessRequestDetailApprovalResponse(BaseModel): model_config = ConfigDict(extra="forbid") id: uuid.UUID external_id: str approval_type: str status: str updated_at: datetime | None class DataAccessRequestDetailCompiledPreviewResponse(BaseModel): model_config = ConfigDict(extra="forbid") provider: str resource_type: str target_project_id: str target_dataset_id: str target_table_id: str | None = None provider_member: str provider_role: str desired_state: str entitlement_version_snapshot: str resource_source_version_snapshot: str resource_source_etag_snapshot: str compiler_version: str managed_reason: str already_present_at_first_apply: bool | None = None class DataAccessRequestDetailRunResponse(BaseModel): model_config = ConfigDict(extra="forbid") id: uuid.UUID run_id: uuid.UUID mode: str effect_status: str provider_outcome: str provider_etag: str | None error_message: str | None trace_id: str | None started_at: datetime completed_at: datetime class DataAccessRequestDetailEffectResponse( DataAccessRequestDetailCompiledPreviewResponse, ): model_config = ConfigDict(extra="forbid") id: uuid.UUID binding_id: uuid.UUID status: str last_provider_etag: str | None last_provider_outcome: str | None last_error: str | None updated_at: datetime | None runs: list[DataAccessRequestDetailRunResponse] class DataAccessRequestDetailItemResponse(BaseModel): model_config = ConfigDict(extra="forbid") id: uuid.UUID line_no: int resource_id: uuid.UUID project_id: str dataset_id: str table_id: str | None = None locator: str display_name: str | None owner_person_id: str | None owner_label: str | None owner_resolution_status: str | None item_status: str display_state: str staleness_reason: str | None source_version_snapshot: str source_etag_snapshot: str binding_id: uuid.UUID | None binding_status: str | None binding_reconcile_status: str | None binding_last_reconcile_at: datetime | None binding_last_reconcile_error: str | None compiled_preview: DataAccessRequestDetailCompiledPreviewResponse effect: DataAccessRequestDetailEffectResponse | None class DataAccessRequestDetailResponse(BaseModel): model_config = ConfigDict(extra="forbid") id: uuid.UUID requester_person_id: uuid.UUID requester_label: str | None principal_email_group_id: uuid.UUID | None principal_id: uuid.UUID | None = None principal_type: str = "google_group" principal_email_snapshot: str principal_google_group_id_snapshot: str | None principal_name: str | None principal_status: str | None entitlement: str entitlement_version: str provider_role: str status: str reconcile_status: str feishu_instance_code: str | None approval_request_id: uuid.UUID | None approval: DataAccessRequestDetailApprovalResponse | None business_reason: str | None trace_id: str | None submitted_at: datetime decided_at: datetime | None last_reconcile_run_id: uuid.UUID | None last_reconcile_at: datetime | None last_reconcile_error: str | None item_count: int items: list[DataAccessRequestDetailItemResponse] class DataAccessGroupAccessPrincipalResponse(BaseModel): model_config = ConfigDict(extra="forbid") id: uuid.UUID email: str name: str status: str google_group_id: str direct_members_count: int | None owner_count: int caller_is_owner: bool class DataAccessGroupAccessSummaryResponse(BaseModel): model_config = ConfigDict(extra="forbid") total: int active: int pending_apply: int failed: int revoked_pending: int revoked_history: int class DataAccessGroupAccessLatestRunResponse(BaseModel): model_config = ConfigDict(extra="forbid") id: uuid.UUID run_id: uuid.UUID mode: str effect_status: str provider_outcome: str provider_etag: str | None error_message: str | None trace_id: str | None completed_at: datetime class DataAccessGroupAccessItemResponse(BaseModel): model_config = ConfigDict(extra="forbid") binding_id: uuid.UUID resource_id: uuid.UUID source_request_id: uuid.UUID | None resource_status: str resource_requestable: bool project_id: str dataset_id: str locator: str display_name: str | None owner_person_id: str | None owner_label: str | None owner_resolution_status: str entitlement: str entitlement_version: str principal_type: str provider_member: str provider_role: str binding_status: str revoke_reason: str | None access_state: str notice_code: str | None reconcile_status: str desired_state: str | None effect_status: str | None latest_provider_outcome: str | None latest_error: str | None latest_run: DataAccessGroupAccessLatestRunResponse | None already_present_at_first_apply: bool | None granted_at: datetime revoked_at: datetime | None last_reconcile_at: datetime | None class DataAccessGroupAccessResponse(BaseModel): model_config = ConfigDict(extra="forbid") principal: DataAccessGroupAccessPrincipalResponse summary: DataAccessGroupAccessSummaryResponse items: list[DataAccessGroupAccessItemResponse] class DataAccessMyGrantItemResponse(BaseModel): model_config = ConfigDict(extra="forbid") binding_id: uuid.UUID resource_id: uuid.UUID source_request_id: uuid.UUID source_request_submitted_at: datetime project_id: str dataset_id: str table_id: str | None resource_type: str locator: str display_name: str | None description: str | None owner_label: str | None principal_type: str principal_email: str provider_member: str provider_role: str entitlement: str access_state: str binding_status: str reconcile_status: str latest_provider_outcome: str | None latest_error: str | None granted_at: datetime revoked_at: datetime | None last_reconcile_at: datetime | None # ── Lifecycle (migration 606) ── revoke_reason: str | None = None lifecycle_state: str | None = None expires_at: datetime | None = None days_to_expiry: int | None = None renewal_count: int = 0 renewals_remaining: int | None = None renewable: bool = False last_renewed_at: datetime | None = None owner_confirmation_due_at: datetime | None = None owner_confirmed_at: datetime | None = None owner_confirmation_overdue_days: int | None = None #: True when the caller is the technical owner who must answer the #: semiannual confirmation for this service-account grant. owner_confirmation_actionable: bool = False class DataAccessFailedApplySummaryResponse(BaseModel): model_config = ConfigDict(extra="forbid") total: int failed: int retryable: int desired_present: int desired_absent: int class DataAccessFailedApplyLatestRunResponse(BaseModel): model_config = ConfigDict(extra="forbid") id: uuid.UUID run_id: uuid.UUID mode: str effect_status: str provider_outcome: str provider_etag: str | None error_message: str | None trace_id: str | None completed_at: datetime class DataAccessFailedApplyItemResponse(BaseModel): model_config = ConfigDict(extra="forbid") effect_id: uuid.UUID binding_id: uuid.UUID resource_id: uuid.UUID request_id: uuid.UUID | None integration_id: str project_id: str dataset_id: str locator: str display_name: str | None principal_email_group_id: uuid.UUID principal_email: str principal_google_group_id: str | None entitlement: str provider_member: str provider_role: str desired_state: str managed_reason: str status: str binding_status: str reconcile_status: str last_provider_outcome: str | None last_provider_etag: str | None last_error: str | None latest_run: DataAccessFailedApplyLatestRunResponse | None created_at: datetime updated_at: datetime | None last_reconcile_at: datetime | None class DataAccessFailedApplyQueueResponse(BaseModel): model_config = ConfigDict(extra="forbid") summary: DataAccessFailedApplySummaryResponse items: list[DataAccessFailedApplyItemResponse] page: int page_size: int total: int class DataAccessFailedApplyRetryResponse(BaseModel): model_config = ConfigDict(extra="forbid") effect_id: uuid.UUID previous_status: str status: str desired_state: str retry_run_id: uuid.UUID apply_run_id: uuid.UUID | None apply_mode: str provider_write_count: int result_count: int blocked_effect_count: int config_error: str | None item: DataAccessFailedApplyItemResponse class DataAccessReconcileRunSummaryResponse(BaseModel): model_config = ConfigDict(extra="forbid") total: int failed: int retryable: int applied: int desired_present: int desired_absent: int class DataAccessReconcileRunItemResponse(BaseModel): model_config = ConfigDict(extra="forbid") id: uuid.UUID run_id: uuid.UUID effect_id: uuid.UUID binding_id: uuid.UUID request_id: uuid.UUID | None resource_id: uuid.UUID integration_id: str mode: str provider: str resource_type: str project_id: str dataset_id: str locator: str display_name: str | None principal_email_group_id: uuid.UUID principal_email: str principal_google_group_id: str | None provider_member: str provider_role: str desired_state: str effect_status: str provider_outcome: str provider_etag: str | None error_message: str | None trace_id: str | None started_at: datetime completed_at: datetime current_effect_status: str binding_status: str binding_reconcile_status: str current_provider_outcome: str | None current_error: str | None created_at: datetime | None class DataAccessReconcileRunHistoryResponse(BaseModel): model_config = ConfigDict(extra="forbid") summary: DataAccessReconcileRunSummaryResponse items: list[DataAccessReconcileRunItemResponse] page: int page_size: int total: int class DataAccessCutoverValidationSummaryResponse(BaseModel): model_config = ConfigDict(extra="forbid") total_resources: int cutover_ready: bool blocking_count: int category_counts: dict[str, int] blocking_category_counts: dict[str, int] provider_mutation_attempted: bool request_handlers_mutate_provider: bool feishu_callbacks_mutate_provider: bool class DataAccessCutoverValidationHistoryResponse(BaseModel): model_config = ConfigDict(extra="forbid") request_item_count: int binding_count: int active_binding_count: int revoked_binding_count: int absent_effect_count: int effect_run_count: int has_preserved_history: bool class DataAccessCutoverValidationItemResponse(BaseModel): model_config = ConfigDict(extra="forbid") resource_id: uuid.UUID integration_id: str provider: str resource_type: str project_id: str dataset_id: str locator: str display_name: str | None catalog_resource_id: str | None status: str requestable: bool owner_source: str owner_person_id: str | None owner_resolution_status: str source_version: str source_etag: str findings: list[str] blocking_findings: list[str] invalid_reasons: list[str] history: DataAccessCutoverValidationHistoryResponse class DataAccessCutoverValidationReportResponse(BaseModel): model_config = ConfigDict(extra="forbid") integration_id: str | None integration_count: int summary: DataAccessCutoverValidationSummaryResponse items: list[DataAccessCutoverValidationItemResponse] page: int page_size: int total: int class DataAccessAdminOverviewDatasetsResponse(BaseModel): model_config = ConfigDict(extra="forbid") total: int active: int requestable: int suspended: int deleted: int class DataAccessAdminOverviewBindingsResponse(BaseModel): model_config = ConfigDict(extra="forbid") active: int revoked_pending: int revoked_history: int groups_with_access: int datasets_with_access: int class DataAccessAdminOverviewPeopleResponse(BaseModel): model_config = ConfigDict(extra="forbid") reach: int groups_with_unknown_size: int class DataAccessAdminOverviewRequestsResponse(BaseModel): model_config = ConfigDict(extra="forbid") total: int pending_approval: int approved: int rejected: int stale: int cancelled: int reconcile_applied: int reconcile_in_progress: int reconcile_failed: int class DataAccessAdminOverviewEffectsResponse(BaseModel): model_config = ConfigDict(extra="forbid") failed: int retryable: int applied: int pending_reconcile: int class DataAccessAdminOverviewApplyModeResponse(BaseModel): model_config = ConfigDict(extra="forbid") integration_count: int apply_enabled_count: int provider_mutation_enabled: bool class DataAccessAdminOverviewTopDatasetResponse(BaseModel): model_config = ConfigDict(extra="forbid") resource_id: uuid.UUID project_id: str dataset_id: str locator: str display_name: str | None active_group_count: int class DataAccessAdminOverviewTopGroupResponse(BaseModel): model_config = ConfigDict(extra="forbid") principal_email_group_id: uuid.UUID email: str name: str direct_members_count: int | None active_dataset_count: int class DataAccessAdminOverviewRecentRequestResponse(BaseModel): model_config = ConfigDict(extra="forbid") id: uuid.UUID requester_label: str | None principal_email: str dataset_count: int status: str reconcile_status: str submitted_at: datetime class DataAccessAdminOverviewResponse(BaseModel): model_config = ConfigDict(extra="forbid") generated_at: datetime datasets: DataAccessAdminOverviewDatasetsResponse bindings: DataAccessAdminOverviewBindingsResponse people: DataAccessAdminOverviewPeopleResponse requests: DataAccessAdminOverviewRequestsResponse effects: DataAccessAdminOverviewEffectsResponse apply_mode: DataAccessAdminOverviewApplyModeResponse top_datasets: list[DataAccessAdminOverviewTopDatasetResponse] top_groups: list[DataAccessAdminOverviewTopGroupResponse] recent_requests: list[DataAccessAdminOverviewRecentRequestResponse]