Skip to content

Commit 59fd00e

Browse files
authored
Merge pull request #11 from ni-kismet/users/fvisser/fix-pagination-bahavior
Implement Continuation Token Pagination
2 parents d725647 + ce98149 commit 59fd00e

File tree

9 files changed

+561
-258
lines changed

9 files changed

+561
-258
lines changed

.github/copilot-instructions.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,27 @@
8181
- Provide clear, actionable error messages that guide users toward solutions
8282
- Support shell completion where feasible
8383

84+
## HTTP API Documentation & Validation
85+
86+
### API Client Implementation Guidelines
87+
- All HTTP client calls must include comprehensive type hints based on OpenAPI specifications
88+
- Use `requests.Response` type annotations for all API responses
89+
- Implement response validation using the OpenAPI spec as the source of truth
90+
- Document any discrepancies between OpenAPI spec and actual service behavior in code comments
91+
- All API client functions must handle standard HTTP error codes (400, 401, 403, 404, 500)
92+
93+
### OpenAPI Specifications (Reference)
94+
- SystemLink DataFrame Service: https://dev-api.lifecyclesolutions.ni.com/nidataframe/swagger/v1/nidataframe.json
95+
- SystemLink Notebook Service: https://dev-api.lifecyclesolutions.ni.com/ninotebook/swagger/v1/ninotebook.yaml
96+
- SystemLink Test Monitor Service: https://dev-api.lifecyclesolutions.ni.com/nitestmonitor/swagger/v2/nitestmonitor-v2.yml
97+
- SystemLink User Service: https://dev-api.lifecyclesolutions.ni.com/niuser/swagger/v1/niuser.yaml
98+
- Work Order Service: https://dev-api.lifecyclesolutions.ni.com/niworkorder/swagger/v1/niworkorder.json
99+
100+
### Implementation Pattern
101+
- Create typed response models based on OpenAPI schemas when implementing new API clients
102+
- Use consistent error handling via `handle_api_error()` for all API interactions
103+
- Add TODO comments when OpenAPI spec doesn't match actual service behavior
104+
84105
## Required Actions After Any Change
85106

86107
- Run `poetry run ni-python-styleguide lint` to check for linting and style issues.

slcli/dff_click.py

Lines changed: 149 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import click
88
import requests
99

10-
from .universal_handlers import UniversalResponseHandler
10+
from .universal_handlers import UniversalResponseHandler, FilteredResponse
1111
from .utils import (
1212
ExitCodes,
1313
get_base_url,
@@ -158,6 +158,139 @@ def validate_field_type(field_type: str) -> None:
158158
)
159159

160160

161+
def _query_all_groups(workspace_filter: Optional[str] = None, workspace_map: Optional[dict] = None):
162+
"""Query all DFF groups using continuation token pagination.
163+
164+
Args:
165+
workspace_filter: Optional workspace ID or name to filter by
166+
workspace_map: Optional workspace mapping to avoid repeated lookups
167+
168+
Returns:
169+
List of all groups, optionally filtered by workspace
170+
"""
171+
url = f"{get_base_url()}/nidynamicformfields/v1/groups"
172+
all_groups = []
173+
continuation_token = None
174+
175+
while True:
176+
# Build parameters for the request
177+
params = {"Take": 100} # Use smaller page size for efficient pagination
178+
if continuation_token:
179+
params["ContinuationToken"] = continuation_token
180+
181+
# Build query string
182+
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
183+
full_url = f"{url}?{query_string}"
184+
185+
resp = make_api_request("GET", full_url)
186+
data = resp.json()
187+
188+
# Extract groups from this page
189+
groups = data.get("groups", [])
190+
all_groups.extend(groups)
191+
192+
# Check if there are more pages
193+
continuation_token = data.get("continuationToken")
194+
if not continuation_token:
195+
break
196+
197+
# Filter by workspace if specified
198+
if workspace_filter and workspace_map:
199+
all_groups = filter_by_workspace(all_groups, workspace_filter, workspace_map)
200+
201+
return all_groups
202+
203+
204+
def _query_all_fields(workspace_filter: Optional[str] = None, workspace_map: Optional[dict] = None):
205+
"""Query all DFF fields using continuation token pagination.
206+
207+
Args:
208+
workspace_filter: Optional workspace ID or name to filter by
209+
workspace_map: Optional workspace mapping to avoid repeated lookups
210+
211+
Returns:
212+
List of all fields, optionally filtered by workspace
213+
"""
214+
url = f"{get_base_url()}/nidynamicformfields/v1/fields"
215+
all_fields = []
216+
continuation_token = None
217+
218+
while True:
219+
# Build parameters for the request
220+
params = {"Take": 500} # Use smaller page size for efficient pagination
221+
if continuation_token:
222+
params["ContinuationToken"] = continuation_token
223+
224+
# Build query string
225+
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
226+
full_url = f"{url}?{query_string}"
227+
228+
resp = make_api_request("GET", full_url)
229+
data = resp.json()
230+
231+
# Extract fields from this page
232+
fields = data.get("fields", [])
233+
all_fields.extend(fields)
234+
235+
# Check if there are more pages
236+
continuation_token = data.get("continuationToken")
237+
if not continuation_token:
238+
break
239+
240+
# Filter by workspace if specified
241+
if workspace_filter and workspace_map:
242+
all_fields = filter_by_workspace(all_fields, workspace_filter, workspace_map)
243+
244+
return all_fields
245+
246+
247+
def _query_all_configurations(
248+
workspace_filter: Optional[str] = None, workspace_map: Optional[dict] = None
249+
):
250+
"""Query all configurations using continuation token pagination.
251+
252+
Args:
253+
workspace_filter: Optional workspace ID or name to filter by
254+
workspace_map: Optional workspace mapping to avoid repeated lookups
255+
256+
Returns:
257+
List of all configurations, optionally filtered by workspace
258+
"""
259+
url = f"{get_base_url()}/nidynamicformfields/v1/configurations"
260+
all_configurations = []
261+
continuation_token = None
262+
263+
while True:
264+
# Build parameters for the request
265+
params = {"Take": 100} # Use smaller page size for efficient pagination
266+
if continuation_token:
267+
params["ContinuationToken"] = continuation_token
268+
269+
# Build query string
270+
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
271+
full_url = f"{url}?{query_string}"
272+
273+
resp = make_api_request("GET", full_url)
274+
data = resp.json()
275+
276+
# Extract configurations from this page
277+
configurations = data.get("configurations", [])
278+
all_configurations.extend(configurations)
279+
280+
# Check if there are more pages
281+
continuation_token = data.get("continuationToken")
282+
if not continuation_token:
283+
break
284+
285+
# Filter by workspace if specified
286+
if workspace_filter and workspace_map:
287+
all_configurations = filter_by_workspace(
288+
all_configurations, workspace_filter, workspace_map
289+
)
290+
291+
return all_configurations
292+
293+
161294
def register_dff_commands(cli):
162295
"""Register the 'dff' command group and its subcommands."""
163296

@@ -190,43 +323,21 @@ def config():
190323
)
191324
def list_configurations(workspace: Optional[str] = None, take: int = 25, format: str = "table"):
192325
"""List dynamic form field configurations."""
193-
url = f"{get_base_url()}/nidynamicformfields/v1/configurations"
194-
195326
try:
196-
params = {"Take": 1000} # Fetch more data for pagination
197-
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
198-
full_url = f"{url}?{query_string}"
199-
200-
resp = make_api_request("GET", full_url)
201-
data = resp.json()
202-
configurations = data.get("configurations", [])
203-
204327
# Get workspace map once and reuse it
205328
workspace_map = get_workspace_map()
206329

207-
# Filter by workspace if specified
208-
if workspace:
209-
configurations = filter_by_workspace(configurations, workspace, workspace_map)
210-
211330
# Use the workspace formatter for consistent formatting
212331
format_config_row = WorkspaceFormatter.create_config_row_formatter(workspace_map)
213332

333+
# Use continuation token pagination following user_click.py pattern
334+
all_configurations = _query_all_configurations(workspace, workspace_map)
335+
214336
# Use UniversalResponseHandler for consistent pagination
215337
from typing import Any
216338

217-
# Create a mock response with filtered data
218-
class FilteredResponse:
219-
def __init__(self, filtered_data):
220-
self._data = {"configurations": filtered_data}
221-
222-
def json(self):
223-
return self._data
224-
225-
@property
226-
def status_code(self):
227-
return 200
228-
229-
filtered_resp: Any = FilteredResponse(configurations)
339+
# Create a mock response with all data
340+
filtered_resp: Any = FilteredResponse({"configurations": all_configurations})
230341

231342
handler = UniversalResponseHandler()
232343
handler.handle_list_response(
@@ -757,43 +868,21 @@ def groups():
757868
)
758869
def list_groups(workspace: Optional[str] = None, take: int = 25, format: str = "table"):
759870
"""List dynamic form field groups."""
760-
url = f"{get_base_url()}/nidynamicformfields/v1/groups"
761-
762871
try:
763-
params = {"Take": 1000} # Fetch more data for pagination
764-
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
765-
full_url = f"{url}?{query_string}"
766-
767-
resp = make_api_request("GET", full_url)
768-
data = resp.json()
769-
groups = data.get("groups", [])
770-
771872
# Get workspace map once and reuse it
772873
workspace_map = get_workspace_map()
773874

774-
# Filter by workspace if specified
775-
if workspace:
776-
groups = filter_by_workspace(groups, workspace, workspace_map)
875+
# Use continuation token pagination following the pattern
876+
all_groups = _query_all_groups(workspace, workspace_map)
777877

778878
# Use the workspace formatter for consistent formatting
779879
format_group_row = WorkspaceFormatter.create_group_field_row_formatter(workspace_map)
780880

781881
# Use UniversalResponseHandler for consistent pagination
782882
from typing import Any
783883

784-
# Create a mock response with filtered data
785-
class FilteredResponse:
786-
def __init__(self, filtered_data):
787-
self._data = {"groups": filtered_data}
788-
789-
def json(self):
790-
return self._data
791-
792-
@property
793-
def status_code(self):
794-
return 200
795-
796-
filtered_resp: Any = FilteredResponse(groups)
884+
# Create a mock response with all data
885+
filtered_resp: Any = FilteredResponse({"groups": all_groups})
797886

798887
handler = UniversalResponseHandler()
799888
handler.handle_list_response(
@@ -835,43 +924,21 @@ def fields():
835924
)
836925
def list_fields(workspace: Optional[str] = None, take: int = 25, format: str = "table"):
837926
"""List dynamic form fields."""
838-
url = f"{get_base_url()}/nidynamicformfields/v1/fields"
839-
840927
try:
841-
params = {"Take": 1000} # Fetch more data for pagination
842-
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
843-
full_url = f"{url}?{query_string}"
844-
845-
resp = make_api_request("GET", full_url)
846-
data = resp.json()
847-
fields = data.get("fields", [])
848-
849928
# Get workspace map once and reuse it
850929
workspace_map = get_workspace_map()
851930

852-
# Filter by workspace if specified
853-
if workspace:
854-
fields = filter_by_workspace(fields, workspace, workspace_map)
931+
# Use continuation token pagination following the pattern
932+
all_fields = _query_all_fields(workspace, workspace_map)
855933

856934
# Use the workspace formatter for consistent formatting
857935
format_field_row = WorkspaceFormatter.create_group_field_row_formatter(workspace_map)
858936

859937
# Use UniversalResponseHandler for consistent pagination
860938
from typing import Any
861939

862-
# Create a mock response with filtered data
863-
class FilteredResponse:
864-
def __init__(self, filtered_data):
865-
self._data = {"fields": filtered_data}
866-
867-
def json(self):
868-
return self._data
869-
870-
@property
871-
def status_code(self):
872-
return 200
873-
874-
filtered_resp: Any = FilteredResponse(fields)
940+
# Create a mock response with all data
941+
filtered_resp: Any = FilteredResponse({"fields": all_fields})
875942

876943
handler = UniversalResponseHandler()
877944
handler.handle_list_response(
@@ -971,7 +1038,7 @@ def query_tables(
9711038
"workspace": workspace_id,
9721039
"resourceType": resource_type,
9731040
"resourceId": resource_id,
974-
"take": 1000, # Fetch more data for pagination
1041+
"take": take,
9751042
"returnCount": return_count,
9761043
}
9771044

@@ -993,18 +1060,7 @@ def query_tables(
9931060
from typing import Any
9941061

9951062
# Create a mock response with filtered data
996-
class FilteredResponse:
997-
def __init__(self, filtered_data):
998-
self._data = {"tables": filtered_data}
999-
1000-
def json(self):
1001-
return self._data
1002-
1003-
@property
1004-
def status_code(self):
1005-
return 200
1006-
1007-
filtered_resp: Any = FilteredResponse(tables)
1063+
filtered_resp: Any = FilteredResponse({"tables": tables})
10081064

10091065
handler = UniversalResponseHandler()
10101066
handler.handle_list_response(

0 commit comments

Comments
 (0)