Skip to content
55 changes: 55 additions & 0 deletions backend/compact-connect/docs/api-specification/latest-oas30.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,61 @@
}
],
"paths": {
"/v1/public/jurisdictions/live": {
"get": {
"summary": "Get live jurisdictions",
"description": "Returns all jurisdictions that are live (enabled for operations) across all compacts or for a specific compact if the optional compact query parameter is provided.",
"parameters": [
{
"name": "compact",
"in": "query",
"required": false,
"description": "Optional compact abbreviation to filter results. If not provided, returns data for all compacts. If an invalid compact is provided, returns a 400 error.",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "200 response - Returns a dictionary with compact abbreviations as keys and arrays of live jurisdiction postal abbreviations as values",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
},
"example": {
"aslp": ["co", "ne", "wy"],
"octp": ["ak", "ky"]
}
}
}
}
},
"400": {
"description": "400 response - Invalid compact abbreviation provided",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Invalid request query param: invalid_compact"
}
}
}
}
}
}
}
}
},
"/v1/compacts/{compact}/jurisdictions/{jurisdiction}/licenses": {
"post": {
"parameters": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,61 @@
}
],
"paths": {
"/v1/public/jurisdictions/live": {
"get": {
"summary": "Get live jurisdictions",
"description": "Returns all jurisdictions that are live (enabled for operations) across all compacts or for a specific compact if the optional compact query parameter is provided.",
"parameters": [
{
"name": "compact",
"in": "query",
"required": false,
"description": "Optional compact abbreviation to filter results. If not provided, returns data for all compacts. If an invalid compact is provided, returns a 400 error.",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "200 response - Returns a dictionary with compact abbreviations as keys and arrays of live jurisdiction postal abbreviations as values",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
},
"example": {
"aslp": ["co", "ne", "wy"],
"octp": ["ak", "ky"]
}
}
}
}
},
"400": {
"description": "400 response - Invalid compact abbreviation provided",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Invalid request query param: invalid_compact"
}
}
}
}
}
}
}
}
},
"/v1/compacts/{compact}": {
"get": {
"parameters": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -450,3 +450,31 @@ def update_compact_configured_states(self, compact: str, configured_states: list
':dou': self.config.current_standard_datetime.isoformat(),
},
)

def get_live_compact_jurisdictions(self, compact: str) -> list[str]:
"""
Get all live (isLive: true) jurisdiction postal abbreviations for a specific compact.

:param compact: The compact abbreviation
:return: List of jurisdiction postal abbreviations that are live in the compact
"""
logger.info('Getting live jurisdictions for compact', compact=compact)
Comment thread
ChiefStief marked this conversation as resolved.

try:
compact_config = self.get_compact_configuration(compact)
except CCNotFoundException:
logger.info('Compact configuration not found, returning empty list', compact=compact)
return []

# Filter configuredStates for those with isLive: true and extract postal abbreviations
live_jurisdictions = [
state['postalAbbreviation'] for state in compact_config.configuredStates if state.get('isLive', False)
]

logger.info(
'Retrieved live jurisdictions for compact',
compact=compact,
live_jurisdictions_count=len(live_jurisdictions),
)

return live_jurisdictions
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ def compact_configuration_api_handler(event: dict, context: LambdaContext): # n
return _get_staff_users_compact_jurisdictions(event, context)
if event['httpMethod'] == 'GET' and event['resource'] == '/v1/public/compacts/{compact}/jurisdictions':
return _get_public_compact_jurisdictions(event, context)
if event['httpMethod'] == 'GET' and event['resource'] == '/v1/public/jurisdictions/live':
return _get_live_public_compact_jurisdictions(event, context)
if event['httpMethod'] == 'GET' and event['resource'] == '/v1/compacts/{compact}':
return _get_staff_users_compact_configuration(event, context)
if event['httpMethod'] == 'PUT' and event['resource'] == '/v1/compacts/{compact}':
Expand Down Expand Up @@ -118,6 +120,41 @@ def _get_public_compact_jurisdictions(event: dict, context: LambdaContext): # n
return CompactJurisdictionsPublicResponseSchema().load(compact_jurisdictions, many=True)


def _get_live_public_compact_jurisdictions(event: dict, context: LambdaContext): # noqa: ARG001 unused-argument
"""
Endpoint to get all live jurisdictions, optionally filtered by compact.

:param event: API Gateway event with optional query parameter 'compact'
:param context: Lambda context
:return: Dictionary with compact abbreviations as keys and lists of live jurisdiction abbreviations as values
"""
query_params = event.get('queryStringParameters') or {}
compact_filter = query_params.get('compact')

# Determine which compacts to query
compacts_to_query = []
if compact_filter:
# Validate the compact
if compact_filter.lower() in config.compacts:
compacts_to_query = [compact_filter.lower()]
logger.info('Getting live jurisdictions for specific compact', compact=compact_filter)
else:
logger.info('Invalid compact provided', compact=compact_filter)
raise CCInvalidRequestException(f'Invalid request query param: {compact_filter}')
else:
logger.info('Getting live jurisdictions for all compacts')
compacts_to_query = config.compacts

# Build result dictionary
result = {}
for compact in compacts_to_query:
live_jurisdictions = config.compact_configuration_client.get_live_compact_jurisdictions(compact=compact)
result[compact] = live_jurisdictions

logger.info('Returning live jurisdictions', compacts_count=len(result))
return result

Comment thread
ChiefStief marked this conversation as resolved.

@authorize_compact_level_only_action(action=CCPermissionsAction.ADMIN)
def _get_staff_users_compact_configuration(event: dict, context: LambdaContext): # noqa: ARG001 unused-argument
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

STAFF_USERS_COMPACT_JURISDICTION_ENDPOINT_RESOURCE = '/v1/compacts/{compact}/jurisdictions'
PUBLIC_COMPACT_JURISDICTION_ENDPOINT_RESOURCE = '/v1/public/compacts/{compact}/jurisdictions'
LIVE_JURISDICTIONS_ENDPOINT_RESOURCE = '/v1/public/jurisdictions/live'

COMPACT_CONFIGURATION_ENDPOINT_RESOURCE = '/v1/compacts/{compact}'
JURISDICTION_CONFIGURATION_ENDPOINT_RESOURCE = '/v1/compacts/{compact}/jurisdictions/{jurisdiction}'
Expand Down Expand Up @@ -191,6 +192,137 @@ def test_get_compact_jurisdictions_returns_list_of_configured_jurisdictions(self
sorted_response,
)

def test_get_public_live_compact_jurisdictions_returns_list_of_all_live_jurisdictions(self):
"""Test getting list of live jurisdictions across all compacts when no query param provided"""
from handlers.compact_configuration import compact_configuration_api_handler

# Create compact configurations with some jurisdictions marked as live
# ASLP compact with some live jurisdictions
self.test_data_generator.put_default_compact_configuration_in_configuration_table(
value_overrides={
'compactAbbr': 'aslp',
'configuredStates': [
{'postalAbbreviation': 'ky', 'isLive': True},
{'postalAbbreviation': 'oh', 'isLive': True},
{'postalAbbreviation': 'ne', 'isLive': False},
],
},
)

# OCTP compact with different live jurisdictions
self.test_data_generator.put_default_compact_configuration_in_configuration_table(
value_overrides={
'compactAbbr': 'octp',
'configuredStates': [
{'postalAbbreviation': 'ne', 'isLive': True},
{'postalAbbreviation': 'oh', 'isLive': False},
]
},
)

# COUN compact with no live jurisdictions
self.test_data_generator.put_default_compact_configuration_in_configuration_table(
value_overrides={
'compactAbbr': 'coun',
'configuredStates': [
{'postalAbbreviation': 'ky', 'isLive': False},
],
},
)

# Create event without query params
event = generate_test_event('GET', LIVE_JURISDICTIONS_ENDPOINT_RESOURCE)
event['queryStringParameters'] = None

response = compact_configuration_api_handler(event, self.mock_context)
self.assertEqual(200, response['statusCode'], msg=json.loads(response['body']))
response_body = json.loads(response['body'])

# Should return all compacts with their live jurisdictions
self.assertIn('aslp', response_body)
self.assertIn('octp', response_body)
self.assertIn('coun', response_body)

# Verify the live jurisdictions for each compact
self.assertCountEqual(['oh', 'ky'], response_body['aslp'])
self.assertCountEqual(['ne'], response_body['octp'])
self.assertCountEqual([], response_body['coun'])

def test_get_public_live_compact_jurisdictions_returns_list_of_live_jurisdictions_in_compact(self):
"""Test getting list of live jurisdictions for compact designated through query param"""
from handlers.compact_configuration import compact_configuration_api_handler

# Create compact configurations
self.test_data_generator.put_default_compact_configuration_in_configuration_table(
value_overrides={
'compactAbbr': 'aslp',
'configuredStates': [
{'postalAbbreviation': 'ky', 'isLive': True},
{'postalAbbreviation': 'oh', 'isLive': True},
{'postalAbbreviation': 'ne', 'isLive': False},
],
},
)

self.test_data_generator.put_default_compact_configuration_in_configuration_table(
value_overrides={
'compactAbbr': 'octp',
'configuredStates': [
{'postalAbbreviation': 'ne', 'isLive': True},
],
},
)

# Create event with compact query param
event = generate_test_event('GET', LIVE_JURISDICTIONS_ENDPOINT_RESOURCE)
event['queryStringParameters'] = {'compact': 'aslp'}

response = compact_configuration_api_handler(event, self.mock_context)
self.assertEqual(200, response['statusCode'], msg=json.loads(response['body']))
response_body = json.loads(response['body'])

# Should only return the specified compact
self.assertIn('aslp', response_body)
self.assertNotIn('octp', response_body)
self.assertNotIn('coun', response_body)

# Verify the live jurisdictions
self.assertCountEqual(['ky', 'oh'], response_body['aslp'])

def test_get_public_live_compact_jurisdictions_returns_400_if_bad_compact_param(self):
"""Test getting list of live jurisdictions returns 400 when invalid query param provided"""
from handlers.compact_configuration import compact_configuration_api_handler

# Create compact configurations
self.test_data_generator.put_default_compact_configuration_in_configuration_table(
value_overrides={
'compactAbbr': 'aslp',
'configuredStates': [
{'postalAbbreviation': 'ky', 'isLive': True},
],
},
)

self.test_data_generator.put_default_compact_configuration_in_configuration_table(
value_overrides={
'compactAbbr': 'octp',
'configuredStates': [
{'postalAbbreviation': 'oh', 'isLive': True},
],
},
)

# Create event with invalid compact query param
event = generate_test_event('GET', LIVE_JURISDICTIONS_ENDPOINT_RESOURCE)
event['queryStringParameters'] = {'compact': 'invalid_compact'}

response = compact_configuration_api_handler(event, self.mock_context)
self.assertEqual(400, response['statusCode'], msg=json.loads(response['body']))
response_body = json.loads(response['body'])

# Verify the error message
self.assertEqual({'message': 'Invalid request query param: invalid_compact'}, response_body)

Comment thread
ChiefStief marked this conversation as resolved.

@mock_aws
@patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat(DEFAULT_DATE_OF_UPDATE_TIMESTAMP))
Expand Down
5 changes: 5 additions & 0 deletions backend/compact-connect/stacks/api_stack/v1_api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ def __init__(
# POST /v1/public/compacts/{compact}/providers/query
# GET /v1/public/compacts/{compact}/providers/{providerId}
self.public_compacts_resource = self.public_resource.add_resource('compacts')
# /v1/public/jurisdictions
self.public_jurisdictions_resource = self.public_resource.add_resource('jurisdictions')
Comment thread
ChiefStief marked this conversation as resolved.
# /v1/public/jurisdictions/live
self.live_jurisdictions_resource = self.public_jurisdictions_resource.add_resource('live')
self.public_compacts_compact_resource = self.public_compacts_resource.add_resource('{compact}')
self.public_compacts_compact_providers_resource = self.public_compacts_compact_resource.add_resource(
'providers'
Expand Down Expand Up @@ -177,6 +181,7 @@ def __init__(
self.compact_configuration_api = CompactConfigurationApi(
api=self.api,
compact_resource=self.compact_resource,
live_jurisdictions_resource=self.live_jurisdictions_resource,
jurisdictions_resource=self.jurisdictions_resource,
public_jurisdictions_resource=self.public_compacts_compact_jurisdictions_resource,
jurisdiction_resource=self.jurisdiction_resource,
Expand Down
Loading