Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ASP-3966] Implement pagination #430

Merged
merged 24 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9a404da
Add pynput dependency
julianaklulo Oct 20, 2023
93c704a
Add render paginated list results
julianaklulo Oct 20, 2023
8e7ef76
Add keyboard listener to list_all applications
julianaklulo Oct 20, 2023
89884c3
Add prototype 2: navigation using inquirer
julianaklulo Oct 24, 2023
286465f
Add pagination module
julianaklulo Oct 25, 2023
2debfe1
Add pagination to list all endpoints
julianaklulo Oct 25, 2023
9c9b428
Remove pynput dependency
julianaklulo Oct 25, 2023
20b3a5f
ASP-3966 Move pagination module to subapps folder
julianaklulo Dec 4, 2023
25147bb
ASP-3966 Add named params to pagination calls
julianaklulo Dec 4, 2023
39d1464
ASP-3966 Add fixtures for pagination tests
julianaklulo Dec 4, 2023
d88e929
ASP-3966 Update unit tests
julianaklulo Dec 4, 2023
556d1d3
ASP-3966 Update imports for handle_pagination
julianaklulo Dec 4, 2023
429d2c4
ASP-3966 Fix issue with one page results
julianaklulo Dec 4, 2023
9df237b
ASP-3966 Sort imports
julianaklulo Dec 4, 2023
9d0ff12
ASP-3966 Add pagination unit tests
julianaklulo Dec 4, 2023
63bca1d
ASP-3966 Lint code
julianaklulo Dec 4, 2023
33779d9
Merge branch 'main' into juliana/ASP-3966--implement-pagination
julianaklulo Dec 4, 2023
e69febf
ASP-3966 Fix merge conflict leftover
julianaklulo Dec 4, 2023
1ac809d
ASP-3966 Address code review requested changes
julianaklulo Dec 7, 2023
fabca65
ASP-3966 Revert changes in poetry.lock
julianaklulo Dec 7, 2023
0a6c3b3
ASP-3966 Add validation when answer is None
julianaklulo Dec 8, 2023
a084991
ASP-3966 Add sorting by descending id as default
julianaklulo Dec 8, 2023
6f0e52d
ASP-3966 Add entry to CHANGELOG
julianaklulo Dec 8, 2023
2215f24
Merge branch 'main' into juliana/ASP-3966--implement-pagination
julianaklulo Dec 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion jobbergate-cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
This file keeps track of all notable changes to jobbergate-cli

## Unreleased

- Fixed the setting `CACHE_DIR` to expand the user home directory, allowing more flexibility on the path [ASP-4053]
- Fixed the question `BooleanList` to allow subquestion to have the same name [ASP-4228]
- Added pagination support for `list` commands [ASP-3966]

## 4.2.0a3 -- 2023-11-30

Expand Down
16 changes: 16 additions & 0 deletions jobbergate-cli/jobbergate_cli/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,19 @@ class FileType(str, Enum):

ENTRYPOINT = "ENTRYPOINT"
SUPPORT = "SUPPORT"


class PaginationChoices(str, Enum):
"""
Enum describing the type of pagination that is available for list commands.
"""

PREVIOUS_PAGE = "Previous page"
NEXT_PAGE = "Next page"
EXIT = "Exit"

def __str__(self) -> str:
"""
Return the string representation of the enum.
"""
return self.value
44 changes: 44 additions & 0 deletions jobbergate-cli/jobbergate_cli/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,47 @@ def render_single_result(
if ctx.full_output or hidden_fields is None:
hidden_fields = []
render_dict(result, hidden_fields=hidden_fields, title=title)


def render_paginated_list_results(
ctx: JobbergateContext,
envelope: ListResponseEnvelope,
title: str = "Results List",
style_mapper: StyleMapper = None,
hidden_fields: List[str] = None,
):
if envelope.total == 0:
terminal_message("There are no results to display", subject="Nothing here...")
return

if ctx.raw_output:
serialized = envelope.json()
deserialized = json.loads(serialized)
render_json(deserialized["items"])
return

current_page = envelope.page
total_pages = envelope.pages

if ctx.full_output or hidden_fields is None:
filtered_results = envelope.items
else:
filtered_results = [{k: v for (k, v) in d.items() if k not in hidden_fields} for d in envelope.items]

first_row = filtered_results[0]

table = Table(
title=title,
caption=f"Page: {current_page} of {total_pages} - Items: {len(filtered_results)} of {envelope.total}",
)
if style_mapper is None:
style_mapper = StyleMapper()
for key in first_row.keys():
table.add_column(key, **style_mapper.map_style(key))

for row in filtered_results:
table.add_row(*[str(v) for v in row.values()])

console = Console()
console.print()
console.print(table)
30 changes: 10 additions & 20 deletions jobbergate-cli/jobbergate_cli/subapps/applications/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@

from jobbergate_cli.constants import OV_CONTACT, SortOrder
from jobbergate_cli.exceptions import handle_abort
from jobbergate_cli.render import StyleMapper, render_list_results, render_single_result, terminal_message
from jobbergate_cli.render import StyleMapper, render_single_result, terminal_message
from jobbergate_cli.requests import make_request
from jobbergate_cli.schemas import JobbergateContext, ListResponseEnvelope
from jobbergate_cli.schemas import JobbergateContext
from jobbergate_cli.subapps.applications.tools import fetch_application_data, save_application_files, upload_application
from jobbergate_cli.subapps.pagination import handle_pagination


# TODO: move hidden field logic to the API
Expand Down Expand Up @@ -61,8 +62,8 @@ def list_all(
show_all: bool = typer.Option(False, "--all", help="Show all applications, even the ones without identifier"),
user_only: bool = typer.Option(False, "--user", help="Show only applications owned by the current user"),
search: Optional[str] = typer.Option(None, help="Apply a search term to results"),
sort_order: SortOrder = typer.Option(SortOrder.UNSORTED, help="Specify sort order"),
sort_field: Optional[str] = typer.Option(None, help="The field by which results should be sorted"),
sort_order: SortOrder = typer.Option(SortOrder.DESCENDING, help="Specify sort order"),
sort_field: Optional[str] = typer.Option("id", help="The field by which results should be sorted"),
):
"""
Show available applications
Expand All @@ -84,22 +85,11 @@ def list_all(
if sort_field is not None:
params["sort_field"] = sort_field

envelope = cast(
ListResponseEnvelope,
make_request(
jg_ctx.client,
"/jobbergate/job-script-templates",
"GET",
expected_status=200,
abort_message="Couldn't retrieve applications list from API",
support=True,
response_model_cls=ListResponseEnvelope,
params=params,
),
)
render_list_results(
jg_ctx,
envelope,
handle_pagination(
jg_ctx=jg_ctx,
url_path="/jobbergate/job-script-templates",
abort_message="Couldn't retrieve applications list from API",
params=params,
title="Applications List",
style_mapper=style_mapper,
hidden_fields=HIDDEN_FIELDS,
Expand Down
30 changes: 10 additions & 20 deletions jobbergate-cli/jobbergate_cli/subapps/job_scripts/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
from jobbergate_cli.config import settings
from jobbergate_cli.constants import SortOrder
from jobbergate_cli.exceptions import Abort, handle_abort
from jobbergate_cli.render import StyleMapper, render_list_results, render_single_result, terminal_message
from jobbergate_cli.render import StyleMapper, render_single_result, terminal_message
from jobbergate_cli.requests import make_request
from jobbergate_cli.schemas import JobbergateContext, JobScriptCreateRequest, JobScriptResponse, ListResponseEnvelope
from jobbergate_cli.schemas import JobbergateContext, JobScriptCreateRequest, JobScriptResponse
from jobbergate_cli.subapps.job_scripts.tools import (
download_job_script_files,
fetch_job_script_data,
Expand All @@ -23,6 +23,7 @@
)
from jobbergate_cli.subapps.job_submissions.app import HIDDEN_FIELDS as JOB_SUBMISSION_HIDDEN_FIELDS
from jobbergate_cli.subapps.job_submissions.tools import create_job_submission
from jobbergate_cli.subapps.pagination import handle_pagination
from jobbergate_cli.text_tools import dedent


Expand Down Expand Up @@ -50,8 +51,8 @@ def list_all(
ctx: typer.Context,
show_all: bool = typer.Option(False, "--all", help="Show all job scripts, even the ones owned by others"),
search: Optional[str] = typer.Option(None, help="Apply a search term to results"),
sort_order: SortOrder = typer.Option(SortOrder.UNSORTED, help="Specify sort order"),
sort_field: Optional[str] = typer.Option(None, help="The field by which results should be sorted"),
sort_order: SortOrder = typer.Option(SortOrder.DESCENDING, help="Specify sort order"),
sort_field: Optional[str] = typer.Option("id", help="The field by which results should be sorted"),
from_application_id: Optional[int] = typer.Option(
None,
help="Filter job-scripts by the application-id they were created from.",
Expand All @@ -76,22 +77,11 @@ def list_all(
if from_application_id is not None:
params["from_job_script_template_id"] = from_application_id

envelope = cast(
ListResponseEnvelope,
make_request(
jg_ctx.client,
"/jobbergate/job-scripts",
"GET",
expected_status=200,
abort_message="Couldn't retrieve job scripts list from API",
support=True,
response_model_cls=ListResponseEnvelope,
params=params,
),
)
render_list_results(
jg_ctx,
envelope,
handle_pagination(
jg_ctx=jg_ctx,
url_path="/jobbergate/job-scripts",
abort_message="Couldn't retrieve job scripts list from API",
params=params,
title="Job Scripts List",
style_mapper=style_mapper,
hidden_fields=HIDDEN_FIELDS,
Expand Down
32 changes: 11 additions & 21 deletions jobbergate-cli/jobbergate_cli/subapps/job_submissions/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@

from pathlib import Path
from textwrap import dedent
from typing import Any, Dict, Optional, cast
from typing import Any, Dict, Optional

import typer

from jobbergate_cli.constants import SortOrder
from jobbergate_cli.exceptions import handle_abort
from jobbergate_cli.render import StyleMapper, render_list_results, render_single_result, terminal_message
from jobbergate_cli.render import StyleMapper, render_single_result, terminal_message
from jobbergate_cli.requests import make_request
from jobbergate_cli.schemas import JobbergateContext, ListResponseEnvelope
from jobbergate_cli.schemas import JobbergateContext
from jobbergate_cli.subapps.job_submissions.tools import create_job_submission, fetch_job_submission_data
from jobbergate_cli.subapps.pagination import handle_pagination


# move hidden field logic to the API
Expand Down Expand Up @@ -120,8 +121,8 @@ def list_all(
help="Show all job submissions, even the ones owned by others",
),
search: Optional[str] = typer.Option(None, help="Apply a search term to results"),
sort_order: SortOrder = typer.Option(SortOrder.UNSORTED, help="Specify sort order"),
sort_field: Optional[str] = typer.Option(None, help="The field by which results should be sorted"),
sort_order: SortOrder = typer.Option(SortOrder.DESCENDING, help="Specify sort order"),
sort_field: Optional[str] = typer.Option("id", help="The field by which results should be sorted"),
from_job_script_id: Optional[int] = typer.Option(
None,
help="Filter job-submissions by the job-script-id they were created from.",
Expand All @@ -146,22 +147,11 @@ def list_all(
if from_job_script_id is not None:
params["from_job_script_id"] = from_job_script_id

envelope = cast(
ListResponseEnvelope,
make_request(
jg_ctx.client,
"/jobbergate/job-submissions",
"GET",
expected_status=200,
abort_message="Couldn't retrieve job submissions list from API",
support=True,
response_model_cls=ListResponseEnvelope,
params=params,
),
)
render_list_results(
jg_ctx,
envelope,
handle_pagination(
jg_ctx=jg_ctx,
url_path="/jobbergate/job-submissions",
abort_message="Couldn't retrieve job submissions list from API",
params=params,
title="Job Submission List",
style_mapper=style_mapper,
hidden_fields=HIDDEN_FIELDS,
Expand Down
102 changes: 102 additions & 0 deletions jobbergate-cli/jobbergate_cli/subapps/pagination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from typing import Any, Dict, List, Optional, cast

import inquirer

from jobbergate_cli.constants import PaginationChoices
from jobbergate_cli.render import StyleMapper, render_paginated_list_results
from jobbergate_cli.requests import make_request
from jobbergate_cli.schemas import JobbergateContext, ListResponseEnvelope


def handle_pagination(
jg_ctx: JobbergateContext,
url_path: str,
abort_message: str = "There was an error communicating with the API",
params: Optional[Dict[str, Any]] = None,
title: str = "Results List",
style_mapper: StyleMapper = None,
hidden_fields: Optional[List[str]] = None,
):
assert jg_ctx is not None
assert jg_ctx.client is not None

current_page = 1

while True:
if params is None:
params = {}
params["page"] = current_page

envelope = cast(
ListResponseEnvelope,
make_request(
jg_ctx.client,
url_path,
"GET",
expected_status=200,
abort_message=abort_message,
support=True,
response_model_cls=ListResponseEnvelope,
params=params,
),
)

render_paginated_list_results(
jg_ctx,
envelope,
title=title,
style_mapper=style_mapper,
hidden_fields=hidden_fields,
)

if envelope.pages <= 1:
return

current_page = envelope.page

message = "Which page would you like to view?"
choices = [PaginationChoices.PREVIOUS_PAGE, PaginationChoices.NEXT_PAGE, PaginationChoices.EXIT]

if current_page == 1:
answer = inquirer.prompt(
[
inquirer.List(
"navigation",
message=message,
choices=choices[1:], # remove previous page option
default=PaginationChoices.NEXT_PAGE,
)
]
)
elif current_page == envelope.pages:
answer = inquirer.prompt(
[
inquirer.List(
"navigation",
message=message,
choices=choices[::2], # remove next page option
default=PaginationChoices.EXIT,
)
]
)
else:
answer = inquirer.prompt(
[
inquirer.List(
"navigation",
message=message,
choices=choices,
default=PaginationChoices.NEXT_PAGE,
)
]
)

if not answer:
return

if answer["navigation"] == PaginationChoices.NEXT_PAGE:
julianaklulo marked this conversation as resolved.
Show resolved Hide resolved
current_page += 1
elif answer["navigation"] == PaginationChoices.PREVIOUS_PAGE:
current_page -= 1
else:
return
Loading