Skip to content
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
12 changes: 7 additions & 5 deletions shared/python/apimrequests.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,13 @@ def _multiRequest(self, method: HTTP_VERB, path: str, runs: int, headers: list[a
'response_time': response_time
})

if sleepMs is not None:
if sleepMs > 0:
time.sleep(sleepMs / 1000)
else:
time.sleep(SLEEP_TIME_BETWEEN_REQUESTS_MS / 1000) # default sleep time
# Sleep only between requests (not after the final run)
if i < runs - 1:
if sleepMs is not None:
if sleepMs > 0:
time.sleep(sleepMs / 1000)
else:
time.sleep(SLEEP_TIME_BETWEEN_REQUESTS_MS / 1000) # default sleep time
finally:
session.close()

Expand Down
600 changes: 213 additions & 387 deletions shared/python/infrastructures.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion shared/python/logging_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

try:
from dotenv import load_dotenv # type: ignore[import-not-found]
except Exception: # pragma: no cover
except Exception:
load_dotenv = None


Expand Down
6 changes: 3 additions & 3 deletions shared/python/show_soft_deleted_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
from datetime import datetime
from pathlib import Path

# APIM Samples imports
import azure_resources as az

# Configure UTF-8 encoding for console output
if sys.stdout.encoding != 'utf-8':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

# APIM Samples imports
import azure_resources as az


def _get_suggested_purge_command() -> str:
"""Return a purge command that works from the current working directory."""
Expand Down
9 changes: 4 additions & 5 deletions shared/python/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import azure_resources as az
from apimtypes import APIM_SKU, HTTP_VERB, INFRASTRUCTURE, Endpoints, Output, get_project_root
from console import print_error, print_info, print_message, print_ok, print_plain, print_warning, print_val
from logging_config import get_configured_level_name
import logging_config

# Configure warning filter to suppress IPython exit warnings
warnings.filterwarnings(
Expand Down Expand Up @@ -48,7 +48,7 @@ def get_deployment_failure_message(deployment_name: str) -> str:
base_message = f"Deployment '{deployment_name}' failed. View deployment details in Azure Portal."

# Only suggest enabling DEBUG logging if it's not already enabled
current_level = get_configured_level_name()
current_level = logging_config.get_configured_level_name()
if current_level != 'DEBUG':
return f"{base_message} Enable DEBUG logging in workspace root .env file, then rerun to see details."

Expand Down Expand Up @@ -644,9 +644,8 @@ def find_project_root() -> str:

while current_dir != os.path.dirname(current_dir): # Stop at filesystem root
if any(os.path.exists(os.path.join(current_dir, marker)) for marker in marker_files):
# Additional check: verify this looks like our project by checking for samples directory
if os.path.exists(os.path.join(current_dir, 'samples')):
return current_dir
# Return as soon as marker files are found; do not require 'samples' directory
return current_dir
current_dir = os.path.dirname(current_dir)

# If we can't find the project root, raise an error
Expand Down
20 changes: 6 additions & 14 deletions start.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ function Invoke-Cmd {
Write-Host ""
Write-Host "Command failed: $_" -ForegroundColor Red
Write-Host ""
Invoke-Pause-Menu
return $false
}
finally {
Expand All @@ -66,7 +65,6 @@ function Invoke-Cmd {
Write-Host ""
Write-Host "Command exited with code $exitCode" -ForegroundColor Yellow
Write-Host ""
Invoke-Pause-Menu
return $false
}

Expand Down Expand Up @@ -101,11 +99,6 @@ except Exception as exc: # pylint: disable=broad-except
}
}

function Invoke-Pause-Menu {
Write-Host ""
Write-Host "=========================="
Read-Host "Press ENTER to return to the menu" | Out-Null
}

while ($true) {
Write-Host ""
Expand All @@ -130,26 +123,25 @@ while ($true) {

switch ($choice) {
'1' {
if (Invoke-Cmd (Get-Python) "$RepoRoot/setup/local_setup.py" "--complete-setup") { Invoke-Pause-Menu }
Invoke-Cmd (Get-Python) "$RepoRoot/setup/local_setup.py" "--complete-setup"
}
'2' {
if (Invoke-Cmd (Get-Python) "$RepoRoot/setup/verify_local_setup.py") { Invoke-Pause-Menu }
Invoke-Cmd (Get-Python) "$RepoRoot/setup/verify_local_setup.py"
}
'3' {
Show-AccountInfo
Invoke-Pause-Menu
}
'4' {
if (Invoke-Cmd (Get-Python) "$RepoRoot/shared/python/show_soft_deleted_resources.py") { Invoke-Pause-Menu }
Invoke-Cmd (Get-Python) "$RepoRoot/shared/python/show_soft_deleted_resources.py"
}
'5' {
if (Invoke-Cmd "$RepoRoot/tests/python/run_pylint.ps1") { Invoke-Pause-Menu }
Invoke-Cmd "$RepoRoot/tests/python/run_pylint.ps1"
}
'6' {
if (Invoke-Cmd "$RepoRoot/tests/python/run_tests.ps1") { Invoke-Pause-Menu }
Invoke-Cmd "$RepoRoot/tests/python/run_tests.ps1"
}
'7' {
if (Invoke-Cmd "$RepoRoot/tests/python/check_python.ps1") { Invoke-Pause-Menu }
Invoke-Cmd "$RepoRoot/tests/python/check_python.ps1"
}
'0' {
Write-Host ""
Expand Down
13 changes: 1 addition & 12 deletions start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ run_cmd() {
echo ""
echo "Command exited with code $status"
echo ""
pause_prompt
return $status
fi
}
Expand Down Expand Up @@ -65,10 +64,6 @@ PY
)
}

pause_prompt() {
echo
read -rp "Press ENTER to return to the menu..." _
}

while true; do
echo ""
Expand All @@ -93,30 +88,24 @@ while true; do
case "$choice" in
1)
run_cmd "$(find_python)" "${REPO_ROOT}/setup/local_setup.py" --complete-setup
pause_prompt
;;
2)
run_cmd "$(find_python)" "${REPO_ROOT}/setup/verify_local_setup.py"
pause_prompt
;;
3)
if show_account; then pause_prompt; fi
show_account
;;
4)
run_cmd "$(find_python)" "${REPO_ROOT}/shared/python/show_soft_deleted_resources.py"
pause_prompt
;;
5)
run_cmd "${REPO_ROOT}/tests/python/run_pylint.sh"
pause_prompt
;;
6)
run_cmd "${REPO_ROOT}/tests/python/run_tests.sh"
pause_prompt
;;
7)
run_cmd "${REPO_ROOT}/tests/python/check_python.sh"
pause_prompt
;;
0)
echo ""
Expand Down
107 changes: 107 additions & 0 deletions tests/python/test_apimrequests.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,16 @@ def test_headers_property_is_dict_reference():
h['X-Ref'] = 'ref'
assert apim.headers['X-Ref'] == 'ref'


def test_subscription_key_setter_updates_and_clears_header():
apim = ApimRequests(DEFAULT_URL, DEFAULT_KEY)

apim.subscriptionKey = 'new-key'
assert apim.headers[SUBSCRIPTION_KEY_PARAMETER_NAME] == 'new-key'

apim.subscriptionKey = None
assert SUBSCRIPTION_KEY_PARAMETER_NAME not in apim.headers

# ------------------------------
# ADDITIONAL COVERAGE TESTS FOR APIMREQUESTS
# ------------------------------
Expand Down Expand Up @@ -432,6 +442,49 @@ def test_multi_request_non_json_response(mock_print_info, mock_session_class, ap
assert result[0]['response'] == 'Plain text response'


@pytest.mark.unit
@patch('apimrequests.time.sleep')
@patch('apimrequests.requests.Session')
def test_multi_request_sleep_zero(mock_session_class, mock_sleep, apim):
"""Test _multiRequest respects sleepMs=0 without sleeping."""
mock_session = MagicMock()
mock_session_class.return_value = mock_session

mock_response = MagicMock()
mock_response.status_code = 200
mock_response.headers = {'Content-Type': 'application/json'}
mock_response.json.return_value = {'ok': True}
mock_response.text = '{"ok": true}'
mock_session.request.return_value = mock_response

with patch.object(apim, '_print_response_code'):
result = apim._multiRequest(HTTP_VERB.GET, '/sleep', 1, sleepMs=0)

assert result[0]['status_code'] == 200
mock_sleep.assert_not_called()


@pytest.mark.unit
@patch('apimrequests.time.sleep')
@patch('apimrequests.requests.Session')
def test_multi_request_sleep_positive(mock_session_class, mock_sleep, apim):
"""Test _multiRequest sleeps when sleepMs is positive."""
mock_session = MagicMock()
mock_session_class.return_value = mock_session

mock_response = MagicMock()
mock_response.status_code = 200
mock_response.headers = {'Content-Type': 'application/json'}
mock_response.json.return_value = {'ok': True}
mock_response.text = '{"ok": true}'
mock_session.request.return_value = mock_response

with patch.object(apim, '_print_response_code'):
apim._multiRequest(HTTP_VERB.GET, '/sleep', 2, sleepMs = 150)

mock_sleep.assert_called_once_with(0.15)


@pytest.mark.unit
@patch('apimrequests.print_val')
def test_print_response_non_200_status(mock_print_val, apim):
Expand All @@ -449,6 +502,60 @@ def test_print_response_non_200_status(mock_print_val, apim):
mock_print_val.assert_any_call('Response body', '{"error": "not found"}', True)


@pytest.mark.unit
@patch('apimrequests.print_val')
def test_print_response_200_invalid_json(mock_print_val, apim):
"""Test _print_response handles invalid JSON body for 200 responses."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.reason = 'OK'
mock_response.headers = {'Content-Type': 'application/json'}
mock_response.text = 'not valid json'

with patch.object(apim, '_print_response_code'):
apim._print_response(mock_response)

mock_print_val.assert_any_call('Response body', 'not valid json', True)


@pytest.mark.unit
@patch('apimrequests.print_val')
def test_print_response_200_valid_json(mock_print_val, apim):
"""Test _print_response prints formatted JSON when parse succeeds."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.reason = 'OK'
mock_response.headers = {'Content-Type': 'application/json'}
mock_response.text = '{"alpha": 1}'

with patch.object(apim, '_print_response_code'):
apim._print_response(mock_response)

mock_print_val.assert_any_call('Response body', '{\n "alpha": 1\n}', True)


@pytest.mark.unit
@patch('apimrequests.print_val')
def test_print_response_code_success_and_error(mock_print_val, apim):
"""Test _print_response_code color formatting for success and error codes."""
class DummyResponse:
status_code = 200
reason = 'OK'

apim._print_response_code(DummyResponse())

class ErrorResponse:
status_code = 500
reason = 'Server Error'

apim._print_response_code(ErrorResponse())

messages = [record.args[1] for record in mock_print_val.call_args_list]

assert any('200 - OK' in msg for msg in messages)
assert any('500 - Server Error' in msg for msg in messages)


@pytest.mark.unit
@patch('apimrequests.requests.get')
@patch('apimrequests.print_info')
Expand Down
Loading
Loading