Repository: Qiskit/qiskit-ibm-runtime (python)

Issue:
Filter jobs using created after and before dates
**What is the expected feature or enhancement?**

The API support filtering jobs created after and before a particular date https://us-east.quantum-computing.cloud.ibm.com/openapi#/Jobs/list_jobs.

Add `created_after` and `created_before` datetime parameters to `QiskitRuntimeService.jobs()`.

Similar to https://github.com/Qiskit/qiskit-ibm-provider/blob/c6b63e19623cec8817cc3a110c3ca4e92dd95037/qiskit_ibm_provider/ibm_provider.py#L558-L559.

Pull request:
Filter jobs with created after/before parameters
<!--
⚠️ The pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

✅ I have added the tests to cover my changes.
✅ I have updated the documentation accordingly.
✅ I have read the CONTRIBUTING document.
-->

### Summary



### Details and comments
Fixes #357 



Patch:
diff --git a/qiskit_ibm_runtime/api/clients/runtime.py b/qiskit_ibm_runtime/api/clients/runtime.py
index ca4b7395..039c1062 100644
--- a/qiskit_ibm_runtime/api/clients/runtime.py
+++ b/qiskit_ibm_runtime/api/clients/runtime.py
@@ -219,6 +219,8 @@ class RuntimeClient(BaseBackendClient):
         project: str = None,
         job_tags: Optional[List[str]] = None,
         session_id: Optional[str] = None,
+        created_after: Optional[python_datetime] = None,
+        created_before: Optional[python_datetime] = None,
         descending: bool = True,
     ) -> Dict:
         """Get job data for all jobs.
@@ -234,6 +236,12 @@ class RuntimeClient(BaseBackendClient):
             project: Filter by project - hub, group, and project must all be specified.
             job_tags: Filter by tags assigned to jobs. Matched jobs are associated with all tags.
             session_id: Job ID of the first job in a runtime session.
+            created_after: Filter by the given start date, in local time. This is used to
+                find jobs whose creation dates are after (greater than or equal to) this
+                local date/time.
+            created_before: Filter by the given end date, in local time. This is used to
+                find jobs whose creation dates are before (less than or equal to) this
+                local date/time.
             descending: If ``True``, return the jobs in descending order of the job
                 creation date (i.e. newest first) until the limit is reached.
 
@@ -250,6 +258,8 @@ class RuntimeClient(BaseBackendClient):
             project=project,
             job_tags=job_tags,
             session_id=session_id,
+            created_after=created_after,
+            created_before=created_before,
             descending=descending,
         )
 
diff --git a/qiskit_ibm_runtime/api/rest/runtime.py b/qiskit_ibm_runtime/api/rest/runtime.py
index aa92f546..25dfb021 100644
--- a/qiskit_ibm_runtime/api/rest/runtime.py
+++ b/qiskit_ibm_runtime/api/rest/runtime.py
@@ -13,6 +13,7 @@
 """Runtime REST adapter."""
 
 import logging
+from datetime import datetime
 from typing import Dict, Any, List, Union, Optional
 import json
 
@@ -20,6 +21,7 @@ from .base import RestAdapterBase
 from .program import Program
 from .program_job import ProgramJob
 from ...utils import RuntimeEncoder
+from ...utils.converters import local_to_utc
 from .cloud_backend import CloudBackend
 
 logger = logging.getLogger(__name__)
@@ -176,6 +178,8 @@ class Runtime(RestAdapterBase):
         project: str = None,
         job_tags: Optional[List[str]] = None,
         session_id: Optional[str] = None,
+        created_after: Optional[datetime] = None,
+        created_before: Optional[datetime] = None,
         descending: bool = True,
     ) -> Dict:
         """Get a list of job data.
@@ -191,6 +195,12 @@ class Runtime(RestAdapterBase):
             project: Filter by project - hub, group, and project must all be specified.
             job_tags: Filter by tags assigned to jobs. Matched jobs are associated with all tags.
             session_id: Job ID of the first job in a runtime session.
+            created_after: Filter by the given start date, in local time. This is used to
+                find jobs whose creation dates are after (greater than or equal to) this
+                local date/time.
+            created_before: Filter by the given end date, in local time. This is used to
+                find jobs whose creation dates are before (less than or equal to) this
+                local date/time.
             descending: If ``True``, return the jobs in descending order of the job
                 creation date (i.e. newest first) until the limit is reached.
 
@@ -211,6 +221,10 @@ class Runtime(RestAdapterBase):
             payload["tags"] = job_tags
         if session_id:
             payload["session_id"] = session_id
+        if created_after:
+            payload["created_after"] = local_to_utc(created_after).isoformat()
+        if created_before:
+            payload["created_before"] = local_to_utc(created_before).isoformat()
         if descending is False:
             payload["sort"] = "ASC"
         if all([hub, group, project]):
diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py
index 02e8b7c1..ca2ccbe3 100644
--- a/qiskit_ibm_runtime/qiskit_runtime_service.py
+++ b/qiskit_ibm_runtime/qiskit_runtime_service.py
@@ -16,6 +16,7 @@ import json
 import logging
 import traceback
 import warnings
+from datetime import datetime
 from collections import OrderedDict
 from typing import Dict, Callable, Optional, Union, List, Any, Type
 
@@ -1219,6 +1220,8 @@ class QiskitRuntimeService:
         instance: Optional[str] = None,
         job_tags: Optional[List[str]] = None,
         session_id: Optional[str] = None,
+        created_after: Optional[datetime] = None,
+        created_before: Optional[datetime] = None,
         descending: bool = True,
     ) -> List[RuntimeJob]:
         """Retrieve all runtime jobs, subject to optional filtering.
@@ -1234,6 +1237,12 @@ class QiskitRuntimeService:
                 hub/group/project format.
             job_tags: Filter by tags assigned to jobs. Matched jobs are associated with all tags.
             session_id: Job ID of the first job in a runtime session.
+            created_after: Filter by the given start date, in local time. This is used to
+                find jobs whose creation dates are after (greater than or equal to) this
+                local date/time.
+            created_before: Filter by the given end date, in local time. This is used to
+                find jobs whose creation dates are before (less than or equal to) this
+                local date/time.
             descending: If ``True``, return the jobs in descending order of the job
                 creation date (i.e. newest first) until the limit is reached.
 
@@ -1266,6 +1275,8 @@ class QiskitRuntimeService:
                 project=project,
                 job_tags=job_tags,
                 session_id=session_id,
+                created_after=created_after,
+                created_before=created_before,
                 descending=descending,
             )
             job_page = jobs_response["jobs"]
diff --git a/releasenotes/notes/creation-date-filters-871a1ca3c0d96fda.yaml b/releasenotes/notes/creation-date-filters-871a1ca3c0d96fda.yaml
new file mode 100644
index 00000000..5f00564e
--- /dev/null
+++ b/releasenotes/notes/creation-date-filters-871a1ca3c0d96fda.yaml
@@ -0,0 +1,6 @@
+---
+upgrade:
+  - |
+    :meth:`~qiskit_ibm_runtime.QiskitRuntimeService.jobs` has two new parameters, ``created_after``
+    and ``created_before``. These can be used to filter jobs by creation date in local time.
+


Test patch:
diff --git a/test/integration/test_retrieve_job.py b/test/integration/test_retrieve_job.py
index db527395..aa46e524 100644
--- a/test/integration/test_retrieve_job.py
+++ b/test/integration/test_retrieve_job.py
@@ -13,7 +13,7 @@
 """Tests for job functions using real runtime service."""
 
 import uuid
-
+from datetime import datetime, timezone
 from qiskit.providers.jobstatus import JobStatus
 
 from ..ibm_test_case import IBMIntegrationJobTestCase
@@ -150,6 +150,19 @@ class TestIntegrationRetrieveJob(IBMIntegrationJobTestCase):
         rjobs = service.jobs(session_id="test")
         self.assertFalse(rjobs)
 
+    @run_integration_test
+    def test_jobs_filter_by_date(self, service):
+        """Test retrieving jobs by creation date."""
+        current_time = datetime.now(timezone.utc)
+        job = self._run_program(service)
+        job.wait_for_final_state()
+        time_after_job = datetime.now(timezone.utc)
+        rjobs = service.jobs(created_before=time_after_job, created_after=current_time)
+        self.assertTrue(job.job_id in [j.job_id for j in rjobs])
+        for job in rjobs:
+            self.assertTrue(job.creation_date <= time_after_job)
+            self.assertTrue(job.creation_date >= current_time)
+
     @run_integration_test
     def test_jobs_filter_by_hgp(self, service):
         """Test retrieving jobs by hgp."""
diff --git a/test/program.py b/test/program.py
index 1ec06f21..68956601 100644
--- a/test/program.py
+++ b/test/program.py
@@ -14,6 +14,7 @@
 
 import uuid
 import copy
+from datetime import datetime, timezone
 from qiskit_ibm_runtime import QiskitRuntimeService
 
 
@@ -102,4 +103,5 @@ def run_program(
         max_execution_time=max_execution_time,
         session_id=session_id,
     )
+    job._creation_date = datetime.now(timezone.utc)
     return job
diff --git a/test/unit/mock/fake_runtime_client.py b/test/unit/mock/fake_runtime_client.py
index c79fbc4f..4c1586ec 100644
--- a/test/unit/mock/fake_runtime_client.py
+++ b/test/unit/mock/fake_runtime_client.py
@@ -16,6 +16,7 @@ import base64
 import json
 import time
 import uuid
+from datetime import timezone, datetime as python_datetime
 from concurrent.futures import ThreadPoolExecutor
 from functools import wraps
 from typing import Optional, Dict, Any, List
@@ -133,6 +134,7 @@ class BaseFakeRuntimeJob:
         self.log_level = log_level
         self._session_id = session_id
         self._max_execution_time = max_execution_time
+        self._creation_date = python_datetime.now(timezone.utc)
         if final_status is None:
             self._future = self._executor.submit(self._auto_progress)
             self._result = None
@@ -421,6 +423,8 @@ class BaseFakeRuntimeClient:
         project=None,
         job_tags=None,
         session_id=None,
+        created_after=None,
+        created_before=None,
         descending=True,
     ):
         """Get all jobs."""
@@ -450,6 +454,12 @@ class BaseFakeRuntimeClient:
         if session_id:
             jobs = [job for job in jobs if job._session_id == session_id]
             count = len(jobs)
+        if created_after:
+            jobs = [job for job in jobs if job._creation_date >= created_after]
+            count = len(jobs)
+        if created_before:
+            jobs = [job for job in jobs if job._creation_date <= created_before]
+            count = len(jobs)
         jobs = jobs[skip : limit + skip]
         if descending is False:
             jobs.reverse()
diff --git a/test/unit/test_job_retrieval.py b/test/unit/test_job_retrieval.py
index 6410e58c..27867be1 100644
--- a/test/unit/test_job_retrieval.py
+++ b/test/unit/test_job_retrieval.py
@@ -12,6 +12,7 @@
 
 """Tests for runtime job retrieval."""
 
+from datetime import datetime, timedelta, timezone
 from qiskit_ibm_runtime.exceptions import IBMInputValueError
 from .mock.fake_runtime_service import FakeRuntimeService
 from ..ibm_test_case import IBMTestCase
@@ -243,6 +244,22 @@ class TestRetrieveJobs(IBMTestCase):
         rjobs = service.jobs(program_id=program_id, session_id="no_test_session_id")
         self.assertFalse(rjobs)
 
+    def test_jobs_filter_by_date(self):
+        """Test retrieving jobs filtered by date."""
+        service = self._ibm_quantum_service
+        current_time = datetime.now(timezone.utc) - timedelta(seconds=5)
+        job = run_program(service=service)
+        with mock_wait_for_final_state(service, job):
+            job.wait_for_final_state()
+        time_after_job = datetime.now(timezone.utc)
+        rjobs = service.jobs(
+            created_before=time_after_job,
+            created_after=current_time,
+        )
+        self.assertTrue(job.job_id in [j.job_id for j in rjobs])
+        self.assertTrue(job._creation_date <= time_after_job)
+        self.assertTrue(job._creation_date >= current_time)
+
     def test_jobs_sort_by_date(self):
         """Test retrieving jobs sorted by the date."""
         service = self._ibm_quantum_service