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

CRAYSAT-1896: Add CFSClient methods to get configurations, sessions #47

Merged
merged 1 commit into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.3.0] - 2024-11-18

### Added
- Added methods to the `CFSV2Client` and `CFSV3Client` classes to get a list of
all the configurations and sessions, handling paging for the CFS v3 API.

## [2.2.4] - 2024-10-23

### Updated
- Updated `poetry.lock` file to fetch latest version of `cray product catalog`
to which hash value remains consistent.
- Updated `poetry.lock` file to fetch latest version of `cray product catalog`
to which hash value remains consistent.

## [2.2.3] - 2024-10-14

Expand Down
77 changes: 67 additions & 10 deletions csm_api_client/service/cfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,34 @@ def get_components(self, params: Dict = None) -> Generator[Dict, None, None]:
"""
pass

@abstractmethod
def get_configurations(self, params: Dict = None) -> Generator[Dict, None, None]:
"""Get all the CFS configurations.

This method must handle paging if necessary.

Args:
params: the parameters to pass to the GET on configurations

Yields:
The CFS configurations.
"""
pass

@abstractmethod
def get_sessions(self, params: Dict = None) -> Generator[Dict, None, None]:
"""Get all the CFS sessions.

This method must handle paging if necessary.

Args:
params: the parameters to pass to the GET on sessions

Yields:
The CFS sessions.
"""
pass

def get_component_ids_using_config(self, config_name: str) -> List[str]:
"""Get a list of CFS components using the given CFS configuration.

Expand Down Expand Up @@ -1564,14 +1592,29 @@ class CFSV2Client(CFSClientBase):
def join_words(*words: str) -> str:
return ''.join([words[0].lower()] + [word.capitalize() for word in words[1:]])

def get_components(self, params: Dict = None) -> Generator[Dict, None, None]:
def get_resource(self, resource: str, params: Dict = None) -> Generator[Dict, None, None]:
"""Get a resource from the CFS API.

Args:
resource: the name of the resource to get (e.g. 'components')
params: the parameters to pass to the GET on the resource
"""
try:
yield from self.get('components', params=params).json()
yield from self.get(resource, params=params).json()
except APIError as err:
raise APIError(f'Failed to get CFS components: {err}')
raise APIError(f'Failed to get CFS {resource}: {err}')
except ValueError as err:
raise APIError(f'Failed to parse JSON in response from CFS when getting '
f'components: {err}')
f'{resource}: {err}')

def get_components(self, params: Dict = None) -> Generator[Dict, None, None]:
yield from self.get_resource('components', params=params)

def get_configurations(self, params: Dict = None) -> Generator[Dict, None, None]:
yield from self.get_resource('configurations', params=params)

def get_sessions(self, params: Dict = None) -> Generator[Dict, None, None]:
yield from self.get_resource('sessions', params=params)


# Create an alias for CFSClient that points at CFSV2Client to preserve backwards compatibility
Expand All @@ -1586,19 +1629,33 @@ class CFSV3Client(CFSClientBase):
def join_words(*words: str) -> str:
return '_'.join([word.lower() for word in words])

def get_components(self, params: Dict = None) -> Generator[Dict, None, None]:
def get_paged_resource(self, resource: str, params: Dict = None) -> Generator[Dict, None, None]:
"""Get a paged resource from the CFS API.

Args:
resource: the name of the resource to get (e.g. 'components')
params: the parameters to pass to the GET on the resource
"""
# On the first request, pass in user-specified parameters
next_params = params
try:
while True:
response = self.get('components', params=next_params).json()
yield from response['components']
# The CFS API preserves user-specified parameters and adds pagination parameters
response = self.get(resource, params=next_params).json()
yield from response[resource]
next_params = response.get('next')
if not next_params:
break
except APIError as err:
raise APIError(f'Failed to get CFS components: {err}')
raise APIError(f'Failed to get CFS {resource}: {err}')
except ValueError as err:
raise APIError(f'Failed to parse JSON in response from CFS when getting '
f'components: {err}')
f'{resource}: {err}')

def get_components(self, params: Dict = None) -> Generator[Dict, None, None]:
yield from self.get_paged_resource('components', params=params)

def get_configurations(self, params: Dict = None) -> Generator[Dict, None, None]:
yield from self.get_paged_resource('configurations', params=params)

def get_sessions(self, params: Dict = None) -> Generator[Dict, None, None]:
yield from self.get_paged_resource('sessions', params=params)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "csm-api-client"
version = "2.2.4"
version = "2.3.0"
description = "Python client library for CSM APIs"
authors = [
"Ryan Haasken <[email protected]>",
Expand Down
110 changes: 110 additions & 0 deletions tests/service/test_cfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1885,4 +1885,114 @@ def test_get_components_unpaged(self):
mock_get.assert_has_calls([
call('components', params=None),
call().json()
])

def test_get_configurations_paged(self):
"""Test get_configurations method of CFSV3Client with paged results"""
cfs_client = CFSV3Client(Mock())
configurations = [
{'name': 'config-1'},
{'name': 'config-2'},
{'name': 'config-3'},
{'name': 'config-4'},
{'name': 'config-5'},
]

base_params = {'limit': 2}
with patch.object(cfs_client, 'get') as mock_get:
mock_get.return_value.json.side_effect = [
{'configurations': configurations[:2], 'next': {'limit': 2, 'after': 'config-2'}},
{'configurations': configurations[2:4], 'next': {'limit': 2, 'after': 'config-4'}},
{'configurations': [configurations[4]], 'next': None}
]

result = list(cfs_client.get_configurations(params=base_params))

self.assertEqual(configurations, result)
mock_get.assert_has_calls([
call('configurations', params=base_params),
call().json(),
call('configurations', params={'limit': 2, 'after': 'config-2'}),
call().json(),
call('configurations', params={'limit': 2, 'after': 'config-4'}),
call().json()
])

def test_get_configurations_unpaged(self):
"""Test get_configurations method of CFSV3Client when results are not paged"""
cfs_client = CFSV3Client(Mock())
configurations = [
{'name': 'config-1'},
{'name': 'config-2'},
{'name': 'config-3'},
{'name': 'config-4'},
{'name': 'config-5'},
]

with patch.object(cfs_client, 'get') as mock_get:
mock_get.return_value.json.side_effect = [
{'configurations': configurations, 'next': None}
]

result = list(cfs_client.get_configurations())

self.assertEqual(configurations, result)
mock_get.assert_has_calls([
call('configurations', params=None),
call().json()
])

def test_get_sessions_paged(self):
"""Test get_sessions method of CFSV3Client with paged results"""
cfs_client = CFSV3Client(Mock())
sessions = [
{'name': 'session-1'},
{'name': 'session-2'},
{'name': 'session-3'},
{'name': 'session-4'},
{'name': 'session-5'},
]

base_params = {'limit': 2}
with patch.object(cfs_client, 'get') as mock_get:
mock_get.return_value.json.side_effect = [
{'sessions': sessions[:2], 'next': {'limit': 2, 'after': 'session-2'}},
{'sessions': sessions[2:4], 'next': {'limit': 2, 'after': 'session-4'}},
{'sessions': [sessions[4]], 'next': None}
]

result = list(cfs_client.get_sessions(params=base_params))

self.assertEqual(sessions, result)
mock_get.assert_has_calls([
call('sessions', params=base_params),
call().json(),
call('sessions', params={'limit': 2, 'after': 'session-2'}),
call().json(),
call('sessions', params={'limit': 2, 'after': 'session-4'}),
call().json()
])

def test_get_sessions_unpaged(self):
"""Test get_sessions method of CFSV3Client when results are not paged"""
cfs_client = CFSV3Client(Mock())
sessions = [
{'name': 'session-1'},
{'name': 'session-2'},
{'name': 'session-3'},
{'name': 'session-4'},
{'name': 'session-5'},
]

with patch.object(cfs_client, 'get') as mock_get:
mock_get.return_value.json.side_effect = [
{'sessions': sessions, 'next': None}
]

result = list(cfs_client.get_sessions())

self.assertEqual(sessions, result)
mock_get.assert_has_calls([
call('sessions', params=None),
call().json()
])
Loading