Repository: intelowlproject/pyintelowl

Issue:
Listing jobs through CLi gives only 10 jobs
Proposal - To create a --page flag in `pyintelowl jobs ls` to specify the page number when listing jobs.

Hints from issue comments:
@pranjalg1331: @mlodic, Kindly give a green signal on the issue and proposed solution, so I can start working on it.@mlodic: feel free to go! :)

Solution patch:
diff --git a/pyintelowl/cli/jobs.py b/pyintelowl/cli/jobs.py
index 0a07b33..dab34b1 100644
--- a/pyintelowl/cli/jobs.py
+++ b/pyintelowl/cli/jobs.py
@@ -42,12 +42,23 @@ def jobs():
     show_choices=True,
     help="Only show jobs having a particular status",
 )
+@click.option(
+    "-p",
+    "--page",
+    type=int,
+    help="""
+    List jobs of a specific page. Each page contains 10 jobs.
+    """,
+)
 @add_options(json_flag_option)
 @click.pass_context
-def ls(ctx: ClickContext, status: str, as_json: bool):
+def ls(ctx: ClickContext, status: str, as_json: bool, page: int):
     ctx.obj.logger.info("Requesting list of jobs..")
     try:
-        ans = ctx.obj.get_all_jobs()
+        if page:
+            ans = ctx.obj.get_jobs_by_page(page)
+        else:
+            ans = ctx.obj.get_all_jobs()
         results = ans.get("results", [])
         ctx.obj.logger.info(results)
         if status:
diff --git a/pyintelowl/pyintelowl.py b/pyintelowl/pyintelowl.py
index bc7fea6..a929240 100644
--- a/pyintelowl/pyintelowl.py
+++ b/pyintelowl/pyintelowl.py
@@ -581,6 +581,24 @@ class IntelOwl:
         response = self.__make_request("GET", url=url)
         return response.json()
 
+    def get_jobs_by_page(self, page: int) -> List[Dict[str, Any]]:
+        """
+        Fetch list of jobs by page number.\n
+        Endpoint: ``/api/jobs``
+
+        Args:
+            page (int): Page number
+
+        Raises:
+            IntelOwlClientException: on client/HTTP error
+
+        Returns:
+            Dict: Dict with 3 keys: "count", "total_pages", "results"
+        """
+        url = self.instance + f"/api/jobs?page={page}"
+        response = self.__make_request("GET", url=url)
+        return response.json()
+
     def get_tag_by_id(self, tag_id: Union[int, str]) -> Dict[str, str]:
         """Fetch tag info by ID.\n
         Endpoint: ``/api/tag/{tag_id}``


Test patch:
diff --git a/tests/mocked_requests.py b/tests/mocked_requests.py
index d03c230..d859293 100644
--- a/tests/mocked_requests.py
+++ b/tests/mocked_requests.py
@@ -104,6 +104,42 @@ def mocked_get_all_jobs(*args, **kwargs):
     )
 
 
+def mocked_get_jobs_by_page(*args, **kwargs):
+    return MockResponse(
+        {
+            "count": 1,
+            "total_pages": 1,
+            "results": [
+                {
+                    "id": 1057,
+                    "user": {"username": "pranjal"},
+                    "tags": [],
+                    "pivots_to_execute": [],
+                    "analyzers_to_execute": ["UltraDNSAnalyzer"],
+                    "connectors_to_execute": [],
+                    "visualizers_to_execute": [],
+                    "playbook_to_execute": [],
+                    "is_sample": False,
+                    "md5": "",
+                    "observable_name": "",
+                    "observable_classification": "",
+                    "file_name": "",
+                    "file_mimetype": "",
+                    "status": "",
+                    "received_request_time": "",
+                    "finished_analysis_time": "",
+                    "process_time": 3.44,
+                    "tlp": "RED",
+                    "investigation": [],
+                    "playbook_requested": [],
+                }
+            ],
+        },
+        200,
+        "/api/jobs",
+    )
+
+
 def mocked_delete_job_by_id(*args, **kwargs):
     return MockResponse(
         True,
diff --git a/tests/test_jobs.py b/tests/test_jobs.py
index 4c92c76..ea71a1f 100644
--- a/tests/test_jobs.py
+++ b/tests/test_jobs.py
@@ -6,6 +6,7 @@ from tests.mocked_requests import (
     mocked_download_job_sample,
     mocked_get_all_jobs,
     mocked_get_job_by_id,
+    mocked_get_jobs_by_page,
     mocked_kill_analyzer,
     mocked_kill_connector,
     mocked_kill_job,
@@ -26,6 +27,24 @@ class TestJobs(BaseTest):
     def test_get_all_jobs_failure(self, mock_requests):
         self.assertRaises(IntelOwlClientException, self.client.get_all_jobs)
 
+    @mock_connections(
+        patch("requests.Session.get", side_effect=mocked_get_jobs_by_page)
+    )
+    def test_get_jobs_by_page_success(self, mock_requests):
+        page = 1
+        jobs = self.client.get_jobs_by_page(page)
+        self.assertIsInstance(jobs, dict)
+        self.assertIsInstance(
+            jobs.get("results"), list, "Results key should contain a list"
+        )
+        for job in jobs.get("results"):
+            self.assertIn("id", job, "Job missing 'id' field")
+
+    @mock_connections(patch("requests.Session.get", side_effect=mocked_raise_exception))
+    def test_get_jobs_by_page_failure(self, mock_requests):
+        page = 1
+        self.assertRaises(IntelOwlClientException, self.client.get_jobs_by_page, page)
+
     @mock_connections(patch("requests.Session.get", side_effect=mocked_get_job_by_id))
     def test_get_job_by_id_valid(self, mock_requests):
         job = self.client.get_job_by_id(self.job_id)