diff --git a/changes/11506.fix.md b/changes/11506.fix.md new file mode 100644 index 00000000000..7f4a0247c0b --- /dev/null +++ b/changes/11506.fix.md @@ -0,0 +1 @@ +Honor `AND`/`OR`/`NOT` clauses in `myDeployments` and `projectDeployments` GraphQL filters, which were previously ignored and caused multi-condition deployment queries to return unfiltered results. diff --git a/changes/11509.enhance.md b/changes/11509.enhance.md new file mode 100644 index 00000000000..ae5f58442bc --- /dev/null +++ b/changes/11509.enhance.md @@ -0,0 +1 @@ +Centralize v2 entity filter `AND` / `OR` / `NOT` clauses on a shared `BaseFilter` and add `convert_and` / `convert_or` / `convert_not` helpers on `BaseFilterAdapter`, applying the corrected `(A AND B) OR (C AND D)` and per-sub-filter `NOT` semantics across every adapter. diff --git a/src/ai/backend/common/dto/manager/v2/agent/request.py b/src/ai/backend/common/dto/manager/v2/agent/request.py index 20781aa40b7..ed3aaf5dff9 100644 --- a/src/ai/backend/common/dto/manager/v2/agent/request.py +++ b/src/ai/backend/common/dto/manager/v2/agent/request.py @@ -16,6 +16,7 @@ AgentStatusFilter, OrderDirection, ) +from ai.backend.common.dto.manager.v2.common import BaseFilter __all__ = ( "AdminSearchAgentsInput", @@ -42,7 +43,7 @@ class AgentPathParam(BaseRequestModel): # --------------------------------------------------------------------------- -class AgentFilter(BaseRequestModel): +class AgentFilter(BaseFilter): """Filter conditions for agent search.""" id: StringFilter | None = Field( @@ -65,18 +66,6 @@ class AgentFilter(BaseRequestModel): "and their case-insensitive and negated variants." ), ) - AND: list[AgentFilter] | None = Field( - default=None, description="All sub-conditions must match." - ) - OR: list[AgentFilter] | None = Field( - default=None, description="At least one sub-condition must match." - ) - NOT: list[AgentFilter] | None = Field( - default=None, description="None of the sub-conditions must match." - ) - - -AgentFilter.model_rebuild() class AgentOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/artifact/request.py b/src/ai/backend/common/dto/manager/v2/artifact/request.py index e596dd0beef..642030ae9a4 100644 --- a/src/ai/backend/common/dto/manager/v2/artifact/request.py +++ b/src/ai/backend/common/dto/manager/v2/artifact/request.py @@ -10,6 +10,7 @@ from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel from ai.backend.common.dto.manager.query import IntFilter, StringFilter, UUIDFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from .types import ( ArtifactAvailability, @@ -291,7 +292,7 @@ class ArtifactStatusChangedInputDTO(BaseRequestModel): artifact_revision_ids: list[UUID] = Field(description="List of artifact revision IDs to watch.") -class ArtifactGQLFilterInputDTO(BaseRequestModel): +class ArtifactGQLFilterInputDTO(BaseFilter): """GQL-facing filter for artifacts.""" type: list[ArtifactType] | None = Field(default=None) @@ -299,12 +300,6 @@ class ArtifactGQLFilterInputDTO(BaseRequestModel): registry: StringFilter | None = Field(default=None) source: StringFilter | None = Field(default=None) availability: list[ArtifactAvailability] | None = Field(default=None) - AND: list[ArtifactGQLFilterInputDTO] | None = Field(default=None) - OR: list[ArtifactGQLFilterInputDTO] | None = Field(default=None) - NOT: list[ArtifactGQLFilterInputDTO] | None = Field(default=None) - - -ArtifactGQLFilterInputDTO.model_rebuild() class ArtifactGQLOrderByInputDTO(BaseRequestModel): @@ -328,7 +323,7 @@ class ArtifactRevisionRemoteStatusFilterDTO(BaseRequestModel): equals: ArtifactRemoteStatus | None = Field(default=None) -class ArtifactRevisionGQLFilterInputDTO(BaseRequestModel): +class ArtifactRevisionGQLFilterInputDTO(BaseFilter): """GQL-facing filter for artifact revisions.""" status: ArtifactRevisionStatusFilterDTO | None = Field(default=None) @@ -336,12 +331,6 @@ class ArtifactRevisionGQLFilterInputDTO(BaseRequestModel): version: StringFilter | None = Field(default=None) artifact_id: UUIDFilter | None = Field(default=None) size: IntFilter | None = Field(default=None) - AND: list[ArtifactRevisionGQLFilterInputDTO] | None = Field(default=None) - OR: list[ArtifactRevisionGQLFilterInputDTO] | None = Field(default=None) - NOT: list[ArtifactRevisionGQLFilterInputDTO] | None = Field(default=None) - - -ArtifactRevisionGQLFilterInputDTO.model_rebuild() class ArtifactRevisionGQLOrderByInputDTO(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/artifact_registry/request.py b/src/ai/backend/common/dto/manager/v2/artifact_registry/request.py index 37c418dfba0..b6ef39af86e 100644 --- a/src/ai/backend/common/dto/manager/v2/artifact_registry/request.py +++ b/src/ai/backend/common/dto/manager/v2/artifact_registry/request.py @@ -11,6 +11,7 @@ from ai.backend.common.api_handlers import BaseRequestModel from ai.backend.common.data.storage.registries.types import ModelTarget from ai.backend.common.dto.manager.query import StringFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from .types import ArtifactRegistryType @@ -98,7 +99,7 @@ class ArtifactOrderingInput(BaseRequestModel): ) -class ArtifactFilterInput(BaseRequestModel): +class ArtifactFilterInput(BaseFilter): """Filtering options for artifacts, supporting recursive AND/OR/NOT composition.""" artifact_type: list[str] | None = Field(default=None, description="Filter by artifact type(s)") @@ -120,9 +121,6 @@ class ArtifactFilterInput(BaseRequestModel): availability: list[str] | None = Field( default=None, description="Filter by availability status" ) - AND: list[ArtifactFilterInput] | None = Field(default=None, description="AND filter group") - OR: list[ArtifactFilterInput] | None = Field(default=None, description="OR filter group") - NOT: list[ArtifactFilterInput] | None = Field(default=None, description="NOT filter group") # --------------------------------------------------------------------------- diff --git a/src/ai/backend/common/dto/manager/v2/audit_log/request.py b/src/ai/backend/common/dto/manager/v2/audit_log/request.py index 937897f9e2e..e2ba7a885f1 100644 --- a/src/ai/backend/common/dto/manager/v2/audit_log/request.py +++ b/src/ai/backend/common/dto/manager/v2/audit_log/request.py @@ -6,6 +6,7 @@ from ai.backend.common.api_handlers import BaseRequestModel from ai.backend.common.dto.manager.query import DateTimeFilter, StringFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from .types import AuditLogOrderField, AuditLogStatus, OrderDirection @@ -30,7 +31,7 @@ class AuditLogStatusFilter(BaseRequestModel): not_in: list[AuditLogStatus] | None = Field(default=None, description="Status is not in list") -class AuditLogFilter(BaseRequestModel): +class AuditLogFilter(BaseFilter): """Filter for audit logs.""" entity_type: StringFilter | None = Field(default=None, description="Entity type filter") @@ -40,16 +41,6 @@ class AuditLogFilter(BaseRequestModel): created_at: DateTimeFilter | None = Field( default=None, description="Filter logs by created_at datetime" ) - AND: list[AuditLogFilter] | None = Field(default=None, description="All conditions must match") - OR: list[AuditLogFilter] | None = Field( - default=None, description="At least one condition must match" - ) - NOT: list[AuditLogFilter] | None = Field( - default=None, description="None of the conditions must match" - ) - - -AuditLogFilter.model_rebuild() class AuditLogOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/common.py b/src/ai/backend/common/dto/manager/v2/common.py index 179b058219b..dfe70cbaed1 100644 --- a/src/ai/backend/common/dto/manager/v2/common.py +++ b/src/ai/backend/common/dto/manager/v2/common.py @@ -5,12 +5,14 @@ from decimal import Decimal from enum import StrEnum from functools import cached_property +from typing import Self from pydantic import Field from ai.backend.common.api_handlers import BaseRequestModel, BaseResponseModel __all__ = ( + "BaseFilter", "BinarySizeInfo", "BinarySizeInput", "EnvironmentVariableEntryInfo", @@ -27,6 +29,21 @@ ) +class BaseFilter(BaseRequestModel): + """Base class for v2 entity filter DTOs. + + Provides the recursive ``AND`` / ``OR`` / ``NOT`` boolean clause fields shared + by every filter so subclasses only declare their typed leaf fields. The + clauses are typed as ``list[Self] | None``: each subclass automatically + references its own concrete type without requiring ``model_rebuild()`` + or repeating the three fields. + """ + + AND: list[Self] | None = Field(default=None, description="AND conjunction of sub-filters.") + OR: list[Self] | None = Field(default=None, description="OR conjunction of sub-filters.") + NOT: list[Self] | None = Field(default=None, description="NOT negation of sub-filters.") + + class BinarySizeInput(BaseRequestModel): """Binary size input accepting bytes integer or human-readable string. diff --git a/src/ai/backend/common/dto/manager/v2/deployment/request.py b/src/ai/backend/common/dto/manager/v2/deployment/request.py index e84677eb734..9fe279e0d80 100644 --- a/src/ai/backend/common/dto/manager/v2/deployment/request.py +++ b/src/ai/backend/common/dto/manager/v2/deployment/request.py @@ -27,7 +27,7 @@ StringFilter, UUIDFilter, ) -from ai.backend.common.dto.manager.v2.common import ResourceSlotEntryInput +from ai.backend.common.dto.manager.v2.common import BaseFilter, ResourceSlotEntryInput from ai.backend.common.dto.manager.v2.deployment.types import ( AccessTokenOrderField, AutoScalingRuleOrderField, @@ -542,7 +542,7 @@ class ReplicaTrafficStatusFilter(BaseRequestModel): ) -class DeploymentFilter(BaseRequestModel): +class DeploymentFilter(BaseFilter): """Filter for deployments.""" name: StringFilter | None = Field(default=None, description="Name filter") @@ -562,15 +562,9 @@ class DeploymentFilter(BaseRequestModel): destroyed_at: NullableDateTimeFilter | None = Field( default=None, description="Destruction datetime filter (supports is_null)" ) - AND: list[DeploymentFilter] | None = Field(default=None, description="AND conjunction") - OR: list[DeploymentFilter] | None = Field(default=None, description="OR conjunction") - NOT: list[DeploymentFilter] | None = Field(default=None, description="NOT negation") -DeploymentFilter.model_rebuild() - - -class RevisionFilter(BaseRequestModel): +class RevisionFilter(BaseFilter): """Filter for deployment revisions.""" revision_number: IntFilter | None = Field(default=None, description="Filter by revision number") @@ -584,15 +578,9 @@ class RevisionFilter(BaseRequestModel): ) cluster_mode: StringFilter | None = Field(default=None, description="Cluster mode filter") created_at: DateTimeFilter | None = Field(default=None, description="Creation datetime filter") - AND: list[RevisionFilter] | None = Field(default=None, description="AND conjunction") - OR: list[RevisionFilter] | None = Field(default=None, description="OR conjunction") - NOT: list[RevisionFilter] | None = Field(default=None, description="NOT negation") - -RevisionFilter.model_rebuild() - -class RouteFilter(BaseRequestModel): +class RouteFilter(BaseFilter): """Filter for deployment routes.""" deployment_id: UUID | None = Field(default=None, description="Filter by deployment ID") @@ -605,15 +593,9 @@ class RouteFilter(BaseRequestModel): traffic_status: list[RouteTrafficStatus] | None = Field( default=None, description="Traffic status filter" ) - AND: list[RouteFilter] | None = Field(default=None, description="AND conjunction") - OR: list[RouteFilter] | None = Field(default=None, description="OR conjunction") - NOT: list[RouteFilter] | None = Field(default=None, description="NOT negation") - - -RouteFilter.model_rebuild() -class AccessTokenFilter(BaseRequestModel): +class AccessTokenFilter(BaseFilter): """Filter for access tokens.""" deployment_id: UUID | None = Field(default=None, description="Filter by deployment ID") @@ -622,15 +604,9 @@ class AccessTokenFilter(BaseRequestModel): default=None, description="Expiration datetime filter" ) created_at: DateTimeFilter | None = Field(default=None, description="Creation datetime filter") - AND: list[AccessTokenFilter] | None = Field(default=None, description="AND conjunction") - OR: list[AccessTokenFilter] | None = Field(default=None, description="OR conjunction") - NOT: list[AccessTokenFilter] | None = Field(default=None, description="NOT negation") -AccessTokenFilter.model_rebuild() - - -class AutoScalingRuleFilter(BaseRequestModel): +class AutoScalingRuleFilter(BaseFilter): """Filter for auto-scaling rules.""" deployment_id: UUID | None = Field(default=None, description="Filter by deployment ID") @@ -638,15 +614,9 @@ class AutoScalingRuleFilter(BaseRequestModel): last_triggered_at: NullableDateTimeFilter | None = Field( default=None, description="Last triggered datetime filter" ) - AND: list[AutoScalingRuleFilter] | None = Field(default=None, description="AND conjunction") - OR: list[AutoScalingRuleFilter] | None = Field(default=None, description="OR conjunction") - NOT: list[AutoScalingRuleFilter] | None = Field(default=None, description="NOT negation") - -AutoScalingRuleFilter.model_rebuild() - -class ReplicaFilter(BaseRequestModel): +class ReplicaFilter(BaseFilter): """Filter for deployment replicas.""" deployment_id: UUID | None = Field(default=None, description="Filter by deployment ID") @@ -654,12 +624,6 @@ class ReplicaFilter(BaseRequestModel): traffic_status: ReplicaTrafficStatusFilter | None = Field( default=None, description="Replica traffic status filter" ) - AND: list[ReplicaFilter] | None = Field(default=None, description="AND conjunction") - OR: list[ReplicaFilter] | None = Field(default=None, description="OR conjunction") - NOT: list[ReplicaFilter] | None = Field(default=None, description="NOT negation") - - -ReplicaFilter.model_rebuild() class DeploymentPolicyFilter(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/deployment_revision_preset/request.py b/src/ai/backend/common/dto/manager/v2/deployment_revision_preset/request.py index 2b00dfdbbce..6c88cf8d251 100644 --- a/src/ai/backend/common/dto/manager/v2/deployment_revision_preset/request.py +++ b/src/ai/backend/common/dto/manager/v2/deployment_revision_preset/request.py @@ -8,6 +8,7 @@ from ai.backend.common.config import ModelDefinition from ai.backend.common.dto.manager.query import StringFilter, UUIDFilter from ai.backend.common.dto.manager.v2.common import ( + BaseFilter, EnvironmentVariableEntryInput, OrderDirection, ResourceSlotEntryInput, @@ -92,16 +93,10 @@ class UpdateDeploymentRevisionPresetInput(BaseRequestModel): deployment_strategy: DeploymentStrategyInput | Sentinel | None = Field(default=SENTINEL) -class DeploymentRevisionPresetFilter(BaseRequestModel): +class DeploymentRevisionPresetFilter(BaseFilter): id: UUIDFilter | None = Field(default=None, description="Filter by preset ID.") name: StringFilter | None = Field(default=None) runtime_variant_id: UUIDFilter | None = Field(default=None) - AND: list[DeploymentRevisionPresetFilter] | None = Field(default=None) - OR: list[DeploymentRevisionPresetFilter] | None = Field(default=None) - NOT: list[DeploymentRevisionPresetFilter] | None = Field(default=None) - - -DeploymentRevisionPresetFilter.model_rebuild() class DeploymentRevisionPresetOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/domain/request.py b/src/ai/backend/common/dto/manager/v2/domain/request.py index fba31424bbd..0a85fc2fca7 100644 --- a/src/ai/backend/common/dto/manager/v2/domain/request.py +++ b/src/ai/backend/common/dto/manager/v2/domain/request.py @@ -10,6 +10,7 @@ from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel from ai.backend.common.dto.manager.defs import DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT from ai.backend.common.dto.manager.query import DateTimeFilter, StringFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from ai.backend.common.dto.manager.v2.domain.types import ( DomainOrderField, DomainProjectFilter, @@ -92,7 +93,7 @@ class PurgeDomainInput(BaseRequestModel): name: str = Field(description="Name of the domain to permanently purge.") -class DomainFilter(BaseRequestModel): +class DomainFilter(BaseFilter): """Filter criteria for searching domains.""" name: StringFilter | None = Field(default=None, description="Filter by domain name.") @@ -108,12 +109,6 @@ class DomainFilter(BaseRequestModel): user: DomainUserFilter | None = Field( default=None, description="Filter by nested user conditions." ) - AND: list[DomainFilter] | None = Field(default=None, description="AND logical combinator.") - OR: list[DomainFilter] | None = Field(default=None, description="OR logical combinator.") - NOT: list[DomainFilter] | None = Field(default=None, description="NOT logical combinator.") - - -DomainFilter.model_rebuild() class DomainOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/fair_share/request.py b/src/ai/backend/common/dto/manager/v2/fair_share/request.py index c0a20ce3691..b6f55a9c09d 100644 --- a/src/ai/backend/common/dto/manager/v2/fair_share/request.py +++ b/src/ai/backend/common/dto/manager/v2/fair_share/request.py @@ -11,6 +11,7 @@ from ai.backend.common.api_handlers import BaseRequestModel from ai.backend.common.dto.manager.query import DateRangeFilter, StringFilter, UUIDFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from .types import ( DomainFairShareOrderField, @@ -97,7 +98,7 @@ class UserFairShareUserNestedFilter(BaseRequestModel): # Filter models -class DomainFairShareFilter(BaseRequestModel): +class DomainFairShareFilter(BaseFilter): """Filter for domain fair share queries.""" resource_group: StringFilter | None = Field(default=None, description="Filter by scaling group") @@ -105,16 +106,9 @@ class DomainFairShareFilter(BaseRequestModel): domain: DomainFairShareDomainNestedFilter | None = Field( default=None, description="Filter by domain entity properties" ) - AND: list[DomainFairShareFilter] | None = Field( - default=None, description="Combine with AND logic" - ) - OR: list[DomainFairShareFilter] | None = Field( - default=None, description="Combine with OR logic" - ) - NOT: list[DomainFairShareFilter] | None = Field(default=None, description="Negate filters") -class ProjectFairShareFilter(BaseRequestModel): +class ProjectFairShareFilter(BaseFilter): """Filter for project fair share queries.""" resource_group: StringFilter | None = Field(default=None, description="Filter by scaling group") @@ -123,16 +117,9 @@ class ProjectFairShareFilter(BaseRequestModel): project: ProjectFairShareProjectNestedFilter | None = Field( default=None, description="Filter by project entity properties" ) - AND: list[ProjectFairShareFilter] | None = Field( - default=None, description="Combine with AND logic" - ) - OR: list[ProjectFairShareFilter] | None = Field( - default=None, description="Combine with OR logic" - ) - NOT: list[ProjectFairShareFilter] | None = Field(default=None, description="Negate filters") -class UserFairShareFilter(BaseRequestModel): +class UserFairShareFilter(BaseFilter): """Filter for user fair share queries.""" resource_group: StringFilter | None = Field(default=None, description="Filter by scaling group") @@ -142,17 +129,6 @@ class UserFairShareFilter(BaseRequestModel): user: UserFairShareUserNestedFilter | None = Field( default=None, description="Filter by user entity properties" ) - AND: list[UserFairShareFilter] | None = Field( - default=None, description="Combine with AND logic" - ) - OR: list[UserFairShareFilter] | None = Field(default=None, description="Combine with OR logic") - NOT: list[UserFairShareFilter] | None = Field(default=None, description="Negate filters") - - -# model_rebuild() required for self-referential fields -DomainFairShareFilter.model_rebuild() -ProjectFairShareFilter.model_rebuild() -UserFairShareFilter.model_rebuild() class DomainUsageBucketFilter(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/group/request.py b/src/ai/backend/common/dto/manager/v2/group/request.py index 25fe3f93594..ceaf82f3f7d 100644 --- a/src/ai/backend/common/dto/manager/v2/group/request.py +++ b/src/ai/backend/common/dto/manager/v2/group/request.py @@ -12,6 +12,7 @@ from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel from ai.backend.common.dto.manager.defs import DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT from ai.backend.common.dto.manager.query import DateTimeFilter, StringFilter, UUIDFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from ai.backend.common.dto.manager.v2.group.types import ( OrderDirection, ProjectDomainFilter, @@ -101,7 +102,7 @@ class PurgeProjectInput(BaseRequestModel): group_id: UUID = Field(description="UUID of the group to permanently purge.") -class ProjectFilter(BaseRequestModel): +class ProjectFilter(BaseFilter): """Filter criteria for searching groups.""" id: UUIDFilter | None = Field(default=None, description="Filter by project ID (UUID).") @@ -121,18 +122,6 @@ class ProjectFilter(BaseRequestModel): user: ProjectUserFilter | None = Field( default=None, description="Nested filter for user conditions." ) - AND: list[ProjectFilter] | None = Field( - default=None, description="Combine filters with AND logic." - ) - OR: list[ProjectFilter] | None = Field( - default=None, description="Combine filters with OR logic." - ) - NOT: list[ProjectFilter] | None = Field( - default=None, description="Negate the specified filters." - ) - - -ProjectFilter.model_rebuild() class ProjectOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/image/request.py b/src/ai/backend/common/dto/manager/v2/image/request.py index 2c597e371ad..bebe5a361ac 100644 --- a/src/ai/backend/common/dto/manager/v2/image/request.py +++ b/src/ai/backend/common/dto/manager/v2/image/request.py @@ -11,6 +11,7 @@ from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel from ai.backend.common.dto.manager.query import DateTimeFilter, StringFilter, UUIDFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from .types import ImageOrderField, ImageStatusType, OrderDirection @@ -73,7 +74,7 @@ class ImageAliasNestedFilterInputDTO(BaseRequestModel): alias: StringFilter | None = Field(default=None, description="Filter by alias string.") -class ImageFilterInputDTO(BaseRequestModel): +class ImageFilterInputDTO(BaseFilter): """Filter options for images.""" id: UUIDFilter | None = Field(default=None, description="Filter by image UUID.") @@ -89,14 +90,6 @@ class ImageFilterInputDTO(BaseRequestModel): last_used: DateTimeFilter | None = Field( default=None, description="Filter by last used datetime (before/after)." ) - AND: list[ImageFilterInputDTO] | None = Field( - default=None, description="Combine with AND logic." - ) - OR: list[ImageFilterInputDTO] | None = Field(default=None, description="Combine with OR logic.") - NOT: list[ImageFilterInputDTO] | None = Field(default=None, description="Negate filters.") - - -ImageFilterInputDTO.model_rebuild() class ImageOrderByInputDTO(BaseRequestModel): @@ -106,21 +99,11 @@ class ImageOrderByInputDTO(BaseRequestModel): direction: OrderDirection = Field(default=OrderDirection.ASC, description="Order direction.") -class ImageAliasFilterInputDTO(BaseRequestModel): +class ImageAliasFilterInputDTO(BaseFilter): """Filter options for image aliases.""" alias: StringFilter | None = Field(default=None, description="Filter by alias string.") image_id: UUIDFilter | None = Field(default=None, description="Filter by image ID.") - AND: list[ImageAliasFilterInputDTO] | None = Field( - default=None, description="Combine with AND logic." - ) - OR: list[ImageAliasFilterInputDTO] | None = Field( - default=None, description="Combine with OR logic." - ) - NOT: list[ImageAliasFilterInputDTO] | None = Field(default=None, description="Negate filters.") - - -ImageAliasFilterInputDTO.model_rebuild() class ImageAliasOrderByInputDTO(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/kernel/request.py b/src/ai/backend/common/dto/manager/v2/kernel/request.py index 65911f0fea0..fd968eaa9e4 100644 --- a/src/ai/backend/common/dto/manager/v2/kernel/request.py +++ b/src/ai/backend/common/dto/manager/v2/kernel/request.py @@ -6,6 +6,7 @@ from ai.backend.common.api_handlers import BaseRequestModel from ai.backend.common.dto.manager.query import UUIDFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from .types import KernelOrderField, KernelStatusFilter, OrderDirection @@ -16,18 +17,12 @@ ) -class KernelFilter(BaseRequestModel): +class KernelFilter(BaseFilter): """Filter conditions for kernel search.""" id: UUIDFilter | None = Field(default=None, description="Filter by kernel ID") session_id: UUIDFilter | None = Field(default=None, description="Filter by session ID") status: KernelStatusFilter | None = Field(default=None, description="Filter by status") - AND: list[KernelFilter] | None = Field(default=None, description="AND logical operator") - OR: list[KernelFilter] | None = Field(default=None, description="OR logical operator") - NOT: list[KernelFilter] | None = Field(default=None, description="NOT logical operator") - - -KernelFilter.model_rebuild() class KernelOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/keypair/request.py b/src/ai/backend/common/dto/manager/v2/keypair/request.py index c1577232f3b..03d8b4c40f8 100644 --- a/src/ai/backend/common/dto/manager/v2/keypair/request.py +++ b/src/ai/backend/common/dto/manager/v2/keypair/request.py @@ -4,7 +4,6 @@ from __future__ import annotations -from typing import Self from uuid import UUID from pydantic import Field @@ -12,7 +11,7 @@ from ai.backend.common.api_handlers import BaseRequestModel from ai.backend.common.dto.manager.defs import MAX_PAGE_LIMIT from ai.backend.common.dto.manager.query import DateTimeFilter, StringFilter -from ai.backend.common.dto.manager.v2.common import OrderDirection +from ai.backend.common.dto.manager.v2.common import BaseFilter, OrderDirection from ai.backend.common.dto.manager.v2.keypair.types import KeypairOrderField __all__ = ( @@ -32,7 +31,7 @@ ) -class KeypairFilter(BaseRequestModel): +class KeypairFilter(BaseFilter): """Filter for keypair search.""" is_active: bool | None = None @@ -42,13 +41,6 @@ class KeypairFilter(BaseRequestModel): created_at: DateTimeFilter | None = None last_used: DateTimeFilter | None = None - AND: list[Self] | None = None - OR: list[Self] | None = None - NOT: list[Self] | None = None - - -KeypairFilter.model_rebuild() - class KeypairOrderBy(BaseRequestModel): """Order by specification for keypairs.""" diff --git a/src/ai/backend/common/dto/manager/v2/login_history/request.py b/src/ai/backend/common/dto/manager/v2/login_history/request.py index 391b326c759..e424774f16e 100644 --- a/src/ai/backend/common/dto/manager/v2/login_history/request.py +++ b/src/ai/backend/common/dto/manager/v2/login_history/request.py @@ -6,6 +6,7 @@ from ai.backend.common.api_handlers import BaseRequestModel from ai.backend.common.dto.manager.query import DateTimeFilter, StringFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from .types import LoginAttemptResult, LoginHistoryOrderField, OrderDirection @@ -33,7 +34,7 @@ class LoginHistoryResultFilter(BaseRequestModel): ) -class LoginHistoryFilter(BaseRequestModel): +class LoginHistoryFilter(BaseFilter): """Filter for login history.""" domain_name: StringFilter | None = Field(default=None, description="Domain name filter") @@ -43,18 +44,6 @@ class LoginHistoryFilter(BaseRequestModel): created_at: DateTimeFilter | None = Field( default=None, description="Filter history by created_at datetime" ) - AND: list[LoginHistoryFilter] | None = Field( - default=None, description="All conditions must match" - ) - OR: list[LoginHistoryFilter] | None = Field( - default=None, description="At least one condition must match" - ) - NOT: list[LoginHistoryFilter] | None = Field( - default=None, description="None of the conditions must match" - ) - - -LoginHistoryFilter.model_rebuild() class LoginHistoryOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/login_session/request.py b/src/ai/backend/common/dto/manager/v2/login_session/request.py index ee207efb10f..d4d6766028d 100644 --- a/src/ai/backend/common/dto/manager/v2/login_session/request.py +++ b/src/ai/backend/common/dto/manager/v2/login_session/request.py @@ -8,6 +8,7 @@ from ai.backend.common.api_handlers import BaseRequestModel from ai.backend.common.dto.manager.query import DateTimeFilter, StringFilter, UUIDFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from .types import LoginSessionOrderField, LoginSessionStatus, OrderDirection @@ -38,7 +39,7 @@ class LoginSessionStatusFilter(BaseRequestModel): ) -class LoginSessionFilter(BaseRequestModel): +class LoginSessionFilter(BaseFilter): """Filter for login sessions.""" user_id: UUIDFilter | None = Field(default=None, description="User ID filter") @@ -50,18 +51,6 @@ class LoginSessionFilter(BaseRequestModel): last_accessed_at: DateTimeFilter | None = Field( default=None, description="Filter sessions by last_accessed_at datetime" ) - AND: list[LoginSessionFilter] | None = Field( - default=None, description="All conditions must match" - ) - OR: list[LoginSessionFilter] | None = Field( - default=None, description="At least one condition must match" - ) - NOT: list[LoginSessionFilter] | None = Field( - default=None, description="None of the conditions must match" - ) - - -LoginSessionFilter.model_rebuild() class LoginSessionOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/model_card/request.py b/src/ai/backend/common/dto/manager/v2/model_card/request.py index 80f53181542..38a3e8bb774 100644 --- a/src/ai/backend/common/dto/manager/v2/model_card/request.py +++ b/src/ai/backend/common/dto/manager/v2/model_card/request.py @@ -6,7 +6,7 @@ from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel from ai.backend.common.dto.manager.query import StringFilter, UUIDFilter -from ai.backend.common.dto.manager.v2.common import OrderDirection +from ai.backend.common.dto.manager.v2.common import BaseFilter, OrderDirection from ai.backend.common.dto.manager.v2.deployment.request import DeploymentStrategyInput from ai.backend.common.dto.manager.v2.model_card.types import ( ModelCardAccessLevel, @@ -72,7 +72,7 @@ class UpdateModelCardInput(BaseRequestModel): access_level: ModelCardAccessLevel | Sentinel | None = Field(default=SENTINEL) -class ModelCardFilter(BaseRequestModel): +class ModelCardFilter(BaseFilter): name: StringFilter | None = Field(default=None) domain_name: StringFilter | None = Field(default=None) project_id: UUIDFilter | None = Field(default=None) @@ -83,12 +83,6 @@ class ModelCardFilter(BaseRequestModel): "Evaluated as an EXISTS subquery joining the model VFolder's host column." ), ) - AND: list[ModelCardFilter] | None = Field(default=None) - OR: list[ModelCardFilter] | None = Field(default=None) - NOT: list[ModelCardFilter] | None = Field(default=None) - - -ModelCardFilter.model_rebuild() class ModelCardOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/notification/request.py b/src/ai/backend/common/dto/manager/v2/notification/request.py index b6d8c6aa0ea..aacd68b794c 100644 --- a/src/ai/backend/common/dto/manager/v2/notification/request.py +++ b/src/ai/backend/common/dto/manager/v2/notification/request.py @@ -11,6 +11,7 @@ from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel from ai.backend.common.dto.manager.query import StringFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from .types import ( NotificationChannelOrderField, @@ -233,7 +234,7 @@ class NotificationChannelTypeFilter(BaseRequestModel): ) -class NotificationChannelFilter(BaseRequestModel): +class NotificationChannelFilter(BaseFilter): """Filter for notification channel search queries.""" name: StringFilter | None = Field(default=None, description="Filter by channel name") @@ -241,16 +242,6 @@ class NotificationChannelFilter(BaseRequestModel): default=None, description="Filter by channel type" ) enabled: bool | None = Field(default=None, description="Filter by enabled status") - AND: list[NotificationChannelFilter] | None = Field( - default=None, description="Combine with AND logic" - ) - OR: list[NotificationChannelFilter] | None = Field( - default=None, description="Combine with OR logic" - ) - NOT: list[NotificationChannelFilter] | None = Field(default=None, description="Negate filters") - - -NotificationChannelFilter.model_rebuild() class NotificationChannelOrder(BaseRequestModel): @@ -290,7 +281,7 @@ class NotificationRuleTypeFilter(BaseRequestModel): ) -class NotificationRuleFilter(BaseRequestModel): +class NotificationRuleFilter(BaseFilter): """Filter for notification rule search queries.""" name: StringFilter | None = Field(default=None, description="Filter by rule name") @@ -298,16 +289,6 @@ class NotificationRuleFilter(BaseRequestModel): default=None, description="Filter by rule type" ) enabled: bool | None = Field(default=None, description="Filter by enabled status") - AND: list[NotificationRuleFilter] | None = Field( - default=None, description="Combine with AND logic" - ) - OR: list[NotificationRuleFilter] | None = Field( - default=None, description="Combine with OR logic" - ) - NOT: list[NotificationRuleFilter] | None = Field(default=None, description="Negate filters") - - -NotificationRuleFilter.model_rebuild() class NotificationRuleOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/prometheus_query_preset/request.py b/src/ai/backend/common/dto/manager/v2/prometheus_query_preset/request.py index 14c32335681..7a8639e989f 100644 --- a/src/ai/backend/common/dto/manager/v2/prometheus_query_preset/request.py +++ b/src/ai/backend/common/dto/manager/v2/prometheus_query_preset/request.py @@ -14,6 +14,7 @@ from ai.backend.common.clients.prometheus.preset import validate_query_template from ai.backend.common.dto.clients.prometheus.defs import PROMETHEUS_DURATION_PATTERN from ai.backend.common.dto.manager.query import StringFilter, UUIDFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from .types import OrderDirection, QueryDefinitionOrderField @@ -166,23 +167,11 @@ class DeleteQueryDefinitionInput(BaseRequestModel): id: UUID = Field(description="Query definition ID to delete") -class QueryDefinitionFilter(BaseRequestModel): +class QueryDefinitionFilter(BaseFilter): """Filter for prometheus query definition search.""" name: StringFilter | None = Field(default=None, description="Filter by name") category_id: UUIDFilter | None = Field(default=None, description="Filter by category ID") - AND: list[QueryDefinitionFilter] | None = Field( - default=None, description="AND logical combinator." - ) - OR: list[QueryDefinitionFilter] | None = Field( - default=None, description="OR logical combinator." - ) - NOT: list[QueryDefinitionFilter] | None = Field( - default=None, description="NOT logical combinator." - ) - - -QueryDefinitionFilter.model_rebuild() class QueryDefinitionOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/rbac/request.py b/src/ai/backend/common/dto/manager/v2/rbac/request.py index 72203d6014f..f5287bcf953 100644 --- a/src/ai/backend/common/dto/manager/v2/rbac/request.py +++ b/src/ai/backend/common/dto/manager/v2/rbac/request.py @@ -10,6 +10,7 @@ from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel from ai.backend.common.dto.manager.query import DateTimeFilter, StringFilter, UUIDFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from .types import ( OperationTypeFilter, @@ -201,50 +202,32 @@ class ReplaceRolePermissionsInput(BaseRequestModel): ) -class RoleFilter(BaseRequestModel): +class RoleFilter(BaseFilter): """Filter for roles.""" name: StringFilter | None = None source: RoleSourceFilter | None = None status: RoleStatusFilter | None = None - AND: list[RoleFilter] | None = None - OR: list[RoleFilter] | None = None - NOT: list[RoleFilter] | None = None -RoleFilter.model_rebuild() - - -class RoleNestedFilter(BaseRequestModel): +class RoleNestedFilter(BaseFilter): """Nested filter for roles within a role assignment.""" name: StringFilter | None = None source: RoleSourceFilter | None = None status: RoleStatusFilter | None = None - AND: list[RoleNestedFilter] | None = None - OR: list[RoleNestedFilter] | None = None - NOT: list[RoleNestedFilter] | None = None - -RoleNestedFilter.model_rebuild() - -class PermissionNestedFilter(BaseRequestModel): +class PermissionNestedFilter(BaseFilter): """Nested filter for permissions within a role assignment.""" scope_id: StringFilter | None = None scope_type: RBACElementTypeFilter | None = None entity_type: RBACElementTypeFilter | None = None operation: OperationTypeFilter | None = None - AND: list[PermissionNestedFilter] | None = None - OR: list[PermissionNestedFilter] | None = None - NOT: list[PermissionNestedFilter] | None = None - - -PermissionNestedFilter.model_rebuild() -class RoleAssignmentFilter(BaseRequestModel): +class RoleAssignmentFilter(BaseFilter): """Filter for role assignments.""" role_id: UUIDFilter | None = None @@ -252,28 +235,16 @@ class RoleAssignmentFilter(BaseRequestModel): permission: PermissionNestedFilter | None = None username: StringFilter | None = None email: StringFilter | None = None - AND: list[RoleAssignmentFilter] | None = None - OR: list[RoleAssignmentFilter] | None = None - NOT: list[RoleAssignmentFilter] | None = None -RoleAssignmentFilter.model_rebuild() - - -class EntityFilter(BaseRequestModel): +class EntityFilter(BaseFilter): """Filter for entity associations.""" entity_type: RBACElementTypeFilter | None = None entity_id: StringFilter | None = None - AND: list[EntityFilter] | None = None - OR: list[EntityFilter] | None = None - NOT: list[EntityFilter] | None = None - -EntityFilter.model_rebuild() - -class PermissionFilter(BaseRequestModel): +class PermissionFilter(BaseFilter): """Filter for scoped permissions.""" role_id: UUIDFilter | None = None @@ -281,12 +252,6 @@ class PermissionFilter(BaseRequestModel): scope_id: StringFilter | None = None entity_type: RBACElementTypeFilter | None = None created_at: DateTimeFilter | None = None - AND: list[PermissionFilter] | None = None - OR: list[PermissionFilter] | None = None - NOT: list[PermissionFilter] | None = None - - -PermissionFilter.model_rebuild() class RoleOrderBy(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/resource_group/request.py b/src/ai/backend/common/dto/manager/v2/resource_group/request.py index 0fafee795aa..e5bdc81afe1 100644 --- a/src/ai/backend/common/dto/manager/v2/resource_group/request.py +++ b/src/ai/backend/common/dto/manager/v2/resource_group/request.py @@ -12,6 +12,7 @@ from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel from ai.backend.common.dto.manager.query import StringFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from ai.backend.common.dto.manager.v2.deployment_options import DeploymentOptionsInput from ai.backend.common.dto.manager.v2.resource_group.types import ( ResourceGroupOrderDirection, @@ -134,19 +135,13 @@ class DeleteResourceGroupInput(BaseRequestModel): ) -class ResourceGroupFilter(BaseRequestModel): +class ResourceGroupFilter(BaseFilter): """Filter criteria for searching resource groups.""" name: StringFilter | None = Field(default=None, description="Filter by name.") description: StringFilter | None = Field(default=None, description="Filter by description.") is_active: bool | None = Field(default=None, description="Filter by active status.") is_public: bool | None = Field(default=None, description="Filter by public status.") - AND: list[ResourceGroupFilter] | None = Field(default=None, description="AND conjunction.") - OR: list[ResourceGroupFilter] | None = Field(default=None, description="OR conjunction.") - NOT: list[ResourceGroupFilter] | None = Field(default=None, description="NOT negation.") - - -ResourceGroupFilter.model_rebuild() class ResourceGroupOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/resource_preset/request.py b/src/ai/backend/common/dto/manager/v2/resource_preset/request.py index b04fd671bbc..6a8190f9063 100644 --- a/src/ai/backend/common/dto/manager/v2/resource_preset/request.py +++ b/src/ai/backend/common/dto/manager/v2/resource_preset/request.py @@ -8,7 +8,11 @@ from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel from ai.backend.common.dto.manager.query import StringFilter -from ai.backend.common.dto.manager.v2.common import BinarySizeInput, ResourceSlotEntryInput +from ai.backend.common.dto.manager.v2.common import ( + BaseFilter, + BinarySizeInput, + ResourceSlotEntryInput, +) from ai.backend.common.dto.manager.v2.resource_preset.types import ( ResourcePresetOrderDirection, ResourcePresetOrderField, @@ -58,19 +62,13 @@ class UpdateResourcePresetInput(BaseRequestModel): ) -class ResourcePresetFilter(BaseRequestModel): +class ResourcePresetFilter(BaseFilter): """Filter criteria for searching resource presets.""" name: StringFilter | None = Field(default=None, description="Filter by name.") resource_group_name: StringFilter | None = Field( default=None, description="Filter by resource group name." ) - AND: list[ResourcePresetFilter] | None = Field(default=None, description="AND conjunction.") - OR: list[ResourcePresetFilter] | None = Field(default=None, description="OR conjunction.") - NOT: list[ResourcePresetFilter] | None = Field(default=None, description="NOT negation.") - - -ResourcePresetFilter.model_rebuild() class ResourcePresetOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/resource_slot/request.py b/src/ai/backend/common/dto/manager/v2/resource_slot/request.py index ead146b1c45..29d3c8dcb84 100644 --- a/src/ai/backend/common/dto/manager/v2/resource_slot/request.py +++ b/src/ai/backend/common/dto/manager/v2/resource_slot/request.py @@ -6,6 +6,7 @@ from ai.backend.common.api_handlers import BaseRequestModel from ai.backend.common.dto.manager.query import StringFilter, UUIDFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from .types import ( AgentResourceOrderField, @@ -34,24 +35,12 @@ # ========== ResourceSlotType ========== -class ResourceSlotTypeFilter(BaseRequestModel): +class ResourceSlotTypeFilter(BaseFilter): """Filter conditions for resource slot type search.""" slot_name: StringFilter | None = Field(default=None, description="Filter by slot name.") slot_type: StringFilter | None = Field(default=None, description="Filter by slot type.") display_name: StringFilter | None = Field(default=None, description="Filter by display name.") - AND: list[ResourceSlotTypeFilter] | None = Field( - default=None, description="Logical AND of multiple filter conditions." - ) - OR: list[ResourceSlotTypeFilter] | None = Field( - default=None, description="Logical OR of multiple filter conditions." - ) - NOT: list[ResourceSlotTypeFilter] | None = Field( - default=None, description="Logical NOT of filter conditions." - ) - - -ResourceSlotTypeFilter.model_rebuild() class ResourceSlotTypeOrder(BaseRequestModel): @@ -86,23 +75,11 @@ class AdminSearchResourceSlotTypesInput(BaseRequestModel): # ========== AgentResource ========== -class AgentResourceFilter(BaseRequestModel): +class AgentResourceFilter(BaseFilter): """Filter conditions for agent resource search.""" slot_name: StringFilter | None = Field(default=None, description="Filter by slot name.") agent_id: StringFilter | None = Field(default=None, description="Filter by agent ID.") - AND: list[AgentResourceFilter] | None = Field( - default=None, description="Logical AND of multiple filter conditions." - ) - OR: list[AgentResourceFilter] | None = Field( - default=None, description="Logical OR of multiple filter conditions." - ) - NOT: list[AgentResourceFilter] | None = Field( - default=None, description="Logical NOT of filter conditions." - ) - - -AgentResourceFilter.model_rebuild() class AgentResourceOrder(BaseRequestModel): @@ -137,23 +114,11 @@ class AdminSearchAgentResourcesInput(BaseRequestModel): # ========== ResourceAllocation ========== -class ResourceAllocationFilter(BaseRequestModel): +class ResourceAllocationFilter(BaseFilter): """Filter conditions for resource allocation search.""" slot_name: StringFilter | None = Field(default=None, description="Filter by slot name.") kernel_id: UUIDFilter | None = Field(default=None, description="Filter by kernel ID.") - AND: list[ResourceAllocationFilter] | None = Field( - default=None, description="Logical AND of multiple filter conditions." - ) - OR: list[ResourceAllocationFilter] | None = Field( - default=None, description="Logical OR of multiple filter conditions." - ) - NOT: list[ResourceAllocationFilter] | None = Field( - default=None, description="Logical NOT of filter conditions." - ) - - -ResourceAllocationFilter.model_rebuild() class ResourceAllocationOrder(BaseRequestModel): @@ -181,22 +146,10 @@ class AdminSearchResourceAllocationsInput(BaseRequestModel): # ========== AllocatedResourceSlot (revision/preset shared) ========== -class AllocatedResourceSlotFilter(BaseRequestModel): +class AllocatedResourceSlotFilter(BaseFilter): """Filter conditions for allocated resource slot search.""" slot_name: StringFilter | None = Field(default=None, description="Filter by slot name.") - AND: list[AllocatedResourceSlotFilter] | None = Field( - default=None, description="Logical AND of multiple filter conditions." - ) - OR: list[AllocatedResourceSlotFilter] | None = Field( - default=None, description="Logical OR of multiple filter conditions." - ) - NOT: list[AllocatedResourceSlotFilter] | None = Field( - default=None, description="Logical NOT of filter conditions." - ) - - -AllocatedResourceSlotFilter.model_rebuild() class AllocatedResourceSlotOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/resource_usage/request.py b/src/ai/backend/common/dto/manager/v2/resource_usage/request.py index 7ceea713a02..a7babca66f2 100644 --- a/src/ai/backend/common/dto/manager/v2/resource_usage/request.py +++ b/src/ai/backend/common/dto/manager/v2/resource_usage/request.py @@ -8,6 +8,7 @@ from ai.backend.common.api_handlers import BaseRequestModel from ai.backend.common.dto.manager.query import DateFilter, StringFilter, UUIDFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from .types import OrderDirection, UsageBucketOrderField @@ -90,7 +91,7 @@ class DomainSearchUserUsageBucketsInput(BaseRequestModel): # GQL Filter/Order DTOs for usage bucket queries -class DomainUsageBucketFilter(BaseRequestModel): +class DomainUsageBucketFilter(BaseFilter): """Filter for domain usage bucket queries.""" resource_group: StringFilter | None = Field( @@ -99,16 +100,6 @@ class DomainUsageBucketFilter(BaseRequestModel): domain_name: StringFilter | None = Field(default=None, description="Filter by domain name") period_start: DateFilter | None = Field(default=None, description="Filter by period start date") period_end: DateFilter | None = Field(default=None, description="Filter by period end date") - AND: list[DomainUsageBucketFilter] | None = Field( - default=None, description="Combine with AND logic" - ) - OR: list[DomainUsageBucketFilter] | None = Field( - default=None, description="Combine with OR logic" - ) - NOT: list[DomainUsageBucketFilter] | None = Field(default=None, description="Negate filters") - - -DomainUsageBucketFilter.model_rebuild() class DomainUsageBucketOrderBy(BaseRequestModel): @@ -118,7 +109,7 @@ class DomainUsageBucketOrderBy(BaseRequestModel): direction: OrderDirection = Field(default=OrderDirection.DESC, description="Order direction") -class ProjectUsageBucketFilter(BaseRequestModel): +class ProjectUsageBucketFilter(BaseFilter): """Filter for project usage bucket queries.""" resource_group: StringFilter | None = Field( @@ -128,16 +119,6 @@ class ProjectUsageBucketFilter(BaseRequestModel): domain_name: StringFilter | None = Field(default=None, description="Filter by domain name") period_start: DateFilter | None = Field(default=None, description="Filter by period start date") period_end: DateFilter | None = Field(default=None, description="Filter by period end date") - AND: list[ProjectUsageBucketFilter] | None = Field( - default=None, description="Combine with AND logic" - ) - OR: list[ProjectUsageBucketFilter] | None = Field( - default=None, description="Combine with OR logic" - ) - NOT: list[ProjectUsageBucketFilter] | None = Field(default=None, description="Negate filters") - - -ProjectUsageBucketFilter.model_rebuild() class ProjectUsageBucketOrderBy(BaseRequestModel): @@ -147,7 +128,7 @@ class ProjectUsageBucketOrderBy(BaseRequestModel): direction: OrderDirection = Field(default=OrderDirection.DESC, description="Order direction") -class UserUsageBucketFilter(BaseRequestModel): +class UserUsageBucketFilter(BaseFilter): """Filter for user usage bucket queries.""" resource_group: StringFilter | None = Field( @@ -158,16 +139,6 @@ class UserUsageBucketFilter(BaseRequestModel): domain_name: StringFilter | None = Field(default=None, description="Filter by domain name") period_start: DateFilter | None = Field(default=None, description="Filter by period start date") period_end: DateFilter | None = Field(default=None, description="Filter by period end date") - AND: list[UserUsageBucketFilter] | None = Field( - default=None, description="Combine with AND logic" - ) - OR: list[UserUsageBucketFilter] | None = Field( - default=None, description="Combine with OR logic" - ) - NOT: list[UserUsageBucketFilter] | None = Field(default=None, description="Negate filters") - - -UserUsageBucketFilter.model_rebuild() class UserUsageBucketOrderBy(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/role_invitation/request.py b/src/ai/backend/common/dto/manager/v2/role_invitation/request.py index e3ee5ccca8b..3d8101480d7 100644 --- a/src/ai/backend/common/dto/manager/v2/role_invitation/request.py +++ b/src/ai/backend/common/dto/manager/v2/role_invitation/request.py @@ -11,7 +11,7 @@ from ai.backend.common.api_handlers import BaseRequestModel from ai.backend.common.dto.manager.query import StringFilter, UUIDFilter -from ai.backend.common.dto.manager.v2.common import OrderDirection +from ai.backend.common.dto.manager.v2.common import BaseFilter, OrderDirection from ai.backend.common.dto.manager.v2.role_invitation.types import RoleInvitationStateDTO @@ -83,19 +83,13 @@ class RoleNestedFilter(BaseRequestModel): name: StringFilter | None = None -RoleNestedFilter.model_rebuild() - - class UserNestedFilter(BaseRequestModel): """Nested filter for a user (inviter or invitee) of an invitation.""" email: StringFilter | None = None -UserNestedFilter.model_rebuild() - - -class RoleInvitationFilter(BaseRequestModel): +class RoleInvitationFilter(BaseFilter): """Filter for role invitations.""" state: RoleInvitationStateFilter | None = None @@ -103,12 +97,6 @@ class RoleInvitationFilter(BaseRequestModel): role: RoleNestedFilter | None = None inviter: UserNestedFilter | None = None invitee: UserNestedFilter | None = None - AND: list[RoleInvitationFilter] | None = None - OR: list[RoleInvitationFilter] | None = None - NOT: list[RoleInvitationFilter] | None = None - - -RoleInvitationFilter.model_rebuild() class SearchRoleInvitationsInput(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/runtime_variant/request.py b/src/ai/backend/common/dto/manager/v2/runtime_variant/request.py index 475da95a370..01dc5e32741 100644 --- a/src/ai/backend/common/dto/manager/v2/runtime_variant/request.py +++ b/src/ai/backend/common/dto/manager/v2/runtime_variant/request.py @@ -6,7 +6,7 @@ from ai.backend.common.api_handlers import BaseRequestModel from ai.backend.common.dto.manager.query import StringFilter -from ai.backend.common.dto.manager.v2.common import OrderDirection +from ai.backend.common.dto.manager.v2.common import BaseFilter, OrderDirection from ai.backend.common.dto.manager.v2.runtime_variant.types import RuntimeVariantOrderField @@ -29,14 +29,8 @@ class DeleteRuntimeVariantsInput(BaseRequestModel): ids: list[UUID] = Field(description="List of runtime variant UUIDs to delete.") -class RuntimeVariantFilter(BaseRequestModel): +class RuntimeVariantFilter(BaseFilter): name: StringFilter | None = Field(default=None) - AND: list[RuntimeVariantFilter] | None = Field(default=None) - OR: list[RuntimeVariantFilter] | None = Field(default=None) - NOT: list[RuntimeVariantFilter] | None = Field(default=None) - - -RuntimeVariantFilter.model_rebuild() class RuntimeVariantOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/runtime_variant_preset/request.py b/src/ai/backend/common/dto/manager/v2/runtime_variant_preset/request.py index 319c1b19ef7..bbaae59ef50 100644 --- a/src/ai/backend/common/dto/manager/v2/runtime_variant_preset/request.py +++ b/src/ai/backend/common/dto/manager/v2/runtime_variant_preset/request.py @@ -8,7 +8,7 @@ from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel from ai.backend.common.dto.manager.query import StringFilter, UUIDFilter -from ai.backend.common.dto.manager.v2.common import OrderDirection +from ai.backend.common.dto.manager.v2.common import BaseFilter, OrderDirection from ai.backend.common.dto.manager.v2.runtime_variant_preset.types import ( PresetTarget, PresetValueType, @@ -102,15 +102,9 @@ def validate_flag_requires_args(self) -> Self: return self -class RuntimeVariantPresetFilter(BaseRequestModel): +class RuntimeVariantPresetFilter(BaseFilter): name: StringFilter | None = Field(default=None) runtime_variant_id: UUIDFilter | None = Field(default=None) - AND: list[RuntimeVariantPresetFilter] | None = Field(default=None) - OR: list[RuntimeVariantPresetFilter] | None = Field(default=None) - NOT: list[RuntimeVariantPresetFilter] | None = Field(default=None) - - -RuntimeVariantPresetFilter.model_rebuild() class RuntimeVariantPresetOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/scheduling_history/request.py b/src/ai/backend/common/dto/manager/v2/scheduling_history/request.py index ce9eac401bd..32dd6824301 100644 --- a/src/ai/backend/common/dto/manager/v2/scheduling_history/request.py +++ b/src/ai/backend/common/dto/manager/v2/scheduling_history/request.py @@ -8,6 +8,7 @@ from ai.backend.common.api_handlers import BaseRequestModel from ai.backend.common.dto.manager.query import DateTimeFilter, StringFilter, UUIDFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from .types import ( DeploymentHistoryOrderField, @@ -45,7 +46,7 @@ class SchedulingResultFilter(BaseRequestModel): model_config = {"populate_by_name": True} -class SessionHistoryFilter(BaseRequestModel): +class SessionHistoryFilter(BaseFilter): """Filter conditions for session scheduling history search.""" id: UUIDFilter | None = Field(default=None, description="Filter by history record ID") @@ -60,12 +61,6 @@ class SessionHistoryFilter(BaseRequestModel): message: StringFilter | None = Field(default=None, description="Filter by message") created_at: DateTimeFilter | None = Field(default=None, description="Filter by created_at") updated_at: DateTimeFilter | None = Field(default=None, description="Filter by updated_at") - AND: list[SessionHistoryFilter] | None = Field(default=None, description="AND conjunction.") - OR: list[SessionHistoryFilter] | None = Field(default=None, description="OR conjunction.") - NOT: list[SessionHistoryFilter] | None = Field(default=None, description="NOT negation.") - - -SessionHistoryFilter.model_rebuild() class SessionHistoryOrder(BaseRequestModel): @@ -86,7 +81,7 @@ class SearchSessionHistoryInput(BaseRequestModel): offset: int = Field(default=0, ge=0, description="Number of items to skip") -class DeploymentHistoryFilter(BaseRequestModel): +class DeploymentHistoryFilter(BaseFilter): """Filter conditions for deployment scheduling history search.""" id: UUIDFilter | None = Field(default=None, description="Filter by history record ID") @@ -101,12 +96,6 @@ class DeploymentHistoryFilter(BaseRequestModel): message: StringFilter | None = Field(default=None, description="Filter by message") created_at: DateTimeFilter | None = Field(default=None, description="Filter by created_at") updated_at: DateTimeFilter | None = Field(default=None, description="Filter by updated_at") - AND: list[DeploymentHistoryFilter] | None = Field(default=None, description="AND conjunction.") - OR: list[DeploymentHistoryFilter] | None = Field(default=None, description="OR conjunction.") - NOT: list[DeploymentHistoryFilter] | None = Field(default=None, description="NOT negation.") - - -DeploymentHistoryFilter.model_rebuild() class DeploymentHistoryOrder(BaseRequestModel): @@ -127,7 +116,7 @@ class SearchDeploymentHistoryInput(BaseRequestModel): offset: int = Field(default=0, ge=0, description="Number of items to skip") -class RouteHistoryFilter(BaseRequestModel): +class RouteHistoryFilter(BaseFilter): """Filter conditions for route scheduling history search.""" id: UUIDFilter | None = Field(default=None, description="Filter by history record ID") @@ -143,12 +132,6 @@ class RouteHistoryFilter(BaseRequestModel): message: StringFilter | None = Field(default=None, description="Filter by message") created_at: DateTimeFilter | None = Field(default=None, description="Filter by created_at") updated_at: DateTimeFilter | None = Field(default=None, description="Filter by updated_at") - AND: list[RouteHistoryFilter] | None = Field(default=None, description="AND conjunction.") - OR: list[RouteHistoryFilter] | None = Field(default=None, description="OR conjunction.") - NOT: list[RouteHistoryFilter] | None = Field(default=None, description="NOT negation.") - - -RouteHistoryFilter.model_rebuild() class RouteHistoryOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/service_catalog/request.py b/src/ai/backend/common/dto/manager/v2/service_catalog/request.py index f58b53e7a74..44bfcc45778 100644 --- a/src/ai/backend/common/dto/manager/v2/service_catalog/request.py +++ b/src/ai/backend/common/dto/manager/v2/service_catalog/request.py @@ -12,6 +12,7 @@ from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel from ai.backend.common.dto.manager.query import StringFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from ai.backend.common.types import ServiceCatalogStatus from .types import OrderDirection, ServiceCatalogOrderField, ServiceCatalogStatusFilter @@ -123,7 +124,7 @@ class HeartbeatInput(BaseRequestModel): id: UUID = Field(description="Service catalog entry ID to heartbeat") -class ServiceCatalogFilter(BaseRequestModel): +class ServiceCatalogFilter(BaseFilter): """Filter conditions for service catalog search.""" service_group: StringFilter | None = Field( @@ -132,18 +133,6 @@ class ServiceCatalogFilter(BaseRequestModel): status: ServiceCatalogStatusFilter | None = Field( default=None, description="Filter by health status." ) - AND: list[ServiceCatalogFilter] | None = Field( - default=None, description="Logical AND of multiple filter conditions." - ) - OR: list[ServiceCatalogFilter] | None = Field( - default=None, description="Logical OR of multiple filter conditions." - ) - NOT: list[ServiceCatalogFilter] | None = Field( - default=None, description="Logical NOT of filter conditions." - ) - - -ServiceCatalogFilter.model_rebuild() class ServiceCatalogOrder(BaseRequestModel): @@ -173,6 +162,3 @@ class AdminSearchServiceCatalogsInput(BaseRequestModel): # Offset-based pagination limit: int | None = Field(default=None, ge=1, description="Maximum number of items to return.") offset: int | None = Field(default=None, ge=0, description="Number of items to skip.") - - -ServiceCatalogFilter.model_rebuild() diff --git a/src/ai/backend/common/dto/manager/v2/session/request.py b/src/ai/backend/common/dto/manager/v2/session/request.py index e8186916d15..02d2612ac91 100644 --- a/src/ai/backend/common/dto/manager/v2/session/request.py +++ b/src/ai/backend/common/dto/manager/v2/session/request.py @@ -14,7 +14,11 @@ from ai.backend.common.api_handlers import BaseRequestModel from ai.backend.common.dto.manager.defs import DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT from ai.backend.common.dto.manager.query import DateTimeFilter, StringFilter, UUIDFilter -from ai.backend.common.dto.manager.v2.common import BinarySizeInput, ResourceSlotEntryInput +from ai.backend.common.dto.manager.v2.common import ( + BaseFilter, + BinarySizeInput, + ResourceSlotEntryInput, +) from ai.backend.common.dto.manager.v2.session.types import ( ClusterModeEnum, CreateSessionTypeEnum, @@ -77,7 +81,7 @@ class SessionIdPathParam(BaseRequestModel): # --------------------------------------------------------------------------- -class SessionFilter(BaseRequestModel): +class SessionFilter(BaseFilter): """Filter criteria for session listing.""" id: UUIDFilter | None = None @@ -87,12 +91,6 @@ class SessionFilter(BaseRequestModel): project_id: UUIDFilter | None = None user_uuid: UUIDFilter | None = None created_at: DateTimeFilter | None = None - AND: list[SessionFilter] | None = None - OR: list[SessionFilter] | None = None - NOT: list[SessionFilter] | None = None - - -SessionFilter.model_rebuild() class SessionOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/user/request.py b/src/ai/backend/common/dto/manager/v2/user/request.py index b2b64e2c62d..bd237c41df7 100644 --- a/src/ai/backend/common/dto/manager/v2/user/request.py +++ b/src/ai/backend/common/dto/manager/v2/user/request.py @@ -12,6 +12,7 @@ from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel from ai.backend.common.dto.manager.defs import DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT from ai.backend.common.dto.manager.query import DateTimeFilter, StringFilter, UUIDFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from ai.backend.common.dto.manager.v2.user.types import ( OrderDirection, UserDomainFilter, @@ -293,7 +294,7 @@ class UpdateMyAllowedClientIPInput(BaseRequestModel): ) -class UserFilter(BaseRequestModel): +class UserFilter(BaseFilter): """Filter criteria for searching users.""" uuid: UUIDFilter | None = Field(default=None, description="Filter by user UUID.") @@ -314,16 +315,6 @@ class UserFilter(BaseRequestModel): project: UserProjectFilter | None = Field( default=None, description="Nested filter for projects a user belongs to." ) - AND: list[UserFilter] | None = Field( - default=None, description="Combine multiple filters with AND logic." - ) - OR: list[UserFilter] | None = Field( - default=None, description="Combine multiple filters with OR logic." - ) - NOT: list[UserFilter] | None = Field(default=None, description="Negate the specified filters.") - - -UserFilter.model_rebuild() class UserOrder(BaseRequestModel): diff --git a/src/ai/backend/common/dto/manager/v2/vfolder/request.py b/src/ai/backend/common/dto/manager/v2/vfolder/request.py index ea64207d509..155f35abb0f 100644 --- a/src/ai/backend/common/dto/manager/v2/vfolder/request.py +++ b/src/ai/backend/common/dto/manager/v2/vfolder/request.py @@ -10,6 +10,7 @@ from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel from ai.backend.common.dto.manager.query import DateTimeFilter, StringFilter +from ai.backend.common.dto.manager.v2.common import BaseFilter from ai.backend.common.dto.manager.v2.deployment.request import DeploymentStrategyInput from ai.backend.common.identifier.deployment_preset import DeploymentPresetID from ai.backend.common.typed_validators import VFolderName @@ -332,7 +333,7 @@ class DeleteInvitationInput(BaseRequestModel): # ============================================================ -class VFolderFilter(BaseRequestModel): +class VFolderFilter(BaseFilter): """Filter criteria for searching virtual folders.""" name: StringFilter | None = Field(default=None, description="Filter by vfolder name.") @@ -345,12 +346,6 @@ class VFolderFilter(BaseRequestModel): ) cloneable: bool | None = Field(default=None, description="Filter by cloneable flag.") created_at: DateTimeFilter | None = Field(default=None, description="Filter by creation time.") - AND: list[VFolderFilter] | None = Field(default=None, description="AND logical combinator.") - OR: list[VFolderFilter] | None = Field(default=None, description="OR logical combinator.") - NOT: list[VFolderFilter] | None = Field(default=None, description="NOT logical combinator.") - - -VFolderFilter.model_rebuild() class VFolderOrder(BaseRequestModel): diff --git a/src/ai/backend/manager/api/adapters/agent/adapter.py b/src/ai/backend/manager/api/adapters/agent/adapter.py index 038e42cd15a..7fa76ab6bce 100644 --- a/src/ai/backend/manager/api/adapters/agent/adapter.py +++ b/src/ai/backend/manager/api/adapters/agent/adapter.py @@ -39,8 +39,6 @@ NoPagination, QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.services.agent.actions.get_total_resources import ( GetTotalResourcesAction, @@ -140,20 +138,11 @@ def _convert_filter(self, f: AgentFilter) -> list[QueryCondition]: if condition is not None: conditions.append(condition) if f.AND: - for sub_filter in f.AND: - conditions.extend(self._convert_filter(sub_filter)) + conditions.extend(self.convert_and([self._convert_filter(sub) for sub in f.AND])) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub_filter in f.OR: - or_conditions.extend(self._convert_filter(sub_filter)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend(self.convert_or([self._convert_filter(sub) for sub in f.OR])) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub_filter in f.NOT: - not_conditions.extend(self._convert_filter(sub_filter)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend(self.convert_not([self._convert_filter(sub) for sub in f.NOT])) return conditions def _convert_id_filter(self, sf: StringFilter) -> QueryCondition | None: diff --git a/src/ai/backend/manager/api/adapters/artifact/adapter.py b/src/ai/backend/manager/api/adapters/artifact/adapter.py index 894315217c6..99e880b9304 100644 --- a/src/ai/backend/manager/api/adapters/artifact/adapter.py +++ b/src/ai/backend/manager/api/adapters/artifact/adapter.py @@ -80,8 +80,6 @@ OffsetPagination, QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.base.updater import Updater from ai.backend.manager.services.artifact.actions.delegate_scan import ( @@ -518,20 +516,15 @@ def _convert_gql_filter( ) if filter.AND: - for sub_filter in filter.AND: - conditions.extend(self._convert_gql_filter(sub_filter)) + conditions.extend( + self.convert_and([self._convert_gql_filter(sub) for sub in filter.AND]) + ) if filter.OR: - or_conditions: list[QueryCondition] = [] - for sub_filter in filter.OR: - or_conditions.extend(self._convert_gql_filter(sub_filter)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend(self.convert_or([self._convert_gql_filter(sub) for sub in filter.OR])) if filter.NOT: - not_conditions: list[QueryCondition] = [] - for sub_filter in filter.NOT: - not_conditions.extend(self._convert_gql_filter(sub_filter)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_gql_filter(sub) for sub in filter.NOT]) + ) return conditions @@ -615,20 +608,17 @@ def _convert_gql_revision_filter( ) if filter.AND: - for sub_filter in filter.AND: - conditions.extend(self._convert_gql_revision_filter(sub_filter)) + conditions.extend( + self.convert_and([self._convert_gql_revision_filter(sub) for sub in filter.AND]) + ) if filter.OR: - or_conditions: list[QueryCondition] = [] - for sub_filter in filter.OR: - or_conditions.extend(self._convert_gql_revision_filter(sub_filter)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend( + self.convert_or([self._convert_gql_revision_filter(sub) for sub in filter.OR]) + ) if filter.NOT: - not_conditions: list[QueryCondition] = [] - for sub_filter in filter.NOT: - not_conditions.extend(self._convert_gql_revision_filter(sub_filter)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_gql_revision_filter(sub) for sub in filter.NOT]) + ) return conditions diff --git a/src/ai/backend/manager/api/adapters/audit_log/adapter.py b/src/ai/backend/manager/api/adapters/audit_log/adapter.py index d418f2b639c..b2e291006bb 100644 --- a/src/ai/backend/manager/api/adapters/audit_log/adapter.py +++ b/src/ai/backend/manager/api/adapters/audit_log/adapter.py @@ -30,8 +30,6 @@ OffsetPagination, QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.services.audit_log.actions.search import SearchAuditLogsAction @@ -135,20 +133,11 @@ def _convert_filter(self, f: AuditLogFilter) -> list[QueryCondition]: if condition is not None: conditions.append(condition) if f.AND: - for sub_filter in f.AND: - conditions.extend(self._convert_filter(sub_filter)) + conditions.extend(self.convert_and([self._convert_filter(sub) for sub in f.AND])) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub_filter in f.OR: - or_conditions.extend(self._convert_filter(sub_filter)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend(self.convert_or([self._convert_filter(sub) for sub in f.OR])) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub_filter in f.NOT: - not_conditions.extend(self._convert_filter(sub_filter)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend(self.convert_not([self._convert_filter(sub) for sub in f.NOT])) return conditions @staticmethod diff --git a/src/ai/backend/manager/api/adapters/deployment/adapter.py b/src/ai/backend/manager/api/adapters/deployment/adapter.py index 535c9289c21..adc1e60c5b8 100644 --- a/src/ai/backend/manager/api/adapters/deployment/adapter.py +++ b/src/ai/backend/manager/api/adapters/deployment/adapter.py @@ -223,8 +223,6 @@ QueryCondition, QueryOrder, Updater, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.deployment.updaters import ( DeploymentMetadataUpdaterSpec, @@ -597,63 +595,7 @@ async def my_search( raise RuntimeError("No authenticated user in context") conditions: list[QueryCondition] = [] if input.filter: - f = input.filter - if f.name is not None: - condition = self.convert_string_filter( - f.name, - contains_factory=DeploymentConditions.by_name_contains, - equals_factory=DeploymentConditions.by_name_equals, - starts_with_factory=DeploymentConditions.by_name_starts_with, - ends_with_factory=DeploymentConditions.by_name_ends_with, - in_factory=DeploymentConditions.by_name_in, - ) - if condition is not None: - conditions.append(condition) - if f.status is not None: - if f.status.equals is not None: - lifecycles = _status_to_lifecycles(ModelDeploymentStatus(f.status.equals)) - if lifecycles: - conditions.append(DeploymentConditions.by_status_in(lifecycles)) - if f.status.in_ is not None: - lifecycles = _statuses_to_lifecycles([ - ModelDeploymentStatus(s) for s in f.status.in_ - ]) - if lifecycles: - conditions.append(DeploymentConditions.by_status_in(lifecycles)) - if f.status.not_equals is not None: - lifecycles = _status_to_lifecycles(ModelDeploymentStatus(f.status.not_equals)) - if lifecycles: - conditions.append(DeploymentConditions.by_status_not_in(lifecycles)) - if f.status.not_in is not None: - lifecycles = _statuses_to_lifecycles([ - ModelDeploymentStatus(s) for s in f.status.not_in - ]) - if lifecycles: - conditions.append(DeploymentConditions.by_status_not_in(lifecycles)) - if f.open_to_public is not None: - conditions.append(DeploymentConditions.by_open_to_public(f.open_to_public)) - if f.tags is not None: - condition = self.convert_string_filter( - f.tags, - contains_factory=DeploymentConditions.by_tag_contains, - equals_factory=DeploymentConditions.by_tag_equals, - starts_with_factory=DeploymentConditions.by_tag_starts_with, - ends_with_factory=DeploymentConditions.by_tag_ends_with, - in_factory=DeploymentConditions.by_tag_in, - ) - if condition is not None: - conditions.append(condition) - if f.endpoint_url is not None: - condition = self.convert_string_filter( - f.endpoint_url, - contains_factory=DeploymentConditions.by_url_contains, - equals_factory=DeploymentConditions.by_url_equals, - starts_with_factory=DeploymentConditions.by_url_starts_with, - ends_with_factory=DeploymentConditions.by_url_ends_with, - in_factory=DeploymentConditions.by_url_in, - ) - if condition is not None: - conditions.append(condition) + conditions.extend(self._convert_deployment_filter(input.filter)) orders: list[QueryOrder] = ( self._convert_deployment_orders(input.order) if input.order else [] ) @@ -691,63 +633,7 @@ async def project_search( """Search deployments within a specific project.""" conditions: list[QueryCondition] = [] if input.filter: - f = input.filter - if f.name is not None: - condition = self.convert_string_filter( - f.name, - contains_factory=DeploymentConditions.by_name_contains, - equals_factory=DeploymentConditions.by_name_equals, - starts_with_factory=DeploymentConditions.by_name_starts_with, - ends_with_factory=DeploymentConditions.by_name_ends_with, - in_factory=DeploymentConditions.by_name_in, - ) - if condition is not None: - conditions.append(condition) - if f.status is not None: - if f.status.equals is not None: - lifecycles = _status_to_lifecycles(ModelDeploymentStatus(f.status.equals)) - if lifecycles: - conditions.append(DeploymentConditions.by_status_in(lifecycles)) - if f.status.in_ is not None: - lifecycles = _statuses_to_lifecycles([ - ModelDeploymentStatus(s) for s in f.status.in_ - ]) - if lifecycles: - conditions.append(DeploymentConditions.by_status_in(lifecycles)) - if f.status.not_equals is not None: - lifecycles = _status_to_lifecycles(ModelDeploymentStatus(f.status.not_equals)) - if lifecycles: - conditions.append(DeploymentConditions.by_status_not_in(lifecycles)) - if f.status.not_in is not None: - lifecycles = _statuses_to_lifecycles([ - ModelDeploymentStatus(s) for s in f.status.not_in - ]) - if lifecycles: - conditions.append(DeploymentConditions.by_status_not_in(lifecycles)) - if f.open_to_public is not None: - conditions.append(DeploymentConditions.by_open_to_public(f.open_to_public)) - if f.tags is not None: - condition = self.convert_string_filter( - f.tags, - contains_factory=DeploymentConditions.by_tag_contains, - equals_factory=DeploymentConditions.by_tag_equals, - starts_with_factory=DeploymentConditions.by_tag_starts_with, - ends_with_factory=DeploymentConditions.by_tag_ends_with, - in_factory=DeploymentConditions.by_tag_in, - ) - if condition is not None: - conditions.append(condition) - if f.endpoint_url is not None: - condition = self.convert_string_filter( - f.endpoint_url, - contains_factory=DeploymentConditions.by_url_contains, - equals_factory=DeploymentConditions.by_url_equals, - starts_with_factory=DeploymentConditions.by_url_starts_with, - ends_with_factory=DeploymentConditions.by_url_ends_with, - in_factory=DeploymentConditions.by_url_in, - ) - if condition is not None: - conditions.append(condition) + conditions.extend(self._convert_deployment_filter(input.filter)) orders: list[QueryOrder] = ( self._convert_deployment_orders(input.order) if input.order else [] ) @@ -1659,20 +1545,17 @@ def _convert_deployment_filter(self, f: DeploymentFilter) -> list[QueryCondition if dt_condition is not None: conditions.append(dt_condition) if f.AND: - for sub in f.AND: - conditions.extend(self._convert_deployment_filter(sub)) + conditions.extend( + self.convert_and([self._convert_deployment_filter(sub) for sub in f.AND]) + ) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(self._convert_deployment_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend( + self.convert_or([self._convert_deployment_filter(sub) for sub in f.OR]) + ) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(self._convert_deployment_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_deployment_filter(sub) for sub in f.NOT]) + ) return conditions def _build_deployment_querier(self, input: AdminSearchDeploymentsInput) -> BatchQuerier: @@ -1750,20 +1633,15 @@ def _convert_revision_filter(self, f: RevisionFilter) -> list[QueryCondition]: if dt_condition is not None: conditions.append(dt_condition) if f.AND: - for sub in f.AND: - conditions.extend(self._convert_revision_filter(sub)) + conditions.extend( + self.convert_and([self._convert_revision_filter(sub) for sub in f.AND]) + ) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(self._convert_revision_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend(self.convert_or([self._convert_revision_filter(sub) for sub in f.OR])) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(self._convert_revision_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_revision_filter(sub) for sub in f.NOT]) + ) return conditions def _build_revision_querier( @@ -1811,20 +1689,11 @@ def _convert_route_filter(self, f: RouteFilter) -> list[QueryCondition]: ]) ) if f.AND: - for sub in f.AND: - conditions.extend(self._convert_route_filter(sub)) + conditions.extend(self.convert_and([self._convert_route_filter(sub) for sub in f.AND])) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(self._convert_route_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend(self.convert_or([self._convert_route_filter(sub) for sub in f.OR])) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(self._convert_route_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend(self.convert_not([self._convert_route_filter(sub) for sub in f.NOT])) return conditions def _build_route_querier( @@ -1883,20 +1752,17 @@ def _convert_access_token_filter(self, f: AccessTokenFilter) -> list[QueryCondit if condition is not None: conditions.append(condition) if f.AND: - for sub in f.AND: - conditions.extend(self._convert_access_token_filter(sub)) + conditions.extend( + self.convert_and([self._convert_access_token_filter(sub) for sub in f.AND]) + ) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(self._convert_access_token_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend( + self.convert_or([self._convert_access_token_filter(sub) for sub in f.OR]) + ) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(self._convert_access_token_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_access_token_filter(sub) for sub in f.NOT]) + ) return conditions def _build_access_token_querier( @@ -1952,20 +1818,17 @@ def _convert_auto_scaling_rule_filter(self, f: AutoScalingRuleFilter) -> list[Qu if condition is not None: conditions.append(condition) if f.AND: - for sub in f.AND: - conditions.extend(self._convert_auto_scaling_rule_filter(sub)) + conditions.extend( + self.convert_and([self._convert_auto_scaling_rule_filter(sub) for sub in f.AND]) + ) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(self._convert_auto_scaling_rule_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend( + self.convert_or([self._convert_auto_scaling_rule_filter(sub) for sub in f.OR]) + ) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(self._convert_auto_scaling_rule_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_auto_scaling_rule_filter(sub) for sub in f.NOT]) + ) return conditions def _build_auto_scaling_rule_querier( @@ -2065,20 +1928,15 @@ def _convert_replica_filter(self, f: ReplicaFilter) -> list[QueryCondition]: ]) ) if f.AND: - for sub in f.AND: - conditions.extend(self._convert_replica_filter(sub)) + conditions.extend( + self.convert_and([self._convert_replica_filter(sub) for sub in f.AND]) + ) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(self._convert_replica_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend(self.convert_or([self._convert_replica_filter(sub) for sub in f.OR])) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(self._convert_replica_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_replica_filter(sub) for sub in f.NOT]) + ) return conditions def _build_replica_querier( @@ -2157,20 +2015,23 @@ def _convert_allocated_slot_filter( if cond is not None: conditions.append(cond) if filter_.AND: - for sub in filter_.AND: - conditions.extend(self._convert_allocated_slot_filter(sub, conditions_cls)) + conditions.extend( + self.convert_and([ + self._convert_allocated_slot_filter(sub, conditions_cls) for sub in filter_.AND + ]) + ) if filter_.OR: - or_conds: list[QueryCondition] = [] - for sub in filter_.OR: - or_conds.extend(self._convert_allocated_slot_filter(sub, conditions_cls)) - if or_conds: - conditions.append(combine_conditions_or(or_conds)) + conditions.extend( + self.convert_or([ + self._convert_allocated_slot_filter(sub, conditions_cls) for sub in filter_.OR + ]) + ) if filter_.NOT: - not_conds: list[QueryCondition] = [] - for sub in filter_.NOT: - not_conds.extend(self._convert_allocated_slot_filter(sub, conditions_cls)) - if not_conds: - conditions.append(negate_conditions(not_conds)) + conditions.extend( + self.convert_not([ + self._convert_allocated_slot_filter(sub, conditions_cls) for sub in filter_.NOT + ]) + ) return conditions # ------------------------------------------------------------------ diff --git a/src/ai/backend/manager/api/adapters/deployment_revision_preset/adapter.py b/src/ai/backend/manager/api/adapters/deployment_revision_preset/adapter.py index 70632177e9c..33a4f042369 100644 --- a/src/ai/backend/manager/api/adapters/deployment_revision_preset/adapter.py +++ b/src/ai/backend/manager/api/adapters/deployment_revision_preset/adapter.py @@ -81,7 +81,6 @@ BatchQuerier, QueryCondition, QueryOrder, - combine_conditions_or, ) from ai.backend.manager.repositories.base.creator import Creator from ai.backend.manager.repositories.base.updater import Updater @@ -443,14 +442,17 @@ def _convert_allocated_slot_filter( if cond is not None: conditions.append(cond) if filter_.AND: - for sub in filter_.AND: - conditions.extend(self._convert_allocated_slot_filter(sub)) + conditions.extend( + self.convert_and([self._convert_allocated_slot_filter(sub) for sub in filter_.AND]) + ) if filter_.OR: - or_conds: list[QueryCondition] = [] - for sub in filter_.OR: - or_conds.extend(self._convert_allocated_slot_filter(sub)) - if or_conds: - conditions.append(combine_conditions_or(or_conds)) + conditions.extend( + self.convert_or([self._convert_allocated_slot_filter(sub) for sub in filter_.OR]) + ) + if filter_.NOT: + conditions.extend( + self.convert_not([self._convert_allocated_slot_filter(sub) for sub in filter_.NOT]) + ) return conditions def _convert_filter(self, filter_: DeploymentRevisionPresetFilter) -> list[QueryCondition]: @@ -483,14 +485,11 @@ def _convert_filter(self, filter_: DeploymentRevisionPresetFilter) -> list[Query if cond: conditions.append(cond) if filter_.AND: - for sub in filter_.AND: - conditions.extend(self._convert_filter(sub)) + conditions.extend(self.convert_and([self._convert_filter(sub) for sub in filter_.AND])) if filter_.OR: - or_conds: list[QueryCondition] = [] - for sub in filter_.OR: - or_conds.extend(self._convert_filter(sub)) - if or_conds: - conditions.append(combine_conditions_or(or_conds)) + conditions.extend(self.convert_or([self._convert_filter(sub) for sub in filter_.OR])) + if filter_.NOT: + conditions.extend(self.convert_not([self._convert_filter(sub) for sub in filter_.NOT])) return conditions def _convert_orders(self, orders: list[DeploymentRevisionPresetOrder]) -> list[QueryOrder]: diff --git a/src/ai/backend/manager/api/adapters/domain/adapter.py b/src/ai/backend/manager/api/adapters/domain/adapter.py index 72f02ea8a0f..079922b8765 100644 --- a/src/ai/backend/manager/api/adapters/domain/adapter.py +++ b/src/ai/backend/manager/api/adapters/domain/adapter.py @@ -37,8 +37,6 @@ NoPagination, QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.base.creator import Creator from ai.backend.manager.repositories.base.updater import Updater @@ -313,22 +311,19 @@ def _convert_domain_filter(self, filter: DomainFilter) -> list[QueryCondition]: conditions.append(DomainConditions.by_user_is_active(filter.user.is_active)) if filter.AND: - for sub_filter in filter.AND: - conditions.extend(self._convert_domain_filter(sub_filter)) + conditions.extend( + self.convert_and([self._convert_domain_filter(sub) for sub in filter.AND]) + ) if filter.OR: - or_sub_conditions: list[QueryCondition] = [] - for sub_filter in filter.OR: - or_sub_conditions.extend(self._convert_domain_filter(sub_filter)) - if or_sub_conditions: - conditions.append(combine_conditions_or(or_sub_conditions)) + conditions.extend( + self.convert_or([self._convert_domain_filter(sub) for sub in filter.OR]) + ) if filter.NOT: - not_sub_conditions: list[QueryCondition] = [] - for sub_filter in filter.NOT: - not_sub_conditions.extend(self._convert_domain_filter(sub_filter)) - if not_sub_conditions: - conditions.append(negate_conditions(not_sub_conditions)) + conditions.extend( + self.convert_not([self._convert_domain_filter(sub) for sub in filter.NOT]) + ) return conditions diff --git a/src/ai/backend/manager/api/adapters/fair_share/adapter.py b/src/ai/backend/manager/api/adapters/fair_share/adapter.py index be4e135f74a..7b9e0aaf6b4 100644 --- a/src/ai/backend/manager/api/adapters/fair_share/adapter.py +++ b/src/ai/backend/manager/api/adapters/fair_share/adapter.py @@ -87,8 +87,6 @@ from ai.backend.manager.repositories.base import ( QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.fair_share.types import ( DomainFairShareSearchScope, @@ -494,8 +492,7 @@ async def bulk_upsert_user( # ------------------------------------------------------------------ filter helpers (domain) - @staticmethod - def _convert_domain_filter(filter: DomainFairShareFilter) -> list[QueryCondition]: + def _convert_domain_filter(self, filter: DomainFairShareFilter) -> list[QueryCondition]: conditions: list[QueryCondition] = [] if filter.resource_group is not None: cond = filter.resource_group.build_query_condition( @@ -522,24 +519,20 @@ def _convert_domain_filter(filter: DomainFairShareFilter) -> list[QueryCondition DomainFairShareConditions.by_domain_is_active(filter.domain.is_active) ) if filter.AND: - for sub_filter in filter.AND: - conditions.extend(FairShareAdapter._convert_domain_filter(sub_filter)) + conditions.extend( + self.convert_and([self._convert_domain_filter(sub) for sub in filter.AND]) + ) if filter.OR: - or_conditions: list[QueryCondition] = [] - for sub_filter in filter.OR: - or_conditions.extend(FairShareAdapter._convert_domain_filter(sub_filter)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend( + self.convert_or([self._convert_domain_filter(sub) for sub in filter.OR]) + ) if filter.NOT: - not_conditions: list[QueryCondition] = [] - for sub_filter in filter.NOT: - not_conditions.extend(FairShareAdapter._convert_domain_filter(sub_filter)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_domain_filter(sub) for sub in filter.NOT]) + ) return conditions - @staticmethod - def _convert_domain_filter_rg(filter: DomainFairShareFilter) -> list[QueryCondition]: + def _convert_domain_filter_rg(self, filter: DomainFairShareFilter) -> list[QueryCondition]: conditions: list[QueryCondition] = [] if filter.resource_group is not None: cond = filter.resource_group.build_query_condition( @@ -566,20 +559,17 @@ def _convert_domain_filter_rg(filter: DomainFairShareFilter) -> list[QueryCondit DomainFairShareConditions.by_domain_is_active(filter.domain.is_active) ) if filter.AND: - for sub_filter in filter.AND: - conditions.extend(FairShareAdapter._convert_domain_filter_rg(sub_filter)) + conditions.extend( + self.convert_and([self._convert_domain_filter_rg(sub) for sub in filter.AND]) + ) if filter.OR: - or_conditions: list[QueryCondition] = [] - for sub_filter in filter.OR: - or_conditions.extend(FairShareAdapter._convert_domain_filter_rg(sub_filter)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend( + self.convert_or([self._convert_domain_filter_rg(sub) for sub in filter.OR]) + ) if filter.NOT: - not_conditions: list[QueryCondition] = [] - for sub_filter in filter.NOT: - not_conditions.extend(FairShareAdapter._convert_domain_filter_rg(sub_filter)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_domain_filter_rg(sub) for sub in filter.NOT]) + ) return conditions @staticmethod @@ -616,8 +606,7 @@ def _convert_domain_orders_rg(orders: list[DomainFairShareOrder]) -> list[QueryO # ------------------------------------------------------------------ filter helpers (project) - @staticmethod - def _convert_project_filter(filter: ProjectFairShareFilter) -> list[QueryCondition]: + def _convert_project_filter(self, filter: ProjectFairShareFilter) -> list[QueryCondition]: conditions: list[QueryCondition] = [] if filter.resource_group is not None: cond = filter.resource_group.build_query_condition( @@ -651,24 +640,20 @@ def _convert_project_filter(filter: ProjectFairShareFilter) -> list[QueryConditi ProjectFairShareConditions.by_project_is_active(filter.project.is_active) ) if filter.AND: - for sub_filter in filter.AND: - conditions.extend(FairShareAdapter._convert_project_filter(sub_filter)) + conditions.extend( + self.convert_and([self._convert_project_filter(sub) for sub in filter.AND]) + ) if filter.OR: - or_conditions: list[QueryCondition] = [] - for sub_filter in filter.OR: - or_conditions.extend(FairShareAdapter._convert_project_filter(sub_filter)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend( + self.convert_or([self._convert_project_filter(sub) for sub in filter.OR]) + ) if filter.NOT: - not_conditions: list[QueryCondition] = [] - for sub_filter in filter.NOT: - not_conditions.extend(FairShareAdapter._convert_project_filter(sub_filter)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_project_filter(sub) for sub in filter.NOT]) + ) return conditions - @staticmethod - def _convert_project_filter_rg(filter: ProjectFairShareFilter) -> list[QueryCondition]: + def _convert_project_filter_rg(self, filter: ProjectFairShareFilter) -> list[QueryCondition]: conditions: list[QueryCondition] = [] if filter.resource_group is not None: cond = filter.resource_group.build_query_condition( @@ -702,20 +687,17 @@ def _convert_project_filter_rg(filter: ProjectFairShareFilter) -> list[QueryCond ProjectFairShareConditions.by_project_is_active(filter.project.is_active) ) if filter.AND: - for sub_filter in filter.AND: - conditions.extend(FairShareAdapter._convert_project_filter_rg(sub_filter)) + conditions.extend( + self.convert_and([self._convert_project_filter_rg(sub) for sub in filter.AND]) + ) if filter.OR: - or_conditions: list[QueryCondition] = [] - for sub_filter in filter.OR: - or_conditions.extend(FairShareAdapter._convert_project_filter_rg(sub_filter)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend( + self.convert_or([self._convert_project_filter_rg(sub) for sub in filter.OR]) + ) if filter.NOT: - not_conditions: list[QueryCondition] = [] - for sub_filter in filter.NOT: - not_conditions.extend(FairShareAdapter._convert_project_filter_rg(sub_filter)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_project_filter_rg(sub) for sub in filter.NOT]) + ) return conditions @staticmethod @@ -746,8 +728,7 @@ def _convert_project_orders_rg(orders: list[ProjectFairShareOrder]) -> list[Quer # ------------------------------------------------------------------ filter helpers (user) - @staticmethod - def _convert_user_filter(filter: UserFairShareFilter) -> list[QueryCondition]: + def _convert_user_filter(self, filter: UserFairShareFilter) -> list[QueryCondition]: conditions: list[QueryCondition] = [] if filter.resource_group is not None: cond = filter.resource_group.build_query_condition( @@ -786,24 +767,20 @@ def _convert_user_filter(filter: UserFairShareFilter) -> list[QueryCondition]: if filter.user is not None and filter.user.is_active is not None: conditions.append(UserFairShareConditions.by_user_is_active(filter.user.is_active)) if filter.AND: - for sub_filter in filter.AND: - conditions.extend(FairShareAdapter._convert_user_filter(sub_filter)) + conditions.extend( + self.convert_and([self._convert_user_filter(sub) for sub in filter.AND]) + ) if filter.OR: - or_conditions: list[QueryCondition] = [] - for sub_filter in filter.OR: - or_conditions.extend(FairShareAdapter._convert_user_filter(sub_filter)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend( + self.convert_or([self._convert_user_filter(sub) for sub in filter.OR]) + ) if filter.NOT: - not_conditions: list[QueryCondition] = [] - for sub_filter in filter.NOT: - not_conditions.extend(FairShareAdapter._convert_user_filter(sub_filter)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_user_filter(sub) for sub in filter.NOT]) + ) return conditions - @staticmethod - def _convert_user_filter_rg(filter: UserFairShareFilter) -> list[QueryCondition]: + def _convert_user_filter_rg(self, filter: UserFairShareFilter) -> list[QueryCondition]: conditions: list[QueryCondition] = [] if filter.resource_group is not None: cond = filter.resource_group.build_query_condition( @@ -842,20 +819,17 @@ def _convert_user_filter_rg(filter: UserFairShareFilter) -> list[QueryCondition] if filter.user is not None and filter.user.is_active is not None: conditions.append(UserFairShareConditions.by_user_is_active(filter.user.is_active)) if filter.AND: - for sub_filter in filter.AND: - conditions.extend(FairShareAdapter._convert_user_filter_rg(sub_filter)) + conditions.extend( + self.convert_and([self._convert_user_filter_rg(sub) for sub in filter.AND]) + ) if filter.OR: - or_conditions: list[QueryCondition] = [] - for sub_filter in filter.OR: - or_conditions.extend(FairShareAdapter._convert_user_filter_rg(sub_filter)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend( + self.convert_or([self._convert_user_filter_rg(sub) for sub in filter.OR]) + ) if filter.NOT: - not_conditions: list[QueryCondition] = [] - for sub_filter in filter.NOT: - not_conditions.extend(FairShareAdapter._convert_user_filter_rg(sub_filter)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_user_filter_rg(sub) for sub in filter.NOT]) + ) return conditions @staticmethod diff --git a/src/ai/backend/manager/api/adapters/image/adapter.py b/src/ai/backend/manager/api/adapters/image/adapter.py index c86631daca6..0d58134935a 100644 --- a/src/ai/backend/manager/api/adapters/image/adapter.py +++ b/src/ai/backend/manager/api/adapters/image/adapter.py @@ -60,8 +60,6 @@ OffsetPagination, QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.image.updaters import ImageUpdaterSpec from ai.backend.manager.services.image.actions.alias_image import AliasImageByIdAction @@ -429,22 +427,13 @@ def _convert_filter( conditions.append(ImageConditions.by_last_used_after(lu.after)) if filter.AND: - for sub_filter in filter.AND: - conditions.extend(self._convert_filter(sub_filter)) + conditions.extend(self.convert_and([self._convert_filter(sub) for sub in filter.AND])) if filter.OR: - or_sub: list[QueryCondition] = [] - for sub_filter in filter.OR: - or_sub.extend(self._convert_filter(sub_filter)) - if or_sub: - conditions.append(combine_conditions_or(or_sub)) + conditions.extend(self.convert_or([self._convert_filter(sub) for sub in filter.OR])) if filter.NOT: - not_sub: list[QueryCondition] = [] - for sub_filter in filter.NOT: - not_sub.extend(self._convert_filter(sub_filter)) - if not_sub: - conditions.append(negate_conditions(not_sub)) + conditions.extend(self.convert_not([self._convert_filter(sub) for sub in filter.NOT])) return conditions @@ -474,22 +463,19 @@ def _convert_alias_filter( conditions.append(ImageAliasConditions.by_image_ids([ImageID(i) for i in iid.in_])) if filter.AND: - for sub_filter in filter.AND: - conditions.extend(self._convert_alias_filter(sub_filter)) + conditions.extend( + self.convert_and([self._convert_alias_filter(sub) for sub in filter.AND]) + ) if filter.OR: - or_sub: list[QueryCondition] = [] - for sub_filter in filter.OR: - or_sub.extend(self._convert_alias_filter(sub_filter)) - if or_sub: - conditions.append(combine_conditions_or(or_sub)) + conditions.extend( + self.convert_or([self._convert_alias_filter(sub) for sub in filter.OR]) + ) if filter.NOT: - not_sub: list[QueryCondition] = [] - for sub_filter in filter.NOT: - not_sub.extend(self._convert_alias_filter(sub_filter)) - if not_sub: - conditions.append(negate_conditions(not_sub)) + conditions.extend( + self.convert_not([self._convert_alias_filter(sub) for sub in filter.NOT]) + ) return conditions diff --git a/src/ai/backend/manager/api/adapters/login_history/adapter.py b/src/ai/backend/manager/api/adapters/login_history/adapter.py index fdd90b6c62e..7c5b8234035 100644 --- a/src/ai/backend/manager/api/adapters/login_history/adapter.py +++ b/src/ai/backend/manager/api/adapters/login_history/adapter.py @@ -30,8 +30,6 @@ from ai.backend.manager.repositories.base import ( QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.services.auth.actions.search_login_history import ( AdminSearchLoginHistoryAction, @@ -133,20 +131,11 @@ def _convert_filter(self, f: LoginHistoryFilter) -> list[QueryCondition]: if condition is not None: conditions.append(condition) if f.AND: - for sub_filter in f.AND: - conditions.extend(self._convert_filter(sub_filter)) + conditions.extend(self.convert_and([self._convert_filter(sub) for sub in f.AND])) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub_filter in f.OR: - or_conditions.extend(self._convert_filter(sub_filter)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend(self.convert_or([self._convert_filter(sub) for sub in f.OR])) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub_filter in f.NOT: - not_conditions.extend(self._convert_filter(sub_filter)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend(self.convert_not([self._convert_filter(sub) for sub in f.NOT])) return conditions @staticmethod diff --git a/src/ai/backend/manager/api/adapters/login_session/adapter.py b/src/ai/backend/manager/api/adapters/login_session/adapter.py index 6f351817502..9c46c5f8fdc 100644 --- a/src/ai/backend/manager/api/adapters/login_session/adapter.py +++ b/src/ai/backend/manager/api/adapters/login_session/adapter.py @@ -35,8 +35,6 @@ from ai.backend.manager.repositories.base import ( QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.services.auth.actions.revoke_login_session import ( AdminRevokeLoginSessionAction, @@ -187,20 +185,11 @@ def _convert_filter(self, f: LoginSessionFilter) -> list[QueryCondition]: if condition is not None: conditions.append(condition) if f.AND: - for sub_filter in f.AND: - conditions.extend(self._convert_filter(sub_filter)) + conditions.extend(self.convert_and([self._convert_filter(sub) for sub in f.AND])) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub_filter in f.OR: - or_conditions.extend(self._convert_filter(sub_filter)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend(self.convert_or([self._convert_filter(sub) for sub in f.OR])) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub_filter in f.NOT: - not_conditions.extend(self._convert_filter(sub_filter)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend(self.convert_not([self._convert_filter(sub) for sub in f.NOT])) return conditions @staticmethod diff --git a/src/ai/backend/manager/api/adapters/model_card/adapter.py b/src/ai/backend/manager/api/adapters/model_card/adapter.py index 670d6586093..969b4f80311 100644 --- a/src/ai/backend/manager/api/adapters/model_card/adapter.py +++ b/src/ai/backend/manager/api/adapters/model_card/adapter.py @@ -69,8 +69,6 @@ from ai.backend.manager.repositories.base import ( QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.base.purger import Purger from ai.backend.manager.repositories.base.rbac.entity_creator import RBACEntityCreator @@ -589,20 +587,11 @@ def _convert_filter(self, filter_: ModelCardFilter) -> list[QueryCondition]: if cond: conditions.append(cond) if filter_.AND: - for sub in filter_.AND: - conditions.extend(self._convert_filter(sub)) + conditions.extend(self.convert_and([self._convert_filter(sub) for sub in filter_.AND])) if filter_.OR: - or_conds: list[QueryCondition] = [] - for sub in filter_.OR: - or_conds.extend(self._convert_filter(sub)) - if or_conds: - conditions.append(combine_conditions_or(or_conds)) + conditions.extend(self.convert_or([self._convert_filter(sub) for sub in filter_.OR])) if filter_.NOT: - not_conds: list[QueryCondition] = [] - for sub in filter_.NOT: - not_conds.extend(self._convert_filter(sub)) - if not_conds: - conditions.append(negate_conditions(not_conds)) + conditions.extend(self.convert_not([self._convert_filter(sub) for sub in filter_.NOT])) return conditions def _convert_orders(self, orders: list[ModelCardOrder]) -> list[QueryOrder]: diff --git a/src/ai/backend/manager/api/adapters/notification/adapter.py b/src/ai/backend/manager/api/adapters/notification/adapter.py index 7b6b91df66b..9444ef13545 100644 --- a/src/ai/backend/manager/api/adapters/notification/adapter.py +++ b/src/ai/backend/manager/api/adapters/notification/adapter.py @@ -83,8 +83,6 @@ QueryCondition, QueryOrder, Updater, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.base.rbac.entity_creator import RBACEntityCreator from ai.backend.manager.repositories.notification.creators import ( @@ -527,8 +525,7 @@ def _build_rule_updater_spec( ), ) - @staticmethod - def _convert_channel_filter(f: NotificationChannelFilter) -> list[QueryCondition]: + def _convert_channel_filter(self, f: NotificationChannelFilter) -> list[QueryCondition]: """Convert NotificationChannelFilter DTO to QueryCondition list.""" conditions: list[QueryCondition] = [] @@ -574,22 +571,17 @@ def _convert_channel_filter(f: NotificationChannelFilter) -> list[QueryCondition conditions.append(NotificationChannelConditions.by_enabled(f.enabled)) if f.AND: - for sub in f.AND: - conditions.extend(NotificationAdapter._convert_channel_filter(sub)) + conditions.extend( + self.convert_and([self._convert_channel_filter(sub) for sub in f.AND]) + ) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(NotificationAdapter._convert_channel_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend(self.convert_or([self._convert_channel_filter(sub) for sub in f.OR])) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(NotificationAdapter._convert_channel_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_channel_filter(sub) for sub in f.NOT]) + ) return conditions @@ -608,8 +600,7 @@ def _convert_channel_orders(orders: list[NotificationChannelOrder]) -> list[Quer result.append(NotificationChannelOrders.updated_at(ascending)) return result - @staticmethod - def _convert_rule_filter(f: NotificationRuleFilter) -> list[QueryCondition]: + def _convert_rule_filter(self, f: NotificationRuleFilter) -> list[QueryCondition]: """Convert NotificationRuleFilter DTO to QueryCondition list.""" conditions: list[QueryCondition] = [] @@ -655,22 +646,13 @@ def _convert_rule_filter(f: NotificationRuleFilter) -> list[QueryCondition]: conditions.append(NotificationRuleConditions.by_enabled(f.enabled)) if f.AND: - for sub in f.AND: - conditions.extend(NotificationAdapter._convert_rule_filter(sub)) + conditions.extend(self.convert_and([self._convert_rule_filter(sub) for sub in f.AND])) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(NotificationAdapter._convert_rule_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend(self.convert_or([self._convert_rule_filter(sub) for sub in f.OR])) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(NotificationAdapter._convert_rule_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend(self.convert_not([self._convert_rule_filter(sub) for sub in f.NOT])) return conditions diff --git a/src/ai/backend/manager/api/adapters/project/adapter.py b/src/ai/backend/manager/api/adapters/project/adapter.py index 3c7632bfee7..cbb35624fbb 100644 --- a/src/ai/backend/manager/api/adapters/project/adapter.py +++ b/src/ai/backend/manager/api/adapters/project/adapter.py @@ -56,8 +56,6 @@ NoPagination, QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.base.creator import Creator from ai.backend.manager.repositories.base.updater import Updater @@ -374,22 +372,19 @@ def _convert_group_filter(self, filter: ProjectFilter) -> list[QueryCondition]: conditions.extend(self._convert_user_nested_filter(filter.user)) if filter.AND: - for sub_filter in filter.AND: - conditions.extend(self._convert_group_filter(sub_filter)) + conditions.extend( + self.convert_and([self._convert_group_filter(sub) for sub in filter.AND]) + ) if filter.OR: - or_sub_conditions: list[QueryCondition] = [] - for sub_filter in filter.OR: - or_sub_conditions.extend(self._convert_group_filter(sub_filter)) - if or_sub_conditions: - conditions.append(combine_conditions_or(or_sub_conditions)) + conditions.extend( + self.convert_or([self._convert_group_filter(sub) for sub in filter.OR]) + ) if filter.NOT: - not_sub_conditions: list[QueryCondition] = [] - for sub_filter in filter.NOT: - not_sub_conditions.extend(self._convert_group_filter(sub_filter)) - if not_sub_conditions: - conditions.append(negate_conditions(not_sub_conditions)) + conditions.extend( + self.convert_not([self._convert_group_filter(sub) for sub in filter.NOT]) + ) return conditions @@ -420,8 +415,7 @@ def _convert_domain_name_filter(self, sf: StringFilter) -> QueryCondition | None in_factory=GroupConditions.by_domain_name_in, ) - @staticmethod - def _convert_type_filter(type_filter: ProjectTypeFilter) -> list[QueryCondition]: + def _convert_type_filter(self, type_filter: ProjectTypeFilter) -> list[QueryCondition]: conditions: list[QueryCondition] = [] if type_filter.equals is not None: conditions.append( @@ -432,17 +426,19 @@ def _convert_type_filter(type_filter: ProjectTypeFilter) -> list[QueryCondition] GroupConditions.by_type_in([DataProjectType(t.value) for t in type_filter.in_]) ) if type_filter.not_equals is not None: - conditions.append( - negate_conditions([ - GroupConditions.by_type_equals(DataProjectType(type_filter.not_equals.value)) + conditions.extend( + self.convert_not([ + [GroupConditions.by_type_equals(DataProjectType(type_filter.not_equals.value))] ]) ) if type_filter.not_in is not None: - conditions.append( - negate_conditions([ - GroupConditions.by_type_in([ - DataProjectType(t.value) for t in type_filter.not_in - ]) + conditions.extend( + self.convert_not([ + [ + GroupConditions.by_type_in([ + DataProjectType(t.value) for t in type_filter.not_in + ]) + ] ]) ) return conditions diff --git a/src/ai/backend/manager/api/adapters/prometheus_query_preset/adapter.py b/src/ai/backend/manager/api/adapters/prometheus_query_preset/adapter.py index aab4fae9cc4..71a438d6473 100644 --- a/src/ai/backend/manager/api/adapters/prometheus_query_preset/adapter.py +++ b/src/ai/backend/manager/api/adapters/prometheus_query_preset/adapter.py @@ -55,8 +55,6 @@ QueryCondition, QueryOrder, Updater, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.prometheus_query_preset.creators import ( PrometheusQueryPresetCreatorSpec, @@ -287,22 +285,13 @@ def _convert_filter(self, filter: QueryDefinitionFilter) -> list[QueryCondition] conditions.append(condition) if filter.AND: - for sub_filter in filter.AND: - conditions.extend(self._convert_filter(sub_filter)) + conditions.extend(self.convert_and([self._convert_filter(sub) for sub in filter.AND])) if filter.OR: - or_sub_conditions: list[QueryCondition] = [] - for sub_filter in filter.OR: - or_sub_conditions.extend(self._convert_filter(sub_filter)) - if or_sub_conditions: - conditions.append(combine_conditions_or(or_sub_conditions)) + conditions.extend(self.convert_or([self._convert_filter(sub) for sub in filter.OR])) if filter.NOT: - not_sub_conditions: list[QueryCondition] = [] - for sub_filter in filter.NOT: - not_sub_conditions.extend(self._convert_filter(sub_filter)) - if not_sub_conditions: - conditions.append(negate_conditions(not_sub_conditions)) + conditions.extend(self.convert_not([self._convert_filter(sub) for sub in filter.NOT])) return conditions diff --git a/src/ai/backend/manager/api/adapters/rbac/adapter.py b/src/ai/backend/manager/api/adapters/rbac/adapter.py index be70202fb36..7c43bd88f50 100644 --- a/src/ai/backend/manager/api/adapters/rbac/adapter.py +++ b/src/ai/backend/manager/api/adapters/rbac/adapter.py @@ -219,8 +219,6 @@ Purger, QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.base.creator import Creator from ai.backend.manager.repositories.base.updater import Updater @@ -1325,20 +1323,17 @@ def _convert_permission_filter(self, f: PermissionFilterDTO) -> list[QueryCondit if cond is not None: conditions.append(cond) if f.AND: - for sub in f.AND: - conditions.extend(self._convert_permission_filter(sub)) + conditions.extend( + self.convert_and([self._convert_permission_filter(sub) for sub in f.AND]) + ) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(self._convert_permission_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend( + self.convert_or([self._convert_permission_filter(sub) for sub in f.OR]) + ) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(self._convert_permission_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_permission_filter(sub) for sub in f.NOT]) + ) return conditions @staticmethod @@ -1400,20 +1395,15 @@ def _convert_role_filter_gql(self, f: RoleFilterDTO) -> list[QueryCondition]: RoleConditions.by_status_not_in([InternalRoleStatus(s) for s in st.not_in]) ) if f.AND: - for sub in f.AND: - conditions.extend(self._convert_role_filter_gql(sub)) + conditions.extend( + self.convert_and([self._convert_role_filter_gql(sub) for sub in f.AND]) + ) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(self._convert_role_filter_gql(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend(self.convert_or([self._convert_role_filter_gql(sub) for sub in f.OR])) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(self._convert_role_filter_gql(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_role_filter_gql(sub) for sub in f.NOT]) + ) return conditions @staticmethod @@ -1482,20 +1472,17 @@ def _convert_role_nested_filter(self, f: RoleNestedFilterDTO) -> list[QueryCondi if raw_conditions: conditions.append(AssignedUserConditions.exists_role_combined(raw_conditions)) if f.AND: - for sub in f.AND: - conditions.extend(self._convert_role_nested_filter(sub)) + conditions.extend( + self.convert_and([self._convert_role_nested_filter(sub) for sub in f.AND]) + ) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(self._convert_role_nested_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend( + self.convert_or([self._convert_role_nested_filter(sub) for sub in f.OR]) + ) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(self._convert_role_nested_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_role_nested_filter(sub) for sub in f.NOT]) + ) return conditions def _convert_permission_nested_filter( @@ -1547,20 +1534,17 @@ def _convert_permission_nested_filter( if raw_conditions: conditions.append(AssignedUserConditions.exists_permission_combined(raw_conditions)) if f.AND: - for sub in f.AND: - conditions.extend(self._convert_permission_nested_filter(sub)) + conditions.extend( + self.convert_and([self._convert_permission_nested_filter(sub) for sub in f.AND]) + ) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(self._convert_permission_nested_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend( + self.convert_or([self._convert_permission_nested_filter(sub) for sub in f.OR]) + ) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(self._convert_permission_nested_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_permission_nested_filter(sub) for sub in f.NOT]) + ) return conditions def _convert_assignment_filter(self, f: RoleAssignmentFilterDTO) -> list[QueryCondition]: @@ -1600,20 +1584,17 @@ def _convert_assignment_filter(self, f: RoleAssignmentFilterDTO) -> list[QueryCo if condition is not None: conditions.append(condition) if f.AND: - for sub in f.AND: - conditions.extend(self._convert_assignment_filter(sub)) + conditions.extend( + self.convert_and([self._convert_assignment_filter(sub) for sub in f.AND]) + ) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(self._convert_assignment_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend( + self.convert_or([self._convert_assignment_filter(sub) for sub in f.OR]) + ) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(self._convert_assignment_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_assignment_filter(sub) for sub in f.NOT]) + ) return conditions @staticmethod @@ -1653,20 +1634,11 @@ def _convert_entity_filter(self, f: EntityFilterDTO) -> list[QueryCondition]: if condition is not None: conditions.append(condition) if f.AND: - for sub in f.AND: - conditions.extend(self._convert_entity_filter(sub)) + conditions.extend(self.convert_and([self._convert_entity_filter(sub) for sub in f.AND])) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(self._convert_entity_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend(self.convert_or([self._convert_entity_filter(sub) for sub in f.OR])) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(self._convert_entity_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend(self.convert_not([self._convert_entity_filter(sub) for sub in f.NOT])) return conditions @staticmethod @@ -1887,20 +1859,17 @@ def _convert_invitation_filter(self, f: RoleInvitationFilterDTO) -> list[QueryCo if f.invitee is not None: conditions.extend(self._convert_invitation_user_nested_filter(f.invitee, "invitee")) if f.AND: - for sub in f.AND: - conditions.extend(self._convert_invitation_filter(sub)) + conditions.extend( + self.convert_and([self._convert_invitation_filter(sub) for sub in f.AND]) + ) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(self._convert_invitation_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend( + self.convert_or([self._convert_invitation_filter(sub) for sub in f.OR]) + ) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(self._convert_invitation_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_invitation_filter(sub) for sub in f.NOT]) + ) return conditions @staticmethod diff --git a/src/ai/backend/manager/api/adapters/resource_group/adapter.py b/src/ai/backend/manager/api/adapters/resource_group/adapter.py index 2ab70d4e67c..c850f5c1b2a 100644 --- a/src/ai/backend/manager/api/adapters/resource_group/adapter.py +++ b/src/ai/backend/manager/api/adapters/resource_group/adapter.py @@ -89,8 +89,6 @@ NoPagination, OffsetPagination, QueryCondition, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.base.creator import Creator from ai.backend.manager.repositories.base.purger import Purger @@ -318,20 +316,11 @@ def _convert_filter(self, filter_: ResourceGroupFilter) -> list[QueryCondition]: if filter_.is_public is not None: conditions.append(ScalingGroupConditions.by_is_public(filter_.is_public)) if filter_.AND: - for sub in filter_.AND: - conditions.extend(self._convert_filter(sub)) + conditions.extend(self.convert_and([self._convert_filter(sub) for sub in filter_.AND])) if filter_.OR: - or_conds: list[QueryCondition] = [] - for sub in filter_.OR: - or_conds.extend(self._convert_filter(sub)) - if or_conds: - conditions.append(combine_conditions_or(or_conds)) + conditions.extend(self.convert_or([self._convert_filter(sub) for sub in filter_.OR])) if filter_.NOT: - not_conds: list[QueryCondition] = [] - for sub in filter_.NOT: - not_conds.extend(self._convert_filter(sub)) - if not_conds: - conditions.append(negate_conditions(not_conds)) + conditions.extend(self.convert_not([self._convert_filter(sub) for sub in filter_.NOT])) return conditions def _convert_orders(self, orders: list[ResourceGroupOrder]) -> list[Any]: diff --git a/src/ai/backend/manager/api/adapters/resource_preset/adapter.py b/src/ai/backend/manager/api/adapters/resource_preset/adapter.py index 4c090bc9e04..d9728e3f5f6 100644 --- a/src/ai/backend/manager/api/adapters/resource_preset/adapter.py +++ b/src/ai/backend/manager/api/adapters/resource_preset/adapter.py @@ -41,8 +41,6 @@ BatchQuerier, OffsetPagination, QueryCondition, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.base.creator import Creator from ai.backend.manager.repositories.base.updater import Updater @@ -237,20 +235,11 @@ def _convert_filter(self, filter_: ResourcePresetFilter) -> list[QueryCondition] if cond: conditions.append(cond) if filter_.AND: - for sub in filter_.AND: - conditions.extend(self._convert_filter(sub)) + conditions.extend(self.convert_and([self._convert_filter(sub) for sub in filter_.AND])) if filter_.OR: - or_conds: list[QueryCondition] = [] - for sub in filter_.OR: - or_conds.extend(self._convert_filter(sub)) - if or_conds: - conditions.append(combine_conditions_or(or_conds)) + conditions.extend(self.convert_or([self._convert_filter(sub) for sub in filter_.OR])) if filter_.NOT: - not_conds: list[QueryCondition] = [] - for sub in filter_.NOT: - not_conds.extend(self._convert_filter(sub)) - if not_conds: - conditions.append(negate_conditions(not_conds)) + conditions.extend(self.convert_not([self._convert_filter(sub) for sub in filter_.NOT])) return conditions def _convert_orders(self, orders: list[ResourcePresetOrder]) -> list[Any]: diff --git a/src/ai/backend/manager/api/adapters/resource_usage/adapter.py b/src/ai/backend/manager/api/adapters/resource_usage/adapter.py index 948b12bff75..6ab99f60949 100644 --- a/src/ai/backend/manager/api/adapters/resource_usage/adapter.py +++ b/src/ai/backend/manager/api/adapters/resource_usage/adapter.py @@ -54,8 +54,6 @@ OffsetPagination, QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.resource_usage_history import ( DomainUsageBucketConditions, @@ -702,22 +700,19 @@ def _convert_domain_filter( conditions.append(pe_condition) if filter_req.AND: - for sub_filter in filter_req.AND: - conditions.extend(self._convert_domain_filter(sub_filter)) + conditions.extend( + self.convert_and([self._convert_domain_filter(sub) for sub in filter_req.AND]) + ) if filter_req.OR: - or_conditions: list[QueryCondition] = [] - for sub_filter in filter_req.OR: - or_conditions.extend(self._convert_domain_filter(sub_filter)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend( + self.convert_or([self._convert_domain_filter(sub) for sub in filter_req.OR]) + ) if filter_req.NOT: - not_conditions: list[QueryCondition] = [] - for sub_filter in filter_req.NOT: - not_conditions.extend(self._convert_domain_filter(sub_filter)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_domain_filter(sub) for sub in filter_req.NOT]) + ) return conditions @@ -794,22 +789,19 @@ def _convert_project_filter( conditions.append(pe_condition) if filter_req.AND: - for sub_filter in filter_req.AND: - conditions.extend(self._convert_project_filter(sub_filter)) + conditions.extend( + self.convert_and([self._convert_project_filter(sub) for sub in filter_req.AND]) + ) if filter_req.OR: - or_conditions_p: list[QueryCondition] = [] - for sub_filter in filter_req.OR: - or_conditions_p.extend(self._convert_project_filter(sub_filter)) - if or_conditions_p: - conditions.append(combine_conditions_or(or_conditions_p)) + conditions.extend( + self.convert_or([self._convert_project_filter(sub) for sub in filter_req.OR]) + ) if filter_req.NOT: - not_conditions_p: list[QueryCondition] = [] - for sub_filter in filter_req.NOT: - not_conditions_p.extend(self._convert_project_filter(sub_filter)) - if not_conditions_p: - conditions.append(negate_conditions(not_conditions_p)) + conditions.extend( + self.convert_not([self._convert_project_filter(sub) for sub in filter_req.NOT]) + ) return conditions @@ -895,22 +887,19 @@ def _convert_user_filter( conditions.append(pe_condition) if filter_req.AND: - for sub_filter in filter_req.AND: - conditions.extend(self._convert_user_filter(sub_filter)) + conditions.extend( + self.convert_and([self._convert_user_filter(sub) for sub in filter_req.AND]) + ) if filter_req.OR: - or_conditions_u: list[QueryCondition] = [] - for sub_filter in filter_req.OR: - or_conditions_u.extend(self._convert_user_filter(sub_filter)) - if or_conditions_u: - conditions.append(combine_conditions_or(or_conditions_u)) + conditions.extend( + self.convert_or([self._convert_user_filter(sub) for sub in filter_req.OR]) + ) if filter_req.NOT: - not_conditions_u: list[QueryCondition] = [] - for sub_filter in filter_req.NOT: - not_conditions_u.extend(self._convert_user_filter(sub_filter)) - if not_conditions_u: - conditions.append(negate_conditions(not_conditions_u)) + conditions.extend( + self.convert_not([self._convert_user_filter(sub) for sub in filter_req.NOT]) + ) return conditions diff --git a/src/ai/backend/manager/api/adapters/runtime_variant/adapter.py b/src/ai/backend/manager/api/adapters/runtime_variant/adapter.py index b3e2537ab8e..c58a80f9e07 100644 --- a/src/ai/backend/manager/api/adapters/runtime_variant/adapter.py +++ b/src/ai/backend/manager/api/adapters/runtime_variant/adapter.py @@ -33,8 +33,6 @@ OffsetPagination, QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.base.creator import Creator from ai.backend.manager.repositories.base.updater import Updater @@ -196,20 +194,11 @@ def _convert_filter(self, filter_: RuntimeVariantFilter) -> list[QueryCondition] if cond: conditions.append(cond) if filter_.AND: - for sub in filter_.AND: - conditions.extend(self._convert_filter(sub)) + conditions.extend(self.convert_and([self._convert_filter(sub) for sub in filter_.AND])) if filter_.OR: - or_conds: list[QueryCondition] = [] - for sub in filter_.OR: - or_conds.extend(self._convert_filter(sub)) - if or_conds: - conditions.append(combine_conditions_or(or_conds)) + conditions.extend(self.convert_or([self._convert_filter(sub) for sub in filter_.OR])) if filter_.NOT: - not_conds: list[QueryCondition] = [] - for sub in filter_.NOT: - not_conds.extend(self._convert_filter(sub)) - if not_conds: - conditions.append(negate_conditions(not_conds)) + conditions.extend(self.convert_not([self._convert_filter(sub) for sub in filter_.NOT])) return conditions def _convert_orders(self, orders: list[RuntimeVariantOrder]) -> list[QueryOrder]: diff --git a/src/ai/backend/manager/api/adapters/runtime_variant_preset/adapter.py b/src/ai/backend/manager/api/adapters/runtime_variant_preset/adapter.py index 94d1773fbab..b7704b91e0b 100644 --- a/src/ai/backend/manager/api/adapters/runtime_variant_preset/adapter.py +++ b/src/ai/backend/manager/api/adapters/runtime_variant_preset/adapter.py @@ -43,8 +43,6 @@ from ai.backend.manager.repositories.base import ( QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.base.creator import Creator from ai.backend.manager.repositories.base.updater import Updater @@ -256,20 +254,11 @@ def _convert_filter(self, filter_: RuntimeVariantPresetFilter) -> list[QueryCond if cond: conditions.append(cond) if filter_.AND: - for sub in filter_.AND: - conditions.extend(self._convert_filter(sub)) + conditions.extend(self.convert_and([self._convert_filter(sub) for sub in filter_.AND])) if filter_.OR: - or_conds: list[QueryCondition] = [] - for sub in filter_.OR: - or_conds.extend(self._convert_filter(sub)) - if or_conds: - conditions.append(combine_conditions_or(or_conds)) + conditions.extend(self.convert_or([self._convert_filter(sub) for sub in filter_.OR])) if filter_.NOT: - not_conds: list[QueryCondition] = [] - for sub in filter_.NOT: - not_conds.extend(self._convert_filter(sub)) - if not_conds: - conditions.append(negate_conditions(not_conds)) + conditions.extend(self.convert_not([self._convert_filter(sub) for sub in filter_.NOT])) return conditions def _convert_orders(self, orders: list[RuntimeVariantPresetOrder]) -> list[QueryOrder]: diff --git a/src/ai/backend/manager/api/adapters/scheduling_history/adapter.py b/src/ai/backend/manager/api/adapters/scheduling_history/adapter.py index 8601270ef27..57af7672c12 100644 --- a/src/ai/backend/manager/api/adapters/scheduling_history/adapter.py +++ b/src/ai/backend/manager/api/adapters/scheduling_history/adapter.py @@ -57,8 +57,6 @@ OffsetPagination, QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.scheduling_history.types import ( DeploymentHistorySearchScope, @@ -328,20 +326,17 @@ def _convert_session_filter(self, filter: SessionHistoryFilter) -> list[QueryCon if condition is not None: conditions.append(condition) if filter.AND: - for sub in filter.AND: - conditions.extend(self._convert_session_filter(sub)) + conditions.extend( + self.convert_and([self._convert_session_filter(sub) for sub in filter.AND]) + ) if filter.OR: - or_conds: list[QueryCondition] = [] - for sub in filter.OR: - or_conds.extend(self._convert_session_filter(sub)) - if or_conds: - conditions.append(combine_conditions_or(or_conds)) + conditions.extend( + self.convert_or([self._convert_session_filter(sub) for sub in filter.OR]) + ) if filter.NOT: - not_conds: list[QueryCondition] = [] - for sub in filter.NOT: - not_conds.extend(self._convert_session_filter(sub)) - if not_conds: - conditions.append(negate_conditions(not_conds)) + conditions.extend( + self.convert_not([self._convert_session_filter(sub) for sub in filter.NOT]) + ) return conditions @staticmethod @@ -491,20 +486,17 @@ def _convert_deployment_filter(self, filter: DeploymentHistoryFilter) -> list[Qu if condition is not None: conditions.append(condition) if filter.AND: - for sub in filter.AND: - conditions.extend(self._convert_deployment_filter(sub)) + conditions.extend( + self.convert_and([self._convert_deployment_filter(sub) for sub in filter.AND]) + ) if filter.OR: - or_conds: list[QueryCondition] = [] - for sub in filter.OR: - or_conds.extend(self._convert_deployment_filter(sub)) - if or_conds: - conditions.append(combine_conditions_or(or_conds)) + conditions.extend( + self.convert_or([self._convert_deployment_filter(sub) for sub in filter.OR]) + ) if filter.NOT: - not_conds: list[QueryCondition] = [] - for sub in filter.NOT: - not_conds.extend(self._convert_deployment_filter(sub)) - if not_conds: - conditions.append(negate_conditions(not_conds)) + conditions.extend( + self.convert_not([self._convert_deployment_filter(sub) for sub in filter.NOT]) + ) return conditions @staticmethod @@ -662,20 +654,17 @@ def _convert_route_filter(self, filter: RouteHistoryFilter) -> list[QueryConditi if condition is not None: conditions.append(condition) if filter.AND: - for sub in filter.AND: - conditions.extend(self._convert_route_filter(sub)) + conditions.extend( + self.convert_and([self._convert_route_filter(sub) for sub in filter.AND]) + ) if filter.OR: - or_conds: list[QueryCondition] = [] - for sub in filter.OR: - or_conds.extend(self._convert_route_filter(sub)) - if or_conds: - conditions.append(combine_conditions_or(or_conds)) + conditions.extend( + self.convert_or([self._convert_route_filter(sub) for sub in filter.OR]) + ) if filter.NOT: - not_conds: list[QueryCondition] = [] - for sub in filter.NOT: - not_conds.extend(self._convert_route_filter(sub)) - if not_conds: - conditions.append(negate_conditions(not_conds)) + conditions.extend( + self.convert_not([self._convert_route_filter(sub) for sub in filter.NOT]) + ) return conditions @staticmethod diff --git a/src/ai/backend/manager/api/adapters/service_catalog/adapter.py b/src/ai/backend/manager/api/adapters/service_catalog/adapter.py index 02bdb5448cd..8c67a41736f 100644 --- a/src/ai/backend/manager/api/adapters/service_catalog/adapter.py +++ b/src/ai/backend/manager/api/adapters/service_catalog/adapter.py @@ -31,8 +31,6 @@ OffsetPagination, QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.services.service_catalog.actions.search import ( SearchServiceCatalogsAction, @@ -89,20 +87,11 @@ def _convert_filter(self, filter: ServiceCatalogFilter) -> list[QueryCondition]: if filter.status is not None: conditions.extend(self._convert_status_filter(filter.status)) if filter.AND: - for sub in filter.AND: - conditions.extend(self._convert_filter(sub)) + conditions.extend(self.convert_and([self._convert_filter(sub) for sub in filter.AND])) if filter.OR: - or_conds: list[QueryCondition] = [] - for sub in filter.OR: - or_conds.extend(self._convert_filter(sub)) - if or_conds: - conditions.append(combine_conditions_or(or_conds)) + conditions.extend(self.convert_or([self._convert_filter(sub) for sub in filter.OR])) if filter.NOT: - not_conds: list[QueryCondition] = [] - for sub in filter.NOT: - not_conds.extend(self._convert_filter(sub)) - if not_conds: - conditions.append(negate_conditions(not_conds)) + conditions.extend(self.convert_not([self._convert_filter(sub) for sub in filter.NOT])) return conditions def _convert_string_filter(self, sf: StringFilter) -> QueryCondition | None: diff --git a/src/ai/backend/manager/api/adapters/session/adapter.py b/src/ai/backend/manager/api/adapters/session/adapter.py index 450cec581bb..64c197a6558 100644 --- a/src/ai/backend/manager/api/adapters/session/adapter.py +++ b/src/ai/backend/manager/api/adapters/session/adapter.py @@ -114,8 +114,6 @@ NoPagination, QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.session.types import ProjectSessionSearchScope from ai.backend.manager.services.session.actions.enqueue_session import ( @@ -587,20 +585,15 @@ def _convert_session_filter(self, f: SessionFilter) -> list[QueryCondition]: if c is not None: conditions.append(c) if f.AND: - for sub in f.AND: - conditions.extend(self._convert_session_filter(sub)) + conditions.extend( + self.convert_and([self._convert_session_filter(sub) for sub in f.AND]) + ) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(self._convert_session_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend(self.convert_or([self._convert_session_filter(sub) for sub in f.OR])) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(self._convert_session_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_session_filter(sub) for sub in f.NOT]) + ) return conditions @staticmethod @@ -747,20 +740,11 @@ def _convert_kernel_filter(self, f: KernelFilter) -> list[QueryCondition]: if f.status is not None: conditions.extend(self._convert_kernel_status_filter(f.status)) if f.AND: - for sub in f.AND: - conditions.extend(self._convert_kernel_filter(sub)) + conditions.extend(self.convert_and([self._convert_kernel_filter(sub) for sub in f.AND])) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(self._convert_kernel_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend(self.convert_or([self._convert_kernel_filter(sub) for sub in f.OR])) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(self._convert_kernel_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend(self.convert_not([self._convert_kernel_filter(sub) for sub in f.NOT])) return conditions @staticmethod diff --git a/src/ai/backend/manager/api/adapters/user/adapter.py b/src/ai/backend/manager/api/adapters/user/adapter.py index cb8bc2912aa..cb40bb3f12e 100644 --- a/src/ai/backend/manager/api/adapters/user/adapter.py +++ b/src/ai/backend/manager/api/adapters/user/adapter.py @@ -107,8 +107,6 @@ OffsetPagination, QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.base.creator import Creator from ai.backend.manager.repositories.base.updater import Updater @@ -918,22 +916,19 @@ def _convert_keypair_filter(self, filter_req: KeypairFilter) -> list[QueryCondit conditions.append(condition) if filter_req.AND: - for sub_filter in filter_req.AND: - conditions.extend(self._convert_keypair_filter(sub_filter)) + conditions.extend( + self.convert_and([self._convert_keypair_filter(sub) for sub in filter_req.AND]) + ) if filter_req.OR: - or_sub_conditions: list[QueryCondition] = [] - for sub_filter in filter_req.OR: - or_sub_conditions.extend(self._convert_keypair_filter(sub_filter)) - if or_sub_conditions: - conditions.append(combine_conditions_or(or_sub_conditions)) + conditions.extend( + self.convert_or([self._convert_keypair_filter(sub) for sub in filter_req.OR]) + ) if filter_req.NOT: - not_sub_conditions: list[QueryCondition] = [] - for sub_filter in filter_req.NOT: - not_sub_conditions.extend(self._convert_keypair_filter(sub_filter)) - if not_sub_conditions: - conditions.append(negate_conditions(not_sub_conditions)) + conditions.extend( + self.convert_not([self._convert_keypair_filter(sub) for sub in filter_req.NOT]) + ) return conditions @@ -1039,27 +1034,23 @@ def _convert_gql_filter(self, filter_req: UserFilter) -> list[QueryCondition]: conditions.extend(self._convert_project_nested_filter(filter_req.project)) if filter_req.AND: - for sub_filter in filter_req.AND: - conditions.extend(self._convert_gql_filter(sub_filter)) + conditions.extend( + self.convert_and([self._convert_gql_filter(sub) for sub in filter_req.AND]) + ) if filter_req.OR: - or_sub_conditions: list[QueryCondition] = [] - for sub_filter in filter_req.OR: - or_sub_conditions.extend(self._convert_gql_filter(sub_filter)) - if or_sub_conditions: - conditions.append(combine_conditions_or(or_sub_conditions)) + conditions.extend( + self.convert_or([self._convert_gql_filter(sub) for sub in filter_req.OR]) + ) if filter_req.NOT: - not_sub_conditions: list[QueryCondition] = [] - for sub_filter in filter_req.NOT: - not_sub_conditions.extend(self._convert_gql_filter(sub_filter)) - if not_sub_conditions: - conditions.append(negate_conditions(not_sub_conditions)) + conditions.extend( + self.convert_not([self._convert_gql_filter(sub) for sub in filter_req.NOT]) + ) return conditions - @staticmethod - def _convert_status_filter(sf: UserStatusFilter) -> list[QueryCondition]: + def _convert_status_filter(self, sf: UserStatusFilter) -> list[QueryCondition]: conditions: list[QueryCondition] = [] if sf.equals is not None: conditions.append(UserConditions.by_status_equals(DataUserStatus(sf.equals.value))) @@ -1068,36 +1059,35 @@ def _convert_status_filter(sf: UserStatusFilter) -> list[QueryCondition]: UserConditions.by_status_in([DataUserStatus(s.value) for s in sf.in_]) ) if sf.not_equals is not None: - conditions.append( - negate_conditions([ - UserConditions.by_status_equals(DataUserStatus(sf.not_equals.value)) + conditions.extend( + self.convert_not([ + [UserConditions.by_status_equals(DataUserStatus(sf.not_equals.value))] ]) ) if sf.not_in is not None: - conditions.append( - negate_conditions([ - UserConditions.by_status_in([DataUserStatus(s.value) for s in sf.not_in]) + conditions.extend( + self.convert_not([ + [UserConditions.by_status_in([DataUserStatus(s.value) for s in sf.not_in])] ]) ) return conditions - @staticmethod - def _convert_role_filter(rf: UserRoleFilter) -> list[QueryCondition]: + def _convert_role_filter(self, rf: UserRoleFilter) -> list[QueryCondition]: conditions: list[QueryCondition] = [] if rf.equals is not None: conditions.append(UserConditions.by_role_equals(DataUserRole(rf.equals.value))) if rf.in_ is not None: conditions.append(UserConditions.by_role_in([DataUserRole(r.value) for r in rf.in_])) if rf.not_equals is not None: - conditions.append( - negate_conditions([ - UserConditions.by_role_equals(DataUserRole(rf.not_equals.value)) + conditions.extend( + self.convert_not([ + [UserConditions.by_role_equals(DataUserRole(rf.not_equals.value))] ]) ) if rf.not_in is not None and len(rf.not_in) > 0: - conditions.append( - negate_conditions([ - UserConditions.by_role_in([DataUserRole(r.value) for r in rf.not_in]) + conditions.extend( + self.convert_not([ + [UserConditions.by_role_in([DataUserRole(r.value) for r in rf.not_in])] ]) ) return conditions @@ -1248,15 +1238,19 @@ def _convert_filter(self, filter_req: UserFilter) -> list[QueryCondition]: UserConditions.by_status_in([UserStatus(s.value) for s in status_f.in_]) ) if status_f.not_equals is not None: - conditions.append( - negate_conditions([ - UserConditions.by_status_equals(UserStatus(status_f.not_equals.value)) + conditions.extend( + self.convert_not([ + [UserConditions.by_status_equals(UserStatus(status_f.not_equals.value))] ]) ) if status_f.not_in is not None and len(status_f.not_in) > 0: - conditions.append( - negate_conditions([ - UserConditions.by_status_in([UserStatus(s.value) for s in status_f.not_in]) + conditions.extend( + self.convert_not([ + [ + UserConditions.by_status_in([ + UserStatus(s.value) for s in status_f.not_in + ]) + ] ]) ) @@ -1269,15 +1263,15 @@ def _convert_filter(self, filter_req: UserFilter) -> list[QueryCondition]: UserConditions.by_role_in([UserRole(r.value) for r in role_f.in_]) ) if role_f.not_equals is not None: - conditions.append( - negate_conditions([ - UserConditions.by_role_equals(UserRole(role_f.not_equals.value)) + conditions.extend( + self.convert_not([ + [UserConditions.by_role_equals(UserRole(role_f.not_equals.value))] ]) ) if role_f.not_in is not None and len(role_f.not_in) > 0: - conditions.append( - negate_conditions([ - UserConditions.by_role_in([UserRole(r.value) for r in role_f.not_in]) + conditions.extend( + self.convert_not([ + [UserConditions.by_role_in([UserRole(r.value) for r in role_f.not_in])] ]) ) @@ -1297,20 +1291,15 @@ def _convert_filter(self, filter_req: UserFilter) -> list[QueryCondition]: conditions.extend(self._convert_project_nested_filter(filter_req.project)) if filter_req.AND: - for sub_filter in filter_req.AND: - conditions.extend(self._convert_filter(sub_filter)) + conditions.extend( + self.convert_and([self._convert_filter(sub) for sub in filter_req.AND]) + ) if filter_req.OR: - or_sub_conditions: list[QueryCondition] = [] - for sub_filter in filter_req.OR: - or_sub_conditions.extend(self._convert_filter(sub_filter)) - if or_sub_conditions: - conditions.append(combine_conditions_or(or_sub_conditions)) + conditions.extend(self.convert_or([self._convert_filter(sub) for sub in filter_req.OR])) if filter_req.NOT: - not_sub_conditions: list[QueryCondition] = [] - for sub_filter in filter_req.NOT: - not_sub_conditions.extend(self._convert_filter(sub_filter)) - if not_sub_conditions: - conditions.append(negate_conditions(not_sub_conditions)) + conditions.extend( + self.convert_not([self._convert_filter(sub) for sub in filter_req.NOT]) + ) return conditions diff --git a/src/ai/backend/manager/api/adapters/vfolder/adapter.py b/src/ai/backend/manager/api/adapters/vfolder/adapter.py index cb378a16674..ffa38335ea0 100644 --- a/src/ai/backend/manager/api/adapters/vfolder/adapter.py +++ b/src/ai/backend/manager/api/adapters/vfolder/adapter.py @@ -95,8 +95,6 @@ from ai.backend.manager.repositories.base import ( QueryCondition, QueryOrder, - combine_conditions_or, - negate_conditions, ) from ai.backend.manager.repositories.vfolder.types import ( ProjectVFolderSearchScope, @@ -721,20 +719,15 @@ def _convert_vfolder_filter(self, f: VFolderFilter) -> list[QueryCondition]: if f.cloneable is not None: conditions.append(VFolderConditions.by_cloneable(f.cloneable)) if f.AND: - for sub in f.AND: - conditions.extend(self._convert_vfolder_filter(sub)) + conditions.extend( + self.convert_and([self._convert_vfolder_filter(sub) for sub in f.AND]) + ) if f.OR: - or_conditions: list[QueryCondition] = [] - for sub in f.OR: - or_conditions.extend(self._convert_vfolder_filter(sub)) - if or_conditions: - conditions.append(combine_conditions_or(or_conditions)) + conditions.extend(self.convert_or([self._convert_vfolder_filter(sub) for sub in f.OR])) if f.NOT: - not_conditions: list[QueryCondition] = [] - for sub in f.NOT: - not_conditions.extend(self._convert_vfolder_filter(sub)) - if not_conditions: - conditions.append(negate_conditions(not_conditions)) + conditions.extend( + self.convert_not([self._convert_vfolder_filter(sub) for sub in f.NOT]) + ) return conditions @staticmethod diff --git a/src/ai/backend/manager/api/rest/adapter.py b/src/ai/backend/manager/api/rest/adapter.py index 6bcccc9cbf0..40c630fa3fa 100644 --- a/src/ai/backend/manager/api/rest/adapter.py +++ b/src/ai/backend/manager/api/rest/adapter.py @@ -14,8 +14,17 @@ UUIDEqualMatchSpec, UUIDInMatchSpec, ) -from ai.backend.common.dto.manager.query import IntFilter, StringFilter, UUIDFilter -from ai.backend.manager.repositories.base import QueryCondition +from ai.backend.common.dto.manager.query import ( + IntFilter, + StringFilter, + UUIDFilter, +) +from ai.backend.manager.repositories.base import ( + QueryCondition, + combine_conditions_and, + combine_conditions_or, + negate_conditions, +) class BaseFilterAdapter: @@ -202,3 +211,49 @@ def convert_int_filter( less_than_factory=int_conditions.lt, less_than_or_equal_factory=int_conditions.lte, ) + + @final + def convert_and( + self, + sub_condition_lists: list[list[QueryCondition]], + ) -> list[QueryCondition]: + """Flatten sub-filter conditions for ``AND`` combination. + + Callers ``extend()`` the result into the parent's conditions list. + """ + conditions: list[QueryCondition] = [] + for sub_conditions in sub_condition_lists: + conditions.extend(sub_conditions) + return conditions + + @final + def convert_or( + self, + sub_condition_lists: list[list[QueryCondition]], + ) -> list[QueryCondition]: + """OR-combine sub-filters, preserving each sub's internal AND grouping (``(A AND B) OR (C AND D)``). + + Callers ``extend()`` the result into the parent's conditions list. + """ + or_groups: list[QueryCondition] = [] + for sub_conditions in sub_condition_lists: + if sub_conditions: + or_groups.append(combine_conditions_and(sub_conditions)) + if not or_groups: + return [] + return [combine_conditions_or(or_groups)] + + @final + def convert_not( + self, + sub_condition_lists: list[list[QueryCondition]], + ) -> list[QueryCondition]: + """Negate each sub-filter independently (``NOT (A AND B) AND NOT (C AND D)``). + + Callers ``extend()`` the result into the parent's conditions list. + """ + conditions: list[QueryCondition] = [] + for sub_conditions in sub_condition_lists: + if sub_conditions: + conditions.append(negate_conditions(sub_conditions)) + return conditions diff --git a/src/ai/backend/manager/repositories/base/__init__.py b/src/ai/backend/manager/repositories/base/__init__.py index 118e6626700..372712a218f 100644 --- a/src/ai/backend/manager/repositories/base/__init__.py +++ b/src/ai/backend/manager/repositories/base/__init__.py @@ -87,6 +87,7 @@ execute_upserter, ) from .utils import ( + combine_conditions_and, combine_conditions_or, negate_conditions, ) @@ -174,6 +175,7 @@ "BatchPurgerResult", "execute_batch_purger", # Utils + "combine_conditions_and", "combine_conditions_or", "negate_conditions", ] diff --git a/src/ai/backend/manager/repositories/base/utils.py b/src/ai/backend/manager/repositories/base/utils.py index 6df8b43facf..1e0d4e797ed 100644 --- a/src/ai/backend/manager/repositories/base/utils.py +++ b/src/ai/backend/manager/repositories/base/utils.py @@ -24,6 +24,23 @@ def inner() -> sa.sql.expression.ColumnElement[bool]: return inner +def combine_conditions_and(conditions: list[QueryCondition]) -> QueryCondition: + """Combine multiple QueryConditions with AND logic. + + Args: + conditions: List of QueryCondition callables to combine + + Returns: + A single QueryCondition that applies all conditions with AND logic + """ + + def inner() -> sa.sql.expression.ColumnElement[bool]: + clauses = [cond() for cond in conditions] + return sa.and_(*clauses) + + return inner + + def negate_conditions(conditions: list[QueryCondition]) -> QueryCondition: """Negate multiple QueryConditions with NOT logic. diff --git a/tests/component/deployment/test_deployment.py b/tests/component/deployment/test_deployment.py index cb80f39e0c8..7844c8d8d79 100644 --- a/tests/component/deployment/test_deployment.py +++ b/tests/component/deployment/test_deployment.py @@ -9,14 +9,18 @@ import secrets import uuid +from typing import TYPE_CHECKING +from unittest.mock import MagicMock import pytest from ai.backend.client.v2.exceptions import NotFoundError from ai.backend.client.v2.registry import BackendAIClientRegistry from ai.backend.common.config import ModelDefinitionDraft +from ai.backend.common.contexts.user import with_user from ai.backend.common.data.endpoint.types import EndpointLifecycle from ai.backend.common.data.model_deployment.types import DeploymentStrategy, ModelDeploymentStatus +from ai.backend.common.data.user.types import UserData, UserRole from ai.backend.common.dto.manager.deployment import ( CreateDeploymentRequest, DeactivateRevisionResponse, @@ -38,10 +42,22 @@ ) from ai.backend.common.dto.manager.deployment.request import ClusterConfigInput from ai.backend.common.dto.manager.query import StringFilter +from ai.backend.common.dto.manager.v2.deployment.request import ( + AdminSearchDeploymentsInput, +) +from ai.backend.common.dto.manager.v2.deployment.request import ( + DeploymentFilter as DeploymentFilterV2, +) from ai.backend.common.identifier.image import ImageID from ai.backend.common.identifier.vfolder import VFolderUUID from ai.backend.common.types import ClusterMode +from ai.backend.manager.api.adapters.deployment.adapter import DeploymentAdapter +from ai.backend.manager.services.deployment.processors import DeploymentProcessors from ai.backend.manager.services.deployment.service import _map_lifecycle_to_status +from ai.backend.manager.services.processors import Processors + +if TYPE_CHECKING: + from tests.component.conftest import UserFixtureData class TestSearchDeployments: @@ -315,6 +331,297 @@ async def test_deactivate_revision_stub( assert result.success is True +class TestDeploymentAdapterFilter: + """Verify the GQL adapter honors AND/OR/NOT in DeploymentFilter. + + The adapter's ``my_search`` and ``project_search`` previously inlined + the filter conversion and silently dropped nested ``AND``/``OR``/``NOT`` + clauses, so multi-condition filters degenerated into "no filter at all". + These tests pin the corrected behavior. + """ + + @pytest.fixture + def deployment_adapter( + self, + deployment_processors: DeploymentProcessors, + ) -> DeploymentAdapter: + processors_mock = MagicMock(spec=Processors) + processors_mock.deployment = deployment_processors + return DeploymentAdapter(processors_mock, deployment_coordinator=MagicMock()) + + @staticmethod + def _admin_user_data(user_uuid: uuid.UUID, domain: str) -> UserData: + return UserData( + user_id=user_uuid, + is_authorized=True, + is_admin=True, + is_superadmin=True, + role=UserRole.SUPERADMIN, + domain_name=domain, + ) + + @staticmethod + async def _create_deployment_with_tags( + admin_registry: BackendAIClientRegistry, + project_id: uuid.UUID, + domain: str, + scaling_group: str, + seed_data: tuple[ImageID, VFolderUUID], + tags: list[str], + name: str | None = None, + ) -> uuid.UUID: + image_id, vfolder_id = seed_data + request = CreateDeploymentRequest( + metadata=DeploymentMetadataInput( + project_id=project_id, + domain_name=domain, + name=name or f"test-deployment-{secrets.token_hex(4)}", + tags=tags, + ), + network_access=NetworkAccessInput(open_to_public=False), + default_deployment_strategy=DeploymentStrategyInput( + type=DeploymentStrategy.ROLLING, + ), + replica_count=1, + initial_revision=RevisionInput( + name="v1", + cluster_config=ClusterConfigInput(mode=ClusterMode.SINGLE_NODE, size=1), + resource_config=ResourceConfigInput( + resource_group=scaling_group, + resource_slots={"cpu": "2", "mem": "2147483648"}, + ), + image=ImageInput(id=image_id), + model_runtime_config=ModelRuntimeConfigInput(), + model_mount_config=ModelMountConfigInput( + vfolder_id=vfolder_id, + mount_destination="/models", + definition_path="model-definition.yaml", + ), + model_definition=ModelDefinitionDraft(), + ), + ) + response = await admin_registry.deployment.create_deployment(request) + return response.deployment.id + + async def test_my_search_and_filter_returns_intersection( + self, + admin_registry: BackendAIClientRegistry, + admin_user_fixture: UserFixtureData, + deployment_adapter: DeploymentAdapter, + group_fixture: uuid.UUID, + domain_fixture: str, + scaling_group_fixture: str, + deployment_seed_data: tuple[ImageID, VFolderUUID], + ) -> None: + """AND clause must narrow results to deployments matching every nested filter.""" + await self._create_deployment_with_tags( + admin_registry, + group_fixture, + domain_fixture, + scaling_group_fixture, + deployment_seed_data, + ["alpha", "production"], + ) + await self._create_deployment_with_tags( + admin_registry, + group_fixture, + domain_fixture, + scaling_group_fixture, + deployment_seed_data, + ["beta", "production"], + ) + target_id = await self._create_deployment_with_tags( + admin_registry, + group_fixture, + domain_fixture, + scaling_group_fixture, + deployment_seed_data, + ["alpha", "beta"], + ) + + filter_input = DeploymentFilterV2( + AND=[ + DeploymentFilterV2(tags=StringFilter(i_contains="alpha")), + DeploymentFilterV2(tags=StringFilter(i_contains="beta")), + ], + ) + with with_user(self._admin_user_data(admin_user_fixture.user_uuid, domain_fixture)): + payload = await deployment_adapter.my_search( + AdminSearchDeploymentsInput(filter=filter_input, limit=50), + ) + + assert payload.total_count == 1 + assert [item.id for item in payload.items] == [target_id] + + async def test_my_search_or_filter_returns_union( + self, + admin_registry: BackendAIClientRegistry, + admin_user_fixture: UserFixtureData, + deployment_adapter: DeploymentAdapter, + group_fixture: uuid.UUID, + domain_fixture: str, + scaling_group_fixture: str, + deployment_seed_data: tuple[ImageID, VFolderUUID], + ) -> None: + """OR clause must widen results to deployments matching any nested filter.""" + alpha_id = await self._create_deployment_with_tags( + admin_registry, + group_fixture, + domain_fixture, + scaling_group_fixture, + deployment_seed_data, + ["alpha"], + ) + beta_id = await self._create_deployment_with_tags( + admin_registry, + group_fixture, + domain_fixture, + scaling_group_fixture, + deployment_seed_data, + ["beta"], + ) + await self._create_deployment_with_tags( + admin_registry, + group_fixture, + domain_fixture, + scaling_group_fixture, + deployment_seed_data, + ["gamma"], + ) + + filter_input = DeploymentFilterV2( + OR=[ + DeploymentFilterV2(tags=StringFilter(i_contains="alpha")), + DeploymentFilterV2(tags=StringFilter(i_contains="beta")), + ], + ) + with with_user(self._admin_user_data(admin_user_fixture.user_uuid, domain_fixture)): + payload = await deployment_adapter.my_search( + AdminSearchDeploymentsInput(filter=filter_input, limit=50), + ) + + assert payload.total_count == 2 + assert {item.id for item in payload.items} == {alpha_id, beta_id} + + async def test_my_search_or_filter_groups_multi_field_subfilters( + self, + admin_registry: BackendAIClientRegistry, + admin_user_fixture: UserFixtureData, + deployment_adapter: DeploymentAdapter, + group_fixture: uuid.UUID, + domain_fixture: str, + scaling_group_fixture: str, + deployment_seed_data: tuple[ImageID, VFolderUUID], + ) -> None: + """OR with multi-field sub-filters must AND fields within each branch. + + Pins ``(A AND B) OR (C AND D)`` semantics: each OR sub-filter is an AND + of its own fields, then the sub-filters are OR'd. A regression that + flattens sub-conditions would degenerate into ``A OR B OR C OR D`` and + return rows that match neither full branch. + """ + suffix = secrets.token_hex(4) + branch1_id = await self._create_deployment_with_tags( + admin_registry, + group_fixture, + domain_fixture, + scaling_group_fixture, + deployment_seed_data, + ["alpha"], + name=f"bar-x-{suffix}", + ) + branch2_id = await self._create_deployment_with_tags( + admin_registry, + group_fixture, + domain_fixture, + scaling_group_fixture, + deployment_seed_data, + ["beta"], + name=f"foo-x-{suffix}", + ) + await self._create_deployment_with_tags( + admin_registry, + group_fixture, + domain_fixture, + scaling_group_fixture, + deployment_seed_data, + ["beta"], + name=f"bar-y-{suffix}", + ) + await self._create_deployment_with_tags( + admin_registry, + group_fixture, + domain_fixture, + scaling_group_fixture, + deployment_seed_data, + ["alpha"], + name=f"foo-y-{suffix}", + ) + + filter_input = DeploymentFilterV2( + OR=[ + DeploymentFilterV2( + name=StringFilter(i_contains="bar"), + tags=StringFilter(i_contains="alpha"), + ), + DeploymentFilterV2( + name=StringFilter(i_contains="foo"), + tags=StringFilter(i_contains="beta"), + ), + ], + ) + with with_user(self._admin_user_data(admin_user_fixture.user_uuid, domain_fixture)): + payload = await deployment_adapter.my_search( + AdminSearchDeploymentsInput(filter=filter_input, limit=50), + ) + + assert payload.total_count == 2 + assert {item.id for item in payload.items} == {branch1_id, branch2_id} + + async def test_project_search_and_filter_returns_intersection( + self, + admin_registry: BackendAIClientRegistry, + admin_user_fixture: UserFixtureData, + deployment_adapter: DeploymentAdapter, + group_fixture: uuid.UUID, + domain_fixture: str, + scaling_group_fixture: str, + deployment_seed_data: tuple[ImageID, VFolderUUID], + ) -> None: + """project_search must also honor AND across nested filters.""" + await self._create_deployment_with_tags( + admin_registry, + group_fixture, + domain_fixture, + scaling_group_fixture, + deployment_seed_data, + ["alpha"], + ) + target_id = await self._create_deployment_with_tags( + admin_registry, + group_fixture, + domain_fixture, + scaling_group_fixture, + deployment_seed_data, + ["alpha", "beta"], + ) + + filter_input = DeploymentFilterV2( + AND=[ + DeploymentFilterV2(tags=StringFilter(i_contains="alpha")), + DeploymentFilterV2(tags=StringFilter(i_contains="beta")), + ], + ) + with with_user(self._admin_user_data(admin_user_fixture.user_uuid, domain_fixture)): + payload = await deployment_adapter.project_search( + group_fixture, + AdminSearchDeploymentsInput(filter=filter_input, limit=50), + ) + + assert payload.total_count == 1 + assert [item.id for item in payload.items] == [target_id] + + class TestStatusMapping: def test_lifecycle_to_status_mapping(self) -> None: """Verify EndpointLifecycle maps correctly to ModelDeploymentStatus. diff --git a/tests/unit/manager/api/adapters/test_filter_helper.py b/tests/unit/manager/api/adapters/test_filter_helper.py new file mode 100644 index 00000000000..b17259eeb4f --- /dev/null +++ b/tests/unit/manager/api/adapters/test_filter_helper.py @@ -0,0 +1,107 @@ +"""Unit tests for ``BaseFilterAdapter.convert_and / convert_or / convert_not``. + +Each helper takes pre-converted ``list[list[QueryCondition]]`` so adapters +keep ownership of per-field conversion. The tests exercise the boolean-clause +grouping (notably the ``(A AND B) OR (C AND D)`` shape) by feeding sentinel +``QueryCondition`` callables and compiling the produced SQL — a regression +that flattens a sub-filter group would surface here as a different SQL tree. +""" + +from __future__ import annotations + +from typing import Any + +import sqlalchemy as sa + +from ai.backend.manager.api.rest.adapter import BaseFilterAdapter +from ai.backend.manager.repositories.base import QueryCondition + + +def _const(label: str) -> QueryCondition: + """A ``QueryCondition`` whose compiled SQL is the literal ``'label'``.""" + + def _inner() -> Any: + return sa.literal_column(f"'{label}'") + + return _inner + + +def _compile(qc: QueryCondition) -> str: + return str(qc().compile(compile_kwargs={"literal_binds": True})) + + +class _Adapter(BaseFilterAdapter): + pass + + +class TestConvertAnd: + def test_empty_input_returns_empty(self) -> None: + assert _Adapter().convert_and([]) == [] + + def test_single_sub_with_single_condition_passes_through(self) -> None: + result = _Adapter().convert_and([[_const("A")]]) + assert [_compile(c) for c in result] == ["'A'"] + + def test_multiple_subs_flatten_in_order(self) -> None: + """AND sub-conditions append individually; ``BatchQuerier`` AND-combines them downstream.""" + result = _Adapter().convert_and([[_const("A"), _const("B")], [_const("C")]]) + assert [_compile(c) for c in result] == ["'A'", "'B'", "'C'"] + + def test_empty_sub_lists_are_skipped_implicitly(self) -> None: + result = _Adapter().convert_and([[], [_const("A")], []]) + assert [_compile(c) for c in result] == ["'A'"] + + +class TestConvertOr: + def test_empty_input_returns_empty(self) -> None: + assert _Adapter().convert_or([]) == [] + + def test_all_empty_sub_lists_returns_empty(self) -> None: + assert _Adapter().convert_or([[], [], []]) == [] + + def test_single_sub_with_single_condition_yields_a_one_term_or(self) -> None: + """A single non-empty sub still goes through the AND-then-OR pipeline.""" + result = _Adapter().convert_or([[_const("A")]]) + assert len(result) == 1 + assert _compile(result[0]) == "'A'" + + def test_multi_field_subs_preserve_internal_and_grouping(self) -> None: + """``[(A, B), (C, D)]`` must compile to ``A AND B OR C AND D`` (AND binds tighter than OR). + + Regression guard for BA-5975: a flat OR over all sub-conditions would + compile to ``A OR B OR C OR D`` and silently widen the result set. + SQLAlchemy elides redundant parentheses, so the textual form has none — + but the operator counts pin the structure. + """ + result = _Adapter().convert_or([ + [_const("A"), _const("B")], + [_const("C"), _const("D")], + ]) + assert len(result) == 1 + sql = _compile(result[0]) + assert sql == "'A' AND 'B' OR 'C' AND 'D'" + # The bug would drop both AND operators in favor of a flat OR. + assert sql.count(" AND ") == 2 + assert sql.count(" OR ") == 1 + + def test_empty_sub_lists_are_dropped_from_grouping(self) -> None: + result = _Adapter().convert_or([[], [_const("A"), _const("B")], []]) + assert len(result) == 1 + assert _compile(result[0]) == "'A' AND 'B'" + + +class TestConvertNot: + def test_empty_input_returns_empty(self) -> None: + assert _Adapter().convert_not([]) == [] + + def test_each_sub_is_negated_independently(self) -> None: + """``NOT [{A, B}, {C}]`` produces ``NOT (A AND B)`` and ``NOT C`` as separate clauses.""" + result = _Adapter().convert_not([[_const("A"), _const("B")], [_const("C")]]) + assert len(result) == 2 + assert _compile(result[0]) == "NOT ('A' AND 'B')" + assert _compile(result[1]) == "NOT 'C'" + + def test_empty_sub_lists_are_skipped(self) -> None: + result = _Adapter().convert_not([[], [_const("A")], []]) + assert len(result) == 1 + assert _compile(result[0]) == "NOT 'A'"