Repository: aneoconsulting/ArmoniK.Api (language: rust)

Issue:
Error: Cannot make a conjunction of disjunctions
Hello,

I'm trying to filter tasks using Python SDK. Thus, I want to get from ArmoniK only specifics status within a specific range, so I do need to use multiple nested `and` and `or` operands.

According to Filter class:
```py
    def __and__(self, other: "Filter") -> "Filter":
        # [...]
        if self.is_true_disjunction() or other.is_true_disjunction():
            raise Exception("Cannot make a conjunction of disjunctions")
```

It looks like the filter doesn't support this basic logical expression: `(a|b)&c`
Is there another way of doing this kind of filtering?


Discussion / hints:
Hi !
First of all thanks for this report, it's indeed a limitation of the current implementation.
If you want to achieve (a|b)&c (conjunction of disjunction) , currently you need to do (a&c)|(b&c) (disjunction of conjunction)

Best regards!
Hi @dbrasseur-aneo,

I recently had an issue with a workaround I used to address this lack. Meanwhile I saw that you opened a branch for this fix, is there any delivery date currently scheduled?

Thanks
Hi ! No delivery date yet, but it should be done after my vacations. 

Fix patch:
diff --git a/packages/python/proto2python.sh b/packages/python/proto2python.sh
index 9b2aa93cf..bc00d0068 100755
--- a/packages/python/proto2python.sh
+++ b/packages/python/proto2python.sh
@@ -32,7 +32,7 @@ mkdir -p $ARMONIK_WORKER $ARMONIK_CLIENT $ARMONIK_COMMON $PACKAGE_PATH
 python -m venv $PYTHON_VENV
 source $PYTHON_VENV/bin/activate
 python -m pip install uv
-python -m uv pip install build grpcio grpcio-tools setuptools_scm[toml]
+python -m uv pip install build "grpcio==1.62.3" "grpcio-tools==1.62.3" setuptools_scm[toml] "setuptools>=61" deprecation
 
 unset proto_files
 for proto in ${armonik_worker_files[@]}; do
@@ -61,6 +61,7 @@ python -m grpc_tools.protoc -I $PROTO_PATH --proto_path=$PROTO_PATH \
 touch $ARMONIK_WORKER/__init__.py
 touch $ARMONIK_CLIENT/__init__.py
 touch $ARMONIK_COMMON/__init__.py
+touch $GENERATED_PATH/__init__.py
 
 # Need to fix the relative import
 python fix_imports.py $GENERATED_PATH
diff --git a/packages/python/pyproject.toml b/packages/python/pyproject.toml
index 437dde4b0..d860a9202 100644
--- a/packages/python/pyproject.toml
+++ b/packages/python/pyproject.toml
@@ -15,8 +15,8 @@ classifiers = [
     "Programming Language :: Python :: 3",
 ]
 dependencies = [
-    "grpcio",
-    "grpcio-tools",
+    "grpcio==1.62.3",
+    "grpcio-tools==1.62.3",
     "deprecation"
 ]
 [project.urls]
diff --git a/packages/python/src/armonik/client/events.py b/packages/python/src/armonik/client/events.py
index 5cc8c6ba7..bb7407393 100644
--- a/packages/python/src/armonik/client/events.py
+++ b/packages/python/src/armonik/client/events.py
@@ -5,7 +5,6 @@
 from .results import ArmoniKResults
 from ..common import (
     EventTypes,
-    Filter,
     NewTaskEvent,
     NewResultEvent,
     ResultOwnerUpdateEvent,
@@ -15,6 +14,7 @@
     Event,
 )
 from .results import ResultFieldFilter
+from ..common.filter import Filter
 from ..protogen.client.events_service_pb2_grpc import EventsStub
 from ..protogen.common.events_common_pb2 import EventSubscriptionRequest
 from ..protogen.common.results_filters_pb2 import Filters as rawResultFilters
diff --git a/packages/python/src/armonik/client/partitions.py b/packages/python/src/armonik/client/partitions.py
index a80d65eb5..f40857124 100644
--- a/packages/python/src/armonik/client/partitions.py
+++ b/packages/python/src/armonik/client/partitions.py
@@ -1,9 +1,11 @@
 from typing import List, Tuple, cast, Optional
 
+import deprecation
 from grpc import Channel
 
+from .. import __version__
 from ..common import Direction, Partition
-from ..common.filter import Filter, NumberFilter
+from ..common.filter import Filter, PartitionFilter
 from ..protogen.client.partitions_service_pb2_grpc import PartitionsStub
 from ..protogen.common.partitions_common_pb2 import (
     GetPartitionRequest,
@@ -11,31 +13,14 @@
     ListPartitionsResponse,
 )
 from ..protogen.common.partitions_fields_pb2 import (
-    PARTITION_RAW_ENUM_FIELD_PRIORITY,
     PartitionField,
-    PartitionRawField,
-)
-from ..protogen.common.partitions_filters_pb2 import (
-    FilterField as rawFilterField,
-)
-from ..protogen.common.partitions_filters_pb2 import (
-    Filters as rawFilters,
-)
-from ..protogen.common.partitions_filters_pb2 import (
-    FiltersAnd as rawFiltersAnd,
 )
 from ..protogen.common.sort_direction_pb2 import SortDirection
 
 
+@deprecation.deprecated("3.19.0", None, __version__, "Use Partition.<name of the field> instead")
 class PartitionFieldFilter:
-    PRIORITY = NumberFilter(
-        PartitionField(
-            partition_raw_field=PartitionRawField(field=PARTITION_RAW_ENUM_FIELD_PRIORITY)
-        ),
-        rawFilters,
-        rawFiltersAnd,
-        rawFilterField,
-    )
+    PRIORITY = Partition.priority
 
 
 class ArmoniKPartitions:
@@ -52,7 +37,7 @@ def list_partitions(
         partition_filter: Optional[Filter] = None,
         page: int = 0,
         page_size: int = 1000,
-        sort_field: Filter = PartitionFieldFilter.PRIORITY,
+        sort_field: Filter = Partition.priority,
         sort_direction: SortDirection = Direction.ASC,
     ) -> Tuple[int, List[Partition]]:
         """List partitions based on a filter.
@@ -72,9 +57,11 @@ def list_partitions(
         request = ListPartitionsRequest(
             page=page,
             page_size=page_size,
-            filters=cast(rawFilters, partition_filter.to_disjunction().to_message())
-            if partition_filter
-            else rawFilters(),
+            filters=(
+                PartitionFilter().to_message()
+                if partition_filter is None
+                else partition_filter.to_message()
+            ),
             sort=ListPartitionsRequest.Sort(
                 field=cast(PartitionField, sort_field.field), direction=sort_direction
             ),
diff --git a/packages/python/src/armonik/client/results.py b/packages/python/src/armonik/client/results.py
index a2da2526d..788b0e8e7 100644
--- a/packages/python/src/armonik/client/results.py
+++ b/packages/python/src/armonik/client/results.py
@@ -5,8 +5,9 @@
 from deprecation import deprecated
 from grpc import Channel
 
+from .. import __version__
 from ..common import Direction, Result
-from ..common.filter import Filter, StatusFilter, StringFilter
+from ..common.filter import Filter, ResultFilter
 from ..common.helpers import batched
 from ..protogen.client.results_service_pb2_grpc import ResultsStub
 from ..protogen.common.objects_pb2 import Empty
@@ -27,40 +28,15 @@
     UploadResultDataRequest,
 )
 from ..protogen.common.results_fields_pb2 import (
-    RESULT_RAW_ENUM_FIELD_RESULT_ID,
-    RESULT_RAW_ENUM_FIELD_STATUS,
     ResultField,
-    ResultRawField,
-)
-from ..protogen.common.results_filters_pb2 import (
-    FilterField as rawFilterField,
-)
-from ..protogen.common.results_filters_pb2 import (
-    Filters as rawFilters,
-)
-from ..protogen.common.results_filters_pb2 import (
-    FiltersAnd as rawFilterAnd,
-)
-from ..protogen.common.results_filters_pb2 import (
-    FilterStatus as rawFilterStatus,
 )
 from ..protogen.common.sort_direction_pb2 import SortDirection
 
 
+@deprecated("3.19.0", None, __version__, "Use Result.<name of the field> instead")
 class ResultFieldFilter:
-    STATUS = StatusFilter(
-        ResultField(result_raw_field=ResultRawField(field=RESULT_RAW_ENUM_FIELD_STATUS)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-        rawFilterStatus,
-    )
-    RESULT_ID = StringFilter(
-        ResultField(result_raw_field=ResultRawField(field=RESULT_RAW_ENUM_FIELD_RESULT_ID)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
+    STATUS = Result.status
+    RESULT_ID = Result.result_id
 
 
 class ArmoniKResults:
@@ -95,7 +71,7 @@ def list_results(
         result_filter: Optional[Filter] = None,
         page: int = 0,
         page_size: int = 1000,
-        sort_field: Filter = ResultFieldFilter.STATUS,
+        sort_field: Filter = Result.status,
         sort_direction: SortDirection = Direction.ASC,
     ) -> Tuple[int, List[Result]]:
         """List results based on a filter.
@@ -114,9 +90,9 @@ def list_results(
         request: ListResultsRequest = ListResultsRequest(
             page=page,
             page_size=page_size,
-            filters=cast(rawFilters, result_filter.to_disjunction().to_message())
-            if result_filter
-            else rawFilters(),
+            filters=(
+                ResultFilter().to_message() if result_filter is None else result_filter.to_message()
+            ),
             sort=ListResultsRequest.Sort(
                 field=cast(ResultField, sort_field.field), direction=sort_direction
             ),
diff --git a/packages/python/src/armonik/client/sessions.py b/packages/python/src/armonik/client/sessions.py
index 8d170717a..7138c1f8e 100644
--- a/packages/python/src/armonik/client/sessions.py
+++ b/packages/python/src/armonik/client/sessions.py
@@ -2,10 +2,12 @@
 
 from typing import List, Optional, Tuple, cast
 
+from deprecation import deprecated
 from grpc import Channel
 
+from .. import __version__
 from ..common import Direction, Session, TaskOptions
-from ..common.filter import Filter, StatusFilter, StringFilter
+from ..common.filter import Filter, StringFilter, SessionFilter
 from ..protogen.client.sessions_service_pb2_grpc import SessionsStub
 from ..protogen.common.sessions_common_pb2 import (
     CancelSessionRequest,
@@ -29,38 +31,18 @@
     ListSessionsResponse,
 )
 from ..protogen.common.sessions_fields_pb2 import (
-    SESSION_RAW_ENUM_FIELD_STATUS,
     SessionField,
-    SessionRawField,
-    TaskOptionGenericField,
-)
-from ..protogen.common.sessions_filters_pb2 import (
-    FilterField as rawFilterField,
-)
-from ..protogen.common.sessions_filters_pb2 import (
-    Filters as rawFilters,
-)
-from ..protogen.common.sessions_filters_pb2 import (
-    FiltersAnd as rawFilterAnd,
-)
-from ..protogen.common.sessions_filters_pb2 import (
-    FilterStatus as rawFilterStatus,
 )
 from ..protogen.common.sort_direction_pb2 import SortDirection
 
 
+@deprecated("3.19.0", None, __version__, "Use Session.<name of the field> instead")
 class SessionFieldFilter:
     """
     Enumeration of the available filters
     """
 
-    STATUS = StatusFilter(
-        SessionField(session_raw_field=SessionRawField(field=SESSION_RAW_ENUM_FIELD_STATUS)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-        rawFilterStatus,
-    )
+    STATUS = Session.status
 
     @staticmethod
     def task_options_key(option_key: str) -> StringFilter:
@@ -72,12 +54,7 @@ def task_options_key(option_key: str) -> StringFilter:
         Returns:
             Corresponding filter
         """
-        return StringFilter(
-            SessionField(task_option_generic_field=TaskOptionGenericField(field=option_key)),
-            rawFilters,
-            rawFilterAnd,
-            rawFilterField,
-        )
+        return Session.options[option_key]
 
 
 class ArmoniKSessions:
@@ -130,7 +107,7 @@ def list_sessions(
         session_filter: Optional[Filter] = None,
         page: int = 0,
         page_size: int = 1000,
-        sort_field: Filter = SessionFieldFilter.STATUS,
+        sort_field: Filter = Session.status,
         sort_direction: SortDirection = Direction.ASC,
     ) -> Tuple[int, List[Session]]:
         """
@@ -151,9 +128,11 @@ def list_sessions(
         request = ListSessionsRequest(
             page=page,
             page_size=page_size,
-            filters=cast(rawFilters, session_filter.to_disjunction().to_message())
-            if session_filter
-            else rawFilters(),
+            filters=(
+                SessionFilter().to_message()
+                if session_filter is None
+                else session_filter.to_message()
+            ),
             sort=ListSessionsRequest.Sort(
                 field=cast(SessionField, sort_field.field), direction=sort_direction
             ),
diff --git a/packages/python/src/armonik/client/tasks.py b/packages/python/src/armonik/client/tasks.py
index e945d8f38..647e99fb0 100644
--- a/packages/python/src/armonik/client/tasks.py
+++ b/packages/python/src/armonik/client/tasks.py
@@ -1,18 +1,13 @@
 from __future__ import annotations
 
-from typing import Dict, List, Optional, Tuple, cast
+from typing import Dict, List, Optional, Tuple
 
+from deprecation import deprecated
 from grpc import Channel
 
+from .. import __version__
 from ..common import Direction, Task, TaskDefinition, TaskOptions, TaskStatus
-from ..common.filter import (
-    DateFilter,
-    DurationFilter,
-    Filter,
-    NumberFilter,
-    StatusFilter,
-    StringFilter,
-)
+from ..common.filter import Filter, StringFilter, TaskFilter
 from ..common.helpers import batched
 from ..protogen.client.tasks_service_pb2_grpc import TasksStub
 from ..protogen.common.sort_direction_pb2 import SortDirection
@@ -29,224 +24,40 @@
     ListTasksResponse,
     SubmitTasksRequest,
 )
-from ..protogen.common.tasks_fields_pb2 import (
-    TASK_OPTION_ENUM_FIELD_APPLICATION_NAME,
-    TASK_OPTION_ENUM_FIELD_APPLICATION_NAMESPACE,
-    TASK_OPTION_ENUM_FIELD_APPLICATION_SERVICE,
-    TASK_OPTION_ENUM_FIELD_APPLICATION_VERSION,
-    TASK_OPTION_ENUM_FIELD_ENGINE_TYPE,
-    TASK_OPTION_ENUM_FIELD_MAX_DURATION,
-    TASK_OPTION_ENUM_FIELD_MAX_RETRIES,
-    TASK_OPTION_ENUM_FIELD_PARTITION_ID,
-    TASK_OPTION_ENUM_FIELD_PRIORITY,
-    TASK_SUMMARY_ENUM_FIELD_ACQUIRED_AT,
-    TASK_SUMMARY_ENUM_FIELD_CREATED_AT,
-    TASK_SUMMARY_ENUM_FIELD_CREATION_TO_END_DURATION,
-    TASK_SUMMARY_ENUM_FIELD_ENDED_AT,
-    TASK_SUMMARY_ENUM_FIELD_ERROR,
-    TASK_SUMMARY_ENUM_FIELD_INITIAL_TASK_ID,
-    TASK_SUMMARY_ENUM_FIELD_OWNER_POD_ID,
-    TASK_SUMMARY_ENUM_FIELD_POD_HOSTNAME,
-    TASK_SUMMARY_ENUM_FIELD_POD_TTL,
-    TASK_SUMMARY_ENUM_FIELD_PROCESSING_TO_END_DURATION,
-    TASK_SUMMARY_ENUM_FIELD_RECEIVED_AT,
-    TASK_SUMMARY_ENUM_FIELD_SESSION_ID,
-    TASK_SUMMARY_ENUM_FIELD_STARTED_AT,
-    TASK_SUMMARY_ENUM_FIELD_STATUS,
-    TASK_SUMMARY_ENUM_FIELD_SUBMITTED_AT,
-    TASK_SUMMARY_ENUM_FIELD_TASK_ID,
-    TaskField,
-    TaskOptionField,
-    TaskOptionGenericField,
-    TaskSummaryField,
-)
-from ..protogen.common.tasks_filters_pb2 import (
-    FilterField as rawFilterField,
-)
-from ..protogen.common.tasks_filters_pb2 import (
-    Filters as rawFilters,
-)
-from ..protogen.common.tasks_filters_pb2 import (
-    FiltersAnd as rawFilterAnd,
-)
-from ..protogen.common.tasks_filters_pb2 import (
-    FilterStatus as rawFilterStatus,
-)
 
 
+@deprecated("3.19.0", None, __version__, "Use Task.<name of the field> instead")
 class TaskFieldFilter:
     """
     Enumeration of the available filters
     """
 
-    TASK_ID = StringFilter(
-        TaskField(task_summary_field=TaskSummaryField(field=TASK_SUMMARY_ENUM_FIELD_TASK_ID)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    SESSION_ID = StringFilter(
-        TaskField(task_summary_field=TaskSummaryField(field=TASK_SUMMARY_ENUM_FIELD_SESSION_ID)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    OWNER_POD_ID = StringFilter(
-        TaskField(task_summary_field=TaskSummaryField(field=TASK_SUMMARY_ENUM_FIELD_OWNER_POD_ID)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    INITIAL_TASK_ID = StringFilter(
-        TaskField(
-            task_summary_field=TaskSummaryField(field=TASK_SUMMARY_ENUM_FIELD_INITIAL_TASK_ID)
-        ),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    STATUS = StatusFilter(
-        TaskField(task_summary_field=TaskSummaryField(field=TASK_SUMMARY_ENUM_FIELD_STATUS)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-        rawFilterStatus,
-    )
-    CREATED_AT = DateFilter(
-        TaskField(task_summary_field=TaskSummaryField(field=TASK_SUMMARY_ENUM_FIELD_CREATED_AT)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    SUBMITTED_AT = DateFilter(
-        TaskField(task_summary_field=TaskSummaryField(field=TASK_SUMMARY_ENUM_FIELD_SUBMITTED_AT)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    STARTED_AT = DateFilter(
-        TaskField(task_summary_field=TaskSummaryField(field=TASK_SUMMARY_ENUM_FIELD_STARTED_AT)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    ENDED_AT = DateFilter(
-        TaskField(task_summary_field=TaskSummaryField(field=TASK_SUMMARY_ENUM_FIELD_ENDED_AT)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    CREATION_TO_END_DURATION = DurationFilter(
-        TaskField(
-            task_summary_field=TaskSummaryField(
-                field=TASK_SUMMARY_ENUM_FIELD_CREATION_TO_END_DURATION
-            )
-        ),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    PROCESSING_TO_END_DURATION = DurationFilter(
-        TaskField(
-            task_summary_field=TaskSummaryField(
-                field=TASK_SUMMARY_ENUM_FIELD_PROCESSING_TO_END_DURATION
-            )
-        ),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    POD_TTL = DateFilter(
-        TaskField(task_summary_field=TaskSummaryField(field=TASK_SUMMARY_ENUM_FIELD_POD_TTL)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    POD_HOSTNAME = StringFilter(
-        TaskField(task_summary_field=TaskSummaryField(field=TASK_SUMMARY_ENUM_FIELD_POD_HOSTNAME)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    RECEIVED_AT = DateFilter(
-        TaskField(task_summary_field=TaskSummaryField(field=TASK_SUMMARY_ENUM_FIELD_RECEIVED_AT)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    ACQUIRED_AT = DateFilter(
-        TaskField(task_summary_field=TaskSummaryField(field=TASK_SUMMARY_ENUM_FIELD_ACQUIRED_AT)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    ERROR = StringFilter(
-        TaskField(task_summary_field=TaskSummaryField(field=TASK_SUMMARY_ENUM_FIELD_ERROR)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-
-    MAX_DURATION = DurationFilter(
-        TaskField(task_option_field=TaskOptionField(field=TASK_OPTION_ENUM_FIELD_MAX_DURATION)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    MAX_RETRIES = NumberFilter(
-        TaskField(task_option_field=TaskOptionField(field=TASK_OPTION_ENUM_FIELD_MAX_RETRIES)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    PRIORITY = NumberFilter(
-        TaskField(task_option_field=TaskOptionField(field=TASK_OPTION_ENUM_FIELD_PRIORITY)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    PARTITION_ID = StringFilter(
-        TaskField(task_option_field=TaskOptionField(field=TASK_OPTION_ENUM_FIELD_PARTITION_ID)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    APPLICATION_NAME = StringFilter(
-        TaskField(task_option_field=TaskOptionField(field=TASK_OPTION_ENUM_FIELD_APPLICATION_NAME)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    APPLICATION_VERSION = StringFilter(
-        TaskField(
-            task_option_field=TaskOptionField(field=TASK_OPTION_ENUM_FIELD_APPLICATION_VERSION)
-        ),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    APPLICATION_NAMESPACE = StringFilter(
-        TaskField(
-            task_option_field=TaskOptionField(field=TASK_OPTION_ENUM_FIELD_APPLICATION_NAMESPACE)
-        ),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    APPLICATION_SERVICE = StringFilter(
-        TaskField(
-            task_option_field=TaskOptionField(field=TASK_OPTION_ENUM_FIELD_APPLICATION_SERVICE)
-        ),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
-    ENGINE_TYPE = StringFilter(
-        TaskField(task_option_field=TaskOptionField(field=TASK_OPTION_ENUM_FIELD_ENGINE_TYPE)),
-        rawFilters,
-        rawFilterAnd,
-        rawFilterField,
-    )
+    TASK_ID = Task.id
+    SESSION_ID = Task.session_id
+    OWNER_POD_ID = Task.owner_pod_id
+    INITIAL_TASK_ID = Task.initial_task_id
+    STATUS = Task.status
+    CREATED_AT = Task.created_at
+    SUBMITTED_AT = Task.submitted_at
+    STARTED_AT = Task.started_at
+    ENDED_AT = Task.ended_at
+    CREATION_TO_END_DURATION = Task.creation_to_end_duration
+    PROCESSING_TO_END_DURATION = Task.processing_to_end_duration
+    POD_TTL = Task.pod_ttl
+    POD_HOSTNAME = Task.pod_hostname
+    RECEIVED_AT = Task.received_at
+    ACQUIRED_AT = Task.acquired_at
+    ERROR = Task.output.error
+
+    MAX_DURATION = Task.options.max_duration
+    MAX_RETRIES = Task.options.max_retries
+    PRIORITY = Task.options.priority
+    PARTITION_ID = Task.options.partition_id
+    APPLICATION_NAME = Task.options.application_name
+    APPLICATION_VERSION = Task.options.application_version
+    APPLICATION_NAMESPACE = Task.options.application_namespace
+    APPLICATION_SERVICE = Task.options.application_service
+    ENGINE_TYPE = Task.options.engine_type
 
     @staticmethod
     def task_options_key(option_key: str) -> StringFilter:
@@ -258,12 +69,7 @@ def task_options_key(option_key: str) -> StringFilter:
         Returns:
             Corresponding filter
         """
-        return StringFilter(
-            TaskField(task_option_generic_field=TaskOptionGenericField(field=option_key)),
-            rawFilters,
-            rawFilterAnd,
-            rawFilterField,
-        )
+        return Task.options[option_key]
 
 
 class ArmoniKTasks:
@@ -293,7 +99,7 @@ def list_tasks(
         with_errors: bool = False,
         page: int = 0,
         page_size: int = 1000,
-        sort_field: Filter = TaskFieldFilter.TASK_ID,
+        sort_field: Filter = Task.id,
         sort_direction: SortDirection = Direction.ASC,
         detailed: bool = True,
     ) -> Tuple[int, List[Task]]:
@@ -318,12 +124,10 @@ def list_tasks(
         request = ListTasksRequest(
             page=page,
             page_size=page_size,
-            filters=cast(rawFilters, task_filter.to_disjunction().to_message())
-            if task_filter
-            else rawFilters(),
-            sort=ListTasksRequest.Sort(
-                field=cast(TaskField, sort_field.field), direction=sort_direction
+            filters=(
+                TaskFilter().to_message() if task_filter is None else task_filter.to_message()
             ),
+            sort=ListTasksRequest.Sort(field=sort_field.field, direction=sort_direction),
             with_errors=with_errors,
         )
         if detailed:
@@ -377,9 +181,7 @@ def count_tasks_by_status(self, task_filter: Optional[Filter] = None) -> Dict[Ta
             A dictionnary mapping each status to the number of filtered tasks.
         """
         request = CountTasksByStatusRequest(
-            filters=cast(rawFilters, task_filter.to_disjunction().to_message())
-            if task_filter
-            else rawFilters()
+            filters=(TaskFilter().to_message() if task_filter is None else task_filter.to_message())
         )
         count_tasks_by_status_response: CountTasksByStatusResponse = (
             self._client.CountTasksByStatus(request)
@@ -423,7 +225,7 @@ def submit_tasks(
             request = SubmitTasksRequest(
                 session_id=session_id,
                 task_creations=task_creations,
-                task_options=default_task_options.to_message() if default_task_options else None,
+                task_options=(default_task_options.to_message() if default_task_options else None),
             )
 
             self._client.SubmitTasks(request)
diff --git a/packages/python/src/armonik/common/__init__.py b/packages/python/src/armonik/common/__init__.py
index c6397e806..b8b853524 100644
--- a/packages/python/src/armonik/common/__init__.py
+++ b/packages/python/src/armonik/common/__init__.py
@@ -15,7 +15,6 @@
     ResultStatusUpdateEvent,
     TaskStatusUpdateEvent,
 )
-from .filter import Filter, StatusFilter, StringFilter
 from .helpers import (
     batched,
     datetime_to_timestamp,
@@ -34,6 +33,7 @@
     TaskDefinition,
     TaskOptions,
 )
+from .filter import Filter
 
 __all__ = [
     "datetime_to_timestamp",
@@ -57,9 +57,6 @@
     "ResultStatus",
     "EventTypes",
     # Include all names from events module
-    "Filter",
-    "StringFilter",
-    "StatusFilter",
     "ServiceHealthCheckStatus",
     "NewResultEvent",
     "NewTaskEvent",
@@ -67,4 +64,5 @@
     "ResultStatusUpdateEvent",
     "TaskStatusUpdateEvent",
     "Event",
+    "Filter",
 ]
diff --git a/packages/python/src/armonik/common/filter.py b/packages/python/src/armonik/common/filter.py
deleted file mode 100644
index f23c3f9bf..000000000
--- a/packages/python/src/armonik/common/filter.py
+++ /dev/null
@@ -1,611 +0,0 @@
-from __future__ import annotations
-
-import json
-from abc import abstractmethod
-from typing import Any, Dict, List, Optional, Type
-
-import google.protobuf.duration_pb2 as duration
-import google.protobuf.timestamp_pb2 as timestamp
-from google.protobuf.message import Message
-
-from ..protogen.common.filters_common_pb2 import (
-    FILTER_STRING_OPERATOR_EQUAL,
-    FILTER_STRING_OPERATOR_NOT_EQUAL,
-    FILTER_STRING_OPERATOR_CONTAINS,
-    FILTER_STRING_OPERATOR_NOT_CONTAINS,
-    FILTER_STRING_OPERATOR_STARTS_WITH,
-    FILTER_STRING_OPERATOR_ENDS_WITH,
-    FilterString,
-    FILTER_NUMBER_OPERATOR_EQUAL,
-    FILTER_NUMBER_OPERATOR_NOT_EQUAL,
-    FILTER_NUMBER_OPERATOR_LESS_THAN,
-    FILTER_NUMBER_OPERATOR_LESS_THAN_OR_EQUAL,
-    FILTER_NUMBER_OPERATOR_GREATER_THAN_OR_EQUAL,
-    FILTER_NUMBER_OPERATOR_GREATER_THAN,
-    FilterNumber,
-    FILTER_DATE_OPERATOR_EQUAL,
-    FILTER_DATE_OPERATOR_NOT_EQUAL,
-    FILTER_DATE_OPERATOR_BEFORE,
-    FILTER_DATE_OPERATOR_BEFORE_OR_EQUAL,
-    FILTER_DATE_OPERATOR_AFTER,
-    FILTER_DATE_OPERATOR_AFTER_OR_EQUAL,
-    FilterDate,
-    FILTER_ARRAY_OPERATOR_CONTAINS,
-    FILTER_ARRAY_OPERATOR_NOT_CONTAINS,
-    FilterArray,
-    FILTER_STATUS_OPERATOR_EQUAL,
-    FILTER_STATUS_OPERATOR_NOT_EQUAL,
-    FILTER_BOOLEAN_OPERATOR_IS,
-    FilterBoolean,
-    FILTER_DURATION_OPERATOR_EQUAL,
-    FILTER_DURATION_OPERATOR_NOT_EQUAL,
-    FILTER_DURATION_OPERATOR_SHORTER_THAN,
-    FILTER_DURATION_OPERATOR_SHORTER_THAN_OR_EQUAL,
-    FILTER_DURATION_OPERATOR_LONGER_THAN,
-    FILTER_DURATION_OPERATOR_LONGER_THAN_OR_EQUAL,
-    FilterDuration,
-)
-
-
-class Filter:
-    """
-    Filter for use with ArmoniK
-
-    Attributes:
-        eq_: equality raw Api operator
-        ne_: inequality raw Api operator
-        lt_: less than raw Api operator
-        le_: less or equal raw Api operator
-        gt_: greater than raw Api operator
-        ge_: greater or equal raw Api operator
-        contains_: contains raw Api operator
-        notcontains_: not contains raw Api operator
-        value_type_: expected type for the value to test against in this filter
-
-        field: field of the filter if it's a simple filter
-        message_type: Api message type of the filter
-        inner_message_type: Api message type of the inner filter (with value and operator)
-        conjunction_type: Type of the conjunction for this filter
-        value: value to test against in this filter if it's a simple filter
-        operator: operator to apply for this filter if it's a simple filter
-    """
-
-    eq_ = None
-    ne_ = None
-    lt_ = None
-    le_ = None
-    gt_ = None
-    ge_ = None
-    contains_ = None
-    notcontains_ = None
-    value_type_ = None
-
-    def __init__(
-        self,
-        field: Optional[Message],
-        disjunction_message_type: Type[Message],
-        conjunction_message_type: Type[Message],
-        message_type: Type[Message],
-        inner_message_type: Optional[Type[Message]],
-        filters: Optional[List[List["Filter"]]] = None,
-        value=None,
-        operator=None,
-    ):
-        self._filters: List[List["Filter"]] = [[]] if filters is None else filters
-        self.field = field
-        self.message_type = message_type
-        self.conjunction_type = conjunction_message_type
-        self.disjunction_type = disjunction_message_type
-        self.inner_message_type = inner_message_type
-        self.value = value
-        self.operator = operator
-
-    def is_true_conjunction(self) -> bool:
-        """
-        Tests whether the filter is a conjunction (logical and)
-        Note : This will only output true if it's an actual conjunction with multiple filters and no disjunction
-        """
-        return self.message_type == self.conjunction_type or (
-            len(self._filters) == 1 and len(self._filters[0]) > 1
-        )
-
-    def is_true_disjunction(self) -> bool:
-        """
-        Tests whether the filter is a disjunction (logical or)
-        Note : This will only output true if it's an actual disjunction with multiple filters
-        """
-        return len(self._filters) > 1
-
-    def to_disjunction(self) -> Filter:
-        """
-        Converts the filter into a disjunction
-
-        """
-        if self.is_true_disjunction():
-            return self
-        if self.is_true_conjunction():
-            return Filter(
-                None,
-                self.disjunction_type,
-                self.conjunction_type,
-                self.disjunction_type,
-                None,
-                self._filters,
-            )
-        return Filter(
-            None,
-            self.disjunction_type,
-            self.conjunction_type,
-            self.disjunction_type,
-            None,
-            [[self]],
-        )
-
-    def __and__(self, other: "Filter") -> "Filter":
-        if not isinstance(other, Filter):
-            msg = f"Cannot create a conjunction between Filter and {other.__class__.__name__}"
-            raise Exception(msg)
-        if self.is_true_disjunction() or other.is_true_disjunction():
-            raise Exception("Cannot make a conjunction of disjunctions")
-        if self.conjunction_type != other.conjunction_type:
-            raise Exception("Conjunction types are different")
-        return Filter(
-            None,
-            self.disjunction_type,
-            self.conjunction_type,
-            self.conjunction_type,
-            None,
-            [self.to_disjunction()._filters[0] + other.to_disjunction()._filters[0]],
-        )
-
-    def __mul__(self, other: Filter) -> "Filter":
-        return self & other
-
-    def __or__(self, other: "Filter") -> "Filter":
-        if not isinstance(other, Filter):
-            msg = f"Cannot create a conjunction between Filter and {other.__class__.__name__}"
-            raise Exception(msg)
-        if self.disjunction_type != other.disjunction_type:
-            raise Exception("Disjunction types are different")
-        return Filter(
-            None,
-            self.disjunction_type,
-            self.conjunction_type,
-            self.disjunction_type,
-            None,
-            self.to_disjunction()._filters + other.to_disjunction()._filters,
-        )
-
-    def __add__(self, other: "Filter") -> "Filter":
-        return self | other
-
-    def __eq__(self, value) -> Filter:
-        return self._check(value, self.__class__.eq_, "==")
-
-    def __ne__(self, value) -> Filter:
-        return self._check(value, self.__class__.ne_, "!=")
-
-    def __lt__(self, value) -> Filter:
-        return self._check(value, self.__class__.lt_, "<")
-
-    def __le__(self, value) -> Filter:
-        return self._check(value, self.__class__.le_, "<=")
-
-    def __gt__(self, value) -> Filter:
-        return self._check(value, self.__class__.gt_, ">")
-
-    def __ge__(self, value) -> Filter:
-        return self._check(value, self.__class__.ge_, ">=")
-
-    def contains(self, value) -> Filter:
-        return self._check(value, self.__class__.contains_, "contains")
-
-    def __invert__(self) -> Filter:
-        """
-        Inverts the test
-
-        Returns:
-            Filter with the test being inverted
-        """
-        if self.operator is None:
-            if self.is_true_conjunction() or self.is_true_disjunction():
-                raise Exception("Cannot invert conjunctions or disjunctions")
-            msg = f"Cannot invert None operator in class {self.__class__.__name__} for field {str(self.field)}"
-            raise Exception(msg)
-        if self.operator == self.__class__.eq_:
-            return self.__ne__(self.value)
-        if self.operator == self.__class__.ne_:
-            return self.__eq__(self.value)
-        if self.operator == self.__class__.lt_:
-            return self.__ge__(self.value)
-        if self.operator == self.__class__.le_:
-            return self.__gt__(self.value)
-        if self.operator == self.__class__.gt_:
-            return self.__le__(self.value)
-        if self.operator == self.__class__.ge_:
-            return self.__lt__(self.value)
-        if self.operator == self.__class__.contains_:
-            return self._check(self.value, self.__class__.notcontains_, "not_contains")
-        if self.operator == self.__class__.notcontains_:
-            return self.contains(self.value)
-        msg = f"{self.__class__.__name__} operator {str(self.operator)} for field {str(self.field)} has no inverted equivalent"
-        raise Exception(msg)
-
-    def __neg__(self) -> "Filter":
-        return ~self
-
-    def to_dict(self) -> Dict:
-        rep = {}
-        if self.is_true_disjunction():
-            rep["or"] = [{"and": [f.to_dict() for f in conj]} for conj in self._filters]
-            return rep
-        if self.is_true_conjunction():
-            rep["and"] = [f.to_dict() for f in self._filters[0]]
-            return rep
-        if len(self._filters) == 1 and len(self._filters[0]) == 1:
-            return self._filters[0][0].to_dict()
-        return {
-            "field": str(self.field),
-            "value": str(self.value),
-            "operator": str(self.operator),
-        }
-
-    def __str__(self) -> str:
-        return json.dumps(self.to_dict())
-
-    def _verify_value(self, value):
-        """
-        Checks if the value is of the expected type
-        Args:
-            value: Value to test
-
-        Raises:
-            Exception if value is not of the expected type
-
-        """
-        if self.__class__.value_type_ is None or isinstance(value, self.__class__.value_type_):
-            return
-        msg = f"Expected value type {str(self.__class__.value_type_)} for field {str(self.field)}, got {str(type(value))} instead"
-        raise Exception(msg)
-
-    def _check(self, value: Any, operator: Any, operator_str: str = "") -> "Filter":
-        """
-        Internal function to create a new filter from the current filter with a different value and/or operator
-        Args:
-            value: Value of the new filter
-            operator: Operator of the new filter
-            operator_str: Optional string for error message clarification
-
-        Returns:
-            new filter with the given value/operator
-
-        Raises:
-            NotImplementedError if the given operator is not available for the given class
-        """
-        if self.is_true_conjunction() or self.is_true_disjunction():
-            raise Exception("Cannot apply operator to a disjunction or a conjunction")
-        self._verify_value(value)
-        if operator is None:
-            msg = f"Operator {operator_str} is not available for {self.__class__.__name__}"
-            raise NotImplementedError(msg)
-        return self.__class__(
-            self.field,
-            self.disjunction_type,
-            self.conjunction_type,
-            self.message_type,
-            self.inner_message_type,
-            self._filters,
-            value,
-            operator,
-        )
-
-    @abstractmethod
-    def to_basic_message(self) -> Message:
-        pass
-
-    def to_message(self) -> Message:
-        def to_conjunction_message(conj: List[Filter]) -> Message:
-            conj_raw = self.conjunction_type()
-            getattr(conj_raw, "and").extend([f.to_basic_message() for f in conj])
-            return conj_raw
-
-        if self.message_type == self.disjunction_type:
-            raw = self.to_disjunction().disjunction_type()
-            getattr(raw, "or").extend([to_conjunction_message(conj) for conj in self._filters])
-            return raw
-        if self.message_type == self.conjunction_type:
-            return to_conjunction_message(self.to_disjunction()._filters[0])
-        return self.to_basic_message()
-
-
-class StringFilter(Filter):
-    """
-    Filter for string comparisons
-    """
-
-    eq_ = FILTER_STRING_OPERATOR_EQUAL
-    ne_ = FILTER_STRING_OPERATOR_NOT_EQUAL
-    contains_ = FILTER_STRING_OPERATOR_CONTAINS
-    notcontains_ = FILTER_STRING_OPERATOR_NOT_CONTAINS
-    value_type_ = str
-
-    def __init__(
-        self,
-        field: Optional[Message],
-        disjunction_message_type: Type[Message],
-        conjunction_message_type: Type[Message],
-        message_type: Type[Message],
-        inner_message_type: Optional[Type[Message]] = FilterString,
-        filters: Optional[List[List["Filter"]]] = None,
-        value=None,
-        operator=None,
-    ):
-        super().__init__(
-            field,
-            disjunction_message_type,
-            conjunction_message_type,
-            message_type,
-            inner_message_type,
-            filters,
-            value,
-            operator,
-        )
-
-    def startswith(self, value: str) -> "StringFilter":
-        return self._check(value, FILTER_STRING_OPERATOR_STARTS_WITH, "startswith")
-
-    def endswith(self, value: str) -> "StringFilter":
-        return self._check(value, FILTER_STRING_OPERATOR_ENDS_WITH, "endswith")
-
-    def to_basic_message(self) -> Message:
-        return self.message_type(
-            field=self.field,
-            filter_string=self.inner_message_type(value=self.value, operator=self.operator),
-        )
-
-    def __repr__(self) -> str:
-        return f'{str(self.field)} {str(self.operator)} "{str(self.value)}"'
-
-
-class StatusFilter(Filter):
-    """
-    Filter for status comparison
-    """
-
-    eq_ = FILTER_STATUS_OPERATOR_EQUAL
-    ne_ = FILTER_STATUS_OPERATOR_NOT_EQUAL
-
-    def __init__(
-        self,
-        field: Optional[Message],
-        disjunction_message_type: Type[Message],
-        conjunction_message_type: Type[Message],
-        message_type: Type[Message],
-        inner_message_type: Type[Message],
-        filters: Optional[List[List["Filter"]]] = None,
-        value=None,
-        operator=None,
-    ):
-        super().__init__(
-            field,
-            disjunction_message_type,
-            conjunction_message_type,
-            message_type,
-            inner_message_type,
-            filters,
-            value,
-            operator,
-        )
-
-    def to_basic_message(self) -> Message:
-        return self.message_type(
-            field=self.field,
-            filter_status=self.inner_message_type(value=self.value, operator=self.operator),
-        )
-
-
-class DateFilter(Filter):
-    """
-    Filter for timestamp comparison
-    """
-
-    eq_ = FILTER_DATE_OPERATOR_EQUAL
-    ne_ = FILTER_DATE_OPERATOR_NOT_EQUAL
-    lt_ = FILTER_DATE_OPERATOR_BEFORE
-    le_ = FILTER_DATE_OPERATOR_BEFORE_OR_EQUAL
-    gt_ = FILTER_DATE_OPERATOR_AFTER
-    ge_ = FILTER_DATE_OPERATOR_AFTER_OR_EQUAL
-    value_type = timestamp.Timestamp
-
-    def __init__(
-        self,
-        field: Optional[Message],
-        disjunction_message_type: Type[Message],
-        conjunction_message_type: Type[Message],
-        message_type: Type[Message],
-        inner_message_type: Optional[Type[Message]] = FilterDate,
-        filters: Optional[List[List["Filter"]]] = None,
-        value=None,
-        operator=None,
-    ):
-        super().__init__(
-            field,
-            disjunction_message_type,
-            conjunction_message_type,
-            message_type,
-            inner_message_type,
-            filters,
-            value,
-            operator,
-        )
-
-    def to_basic_message(self) -> Message:
-        return self.message_type(
-            field=self.field,
-            filter_date=self.inner_message_type(value=self.value, operator=self.operator),
-        )
-
-
-class NumberFilter(Filter):
-    """
-    Filter for int comparison
-    """
-
-    eq_ = FILTER_NUMBER_OPERATOR_EQUAL
-    ne_ = FILTER_NUMBER_OPERATOR_NOT_EQUAL
-    lt_ = FILTER_NUMBER_OPERATOR_LESS_THAN
-    le_ = FILTER_NUMBER_OPERATOR_LESS_THAN_OR_EQUAL
-    gt_ = FILTER_NUMBER_OPERATOR_GREATER_THAN
-    ge_ = FILTER_NUMBER_OPERATOR_GREATER_THAN_OR_EQUAL
-    value_type_ = int
-
-    def __init__(
-        self,
-        field: Optional[Message],
-        disjunction_message_type: Type[Message],
-        conjunction_message_type: Type[Message],
-        message_type: Type[Message],
-        inner_message_type: Optional[Type[Message]] = FilterNumber,
-        filters: Optional[List[List["Filter"]]] = None,
-        value=None,
-        operator=None,
-    ):
-        super().__init__(
-            field,
-            disjunction_message_type,
-            conjunction_message_type,
-            message_type,
-            inner_message_type,
-            filters,
-            value,
-            operator,
-        )
-
-    def to_basic_message(self) -> Message:
-        return self.message_type(
-            field=self.field,
-            filter_number=self.inner_message_type(value=self.value, operator=self.operator),
-        )
-
-
-class BooleanFilter(Filter):
-    """
-    Filter for boolean comparison
-    """
-
-    eq_ = FILTER_BOOLEAN_OPERATOR_IS
-    value_type_ = bool
-
-    def __init__(
-        self,
-        field: Optional[Message],
-        disjunction_message_type: Type[Message],
-        conjunction_message_type: Type[Message],
-        message_type: Type[Message],
-        inner_message_type: Optional[Type[Message]] = FilterBoolean,
-        filters: Optional[List[List["Filter"]]] = None,
-        value=True,
-        operator=FILTER_BOOLEAN_OPERATOR_IS,
-    ):
-        super().__init__(
-            field,
-            disjunction_message_type,
-            conjunction_message_type,
-            message_type,
-            inner_message_type,
-            filters,
-            value,
-            operator,
-        )
-
-    def __ne__(self, value: bool) -> "BooleanFilter":
-        return self.__eq__(not value)
-
-    def __invert__(self) -> "BooleanFilter":
-        return self.__eq__(not self.value)
-
-    def to_basic_message(self) -> Message:
-        return self.message_type(
-            field=self.field,
-            filter_boolean=self.inner_message_type(value=self.value, operator=self.operator),
-        )
-
-
-class ArrayFilter(Filter):
-    """
-    Filter for array comparisons
-    """
-
-    contains_ = FILTER_ARRAY_OPERATOR_CONTAINS
-    notcontains_ = FILTER_ARRAY_OPERATOR_NOT_CONTAINS
-    value_type_ = str
-
-    def __init__(
-        self,
-        field: Optional[Message],
-        disjunction_message_type: Type[Message],
-        conjunction_message_type: Type[Message],
-        message_type: Type[Message],
-        inner_message_type: Optional[Type[Message]] = FilterArray,
-        filters: Optional[List[List["Filter"]]] = None,
-        value=None,
-        operator=None,
-    ):
-        super().__init__(
-            field,
-            disjunction_message_type,
-            conjunction_message_type,
-            message_type,
-            inner_message_type,
-            filters,
-            value,
-            operator,
-        )
-
-    def to_basic_message(self) -> Message:
-        return self.message_type(
-            field=self.field,
-            filter_array=self.inner_message_type(value=self.value, operator=self.operator),
-        )
-
-
-class DurationFilter(Filter):
-    """
-    Filter for duration comparison
-    """
-
-    eq_ = FILTER_DURATION_OPERATOR_EQUAL
-    ne_ = FILTER_DURATION_OPERATOR_NOT_EQUAL
-    lt_ = FILTER_DURATION_OPERATOR_SHORTER_THAN
-    le_ = FILTER_DURATION_OPERATOR_SHORTER_THAN_OR_EQUAL
-    gt_ = FILTER_DURATION_OPERATOR_LONGER_THAN
-    ge_ = FILTER_DURATION_OPERATOR_LONGER_THAN_OR_EQUAL
-    value_type_ = duration.Duration
-
-    def __init__(
-        self,
-        field: Optional[Message],
-        disjunction_message_type: Type[Message],
-        conjunction_message_type: Type[Message],
-        message_type: Type[Message],
-        inner_message_type: Optional[Type[Message]] = FilterDuration,
-        filters: Optional[List[List["Filter"]]] = None,
-        value=None,
-        operator=None,
-    ):
-        super().__init__(
-            field,
-            disjunction_message_type,
-            conjunction_message_type,
-            message_type,
-            inner_message_type,
-            filters,
-            value,
-            operator,
-        )
-
-    def to_basic_message(self) -> Message:
-        return self.message_type(
-            field=self.field,
-            filter_duration=self.inner_message_type(value=self.value, operator=self.operator),
-        )
diff --git a/packages/python/src/armonik/common/filter/__init__.py b/packages/python/src/armonik/common/filter/__init__.py
new file mode 100644
index 000000000..3af0d5480
--- /dev/null
+++ b/packages/python/src/armonik/common/filter/__init__.py
@@ -0,0 +1,56 @@
+from ._message_types import (
+    DisjunctionType,
+    ConjunctionType,
+    BasicMessageType,
+    InnerMessageType,
+)
+from .filter import (
+    Filter,
+    FilterError,
+    NumberFilter,
+    DurationFilter,
+    DateFilter,
+    StringFilter,
+    ArrayFilter,
+    BooleanFilter,
+    StatusFilter,
+    FilterDescriptor,
+)
+
+from ._filter_field import (
+    PartitionFilter,
+    TaskFilter,
+    TaskOptionFilter,
+    SessionFilter,
+    SessionTaskOptionFilter,
+    ResultFilter,
+    ApplicationFilter,
+    GenericTaskOptionsFilter,
+    OutputFilter,
+)
+
+__all__ = [
+    "Filter",
+    "NumberFilter",
+    "DurationFilter",
+    "DateFilter",
+    "StringFilter",
+    "ArrayFilter",
+    "BooleanFilter",
+    "StatusFilter",
+    "TaskFilter",
+    "FilterError",
+    "TaskOptionFilter",
+    "DisjunctionType",
+    "ConjunctionType",
+    "BasicMessageType",
+    "InnerMessageType",
+    "SessionFilter",
+    "SessionTaskOptionFilter",
+    "ResultFilter",
+    "PartitionFilter",
+    "ApplicationFilter",
+    "FilterDescriptor",
+    "GenericTaskOptionsFilter",
+    "OutputFilter",
+]
diff --git a/packages/python/src/armonik/common/filter/_filter_field.py b/packages/python/src/armonik/common/filter/_filter_field.py
new file mode 100644
index 000000000..87e4dc053
--- /dev/null
+++ b/packages/python/src/armonik/common/filter/_filter_field.py
@@ -0,0 +1,378 @@
+from abc import ABC, abstractmethod
+from typing import Any, cast
+
+from google.protobuf.message import Message
+
+from .filter import FilterWrapper, StringFilter, DurationFilter, NumberFilter, FType, _na
+
+from ...protogen.common.tasks_fields_pb2 import (
+    TaskField as rawTaskField,
+    TaskSummaryField,
+    TaskOptionGenericField,
+    TaskOptionField,
+    TASK_SUMMARY_ENUM_FIELD_TASK_ID,
+    TASK_SUMMARY_ENUM_FIELD_SESSION_ID,
+    TASK_SUMMARY_ENUM_FIELD_OWNER_POD_ID,
+    TASK_SUMMARY_ENUM_FIELD_INITIAL_TASK_ID,
+    TASK_SUMMARY_ENUM_FIELD_STATUS,
+    TASK_SUMMARY_ENUM_FIELD_CREATED_AT,
+    TASK_SUMMARY_ENUM_FIELD_SUBMITTED_AT,
+    TASK_SUMMARY_ENUM_FIELD_STARTED_AT,
+    TASK_SUMMARY_ENUM_FIELD_ENDED_AT,
+    TASK_SUMMARY_ENUM_FIELD_CREATION_TO_END_DURATION,
+    TASK_SUMMARY_ENUM_FIELD_PROCESSING_TO_END_DURATION,
+    TASK_SUMMARY_ENUM_FIELD_RECEIVED_TO_END_DURATION,
+    TASK_SUMMARY_ENUM_FIELD_POD_TTL,
+    TASK_SUMMARY_ENUM_FIELD_POD_HOSTNAME,
+    TASK_SUMMARY_ENUM_FIELD_RECEIVED_AT,
+    TASK_SUMMARY_ENUM_FIELD_ACQUIRED_AT,
+    TASK_SUMMARY_ENUM_FIELD_PROCESSED_AT,
+    TASK_SUMMARY_ENUM_FIELD_ERROR,
+    TASK_SUMMARY_ENUM_FIELD_FETCHED_AT,
+    TASK_SUMMARY_ENUM_FIELD_PAYLOAD_ID,
+    TASK_OPTION_ENUM_FIELD_MAX_DURATION,
+    TASK_OPTION_ENUM_FIELD_MAX_RETRIES,
+    TASK_OPTION_ENUM_FIELD_PRIORITY,
+    TASK_OPTION_ENUM_FIELD_PARTITION_ID,
+    TASK_OPTION_ENUM_FIELD_APPLICATION_NAME,
+    TASK_OPTION_ENUM_FIELD_APPLICATION_NAMESPACE,
+    TASK_OPTION_ENUM_FIELD_APPLICATION_VERSION,
+    TASK_OPTION_ENUM_FIELD_APPLICATION_SERVICE,
+    TASK_OPTION_ENUM_FIELD_ENGINE_TYPE,
+)
+
+from ...protogen.common.sessions_fields_pb2 import (
+    SessionField,
+    SessionRawField,
+    SESSION_RAW_ENUM_FIELD_SESSION_ID,
+    SESSION_RAW_ENUM_FIELD_DURATION,
+    SESSION_RAW_ENUM_FIELD_CREATED_AT,
+    SESSION_RAW_ENUM_FIELD_DELETED_AT,
+    SESSION_RAW_ENUM_FIELD_PURGED_AT,
+    SESSION_RAW_ENUM_FIELD_STATUS,
+    SESSION_RAW_ENUM_FIELD_PARTITION_IDS,
+    SESSION_RAW_ENUM_FIELD_CLIENT_SUBMISSION,
+    SESSION_RAW_ENUM_FIELD_WORKER_SUBMISSION,
+    SESSION_RAW_ENUM_FIELD_CLOSED_AT,
+    SESSION_RAW_ENUM_FIELD_CANCELLED_AT,
+    TaskOptionField as SessionOptionField,
+    TaskOptionGenericField as SessionOptionGenericField,
+    TASK_OPTION_ENUM_FIELD_MAX_RETRIES as SESSION_OPTION_MAX_RETRIES,
+    TASK_OPTION_ENUM_FIELD_APPLICATION_NAMESPACE as SESSION_OPTION_APPLICATION_NAMESPACE,
+    TASK_OPTION_ENUM_FIELD_APPLICATION_VERSION as SESSION_OPTION_APPLICATION_VERSION,
+    TASK_OPTION_ENUM_FIELD_PRIORITY as SESSION_OPTION_PRIORITY,
+    TASK_OPTION_ENUM_FIELD_APPLICATION_NAME as SESSION_OPTION_APPLICATION_NAME,
+    TASK_OPTION_ENUM_FIELD_PARTITION_ID as SESSION_OPTION_PARTITION_ID,
+    TASK_OPTION_ENUM_FIELD_ENGINE_TYPE as SESSION_OPTION_ENGINE_TYPE,
+    TASK_OPTION_ENUM_FIELD_MAX_DURATION as SESSION_OPTION_MAX_DURATION,
+    TASK_OPTION_ENUM_FIELD_APPLICATION_SERVICE as SESSION_OPTION_APPLICATION_SERVICE,
+)
+
+from ...protogen.common.results_fields_pb2 import (
+    ResultField,
+    ResultRawField,
+    RESULT_RAW_ENUM_FIELD_RESULT_ID,
+    RESULT_RAW_ENUM_FIELD_SIZE,
+    RESULT_RAW_ENUM_FIELD_NAME,
+    RESULT_RAW_ENUM_FIELD_STATUS,
+    RESULT_RAW_ENUM_FIELD_COMPLETED_AT,
+    RESULT_RAW_ENUM_FIELD_CREATED_AT,
+    RESULT_RAW_ENUM_FIELD_SESSION_ID,
+    RESULT_RAW_ENUM_FIELD_OWNER_TASK_ID,
+)
+
+from ...protogen.common.partitions_fields_pb2 import (
+    PartitionField,
+    PartitionRawField,
+    PARTITION_RAW_ENUM_FIELD_ID,
+    PARTITION_RAW_ENUM_FIELD_PRIORITY,
+    PARTITION_RAW_ENUM_FIELD_PREEMPTION_PERCENTAGE,
+    PARTITION_RAW_ENUM_FIELD_POD_RESERVED,
+    PARTITION_RAW_ENUM_FIELD_PARENT_PARTITION_IDS,
+    PARTITION_RAW_ENUM_FIELD_POD_MAX,
+)
+
+from ...protogen.common.applications_fields_pb2 import (
+    ApplicationField,
+    ApplicationRawField,
+    APPLICATION_RAW_ENUM_FIELD_NAMESPACE,
+    APPLICATION_RAW_ENUM_FIELD_SERVICE,
+    APPLICATION_RAW_ENUM_FIELD_VERSION,
+    APPLICATION_RAW_ENUM_FIELD_NAME,
+)
+
+from ...protogen.common.tasks_filters_pb2 import (
+    FilterField as rawTaskFilterField,
+    FilterStatus as rawTaskFilterStatus,
+    Filters as rawTaskFilters,
+    FiltersAnd as rawTaskFilterAnd,
+)
+
+from ...protogen.common.sessions_filters_pb2 import (
+    FilterField as rawSessionFilterField,
+    FilterStatus as rawSessionFilterStatus,
+    Filters as rawSessionFilters,
+    FiltersAnd as rawSessionFilterAnd,
+)
+
+from ...protogen.common.results_filters_pb2 import (
+    FilterField as rawResultFilterField,
+    FilterStatus as rawResultFilterStatus,
+    Filters as rawResultFilters,
+    FiltersAnd as rawResultFilterAnd,
+)
+
+from ...protogen.common.partitions_filters_pb2 import (
+    FilterField as rawPartitionFilterField,
+    Filters as rawPartitionFilters,
+    FiltersAnd as rawPartitionFilterAnd,
+)
+
+from ...protogen.common.applications_filters_pb2 import (
+    FilterField as rawApplicationFilterField,
+    Filters as rawApplicationFilters,
+    FiltersAnd as rawApplicationFilterAnd,
+)
+
+"""
+This file defines the fields available for each object type. Used internally.
+"""
+
+
+class GenericTaskOptionsFilter(FilterWrapper, ABC):
+    """
+    Filter for task options
+    """
+
+    @abstractmethod
+    def __getitem__(self, item: str) -> StringFilter: ...
+
+    @abstractmethod
+    def _build_field(self, field: Any) -> Message: ...
+
+    @property
+    def max_duration(self) -> DurationFilter:
+        return cast(DurationFilter, self("max_duration"))
+
+    @property
+    def max_retries(self) -> NumberFilter:
+        return cast(NumberFilter, self("max_retries"))
+
+    @property
+    def priority(self) -> NumberFilter:
+        return cast(NumberFilter, self("priority"))
+
+    @property
+    def partition_id(self) -> StringFilter:
+        return cast(StringFilter, self("partition_id"))
+
+    @property
+    def application_name(self) -> StringFilter:
+        return cast(StringFilter, self("application_name"))
+
+    @property
+    def application_namespace(self) -> StringFilter:
+        return cast(StringFilter, self("application_namespace"))
+
+    @property
+    def application_version(self) -> StringFilter:
+        return cast(StringFilter, self("application_version"))
+
+    @property
+    def application_service(self) -> StringFilter:
+        return cast(StringFilter, self("application_service"))
+
+    @property
+    def engine_type(self) -> StringFilter:
+        return cast(StringFilter, self("engine_type"))
+
+
+class OutputFilter(FilterWrapper):
+    def __init__(self):
+        super().__init__(rawTaskFilters, rawTaskFilterAnd, rawTaskFilterField, rawTaskFilterStatus)
+
+    @property
+    def error(self) -> StringFilter:
+        return self._string(self._build_field(TASK_SUMMARY_ENUM_FIELD_ERROR))
+
+    def _build_field(self, field: Any) -> Message:
+        if field != TASK_SUMMARY_ENUM_FIELD_ERROR:
+            _na("")
+        return rawTaskField(task_summary_field=TaskSummaryField(field=field))
+
+
+class TaskFilter(FilterWrapper):
+    _fields = {
+        "id": (FType.STR, TASK_SUMMARY_ENUM_FIELD_TASK_ID),
+        "session_id": (FType.STR, TASK_SUMMARY_ENUM_FIELD_SESSION_ID),
+        "owner_pod_id": (FType.STR, TASK_SUMMARY_ENUM_FIELD_OWNER_POD_ID),
+        "initial_task_id": (FType.STR, TASK_SUMMARY_ENUM_FIELD_INITIAL_TASK_ID),
+        "parent_task_ids": (FType.NA, "parent_task_ids"),
+        "data_dependencies": (FType.NA, "data_dependencies"),
+        "expected_output_ids": (FType.NA, "expected_output_ids"),
+        "retry_of_ids": (FType.NA, "retry_of_ids"),
+        "status": (FType.STATUS, TASK_SUMMARY_ENUM_FIELD_STATUS),
+        "status_message": (FType.NA, "status_message"),
+        "options": (FType.NA, "options"),
+        "created_at": (FType.DATE, TASK_SUMMARY_ENUM_FIELD_CREATED_AT),
+        "submitted_at": (FType.DATE, TASK_SUMMARY_ENUM_FIELD_SUBMITTED_AT),
+        "received_at": (FType.DATE, TASK_SUMMARY_ENUM_FIELD_RECEIVED_AT),
+        "acquired_at": (FType.DATE, TASK_SUMMARY_ENUM_FIELD_ACQUIRED_AT),
+        "fetched_at": (FType.DATE, TASK_SUMMARY_ENUM_FIELD_FETCHED_AT),
+        "started_at": (FType.DATE, TASK_SUMMARY_ENUM_FIELD_STARTED_AT),
+        "processed_at": (FType.DATE, TASK_SUMMARY_ENUM_FIELD_PROCESSED_AT),
+        "ended_at": (FType.DATE, TASK_SUMMARY_ENUM_FIELD_ENDED_AT),
+        "pod_ttl": (FType.DATE, TASK_SUMMARY_ENUM_FIELD_POD_TTL),
+        "creation_to_end_duration": (
+            FType.DURATION,
+            TASK_SUMMARY_ENUM_FIELD_CREATION_TO_END_DURATION,
+        ),
+        "processing_to_end_duration": (
+            FType.DURATION,
+            TASK_SUMMARY_ENUM_FIELD_PROCESSING_TO_END_DURATION,
+        ),
+        "received_to_end_duration": (
+            FType.DURATION,
+            TASK_SUMMARY_ENUM_FIELD_RECEIVED_TO_END_DURATION,
+        ),
+        "output": (FType.NA, "output"),
+        "pod_hostname": (FType.STR, TASK_SUMMARY_ENUM_FIELD_POD_HOSTNAME),
+        "payload_id": (FType.STR, TASK_SUMMARY_ENUM_FIELD_PAYLOAD_ID),
+    }
+
+    def __init__(self):
+        super().__init__(rawTaskFilters, rawTaskFilterAnd, rawTaskFilterField, rawTaskFilterStatus)
+
+    def _build_field(self, field: Any) -> Message:
+        return rawTaskField(task_summary_field=TaskSummaryField(field=field))
+
+
+class TaskOptionFilter(GenericTaskOptionsFilter):
+    _fields = {
+        "max_duration": (FType.DURATION, TASK_OPTION_ENUM_FIELD_MAX_DURATION),
+        "max_retries": (FType.NUM, TASK_OPTION_ENUM_FIELD_MAX_RETRIES),
+        "priority": (FType.NUM, TASK_OPTION_ENUM_FIELD_PRIORITY),
+        "partition_id": (FType.STR, TASK_OPTION_ENUM_FIELD_PARTITION_ID),
+        "application_name": (FType.STR, TASK_OPTION_ENUM_FIELD_APPLICATION_NAME),
+        "application_namespace": (FType.STR, TASK_OPTION_ENUM_FIELD_APPLICATION_NAMESPACE),
+        "application_version": (FType.STR, TASK_OPTION_ENUM_FIELD_APPLICATION_VERSION),
+        "application_service": (FType.STR, TASK_OPTION_ENUM_FIELD_APPLICATION_SERVICE),
+        "engine_type": (FType.STR, TASK_OPTION_ENUM_FIELD_ENGINE_TYPE),
+    }
+
+    def __init__(self):
+        super().__init__(rawTaskFilters, rawTaskFilterAnd, rawTaskFilterField, rawTaskFilterStatus)
+
+    def __getitem__(self, item: str) -> StringFilter:
+        return self._string(
+            rawTaskField(task_option_generic_field=TaskOptionGenericField(field=item))
+        )
+
+    def _build_field(self, field: Any) -> Message:
+        return rawTaskField(task_option_field=TaskOptionField(field=field))
+
+
+class SessionFilter(FilterWrapper):
+    _fields = {
+        "session_id": (FType.STR, SESSION_RAW_ENUM_FIELD_SESSION_ID),
+        "status": (FType.STATUS, SESSION_RAW_ENUM_FIELD_STATUS),
+        "client_submission": (FType.BOOL, SESSION_RAW_ENUM_FIELD_CLIENT_SUBMISSION),
+        "worker_submission": (FType.BOOL, SESSION_RAW_ENUM_FIELD_WORKER_SUBMISSION),
+        "partition_ids": (FType.ARRAY, SESSION_RAW_ENUM_FIELD_PARTITION_IDS),
+        "options": (FType.NA, "options"),
+        "created_at": (FType.DATE, SESSION_RAW_ENUM_FIELD_CREATED_AT),
+        "cancelled_at": (FType.DATE, SESSION_RAW_ENUM_FIELD_CANCELLED_AT),
+        "closed_at": (FType.DATE, SESSION_RAW_ENUM_FIELD_CLOSED_AT),
+        "purged_at": (FType.DATE, SESSION_RAW_ENUM_FIELD_PURGED_AT),
+        "deleted_at": (FType.DATE, SESSION_RAW_ENUM_FIELD_DELETED_AT),
+        "duration": (FType.DURATION, SESSION_RAW_ENUM_FIELD_DURATION),
+    }
+
+    def __init__(self):
+        super().__init__(
+            rawSessionFilters, rawSessionFilterAnd, rawSessionFilterField, rawSessionFilterStatus
+        )
+
+    def _build_field(self, field: Any) -> Message:
+        return SessionField(session_raw_field=SessionRawField(field=field))
+
+
+class SessionTaskOptionFilter(GenericTaskOptionsFilter):
+    _fields = {
+        "max_duration": (FType.DURATION, SESSION_OPTION_MAX_DURATION),
+        "max_retries": (FType.NUM, SESSION_OPTION_MAX_RETRIES),
+        "priority": (FType.NUM, SESSION_OPTION_PRIORITY),
+        "partition_id": (FType.STR, SESSION_OPTION_PARTITION_ID),
+        "application_name": (FType.STR, SESSION_OPTION_APPLICATION_NAME),
+        "application_namespace": (FType.STR, SESSION_OPTION_APPLICATION_NAMESPACE),
+        "application_version": (FType.STR, SESSION_OPTION_APPLICATION_VERSION),
+        "application_service": (FType.STR, SESSION_OPTION_APPLICATION_SERVICE),
+        "engine_type": (FType.STR, SESSION_OPTION_ENGINE_TYPE),
+    }
+
+    def __init__(self):
+        super().__init__(
+            rawSessionFilters, rawSessionFilterAnd, rawSessionFilterField, rawSessionFilterStatus
+        )
+
+    def __getitem__(self, item: str) -> StringFilter:
+        return self._string(
+            SessionField(task_option_generic_field=SessionOptionGenericField(field=item))
+        )
+
+    def _build_field(self, field: Any) -> Message:
+        return SessionField(task_option_field=SessionOptionField(field=field))
+
+
+class ResultFilter(FilterWrapper):
+    _fields = {
+        "session_id": (FType.STR, RESULT_RAW_ENUM_FIELD_SESSION_ID),
+        "status": (FType.STATUS, RESULT_RAW_ENUM_FIELD_STATUS),
+        "name": (FType.STR, RESULT_RAW_ENUM_FIELD_NAME),
+        "created_at": (FType.DATE, RESULT_RAW_ENUM_FIELD_CREATED_AT),
+        "completed_at": (FType.DATE, RESULT_RAW_ENUM_FIELD_COMPLETED_AT),
+        "result_id": (FType.STR, RESULT_RAW_ENUM_FIELD_RESULT_ID),
+        "size": (FType.NUM, RESULT_RAW_ENUM_FIELD_SIZE),
+        "owner_task_id": (FType.STR, RESULT_RAW_ENUM_FIELD_OWNER_TASK_ID),
+    }
+
+    def __init__(self):
+        super().__init__(
+            rawResultFilters, rawResultFilterAnd, rawResultFilterField, rawResultFilterStatus
+        )
+
+    def _build_field(self, field: Any) -> Message:
+        return ResultField(result_raw_field=ResultRawField(field=field))
+
+
+class PartitionFilter(FilterWrapper):
+    _fields = {
+        "id": (FType.STR, PARTITION_RAW_ENUM_FIELD_ID),
+        "priority": (FType.NUM, PARTITION_RAW_ENUM_FIELD_PRIORITY),
+        "preemption_percentage": (FType.NUM, PARTITION_RAW_ENUM_FIELD_PREEMPTION_PERCENTAGE),
+        "pod_reserved": (FType.NUM, PARTITION_RAW_ENUM_FIELD_POD_RESERVED),
+        "pod_max": (FType.NUM, PARTITION_RAW_ENUM_FIELD_POD_MAX),
+        "parent_partition_ids": (FType.ARRAY, PARTITION_RAW_ENUM_FIELD_PARENT_PARTITION_IDS),
+        "pod_configuration": (FType.NA, "pod_configuration"),
+    }
+
+    def __init__(self):
+        super().__init__(rawPartitionFilters, rawPartitionFilterAnd, rawPartitionFilterField)
+
+    def _build_field(self, field: Any) -> Message:
+        return PartitionField(partition_raw_field=PartitionRawField(field=field))
+
+
+class ApplicationFilter(FilterWrapper):
+    _fields = {
+        "name": (FType.STR, APPLICATION_RAW_ENUM_FIELD_NAME),
+        "namespace": (FType.STR, APPLICATION_RAW_ENUM_FIELD_NAMESPACE),
+        "service": (FType.STR, APPLICATION_RAW_ENUM_FIELD_SERVICE),
+        "version": (FType.STR, APPLICATION_RAW_ENUM_FIELD_VERSION),
+    }
+
+    def __init__(self):
+        super().__init__(rawApplicationFilters, rawApplicationFilterAnd, rawApplicationFilterField)
+
+    def _build_field(self, field: Any) -> Message:
+        return ApplicationField(application_field=ApplicationRawField(field=field))
diff --git a/packages/python/src/armonik/common/filter/_message_types.py b/packages/python/src/armonik/common/filter/_message_types.py
new file mode 100644
index 000000000..d659c5407
--- /dev/null
+++ b/packages/python/src/armonik/common/filter/_message_types.py
@@ -0,0 +1,62 @@
+from typing import Union, Any, Type, TYPE_CHECKING, TypeVar
+
+from google.protobuf.message import Message
+
+"""
+This file allows for better typing and autocompletion support
+"""
+
+SimpleOperator = int
+
+
+class BasicMessage(Message):
+    """
+    Message type that requires a "field" kwarg
+    """
+
+    def __init__(self, *, field, **kwargs):
+        self.field = field
+        for k, v in kwargs.items():
+            setattr(self, k, v)
+
+
+if TYPE_CHECKING:
+    BasicMessageType = Union[Type[BasicMessage], Type[Message]]
+else:
+    BasicMessageType = BasicMessage
+
+
+class CombinationMessage(Message):
+    """
+    Message type for the ands and ors
+    """
+
+    def __init__(self, **kwargs):
+        for k, v in kwargs.items():
+            setattr(self, k, v)
+
+
+if TYPE_CHECKING:
+    CombinationMessageType = Union[Type[CombinationMessage], Type[Message]]
+    DisjunctionType = TypeVar("DisjunctionType", bound=CombinationMessageType)
+    ConjunctionType = TypeVar("ConjunctionType", bound=CombinationMessageType)
+else:
+    CombinationMessageType = CombinationMessage
+    DisjunctionType = CombinationMessageType
+    ConjunctionType = CombinationMessageType
+
+
+class InnerMessage(Message):
+    """
+    Message type for the value and operator
+    """
+
+    def __init__(self, *, value: Any, operator: SimpleOperator):
+        self.value = value
+        self.operator = operator
+
+
+if TYPE_CHECKING:
+    InnerMessageType = Union[Type[InnerMessage], Type[Message]]
+else:
+    InnerMessageType = InnerMessage
diff --git a/packages/python/src/armonik/common/filter/filter.py b/packages/python/src/armonik/common/filter/filter.py
new file mode 100644
index 000000000..9a029fdf0
--- /dev/null
+++ b/packages/python/src/armonik/common/filter/filter.py
@@ -0,0 +1,1042 @@
+from __future__ import annotations
+
+import json
+from abc import abstractmethod, ABC
+from datetime import datetime, timedelta
+from enum import Enum, auto
+from typing import Optional, Union, Any, List, Dict, Generic, TypeVar, overload, Type, Tuple
+
+# noinspection PyUnresolvedReferences
+from google.protobuf.duration_pb2 import Duration
+from google.protobuf.message import Message
+
+# noinspection PyUnresolvedReferences
+from google.protobuf.timestamp_pb2 import Timestamp
+
+from ._message_types import (
+    BasicMessageType,
+    InnerMessageType,
+    SimpleOperator,
+    DisjunctionType,
+    ConjunctionType,
+)
+from ..helpers import datetime_to_timestamp, timedelta_to_duration
+from ...protogen.common.filters_common_pb2 import (
+    FILTER_STRING_OPERATOR_EQUAL,
+    FILTER_STRING_OPERATOR_NOT_EQUAL,
+    FILTER_STRING_OPERATOR_CONTAINS,
+    FILTER_STRING_OPERATOR_NOT_CONTAINS,
+    FilterString,
+    FILTER_STRING_OPERATOR_STARTS_WITH,
+    FILTER_STRING_OPERATOR_ENDS_WITH,
+    FILTER_STATUS_OPERATOR_EQUAL,
+    FILTER_STATUS_OPERATOR_NOT_EQUAL,
+    FILTER_DATE_OPERATOR_EQUAL,
+    FILTER_DATE_OPERATOR_NOT_EQUAL,
+    FILTER_DATE_OPERATOR_BEFORE,
+    FILTER_DATE_OPERATOR_BEFORE_OR_EQUAL,
+    FILTER_DATE_OPERATOR_AFTER,
+    FILTER_DATE_OPERATOR_AFTER_OR_EQUAL,
+    FILTER_NUMBER_OPERATOR_EQUAL,
+    FILTER_NUMBER_OPERATOR_NOT_EQUAL,
+    FILTER_NUMBER_OPERATOR_LESS_THAN,
+    FILTER_NUMBER_OPERATOR_LESS_THAN_OR_EQUAL,
+    FILTER_NUMBER_OPERATOR_GREATER_THAN,
+    FILTER_NUMBER_OPERATOR_GREATER_THAN_OR_EQUAL,
+    FilterDate,
+    FilterNumber,
+    FILTER_BOOLEAN_OPERATOR_IS,
+    FilterBoolean,
+    FILTER_ARRAY_OPERATOR_CONTAINS,
+    FILTER_ARRAY_OPERATOR_NOT_CONTAINS,
+    FilterArray,
+    FILTER_DURATION_OPERATOR_EQUAL,
+    FILTER_DURATION_OPERATOR_NOT_EQUAL,
+    FILTER_DURATION_OPERATOR_SHORTER_THAN,
+    FILTER_DURATION_OPERATOR_SHORTER_THAN_OR_EQUAL,
+    FILTER_DURATION_OPERATOR_LONGER_THAN,
+    FILTER_DURATION_OPERATOR_LONGER_THAN_OR_EQUAL,
+    FilterDuration,
+)
+
+
+class FilterError(ValueError):
+    """
+    Exception raised when there is an error related to filter
+    """
+
+    def __init__(self, filter_instance: Optional[Filter], message: str):
+        """
+
+        Args:
+            filter_instance: Instance of the filter
+            message: Accompanying message
+        """
+        self.filter = filter_instance
+        self.message = message
+
+    def __str__(self):
+        return f"Filter: {str(self.filter)} Error: {self.message}"
+
+
+eq_special = object()
+ne_special = object()
+
+
+class Filter:
+    """
+    Base class of all filters.
+    How to use it :
+    A filter should not be created directly by the end user. Instead, users should use the class static properties to indicate what filter they want to use.
+    For example, if a user wants to list tasks that are in session "xxx", then they should use :
+    Task.session_id == "xxx"
+    Users can combine filters using the binary operators (& | ~).
+
+    When combining filters together, the new filter combination is always kept in the disjunctive normal form to be compatible with ArmoniK.
+    """
+
+    eq_ = eq_special  # Special case
+    ne_ = ne_special  # Special case
+    lt_ = None
+    le_ = None
+    gt_ = None
+    ge_ = None
+    contains_ = None
+    notcontains_ = None
+    value_type_ = None
+    inner_message_type_attr_ = None
+
+    def __init__(
+        self,
+        field: Optional[Message],
+        disjunction_message_type: DisjunctionType,
+        conjunction_message_type: ConjunctionType,
+        message_type: Union[BasicMessageType, DisjunctionType],
+        inner_message_type: Optional[InnerMessageType],
+        operator: Union[SimpleOperator, None] = None,
+        value: Any = None,
+    ):
+        """
+        Init a new filter
+        Notes: This class should not be instanced directly. Always use the proxies in armonik.common.filter
+        Args:
+            field: gRPC Message field
+            disjunction_message_type: Message type of the logical OR
+            conjunction_message_type: Message type of the logical AND
+            message_type: Filter gRPC message type
+            inner_message_type: Filter type gRPC message type
+            operator: Filter operator
+            value: Value of the filter
+        """
+        self.field = field
+        self.disjunction_message_type = disjunction_message_type
+        self.conjunction_message_type = conjunction_message_type
+        self.message_type = message_type
+        self.inner_message_type = inner_message_type
+        self.operator = operator
+        self.value = value
+
+    def _disjunction(self, filters: List[List[Filter]]) -> Filter:
+        """
+        Creates a disjunction from the current Filter types and a list of lists of filter
+        Args:
+            filters: List of Lists of Filters (OR of ANDs)
+
+        Returns:
+            New Filter instance as disjunction
+        """
+        return Filter(
+            None,
+            self.disjunction_message_type,
+            self.conjunction_message_type,
+            self.disjunction_message_type,
+            None,
+            None,
+            filters,
+        )
+
+    def _check_compatible(self, other: Filter):
+        """
+        Checks if the given Filter is compatible with the current Filter. Raises ValueError if they are not compatible.
+        Args:
+            other: Other Filter
+
+        Returns:
+            None
+        """
+        if not isinstance(other, Filter):
+            msg = (
+                f"{str(self)} is not compatible with " f"{str(other)} as the latter is not a Filter"
+            )
+            raise ValueError(msg)
+        if not (
+            self.disjunction_message_type == other.disjunction_message_type
+            and self.conjunction_message_type == other.conjunction_message_type
+        ):
+            msg = (
+                f"{str(self)} is not compatible with "
+                f"{str(other)} as they apply to different object types"
+            )
+            raise ValueError(msg)
+
+    def _is_empty(self) -> bool:
+        """
+        Checks if the current Filter is empty
+        Returns:
+            True if the current Filter is empty
+        """
+        return self.value is None or (
+            isinstance(self.value, list)
+            and (
+                len(self.value) == 0
+                or (isinstance(self.value[0], list) and len(self.value[0]) == 0)
+            )
+        )
+
+    def _is_disjunction(self) -> bool:
+        """
+        Checks if the current Filter is a disjunction
+        Returns:
+            True if the current Filter is a disjunction
+        """
+        return self.message_type == self.disjunction_message_type
+
+    def to_disjunction(self) -> Filter:
+        """
+        Wraps the current Filter into a disjunction. Does nothing if the current Filter is already a disjunction.
+        Returns:
+            Disjunction Filter
+        Raises:
+            FilterError if the filter is empty
+        """
+        if self._is_empty():
+            raise FilterError(self, "Empty filter")
+        if isinstance(self.operator, SimpleOperator):
+            return self._disjunction([[self]])
+        return self
+
+    def __and__(self, other: Filter) -> Filter:
+        """
+        Logical and
+        """
+        self._check_compatible(other)
+        # (a | (b&c)) & (d | (e&f)) = a&d | a&e&f | b&c&d | b&c&e&f
+        return self._disjunction(
+            [c1 + c2 for c2 in other.to_disjunction().value for c1 in self.to_disjunction().value]
+        )
+
+    def __mul__(self, other: Filter) -> Filter:
+        """
+        Same as A & B
+        """
+        return self & other
+
+    def __or__(self, other: Filter) -> Filter:
+        """
+        Logical or
+        """
+        self._check_compatible(other)
+        # (a | b&c) | (d | e&f) = a | b&c | d | e&f
+        return self._disjunction(self.to_disjunction().value + other.to_disjunction().value)
+
+    def __add__(self, other: Filter) -> Filter:
+        """
+        Same as Logical or
+        """
+        return self | other
+
+    def __eq__(self, value) -> Filter:
+        """
+        Creates a Filter which checks for the equality of the property compared to the value
+        Args:
+            value: Value to check against
+
+        Returns:
+            New Filter instance
+        """
+        return self._change_operation(value, self.__class__.eq_, "==")
+
+    def __ne__(self, value) -> Filter:
+        """
+        Creates a Filter which checks for the inequality of the property compared to the value
+        Args:
+            value: Value to check against
+
+        Returns:
+            New Filter instance
+        """
+        return self._change_operation(value, self.__class__.ne_, "!=")
+
+    def __lt__(self, value) -> Filter:
+        """
+        Creates a Filter which checks that the object's value is less than the value
+        Args:
+            value: Value to check against
+
+        Returns:
+            New Filter instance
+        """
+        return self._change_operation(value, self.__class__.lt_, "<")
+
+    def __le__(self, value) -> Filter:
+        """
+        Creates a Filter which checks that the object's value is less than or equal to the value
+        Args:
+            value: Value to check against
+
+        Returns:
+            New Filter instance
+        """
+        return self._change_operation(value, self.__class__.le_, "<=")
+
+    def __gt__(self, value) -> Filter:
+        """
+        Creates a Filter which checks that the object's value is greater than to the value
+        Args:
+            value: Value to check against
+
+        Returns:
+            New Filter instance
+        """
+        return self._change_operation(value, self.__class__.gt_, ">")
+
+    def __ge__(self, value) -> Filter:
+        """
+        Creates a Filter which checks that the object's value is greater than or equal to the value
+        Args:
+            value: Value to check against
+
+        Returns:
+            New Filter instance
+        """
+        return self._change_operation(value, self.__class__.ge_, ">=")
+
+    def contains(self, value) -> Filter:
+        """
+        Creates a Filter which checks that the object's value contains the value
+        Args:
+            value: Value to check against
+
+        Returns:
+            New Filter instance
+        """
+        return self._change_operation(value, self.__class__.contains_, "contains")
+
+    def __invert__(self) -> Filter:
+        """
+        Logical not
+        """
+        if self.operator is None:
+            # No operator
+            if self._is_empty():
+                raise FilterError(self, "Cannot invert filter without a value or operator")
+            try:
+                # The filter is a combination
+                # ~(a | (b & c)) => ~a & (~b | ~c) => ~a&~b | ~a&~c
+                new_filter = self._disjunction([[~c] for c in self.value[0]])
+                for conj in self.value[1:]:
+                    new_filter &= self._disjunction([[~c] for c in conj])
+                return new_filter
+            except FilterError as e:
+                msg = f"Could not invert because of an error in subfilter inversion : {str(e)}"
+                raise FilterError(self, msg)
+        # Invert the operation
+        if self.operator == self.__class__.eq_:
+            return self._change_operation(self.value, self.__class__.ne_, "!=", True)
+        if self.operator == self.__class__.ne_:
+            return self._change_operation(self.value, self.__class__.eq_, "==", True)
+        if self.operator == self.__class__.lt_:
+            return self._change_operation(self.value, self.__class__.ge_, ">=", True)
+        if self.operator == self.__class__.le_:
+            return self._change_operation(self.value, self.__class__.gt_, ">", True)
+        if self.operator == self.__class__.gt_:
+            return self._change_operation(self.value, self.__class__.le_, "<=", True)
+        if self.operator == self.__class__.ge_:
+            return self._change_operation(self.value, self.__class__.lt_, "<", True)
+        if self.operator == self.__class__.contains_:
+            return self._change_operation(
+                self.value, self.__class__.notcontains_, "not_contains", True
+            )
+        if self.operator == self.__class__.notcontains_:
+            return self._change_operation(self.value, self.__class__.contains_, "contains", True)
+        msg = f"{self.__class__.__name__} operator {str(self.operator)} for field {str(self.field)} has no inverted equivalent"
+        raise FilterError(self, msg)
+
+    def __neg__(self) -> Filter:
+        """
+        Same as Logical not
+        """
+        return ~self
+
+    def __xor__(self, other: Filter) -> Filter:
+        """
+        Logical xor
+        """
+        return (self & (~other)) | ((~self) & other)
+
+    def _sanitize_value(self, value: Any) -> Any:
+        """
+        Takes an input value and sanitizes it to be compatible with the filter.
+        If the type is incompatible, throws a FilterError
+        Args:
+            value: Value to be sanitized
+
+        Returns:
+            Sanitized value
+        """
+        if self.__class__.value_type_ is None or isinstance(value, self.__class__.value_type_):
+            return value
+        msg = f"Expected value type {str(self.__class__.value_type_)} for field {str(self.field)}, got {str(type(value))} instead"
+        raise FilterError(None, msg)
+
+    def _change_operation(
+        self, value: Any, operator: Any, operator_str: str = "", override: bool = False
+    ) -> Filter:
+        """
+        Internal function to create a new filter from the current filter with a different value and/or operator
+        Args:
+            value: Value of the new filter
+            operator: Operator of the new filter
+            operator_str: Optional string for error message clarification
+            override: If set to true, bypasses the check for reassignment
+        Returns:
+            new filter with the given value/operator
+
+        Raises:
+            FilterError if the given operator is not available for the given class
+        """
+        if not override:
+            if self.value is not None:
+                if isinstance(value, bool):
+                    if operator is self.__class__.eq_ and self.__class__.eq_ is not None:
+                        return self if value else ~self
+                    if operator is self.__class__.ne_ and self.__class__.ne_ is not None:
+                        return ~self if value else self
+                if self._is_disjunction():
+                    raise FilterError(self, "Cannot apply operator to a filter combination")
+                raise FilterError(self, "Cannot apply operator to an already defined filter")
+        if operator is None or self._is_disjunction():
+            msg = f"Operator {operator_str} is not available for {self.__class__.__name__}"
+            raise FilterError(self, msg)
+        return self.__class__(
+            self.field,
+            self.disjunction_message_type,
+            self.conjunction_message_type,
+            self.message_type,
+            self.inner_message_type,
+            operator,
+            self._sanitize_value(value),
+        )
+
+    def to_dict(self) -> Dict[str, Any]:
+        """
+        Transforms the filter in a dictionary describing it
+        Returns:
+            Dictionary representing the filter in a disjunctive normal form
+        """
+        if not self._is_disjunction() or self.value is None:
+            return {
+                "field": str(self.field),
+                "value": str(self.value),
+                "operator": str(self.operator),
+            }
+        if len(self.value) > 1:
+            return {
+                "or": [
+                    ({"and": [c.to_dict() for c in conj]} if len(conj) > 1 else conj[0].to_dict())
+                    for conj in self.value
+                ]
+            }
+        return (
+            (
+                {"and": [c.to_dict() for c in self.value[0]]}
+                if len(self.value[0]) > 1
+                else self.value[0][0].to_dict()
+            )
+            if not self._is_empty()
+            else {}
+        )
+
+    def __str__(self) -> str:
+        return json.dumps(self.to_dict())
+
+    def to_basic_message(self) -> BasicMessageType:
+        """
+        Converts the filter into its base gRPC message
+        Returns:
+            Base gRPC message
+        """
+        if self.__class__.inner_message_type_attr_ is None:
+            raise FilterError(
+                self,
+                "Cannot use an empty filter in combination with others. "
+                "Did you forget to write the condition ?",
+            )
+        return self.message_type(
+            field=self.field,
+            **{
+                self.__class__.inner_message_type_attr_: self.inner_message_type(
+                    value=self.value, operator=self.operator
+                )
+            },
+        )
+
+    def to_message(self) -> DisjunctionType:
+        """
+        Converts the filter into its gRPC disjunctive message
+        Returns:
+            Disjunctive gRPC message
+        """
+        if not self._is_disjunction():
+            # Convert the message to a disjunction if it's not the case
+            return self.to_disjunction().to_message()
+
+        def to_conjunction_message(conj: List[Filter]) -> ConjunctionType:
+            conj_raw = self.conjunction_message_type()
+            # Need to use getattr because and is a reserved name
+            getattr(conj_raw, "and").extend(f.to_basic_message() for f in conj)
+            return conj_raw
+
+        raw = self.disjunction_message_type()
+        # Need to use getattr because or is a reserved name
+        getattr(raw, "or").extend(
+            (to_conjunction_message(conj) for conj in self.value) if self.value is not None else []
+        )
+        return raw
+
+    def __bool__(self):
+        raise FilterError(
+            self,
+            "Filters cannot be transformed into booleans. "
+            "You may see this error if you try to combine filters with 'or', 'and', 'in', or 'not'. "
+            "Use '|', '&', '.contains' or '~' instead.",
+        )
+
+
+class StringFilter(Filter):
+    """
+    Filter for string comparisons
+    """
+
+    eq_ = FILTER_STRING_OPERATOR_EQUAL
+    ne_ = FILTER_STRING_OPERATOR_NOT_EQUAL
+    contains_ = FILTER_STRING_OPERATOR_CONTAINS
+    notcontains_ = FILTER_STRING_OPERATOR_NOT_CONTAINS
+    value_type_ = str
+    inner_message_type_attr_ = "filter_string"
+
+    def __init__(
+        self,
+        field: Optional[Message],
+        disjunction_message_type: DisjunctionType,
+        conjunction_message_type: ConjunctionType,
+        message_type: Union[BasicMessageType, DisjunctionType],
+        inner_message_type: Optional[InnerMessageType] = FilterString,
+        operator: Union[SimpleOperator, None] = None,
+        value: Any = None,
+    ):
+        super().__init__(
+            field,
+            disjunction_message_type,
+            conjunction_message_type,
+            message_type,
+            inner_message_type,
+            operator,
+            value,
+        )
+
+    def startswith(self, value: str) -> "StringFilter":
+        """
+        Creates a Filter which checks that the object's value starts with the value
+        Args:
+            value: Value to check against
+
+        Returns:
+            New Filter instance
+        """
+        return self._change_operation(value, FILTER_STRING_OPERATOR_STARTS_WITH, "startswith")
+
+    def endswith(self, value: str) -> "StringFilter":
+        """
+        Creates a Filter which checks that the object's value ends with the value
+        Args:
+            value: Value to check against
+
+        Returns:
+            New Filter instance
+        """
+        return self._change_operation(value, FILTER_STRING_OPERATOR_ENDS_WITH, "endswith")
+
+    def __repr__(self) -> str:
+        return f'{str(self.field)} {str(self.operator)} "{str(self.value)}"'
+
+
+class StatusFilter(Filter):
+    """
+    Filter for status comparison
+    """
+
+    eq_ = FILTER_STATUS_OPERATOR_EQUAL
+    ne_ = FILTER_STATUS_OPERATOR_NOT_EQUAL
+    inner_message_type_attr_ = "filter_status"
+
+    def __init__(
+        self,
+        field: Optional[Message],
+        disjunction_message_type: DisjunctionType,
+        conjunction_message_type: ConjunctionType,
+        message_type: Union[BasicMessageType, DisjunctionType],
+        inner_message_type: InnerMessageType,
+        operator: Union[SimpleOperator, None] = None,
+        value: Any = None,
+    ):
+        super().__init__(
+            field,
+            disjunction_message_type,
+            conjunction_message_type,
+            message_type,
+            inner_message_type,
+            operator,
+            value,
+        )
+
+
+class DateFilter(Filter):
+    """
+    Filter for timestamp comparison
+    """
+
+    eq_ = FILTER_DATE_OPERATOR_EQUAL
+    ne_ = FILTER_DATE_OPERATOR_NOT_EQUAL
+    lt_ = FILTER_DATE_OPERATOR_BEFORE
+    le_ = FILTER_DATE_OPERATOR_BEFORE_OR_EQUAL
+    gt_ = FILTER_DATE_OPERATOR_AFTER
+    ge_ = FILTER_DATE_OPERATOR_AFTER_OR_EQUAL
+    inner_message_type_attr_ = "filter_date"
+
+    def __init__(
+        self,
+        field: Optional[Message],
+        disjunction_message_type: DisjunctionType,
+        conjunction_message_type: ConjunctionType,
+        message_type: Union[BasicMessageType, DisjunctionType],
+        inner_message_type: InnerMessageType = FilterDate,
+        operator: Union[SimpleOperator, None] = None,
+        value: Any = None,
+    ):
+        super().__init__(
+            field,
+            disjunction_message_type,
+            conjunction_message_type,
+            message_type,
+            inner_message_type,
+            operator,
+            value,
+        )
+
+    def _sanitize_value(self, value: Any) -> Any:
+        if isinstance(value, datetime):
+            return datetime_to_timestamp(value)
+        if isinstance(value, Timestamp):
+            return value
+        msg = (
+            f"Expected value type {datetime.__class__.__name__} or {Timestamp.__class__.__name__}"
+            f"for field {str(self.field)}, got {str(type(value))} instead"
+        )
+        raise FilterError(self, msg)
+
+
+class NumberFilter(Filter):
+    """
+    Filter for int comparison
+    """
+
+    eq_ = FILTER_NUMBER_OPERATOR_EQUAL
+    ne_ = FILTER_NUMBER_OPERATOR_NOT_EQUAL
+    lt_ = FILTER_NUMBER_OPERATOR_LESS_THAN
+    le_ = FILTER_NUMBER_OPERATOR_LESS_THAN_OR_EQUAL
+    gt_ = FILTER_NUMBER_OPERATOR_GREATER_THAN
+    ge_ = FILTER_NUMBER_OPERATOR_GREATER_THAN_OR_EQUAL
+    value_type_ = int
+    inner_message_type_attr_ = "filter_number"
+
+    def __init__(
+        self,
+        field: Optional[Message],
+        disjunction_message_type: DisjunctionType,
+        conjunction_message_type: ConjunctionType,
+        message_type: Union[BasicMessageType, DisjunctionType],
+        inner_message_type: InnerMessageType = FilterNumber,
+        operator: Union[SimpleOperator, None] = None,
+        value: Any = None,
+    ):
+        super().__init__(
+            field,
+            disjunction_message_type,
+            conjunction_message_type,
+            message_type,
+            inner_message_type,
+            operator,
+            value,
+        )
+
+
+class BooleanFilter(Filter):
+    """
+    Filter for boolean comparison
+    """
+
+    eq_ = FILTER_BOOLEAN_OPERATOR_IS
+    value_type_ = bool
+    inner_message_type_attr_ = "filter_boolean"
+
+    def __init__(
+        self,
+        field: Optional[Message],
+        disjunction_message_type: DisjunctionType,
+        conjunction_message_type: ConjunctionType,
+        message_type: Union[BasicMessageType, DisjunctionType],
+        inner_message_type: InnerMessageType = FilterBoolean,
+        operator: Union[SimpleOperator, None] = FILTER_BOOLEAN_OPERATOR_IS,
+        value: Any = True,
+    ):
+        super().__init__(
+            field,
+            disjunction_message_type,
+            conjunction_message_type,
+            message_type,
+            inner_message_type,
+            operator,
+            value,
+        )
+
+    def __ne__(self, value: bool) -> BooleanFilter:
+        return self._change_operation(not value, self.__class__.eq_, "!=", True)
+
+    def __invert__(self) -> BooleanFilter:
+        return self._change_operation(not self.value, self.__class__.eq_, "==", True)
+
+
+class ArrayFilter(Filter):
+    """
+    Filter for array comparisons
+    """
+
+    contains_ = FILTER_ARRAY_OPERATOR_CONTAINS
+    notcontains_ = FILTER_ARRAY_OPERATOR_NOT_CONTAINS
+    value_type_ = str
+    inner_message_type_attr_ = "filter_array"
+
+    def __init__(
+        self,
+        field: Optional[Message],
+        disjunction_message_type: DisjunctionType,
+        conjunction_message_type: ConjunctionType,
+        message_type: Union[BasicMessageType, DisjunctionType],
+        inner_message_type: InnerMessageType = FilterArray,
+        operator: Union[SimpleOperator, None] = None,
+        value: Any = None,
+    ):
+        super().__init__(
+            field,
+            disjunction_message_type,
+            conjunction_message_type,
+            message_type,
+            inner_message_type,
+            operator,
+            value,
+        )
+
+
+class DurationFilter(Filter):
+    """
+    Filter for duration comparison
+    """
+
+    eq_ = FILTER_DURATION_OPERATOR_EQUAL
+    ne_ = FILTER_DURATION_OPERATOR_NOT_EQUAL
+    lt_ = FILTER_DURATION_OPERATOR_SHORTER_THAN
+    le_ = FILTER_DURATION_OPERATOR_SHORTER_THAN_OR_EQUAL
+    gt_ = FILTER_DURATION_OPERATOR_LONGER_THAN
+    ge_ = FILTER_DURATION_OPERATOR_LONGER_THAN_OR_EQUAL
+    value_type_ = Duration
+    inner_message_type_attr_ = "filter_duration"
+
+    def __init__(
+        self,
+        field: Optional[Message],
+        disjunction_message_type: DisjunctionType,
+        conjunction_message_type: ConjunctionType,
+        message_type: Union[BasicMessageType, DisjunctionType],
+        inner_message_type: InnerMessageType = FilterDuration,
+        operator: Union[SimpleOperator, None] = None,
+        value: Any = None,
+    ):
+        super().__init__(
+            field,
+            disjunction_message_type,
+            conjunction_message_type,
+            message_type,
+            inner_message_type,
+            operator,
+            value,
+        )
+
+    def _sanitize_value(self, value: Any) -> Any:
+        if isinstance(value, timedelta):
+            return timedelta_to_duration(value)
+        if isinstance(value, Duration):
+            return value
+        msg = (
+            f"Expected value type {timedelta.__class__.__name__} or {Duration.__class__.__name__}"
+            f"for field {str(self.field)}, got {str(type(value))} instead"
+        )
+        raise FilterError(self, msg)
+
+
+class FType(Enum):
+    """
+    Enum for the type of a filter. Used internally.
+    """
+
+    UNKNOWN = auto()
+    NA = auto()
+    NUM = auto()
+    STR = auto()
+    ARRAY = auto()
+    DURATION = auto()
+    DATE = auto()
+    STATUS = auto()
+    BOOL = auto()
+
+
+def _raise(field):
+    """
+    The field is unknown in the class
+    Args:
+        field: Unknown field
+
+    Returns:
+
+    """
+    msg = f"Unknown field {field}"
+    raise ValueError(msg)
+
+
+def _na(field):
+    msg = f"Field {field} is not available as a filter"
+    raise FilterError(None, msg)
+
+
+class FilterWrapper(Filter, ABC):
+    """
+    Wraps the filter creation to alleviate repetitions
+    """
+
+    _fields: Dict[str, Tuple[FType, Optional[Any]]] = {}
+
+    def __init__(
+        self,
+        disjunction_message_type: DisjunctionType,
+        conjunction_message_type: ConjunctionType,
+        message_type: BasicMessageType,
+        status_type: Optional[InnerMessageType] = None,
+    ):
+        super().__init__(
+            None,
+            disjunction_message_type,
+            conjunction_message_type,
+            disjunction_message_type,
+            status_type,
+        )
+        self._vtable = {
+            FType.UNKNOWN: _raise,
+            FType.NA: _na,
+            FType.NUM: self._number,
+            FType.STR: self._string,
+            FType.ARRAY: self._array,
+            FType.DURATION: self._duration,
+            FType.DATE: self._date,
+            FType.STATUS: self._status,
+            FType.BOOL: self._bool,
+        }
+        self.basic_message_type = message_type
+
+    def _string(self, field: Message) -> StringFilter:
+        """
+        Creates a new string filter on the given field
+        Args:
+            field: field to filter against
+
+        Returns:
+            New string filter on the given field
+        """
+        return StringFilter(
+            field,
+            self.disjunction_message_type,
+            self.conjunction_message_type,
+            self.basic_message_type,
+        )
+
+    def _bool(self, field: Message) -> BooleanFilter:
+        """
+        Creates a new boolean filter on the given field
+        Args:
+            field: field to filter against
+
+        Returns:
+            New boolean filter on the given field
+        """
+        return BooleanFilter(
+            field,
+            self.disjunction_message_type,
+            self.conjunction_message_type,
+            self.basic_message_type,
+        )
+
+    def _array(self, field: Message) -> ArrayFilter:
+        """
+        Creates a new array filter on the given field
+        Args:
+            field: field to filter against
+
+        Returns:
+            New array filter on the given field
+        """
+        return ArrayFilter(
+            field,
+            self.disjunction_message_type,
+            self.conjunction_message_type,
+            self.basic_message_type,
+        )
+
+    def _duration(self, field: Message) -> DurationFilter:
+        """
+        Creates a new duration filter on the given field
+        Args:
+            field: field to filter against
+
+        Returns:
+            New duration filter on the given field
+
+        """
+        return DurationFilter(
+            field,
+            self.disjunction_message_type,
+            self.conjunction_message_type,
+            self.basic_message_type,
+        )
+
+    def _date(self, field: Message) -> DateFilter:
+        """
+        Creates a new date filter on the given field
+        Args:
+            field:  field to filter against
+
+        Returns:
+            New date filter on the given field
+
+        """
+        return DateFilter(
+            field,
+            self.disjunction_message_type,
+            self.conjunction_message_type,
+            self.basic_message_type,
+        )
+
+    def _number(self, field: Message) -> NumberFilter:
+        """
+        Creates a new number filter on the given field
+        Args:
+            field:  field to filter against
+
+        Returns:
+            New number filter on the given field
+
+        """
+        return NumberFilter(
+            field,
+            self.disjunction_message_type,
+            self.conjunction_message_type,
+            self.basic_message_type,
+        )
+
+    def _status(self, field: Message) -> StatusFilter:
+        """
+        Creates a new status filter on the given field
+        Args:
+            field:  field to filter against
+
+        Returns:
+            New status filter on the given field
+
+        """
+        return StatusFilter(
+            field,
+            self.disjunction_message_type,
+            self.conjunction_message_type,
+            self.basic_message_type,
+            self.inner_message_type,
+        )
+
+    @abstractmethod
+    def _build_field(self, field: Any) -> Message:
+        """
+        Build the gRPC field object
+        Args:
+            field: base field enum value
+
+        Returns:
+            Field message
+        """
+        ...
+
+    def __call__(self, field_name: str) -> Filter:
+        """
+        Get the Filter associated with the field name
+        Args:
+            field_name: Field name
+
+        Returns:
+            Filter associated with the field name
+        """
+        ftype, field_value = self.__class__._fields.get(field_name, (FType.UNKNOWN, field_name))
+        return self._vtable[ftype](
+            self._build_field(field_value)
+            if ftype not in (FType.UNKNOWN, FType.NA)
+            else field_value
+        )
+
+
+AttributeType = TypeVar("AttributeType")
+
+
+class FilterDescriptor(Generic[AttributeType]):
+    """
+    Descriptor used to declare a filter
+    """
+
+    def __init__(self, wrapper: FilterWrapper):
+        self.wrapper = wrapper
+
+    def __set_name__(self, owner: Type, name: str):
+        self.name = name
+        self._name = "_" + name
+
+    @overload
+    def __get__(self, instance: None, owner: Type) -> Filter: ...
+
+    @overload
+    def __get__(self, instance: object, owner: Type) -> AttributeType: ...
+
+    def __get__(
+        self, instance: Optional[object], owner: Optional[Type]
+    ) -> Union[Filter, AttributeType]:
+        # When called statically, returns the filter
+        if instance is None:
+            return self.wrapper(self.name)
+        # Otherwise gets the object's hidden property
+        return getattr(instance, self._name)
+
+    def __set__(self, instance: object, value: AttributeType):
+        setattr(instance, self._name, value)
diff --git a/packages/python/src/armonik/common/objects.py b/packages/python/src/armonik/common/objects.py
index 4bac86ce0..3eacb0eca 100644
--- a/packages/python/src/armonik/common/objects.py
+++ b/packages/python/src/armonik/common/objects.py
@@ -6,6 +6,20 @@
 
 from deprecation import deprecated
 
+from ..protogen.common.applications_common_pb2 import ApplicationRaw
+from ..protogen.common.tasks_common_pb2 import TaskDetailed
+from .filter import (
+    FilterDescriptor,
+    GenericTaskOptionsFilter,
+    TaskFilter,
+    TaskOptionFilter,
+    SessionTaskOptionFilter,
+    OutputFilter,
+    SessionFilter,
+    ResultFilter,
+    PartitionFilter,
+    ApplicationFilter,
+)
 from ..protogen.common.objects_pb2 import (
     Empty,
 )
@@ -20,8 +34,6 @@
 from ..protogen.common.results_common_pb2 import ResultRaw
 from ..protogen.common.session_status_pb2 import SessionStatus as RawSessionStatus
 from ..protogen.common.sessions_common_pb2 import SessionRaw
-from ..protogen.common.task_status_pb2 import TaskStatus as RawTaskStatus
-from ..protogen.common.tasks_common_pb2 import TaskDetailed
 from .enumwrapper import ResultStatus, SessionStatus, TaskStatus
 from .helpers import duration_to_timedelta, timedelta_to_duration, timestamp_to_datetime
 
@@ -96,41 +108,116 @@ def __post_init__(self):
             raise ValueError("expected_output_ids must be not be empty")
 
 
-@dataclass()
-class Task:
-    id: Optional[str] = None
-    session_id: Optional[str] = None
-    owner_pod_id: Optional[str] = None
-
-    initial_task_id: Optional[str] = None
-    parent_task_ids: List[str] = field(default_factory=list)
-    data_dependencies: List[str] = field(default_factory=list)
-    expected_output_ids: List[str] = field(default_factory=list)
-    retry_of_ids: List[str] = field(default_factory=list)
+_task_filter = TaskFilter()
 
-    status: RawTaskStatus = TaskStatus.UNSPECIFIED
-    status_message: Optional[str] = None
 
-    options: Optional[TaskOptions] = None
-    created_at: Optional[datetime] = None
-    submitted_at: Optional[datetime] = None
-    received_at: Optional[datetime] = None
-    acquired_at: Optional[datetime] = None
-    fetched_at: Optional[datetime] = None
-    started_at: Optional[datetime] = None
-    processed_at: Optional[datetime] = None
-    ended_at: Optional[datetime] = None
-    pod_ttl: Optional[datetime] = None
+class TaskOptionDescriptor(FilterDescriptor):
+    def __get__(self, instance, owner) -> GenericTaskOptionsFilter:
+        # When called statically, returns the Task option filter
+        if instance is None:
+            return TaskOptionFilter() if issubclass(owner, Task) else SessionTaskOptionFilter()
+        # Otherwise gets the object's hidden property
+        return getattr(instance, self._name)
 
-    creation_to_end_duration: Optional[timedelta] = timedelta(0)
-    processing_to_end_duration: Optional[timedelta] = timedelta(0)
-    received_to_end_duration: Optional[timedelta] = timedelta(0)
 
-    output: Optional[Output] = None
+class OutputDescriptor(FilterDescriptor):
+    def __get__(self, instance, owner) -> OutputFilter:
+        # When called statically, returns the Task option filter
+        if instance is None:
+            return OutputFilter()
+        # Otherwise gets the object's hidden property
+        return getattr(instance, self._name)
 
-    pod_hostname: Optional[str] = None
 
-    payload_id: Optional[str] = None
+class Task:
+    id = FilterDescriptor(_task_filter)
+    session_id = FilterDescriptor(_task_filter)
+    owner_pod_id = FilterDescriptor(_task_filter)
+
+    initial_task_id = FilterDescriptor(_task_filter)
+    parent_task_ids = FilterDescriptor(_task_filter)
+    data_dependencies = FilterDescriptor(_task_filter)
+    expected_output_ids = FilterDescriptor(_task_filter)
+    retry_of_ids = FilterDescriptor(_task_filter)
+
+    status = FilterDescriptor(_task_filter)
+    status_message = FilterDescriptor(_task_filter)
+
+    options = TaskOptionDescriptor(_task_filter)
+    created_at = FilterDescriptor(_task_filter)
+    submitted_at = FilterDescriptor(_task_filter)
+    received_at = FilterDescriptor(_task_filter)
+    acquired_at = FilterDescriptor(_task_filter)
+    fetched_at = FilterDescriptor(_task_filter)
+    started_at = FilterDescriptor(_task_filter)
+    processed_at = FilterDescriptor(_task_filter)
+    ended_at = FilterDescriptor(_task_filter)
+    pod_ttl = FilterDescriptor(_task_filter)
+
+    creation_to_end_duration = FilterDescriptor(_task_filter)
+    processing_to_end_duration = FilterDescriptor(_task_filter)
+    received_to_end_duration = FilterDescriptor(_task_filter)
+
+    output = OutputDescriptor(_task_filter)
+
+    pod_hostname = FilterDescriptor(_task_filter)
+    payload_id = FilterDescriptor(_task_filter)
+
+    def __init__(
+        self,
+        id: Optional[str] = None,
+        session_id: Optional[str] = None,
+        owner_pod_id: Optional[str] = None,
+        initial_task_id: Optional[str] = None,
+        parent_task_ids: Optional[List[str]] = None,
+        data_dependencies: Optional[List[str]] = None,
+        expected_output_ids: Optional[List[str]] = None,
+        retry_of_ids: Optional[List[str]] = None,
+        status: TaskStatus = TaskStatus.UNSPECIFIED,
+        status_message: Optional[str] = None,
+        options: Optional[TaskOptions] = None,
+        created_at: Optional[datetime] = None,
+        submitted_at: Optional[datetime] = None,
+        received_at: Optional[datetime] = None,
+        acquired_at: Optional[datetime] = None,
+        fetched_at: Optional[datetime] = None,
+        started_at: Optional[datetime] = None,
+        processed_at: Optional[datetime] = None,
+        ended_at: Optional[datetime] = None,
+        pod_ttl: Optional[datetime] = None,
+        creation_to_end_duration: timedelta = timedelta(0),
+        processing_to_end_duration: timedelta = timedelta(0),
+        received_to_end_duration: timedelta = timedelta(0),
+        output: Optional[Output] = None,
+        pod_hostname: Optional[str] = None,
+        payload_id: Optional[str] = None,
+    ):
+        self.id = id
+        self.session_id = session_id
+        self.owner_pod_id = owner_pod_id
+        self.initial_task_id = initial_task_id
+        self.parent_task_ids = parent_task_ids if parent_task_ids is not None else []
+        self.data_dependencies = data_dependencies if data_dependencies is not None else []
+        self.expected_output_ids = expected_output_ids if expected_output_ids is not None else []
+        self.retry_of_ids = retry_of_ids if retry_of_ids is not None else []
+        self.status = status
+        self.status_message = status_message
+        self.options = options
+        self.created_at = created_at
+        self.submitted_at = submitted_at
+        self.received_at = received_at
+        self.acquired_at = acquired_at
+        self.fetched_at = fetched_at
+        self.started_at = started_at
+        self.processed_at = processed_at
+        self.ended_at = ended_at
+        self.pod_ttl = pod_ttl
+        self.creation_to_end_duration = creation_to_end_duration
+        self.processing_to_end_duration = processing_to_end_duration
+        self.received_to_end_duration = received_to_end_duration
+        self.output = output
+        self.pod_hostname = pod_hostname
+        self.payload_id = payload_id
 
     def refresh(self, task_client) -> None:
         """Refresh the fields of this task object by using the given task client
@@ -203,6 +290,36 @@ def from_message(cls, task_raw: TaskDetailed) -> "Task":
             payload_id=task_raw.payload_id,
         )
 
+    def __eq__(self, other: "Task") -> bool:
+        return (
+            self.id == other.id
+            and self.session_id == other.session_id
+            and self.owner_pod_id == other.owner_pod_id
+            and self.initial_task_id == other.initial_task_id
+            and self.parent_task_ids == other.parent_task_ids
+            and self.data_dependencies == other.data_dependencies
+            and self.expected_output_ids == other.expected_output_ids
+            and self.retry_of_ids == other.retry_of_ids
+            and self.status == other.status
+            and self.status_message == other.status_message
+            and self.options == other.options
+            and self.created_at == other.created_at
+            and self.submitted_at == other.submitted_at
+            and self.received_at == other.received_at
+            and self.acquired_at == other.acquired_at
+            and self.fetched_at == other.fetched_at
+            and self.started_at == other.started_at
+            and self.processed_at == other.processed_at
+            and self.ended_at == other.ended_at
+            and self.pod_ttl == other.pod_ttl
+            and self.creation_to_end_duration == other.creation_to_end_duration
+            and self.processing_to_end_duration == other.processing_to_end_duration
+            and self.received_to_end_duration == other.received_to_end_duration
+            and self.output == other.output
+            and self.pod_hostname == other.pod_hostname
+            and self.payload_id == other.payload_id
+        )
+
 
 @deprecated(deprecated_in="3.14.0", details="Use sessions, task and results client instead")
 @dataclass
@@ -213,20 +330,50 @@ def is_available(self) -> bool:
         return len(self.errors) == 0
 
 
-@dataclass
+_sessionFilter = SessionFilter()
+
+
 class Session:
-    session_id: Optional[str] = None
-    status: RawSessionStatus = SessionStatus.UNSPECIFIED
-    client_submission: Optional[bool] = None
-    worker_submission: Optional[bool] = None
-    partition_ids: List[str] = field(default_factory=list)
-    options: Optional[TaskOptions] = None
-    created_at: Optional[datetime] = None
-    cancelled_at: Optional[datetime] = None
-    closed_at: Optional[datetime] = None
-    purged_at: Optional[datetime] = None
-    deleted_at: Optional[datetime] = None
-    duration: Optional[timedelta] = None
+    session_id = FilterDescriptor(_sessionFilter)
+    status = FilterDescriptor(_sessionFilter)
+    client_submission = FilterDescriptor(_sessionFilter)
+    worker_submission = FilterDescriptor(_sessionFilter)
+    partition_ids = FilterDescriptor(_sessionFilter)
+    options = TaskOptionDescriptor(_sessionFilter)
+    created_at = FilterDescriptor(_sessionFilter)
+    cancelled_at = FilterDescriptor(_sessionFilter)
+    closed_at = FilterDescriptor(_sessionFilter)
+    purged_at = FilterDescriptor(_sessionFilter)
+    deleted_at = FilterDescriptor(_sessionFilter)
+    duration = FilterDescriptor(_sessionFilter)
+
+    def __init__(
+        self,
+        session_id: Optional[str] = None,
+        status: RawSessionStatus = SessionStatus.UNSPECIFIED,
+        client_submission: Optional[bool] = None,
+        worker_submission: Optional[bool] = None,
+        partition_ids: Optional[List[str]] = None,
+        options: Optional[TaskOptions] = None,
+        created_at: Optional[datetime] = None,
+        cancelled_at: Optional[datetime] = None,
+        closed_at: Optional[datetime] = None,
+        purged_at: Optional[datetime] = None,
+        deleted_at: Optional[datetime] = None,
+        duration: Optional[timedelta] = None,
+    ):
+        self.session_id = session_id
+        self.status = status
+        self.client_submission = client_submission
+        self.worker_submission = worker_submission
+        self.partition_ids = partition_ids if partition_ids is not None else []
+        self.options = options
+        self.created_at = created_at
+        self.cancelled_at = cancelled_at
+        self.closed_at = closed_at
+        self.purged_at = purged_at
+        self.deleted_at = deleted_at
+        self.duration = duration
 
     @classmethod
     def from_message(cls, session_raw: SessionRaw) -> "Session":
@@ -245,17 +392,55 @@ def from_message(cls, session_raw: SessionRaw) -> "Session":
             duration=duration_to_timedelta(session_raw.duration),
         )
 
+    def __eq__(self, other: "Session") -> bool:
+        return (
+            self.session_id == other.session_id
+            and self.status == other.status
+            and self.client_submission == other.client_submission
+            and self.worker_submission == other.worker_submission
+            and self.partition_ids == other.partition_ids
+            and self.options == other.options
+            and self.created_at == other.created_at
+            and self.cancelled_at == other.cancelled_at
+            and self.closed_at == other.closed_at
+            and self.purged_at == other.purged_at
+            and self.deleted_at == other.deleted_at
+            and self.duration == other.duration
+        )
+
+
+_resultFilter = ResultFilter()
+
 
-@dataclass
 class Result:
-    session_id: Optional[str] = None
-    name: Optional[str] = None
-    owner_task_id: Optional[str] = None
-    status: RawResultStatus = ResultStatus.UNSPECIFIED
-    created_at: Optional[datetime] = None
-    completed_at: Optional[datetime] = None
-    result_id: Optional[str] = None
-    size: Optional[int] = None
+    session_id = FilterDescriptor(_resultFilter)
+    name = FilterDescriptor(_resultFilter)
+    owner_task_id = FilterDescriptor(_resultFilter)
+    status = FilterDescriptor(_resultFilter)
+    created_at = FilterDescriptor(_resultFilter)
+    completed_at = FilterDescriptor(_resultFilter)
+    result_id = FilterDescriptor(_resultFilter)
+    size = FilterDescriptor(_resultFilter)
+
+    def __init__(
+        self,
+        session_id: Optional[str] = None,
+        name: Optional[str] = None,
+        owner_task_id: Optional[str] = None,
+        status: RawResultStatus = ResultStatus.UNSPECIFIED,
+        created_at: Optional[datetime] = None,
+        completed_at: Optional[datetime] = None,
+        result_id: Optional[str] = None,
+        size: Optional[int] = None,
+    ):
+        self.session_id = session_id
+        self.name = name
+        self.owner_task_id = owner_task_id
+        self.status = status
+        self.created_at = created_at
+        self.completed_at = completed_at
+        self.result_id = result_id
+        self.size = size
 
     @classmethod
     def from_message(cls, result_raw: ResultRaw) -> "Result":
@@ -270,16 +455,47 @@ def from_message(cls, result_raw: ResultRaw) -> "Result":
             size=result_raw.size,
         )
 
+    def __eq__(self, other: "Result") -> bool:
+        return (
+            self.session_id == other.session_id
+            and self.status == other.status
+            and self.result_id == other.result_id
+            and self.created_at == other.created_at
+            and self.completed_at == other.completed_at
+            and self.result_id == other.result_id
+            and self.size == other.size
+        )
+
+
+_partitionFilter = PartitionFilter()
+
 
-@dataclass
 class Partition:
-    id: str
-    parent_partition_ids: List[str]
-    pod_reserved: int
-    pod_max: int
-    pod_configuration: Dict[str, str]
-    preemption_percentage: int
-    priority: int
+    id = FilterDescriptor(_partitionFilter)
+    parent_partition_ids = FilterDescriptor(_partitionFilter)
+    pod_reserved = FilterDescriptor(_partitionFilter)
+    pod_max = FilterDescriptor(_partitionFilter)
+    pod_configuration = FilterDescriptor(_partitionFilter)
+    preemption_percentage = FilterDescriptor(_partitionFilter)
+    priority = FilterDescriptor(_partitionFilter)
+
+    def __init__(
+        self,
+        id: str,
+        parent_partition_ids: List[str],
+        pod_reserved: int,
+        pod_max: int,
+        pod_configuration: Dict[str, str],
+        preemption_percentage: int,
+        priority: int,
+    ):
+        self.id = id
+        self.parent_partition_ids = parent_partition_ids
+        self.pod_reserved = pod_reserved
+        self.pod_max = pod_max
+        self.pod_configuration = pod_configuration
+        self.preemption_percentage = preemption_percentage
+        self.priority = priority
 
     @classmethod
     def from_message(cls, partition_raw: PartitionRaw) -> "Partition":
@@ -292,3 +508,47 @@ def from_message(cls, partition_raw: PartitionRaw) -> "Partition":
             preemption_percentage=partition_raw.preemption_percentage,
             priority=partition_raw.priority,
         )
+
+    def __eq__(self, other: "Partition") -> bool:
+        return (
+            self.id == other.id
+            and self.parent_partition_ids == other.parent_partition_ids
+            and self.pod_reserved == other.pod_reserved
+            and self.pod_max == other.pod_max
+            and self.pod_configuration == other.pod_configuration
+            and self.preemption_percentage == other.preemption_percentage
+            and self.priority == other.priority
+        )
+
+
+_applicationFilter = ApplicationFilter()
+
+
+class Application:
+    name = FilterDescriptor(_applicationFilter)
+    namespace = FilterDescriptor(_applicationFilter)
+    service = FilterDescriptor(_applicationFilter)
+    version = FilterDescriptor(_applicationFilter)
+
+    def __init__(self, name: str, namespace: str, service: str, version: str):
+        self.name = name
+        self.namespace = namespace
+        self.service = service
+        self.version = version
+
+    @classmethod
+    def from_message(cls, application_raw: ApplicationRaw) -> "Application":
+        return cls(
+            name=application_raw.name,
+            namespace=application_raw.namespace,
+            service=application_raw.service,
+            version=application_raw.version,
+        )
+
+    def __eq__(self, other: "Application") -> bool:
+        return (
+            self.name == other.name
+            and self.namespace == other.namespace
+            and self.service == other.service
+            and self.version == other.version
+        )


Test patch:
diff --git a/packages/python/tests/__init__.py b/packages/python/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/python/tests/conftest.py b/packages/python/tests/conftest.py
index edc74ff6f..9741e0898 100644
--- a/packages/python/tests/conftest.py
+++ b/packages/python/tests/conftest.py
@@ -52,7 +52,8 @@ def clean_up(request):
     # Remove the temporary files created for testing
     os.remove(os.path.join(data_folder, "payload-id"))
     os.remove(os.path.join(data_folder, "dd-id"))
-    os.remove(os.path.join(data_folder, "result-id"))
+    if os.path.exists(os.path.join(data_folder, "result-id")):
+        os.remove(os.path.join(data_folder, "result-id"))
 
     # Reset the mock server counters
     try:
@@ -90,29 +91,27 @@ def get_client(
         ValueError: If the specified service name is not recognized.
 
     Example:
-        >>> result_service = get_service("Results")
-        >>> submitter_service = get_service("Submitter", "custom_endpoint")
+        >>> result_service = get_client("Results")
+        >>> submitter_service = get_client("Submitter", "custom_endpoint")
     """
     channel = grpc.insecure_channel(endpoint).__enter__()
-    match client_name:
-        case "Agent":
-            return AgentStub(channel)
-        case "Events":
-            return ArmoniKEvents(channel)
-        case "HealthChecks":
-            return ArmoniKHealthChecks(channel)
-        case "Partitions":
-            return ArmoniKPartitions(channel)
-        case "Results":
-            return ArmoniKResults(channel)
-        case "Sessions":
-            return ArmoniKSessions(channel)
-        case "Tasks":
-            return ArmoniKTasks(channel)
-        case "Versions":
-            return ArmoniKVersions(channel)
-        case _:
-            raise ValueError("Unknown service name: " + str(client_name))
+    if client_name == "Agent":
+        return AgentStub(channel)
+    if client_name == "Events":
+        return ArmoniKEvents(channel)
+    if client_name == "HealthChecks":
+        return ArmoniKHealthChecks(channel)
+    if client_name == "Partitions":
+        return ArmoniKPartitions(channel)
+    if client_name == "Sessions":
+        return ArmoniKSessions(channel)
+    if client_name == "Tasks":
+        return ArmoniKTasks(channel)
+    if client_name == "Versions":
+        return ArmoniKVersions(channel)
+    if client_name == "Results":
+        return ArmoniKResults(channel)
+    raise ValueError("Unknown service name: " + str(client_name))
 
 
 def rpc_called(
diff --git a/packages/python/tests/test_filters.py b/packages/python/tests/test_filters.py
index 8a8ead89c..899056dcc 100644
--- a/packages/python/tests/test_filters.py
+++ b/packages/python/tests/test_filters.py
@@ -1,66 +1,92 @@
 #!/usr/bin/env python3
+import itertools
+from typing import Dict, cast, Union
 
 import pytest
 
 from dataclasses import dataclass
-from armonik.common.filter import Filter, StringFilter, BooleanFilter, NumberFilter
+
+from armonik.common import Task, Partition
+from armonik.common.filter import (
+    Filter,
+    StringFilter,
+    BooleanFilter,
+    NumberFilter,
+    DisjunctionType,
+    ConjunctionType,
+    BasicMessageType,
+    TaskOptionFilter,
+    FilterError,
+)
 from armonik.protogen.common.filters_common_pb2 import FilterBoolean
 from google.protobuf.message import Message
 
 
 @dataclass
-class DummyMessage(Message):
-    pass
+class DummyMessage(DisjunctionType):
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+        self._disjunction = []
+
+    def __getattr__(self, item):
+        if item == "or":
+            return self._disjunction
+        raise AttributeError()
 
 
 @dataclass
-class DummyMessageAnd(Message):
-    pass
+class DummyMessageAnd(ConjunctionType):
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+        self._conjunction = []
+
+    def __getattr__(self, item):
+        if item == "and":
+            return self._conjunction
+        raise AttributeError()
 
 
 @dataclass
 class Field(Message):
-    pass
+    name: str = ""
+
+
+@dataclass
+class BaseMessage(BasicMessageType):
+    def __init__(self, *, field, **kwargs):
+        super().__init__(field=field, **kwargs)
 
 
 @pytest.mark.parametrize(
     "filt,inverted",
     [
         (
-            StringFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage) == "Test",
-            StringFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage) != "Test",
+            StringFilter(Field(), DummyMessage, DummyMessageAnd, BaseMessage) == "Test",
+            StringFilter(Field(), DummyMessage, DummyMessageAnd, BaseMessage) != "Test",
         ),
         (
-            StringFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage).contains("Test"),
-            ~(StringFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage).contains("Test")),
+            StringFilter(Field(), DummyMessage, DummyMessageAnd, BaseMessage).contains("Test"),
+            ~(StringFilter(Field(), DummyMessage, DummyMessageAnd, BaseMessage).contains("Test")),
         ),
         (
-            BooleanFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage),
-            BooleanFilter(
-                Field(),
-                DummyMessage,
-                DummyMessageAnd,
-                DummyMessage,
-                FilterBoolean,
-                None,
-                False,
-            ),
+            BooleanFilter(Field(), DummyMessage, DummyMessageAnd, BaseMessage),
+            BooleanFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage, value=False),
         ),
         (
-            NumberFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage) > 0,
-            NumberFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage) <= 0,
+            NumberFilter(Field(), DummyMessage, DummyMessageAnd, BaseMessage) > 0,
+            NumberFilter(Field(), DummyMessage, DummyMessageAnd, BaseMessage) <= 0,
         ),
         (
-            NumberFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage) >= 0,
-            NumberFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage) < 0,
+            NumberFilter(Field(), DummyMessage, DummyMessageAnd, BaseMessage) >= 0,
+            NumberFilter(Field(), DummyMessage, DummyMessageAnd, BaseMessage) < 0,
         ),
         (
-            NumberFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage) < 0,
-            NumberFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage) >= 0,
+            NumberFilter(Field(), DummyMessage, DummyMessageAnd, BaseMessage) < 0,
+            NumberFilter(Field(), DummyMessage, DummyMessageAnd, BaseMessage) >= 0,
         ),
         (
-            NumberFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage) <= 0,
-            NumberFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage) > 0,
+            NumberFilter(Field(), DummyMessage, DummyMessageAnd, BaseMessage) <= 0,
+            NumberFilter(Field(), DummyMessage, DummyMessageAnd, BaseMessage) > 0,
         ),
     ],
 )
@@ -75,12 +101,124 @@ def test_inversion(filt: Filter, inverted: Filter):
 @pytest.mark.parametrize(
     "filt",
     [
-        (StringFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage).startswith("Test")),
-        (StringFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage).endswith("Test")),
-        (StringFilter(Field(), DummyMessage, DummyMessageAnd, DummyMessage)),  # No op
+        (StringFilter(Field(), DummyMessage, DummyMessageAnd, BaseMessage).startswith("Test")),
+        (StringFilter(Field(), DummyMessage, DummyMessageAnd, BaseMessage).endswith("Test")),
+        (StringFilter(Field(), DummyMessage, DummyMessageAnd, BaseMessage)),  # No op
     ],
 )
 def test_inversion_raises(filt: Filter):
     with pytest.raises(Exception):
         test = ~filt
         print(test)
+
+
+def test_descriptor():
+    task = Task()
+    assert task.id is None
+    task.id = "xxx"
+    assert isinstance(task.id, str)
+    assert isinstance(Task.id, Filter)
+    assert isinstance(Task.id == "xxx", Filter)
+
+
+def test_extra_operator():
+    with pytest.raises(FilterError):
+        try:
+            print((Task.id == "x") == "y")
+        except FilterError as e:
+            print(e)
+            raise
+
+    with pytest.raises(FilterError):
+        try:
+            print(((Task.id == "x") & (Task.session_id == "y")) == "y")
+        except FilterError as e:
+            print(e)
+            raise
+
+
+def test_extra_bool_compare():
+    def check_eq(f1: Filter, f2: Filter) -> bool:
+        return f1.operator == f2.operator and f1.value == f2.value
+
+    task_filt = cast(Filter, Task.id == "x")
+    task_filt_not = cast(Filter, Task.id != "x")
+
+    assert isinstance(task_filt, Filter)
+    assert isinstance(task_filt_not, Filter)
+
+    task_filt_eq_true = cast(Filter, task_filt == True)  # noqa: E712
+    task_filt_eq_false = cast(Filter, task_filt == False)  # noqa: E712
+    task_filt_ne_true = cast(Filter, task_filt != True)  # noqa: E712
+    task_filt_ne_false = cast(Filter, task_filt != False)  # noqa: E712
+    assert check_eq(task_filt_eq_true, task_filt)
+    assert not check_eq(task_filt_eq_true, task_filt_not)
+    assert not check_eq(task_filt_eq_false, task_filt)
+    assert check_eq(task_filt_eq_false, task_filt_not)
+    assert not check_eq(task_filt_ne_true, task_filt)
+    assert check_eq(task_filt_ne_true, task_filt_not)
+    assert check_eq(task_filt_ne_false, task_filt)
+    assert not check_eq(task_filt_ne_false, task_filt_not)
+
+
+def test_task_option_descriptor():
+    assert isinstance(Task.options, TaskOptionFilter)
+    assert isinstance(Task.options.priority, Filter)
+    with pytest.raises(FilterError):
+        try:
+            print(Task.options == "test")
+        except FilterError as e:
+            print(e)
+            raise
+    with pytest.raises(FilterError):
+        try:
+            print(Task.options.contains("test"))
+        except FilterError as e:
+            print(e)
+            raise
+
+
+def test_unavailable_field():
+    with pytest.raises(FilterError):
+        print(Partition.pod_configuration == {})
+
+
+def apply_filter(state: Dict[str, bool], message: Union[Message, Filter]) -> bool:
+    if isinstance(message, Filter):
+        return apply_filter(state, message.to_message())
+    if hasattr(message, "or"):
+        return any(apply_filter(state, m) for m in getattr(message, "or"))
+    if hasattr(message, "and"):
+        return all(apply_filter(state, m) for m in getattr(message, "and"))
+    vp = cast(FilterBoolean, getattr(message, "filter_boolean"))
+    field = cast(Field, getattr(message, "field"))
+    return vp.value == state[field.name]
+
+
+def get_bool_filter(name: str) -> BooleanFilter:
+    return BooleanFilter(Field(name), DummyMessage, DummyMessageAnd, BaseMessage)
+
+
+def test_combine():
+    a = get_bool_filter("a")
+    b = get_bool_filter("b")
+    c = get_bool_filter("c")
+    d = get_bool_filter("d")
+    e = get_bool_filter("e")
+    for _a, _b, _c, _d, _e in itertools.product([True, False], repeat=5):
+        t = {"a": _a, "b": _b, "c": _c, "d": _d, "e": _e}
+        assert apply_filter(t, a) == _a
+        assert apply_filter(t, (~a)) == (not _a)
+        assert apply_filter(t, (a | b)) == (_a or _b)
+        assert apply_filter(t, ((~a) | b)) == ((not _a) or _b)
+        assert apply_filter(t, (a | (~b))) == (_a or not _b)
+        assert apply_filter(t, ((~a) | (~b))) == (not _a or not _b)
+        assert apply_filter(t, ((~a) & b)) == ((not _a) and _b)
+        assert apply_filter(t, (~(a | b))) == (not (_a or _b))
+        assert apply_filter(t, (~(a & b))) == (not (_a and _b))
+        assert apply_filter(t, (c & (a | b))) == (_c and (_a or _b))
+        assert apply_filter(t, (c | (a & b))) == (_c or (_a and _b))
+        assert apply_filter(t, (a & (b | c)) & (d | e)) == (_a and (_b or _c) and (_d or _e))
+        assert apply_filter(t, ~((a & (b | c)) & (d | e))) == (
+            not (_a and (_b or _c) and (_d or _e))
+        )