-
Notifications
You must be signed in to change notification settings - Fork 63
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
Add pagination and search functionality to job collection API #1029
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -5,6 +5,7 @@ | |||||||||||||||||||||||
import collections | ||||||||||||||||||||||||
import datetime | ||||||||||||||||||||||||
import logging | ||||||||||||||||||||||||
import math | ||||||||||||||||||||||||
import traceback | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
import staticconf | ||||||||||||||||||||||||
|
@@ -310,10 +311,16 @@ def get_data( | |||||||||||||||||||||||
include_action_runs=False, | ||||||||||||||||||||||||
include_action_graph=True, | ||||||||||||||||||||||||
include_node_pool=True, | ||||||||||||||||||||||||
page=None, | ||||||||||||||||||||||||
page_size=None, | ||||||||||||||||||||||||
): | ||||||||||||||||||||||||
jobs = self.job_collection.get_jobs() | ||||||||||||||||||||||||
if page is not None and page_size is not None: | ||||||||||||||||||||||||
start = (page - 1) * page_size | ||||||||||||||||||||||||
jobs = jobs[start : start + page_size] | ||||||||||||||||||||||||
Comment on lines
+317
to
+320
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
might make testing easier (since then we can test the paging logic without testing the rest of |
||||||||||||||||||||||||
return adapter.adapt_many( | ||||||||||||||||||||||||
adapter.JobAdapter, | ||||||||||||||||||||||||
self.job_collection.get_jobs(), | ||||||||||||||||||||||||
jobs, | ||||||||||||||||||||||||
include_job_run, | ||||||||||||||||||||||||
include_action_runs, | ||||||||||||||||||||||||
include_action_graph, | ||||||||||||||||||||||||
|
@@ -350,13 +357,45 @@ def render_GET(self, request): | |||||||||||||||||||||||
"include_node_pool", | ||||||||||||||||||||||||
default=True, | ||||||||||||||||||||||||
) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
page = requestargs.get_integer(request, "page") | ||||||||||||||||||||||||
page_size = requestargs.get_integer(request, "page_size") | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
jobs = self.get_data( | ||||||||||||||||||||||||
include_job_runs, | ||||||||||||||||||||||||
include_action_runs, | ||||||||||||||||||||||||
include_action_graph, | ||||||||||||||||||||||||
include_node_pool, | ||||||||||||||||||||||||
page, | ||||||||||||||||||||||||
page_size, | ||||||||||||||||||||||||
Comment on lines
+369
to
+370
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we're using page/page_size here before validating them - is that intentional? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great catch!!! this is a good point!!! most of the page validation happens a bit after on the code, handling exceptions here makes more sense. |
||||||||||||||||||||||||
) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
if page is not None and page_size is not None: | ||||||||||||||||||||||||
total = len(self.job_collection.get_jobs()) | ||||||||||||||||||||||||
if page < 1 or page_size < 1: | ||||||||||||||||||||||||
return respond( | ||||||||||||||||||||||||
request=request, | ||||||||||||||||||||||||
response={"error": "page and page_size must be positive integers"}, | ||||||||||||||||||||||||
code=http.BAD_REQUEST, | ||||||||||||||||||||||||
) | ||||||||||||||||||||||||
if page > math.ceil(total / page_size): | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit, but the case where we have 0 jobs should still return a page (with no jobs). |
||||||||||||||||||||||||
return respond( | ||||||||||||||||||||||||
request=request, | ||||||||||||||||||||||||
response={"error": "page out of range"}, | ||||||||||||||||||||||||
code=http.BAD_REQUEST, | ||||||||||||||||||||||||
) | ||||||||||||||||||||||||
Comment on lines
+375
to
+386
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might be nice to have a validate function so that we can do something like: request_ok, request_error = self.job_collection.validate_paging_params(page, page_size)
if not request_ok:
return respond(
request=request
response={"error": request_error},
code=http.BAD_REQUEST,
) |
||||||||||||||||||||||||
response = dict(jobs=jobs, pagination=dict(page=page, page_size=page_size, total=total)) | ||||||||||||||||||||||||
else: | ||||||||||||||||||||||||
response = dict(jobs=jobs) | ||||||||||||||||||||||||
Comment on lines
+387
to
+389
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. imo, inlining the dicts rather than calling the dict constructor would be more modern/easier to read - that said, we have pretty mixed usage of dict/{} in this file already so not a blocker |
||||||||||||||||||||||||
return respond(request=request, response=response) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
@AsyncResource.bounded | ||||||||||||||||||||||||
def render_SEARCH(self, request): | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. my understanding is that the but just to confirm: the intent is that we'll have the troncli/ui use SEARCH/QUERY verbs to hit this new function? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes your understanding is correct and I did added I'm more than happy to take suggestions on what endpoint we can drop the search at or we can have a discussion about if it makes sense to have the custom http verb There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TIL: HTTP SEARCH verb is a thing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. imo, search through GET parameters makes sense (e.g. GET /api/jobs?search=query) over a non-standard verb |
||||||||||||||||||||||||
query = requestargs.get_string(request, "query") | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need to do any validation on the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Search is currently very basic we could maybe limit the length of the string 🤷🏽 we also don't use it in a way that could lead to SQL injections |
||||||||||||||||||||||||
jobs = self.job_collection.get_jobs() | ||||||||||||||||||||||||
filtered_jobs = [job for job in jobs if query.lower() in job.get_name().lower()] | ||||||||||||||||||||||||
response = dict( | ||||||||||||||||||||||||
jobs=self.get_data( | ||||||||||||||||||||||||
include_job_runs, | ||||||||||||||||||||||||
include_action_runs, | ||||||||||||||||||||||||
include_action_graph, | ||||||||||||||||||||||||
include_node_pool, | ||||||||||||||||||||||||
), | ||||||||||||||||||||||||
jobs=adapter.adapt_many(adapter.JobAdapter, filtered_jobs), | ||||||||||||||||||||||||
) | ||||||||||||||||||||||||
return respond(request=request, response=response) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we should probably set the
spec
on these to whatever the real type of these is so that the tests better match reality (e.g., so that the mocks fail should we ever removeget_name
from the actual type, etc)