diff --git a/objection/console/cli.py b/objection/console/cli.py index ff490b49..89c35ba9 100644 --- a/objection/console/cli.py +++ b/objection/console/cli.py @@ -272,7 +272,7 @@ def version() -> None: @click.option('--script-source', '-l', default=None, help=( 'A script file to use with the the "path" config type. ' 'Remember that use the name of this file in your "path". It will be next to the config.'), show_default=False) -@click.option('--bundle-id', '-b', default=None, help='The bundleid to set when codesigning the IPA') +@click.option('--bundle-id', '-B', default=None, help='The bundleid to set when codesigning the IPA') def patchipa(source: str, gadget_version: str, codesign_signature: str, provision_file: str, binary_name: str, skip_cleanup: bool, pause: bool, unzip_unicode: bool, gadget_config: str, script_source: str, bundle_id: str) -> None: diff --git a/pyproject.toml b/pyproject.toml index 1eed718e..6fe2c092 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,12 @@ Homepage = "https://github.com/sensepost/objection" Repository = "https://github.com/sensepost/objection" "Bug Tracker" = "https://github.com/sensepost/objection/issues" +[project.optional-dependencies] +test = [ + "pytest>=7.0.0", + "pytest-cov>=4.0.0", +] + [project.scripts] objection = "objection.console.cli:cli" @@ -55,3 +61,10 @@ objection = [ "utils/assets/*.xml", "agent.js", ] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = "test_*.py" +python_classes = "Test*" +python_functions = "test_*" +addopts = "-v --strict-markers" diff --git a/tests/commands/android/test_keystore.py b/tests/commands/android/test_keystore.py index cbd078c7..817f6d73 100644 --- a/tests/commands/android/test_keystore.py +++ b/tests/commands/android/test_keystore.py @@ -2,7 +2,7 @@ from unittest import mock from objection.commands.android.keystore import entries, clear -from ...helpers import capture +from ...helpers import capture, normalize_table_whitespace class TestKeystore(unittest.TestCase): @@ -13,11 +13,11 @@ def test_entries_handles_empty_data(self, mock_api): with capture(entries, []) as o: output = o - expected_output = """Alias Key Certificate -------- ----- ------------- + expected_output = """Alias Key Certificate +----- --- ----------- """ - self.assertEqual(output, expected_output) + self.assertEqual(normalize_table_whitespace(output), normalize_table_whitespace(expected_output)) @mock.patch('objection.state.connection.state_connection.get_api') def test_entries_handles(self, mock_api): @@ -30,12 +30,12 @@ def test_entries_handles(self, mock_api): with capture(entries, []) as o: output = o - expected_output = """Alias Key Certificate -------- ----- ------------- -test True True + expected_output = """Alias Key Certificate +----- ---- ----------- +test True True """ - self.assertEqual(output, expected_output) + self.assertEqual(normalize_table_whitespace(output), normalize_table_whitespace(expected_output)) @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.android.keystore.click.confirm') diff --git a/tests/commands/ios/test_bundles.py b/tests/commands/ios/test_bundles.py index 0901f1a1..e04272fd 100644 --- a/tests/commands/ios/test_bundles.py +++ b/tests/commands/ios/test_bundles.py @@ -3,7 +3,7 @@ from objection.commands.ios.bundles import show_frameworks, _should_include_apple_bundles, _should_print_full_path, \ _is_apple_bundle, show_bundles -from ...helpers import capture +from ...helpers import capture, normalize_table_whitespace class TestBundles(unittest.TestCase): @@ -76,7 +76,7 @@ def test_show_frameworks_prints_without_apple_bundles(self, mock_api): MapKit za.apple.MapKit 1 /MapKit """ - self.assertEqual(output, expected) + self.assertEqual(normalize_table_whitespace(output), normalize_table_whitespace(expected)) @mock.patch('objection.state.connection.state_connection.get_api') def test_show_frameworks_prints_with_apple_bundles(self, mock_api): @@ -93,7 +93,7 @@ def test_show_frameworks_prints_with_apple_bundles(self, mock_api): MapKit za.apple.MapKit 1 /MapKit """ - self.assertEqual(output, expected) + self.assertEqual(normalize_table_whitespace(output), normalize_table_whitespace(expected)) @mock.patch('objection.state.connection.state_connection.get_api') def test_show_frameworks_prints_with_apple_bundles_and_full_paths(self, mock_api): @@ -110,7 +110,7 @@ def test_show_frameworks_prints_with_apple_bundles_and_full_paths(self, mock_api MapKit za.apple.MapKit 1 /MapKit """ - self.assertEqual(output, expected) + self.assertEqual(normalize_table_whitespace(output), normalize_table_whitespace(expected)) @mock.patch('objection.state.connection.state_connection.get_api') def test_show_bundles_prints_bundles(self, mock_api): @@ -119,15 +119,15 @@ def test_show_bundles_prints_bundles(self, mock_api): with capture(show_bundles, []) as o: output = o - expected = """Executable Bundle Version Path ------------------------- ---------------------------------- --------- ------------------------------------------- -AppleIDSSOAuthentication com.apple.AppleIDSSOAuthentication 1 /AppleIDSSOAuthentication -LinguisticData com.apple.LinguisticData 1 ...nguisticDataLinguisticDataLinguisticData -hockeyapp net.hockeyapp.sdk.ios 1 /hockeyapp -MapKit za.apple.MapKit 1 /MapKit + expected = """Executable Bundle Version Path +------------------------ ---------------------------------- ------- ------------------------------------------------------------------------ +AppleIDSSOAuthentication com.apple.AppleIDSSOAuthentication 1 /AppleIDSSOAuthentication +LinguisticData com.apple.LinguisticData 1 /LinguisticData/LinguisticDataLinguisticDataLinguisticDataLinguisticData +hockeyapp net.hockeyapp.sdk.ios 1 /hockeyapp +MapKit za.apple.MapKit 1 /MapKit """ - self.assertEqual(output, expected) + self.assertEqual(normalize_table_whitespace(output), normalize_table_whitespace(expected)) @mock.patch('objection.state.connection.state_connection.get_api') def test_show_bundles_prints_bundles(self, mock_api): @@ -144,4 +144,4 @@ def test_show_bundles_prints_bundles(self, mock_api): MapKit za.apple.MapKit 1 /MapKit """ - self.assertEqual(output, expected) + self.assertEqual(normalize_table_whitespace(output), normalize_table_whitespace(expected)) diff --git a/tests/commands/ios/test_cookies.py b/tests/commands/ios/test_cookies.py index 6cddfa4d..8ede6b82 100644 --- a/tests/commands/ios/test_cookies.py +++ b/tests/commands/ios/test_cookies.py @@ -2,7 +2,7 @@ from unittest import mock from objection.commands.ios.cookies import get -from ...helpers import capture +from ...helpers import capture, normalize_table_whitespace class TestCookies(unittest.TestCase): @@ -30,9 +30,9 @@ def test_get(self, mock_api): with capture(get, []) as o: output = o - expected_output = """Name Value Expires Domain Path Secure HTTPOnly ------- ------- ------------------------- -------- ------ -------- ---------- -foo bar 01-01-1970 00:00:00 +0000 foo.com / false true + expected_output = """Name Value Expires Domain Path Secure HTTPOnly +---- ----- ------------------------- ------- ---- ------ -------- +foo bar 01-01-1970 00:00:00 +0000 foo.com / false true """ - self.assertEqual(output, expected_output) + self.assertEqual(normalize_table_whitespace(output), normalize_table_whitespace(expected_output)) diff --git a/tests/commands/ios/test_keychain.py b/tests/commands/ios/test_keychain.py index 56c0904c..bed8db96 100644 --- a/tests/commands/ios/test_keychain.py +++ b/tests/commands/ios/test_keychain.py @@ -3,7 +3,7 @@ from objection.commands.ios.keychain import _should_output_json, dump, dump_raw, clear, add, \ _data_flag_has_identifier, _get_flag_value, _should_do_smart_decode -from ...helpers import capture +from ...helpers import capture, normalize_table_whitespace class TestKeychain(unittest.TestCase): @@ -68,10 +68,10 @@ def test_dump_to_screen_handles_empty_data(self, mock_api): expected_output = """Note: You may be asked to authenticate using the devices passcode or TouchID Save the output by adding `--json keychain.json` to this command Dumping the iOS keychain... -Created Accessible ACL Type Account Service Data ---------- ------------ ----- ------ --------- --------- ------ +Created Accessible ACL Type Account Service Data +------- ---------- --- ---- ------- ------- ---- """ - self.assertEqual(output, expected_output) + self.assertEqual(normalize_table_whitespace(output), normalize_table_whitespace(expected_output)) @mock.patch('objection.state.connection.state_connection.get_api') def test_dump_to_screen(self, mock_api): @@ -87,11 +87,11 @@ def test_dump_to_screen(self, mock_api): expected_output = """Note: You may be asked to authenticate using the devices passcode or TouchID Save the output by adding `--json keychain.json` to this command Dumping the iOS keychain... -Created Accessible ACL Type Account Service Data ---------- ------------ ----- ------ --------- --------- ------ -now None None foo foo bar +Created Accessible ACL Type Account Service Data +------- ---------- ---- ---- ------- ------- ---- +now None None foo foo bar """ - self.assertEqual(output, expected_output) + self.assertEqual(normalize_table_whitespace(output), normalize_table_whitespace(expected_output)) @mock.patch('objection.state.connection.state_connection.get_api') def test_dump_raw(self, mock_api): @@ -141,7 +141,7 @@ def test_adds_item_validates_data_key_to_need_identifier(self): with capture(add, ['--data', 'test_data']) as o: output = o - self.assertEqual(output, 'When specifying the --data flag, either --account or --server should also be added\n') + self.assertEqual(output, 'When specifying the --data flag, either --account or --service should also be added\n') @mock.patch('objection.state.connection.state_connection.get_api') def test_adds_item_successfully(self, mock_api): diff --git a/tests/commands/ios/test_nsurlcredentialstorage.py b/tests/commands/ios/test_nsurlcredentialstorage.py index 531bcb79..1ab84941 100644 --- a/tests/commands/ios/test_nsurlcredentialstorage.py +++ b/tests/commands/ios/test_nsurlcredentialstorage.py @@ -2,7 +2,7 @@ from unittest import mock from objection.commands.ios.nsurlcredentialstorage import dump -from ...helpers import capture +from ...helpers import capture, normalize_table_whitespace class TestNsusercredentialstorage(unittest.TestCase): @@ -20,9 +20,9 @@ def test_dump(self, mock_api): with capture(dump, []) as o: output = o - expected_output = """Protocol Host Port Authentication Method User Password ----------- ------- ------ ----------------------- ------ ---------- -https foo.bar 80 Default foo bar + expected_output = """Protocol Host Port Authentication Method User Password +-------- ------- ---- --------------------- ---- -------- +https foo.bar 80 Default foo bar """ - self.assertEqual(output, expected_output) + self.assertEqual(normalize_table_whitespace(output), normalize_table_whitespace(expected_output)) diff --git a/tests/commands/test_filemanager.py b/tests/commands/test_filemanager.py index 5bfa4c9f..107eebd5 100644 --- a/tests/commands/test_filemanager.py +++ b/tests/commands/test_filemanager.py @@ -5,7 +5,7 @@ _pwd_android, ls, _ls_ios, _ls_android, download, _download_ios, _download_android, upload, rm, _rm_android from objection.state.device import device_state, Ios, Android from objection.state.filemanager import file_manager_state -from ..helpers import capture +from ..helpers import capture, normalize_table_whitespace class TestFileManager(unittest.TestCase): @@ -278,13 +278,16 @@ def test_lists_readable_ios_directory_using_helper_method(self, mock_api): output = o expected_outut = """NSFileType Perms NSFileProtection Read Write Owner Group Size Creation Name ------------- ------- ------------------ ------ ------- ------- ------- --------- ---------- ------ -A B C True False D (E) F (G) 115.4 GiB H test + ------------ ------- ------------------ ------ ------- ------- ------- --------- ---------- ------ + A B C True False D (E) F (G) 115.4 GiB H test -Readable: True Writable: False -""" + Readable: True Writable: False + """ - self.assertEqual(output, expected_outut) + self.assertEqual( + normalize_table_whitespace(output), + normalize_table_whitespace(expected_outut), + ) @mock.patch('objection.state.connection.state_connection.get_api') def test_lists_readable_ios_directory_using_helper_method_no_attributes(self, mock_api): @@ -306,13 +309,16 @@ def test_lists_readable_ios_directory_using_helper_method_no_attributes(self, mo output = o expected_outut = """NSFileType Perms NSFileProtection Read Write Owner Group Size Creation Name ------------- ------- ------------------ ------ ------- --------- --------- ------ ---------- ------ -n/a n/a n/a True True n/a (n/a) n/a (n/a) n/a n/a test + ------------ ------- ------------------ ------ ------- --------- --------- ------ ---------- ------ + n/a n/a n/a True True n/a (n/a) n/a (n/a) n/a n/a test -Readable: True Writable: True -""" + Readable: True Writable: True + """ - self.assertEqual(output, expected_outut) + self.assertEqual( + normalize_table_whitespace(output), + normalize_table_whitespace(expected_outut), + ) @mock.patch('objection.state.connection.state_connection.get_api') def test_lists_unreadable_ios_directory_using_helper_method(self, mock_api): @@ -354,13 +360,16 @@ def test_lists_readable_android_directory_using_helper_method(self, mock_api): output = o expected_outut = """Type Last Modified Read Write Hidden Size Name ------- ----------------------- ------ ------- -------- ------- ------ -File 2017-10-05 07:36:41 GMT True True False 249.0 B test + ------ ----------------------- ------ ------- -------- ------- ------ + File 2017-10-05 07:36:41 GMT True True False 249.0 B test -Readable: True Writable: True -""" + Readable: True Writable: True + """ - self.assertEqual(output, expected_outut) + self.assertEqual( + normalize_table_whitespace(output), + normalize_table_whitespace(expected_outut), + ) @mock.patch('objection.state.connection.state_connection.get_api') def test_lists_unreadable_android_directory_using_helper_method(self, mock_api): @@ -407,7 +416,7 @@ def test_downloads_file_with_ios_helper(self, mock_open, mock_api): file_manager_state.cwd = '/foo' - with capture(_download_ios, '/foo', '/bar') as o: + with capture(_download_ios, '/foo', '/bar', False) as o: output = o expected_output = """Downloading /foo to /bar @@ -423,7 +432,7 @@ def test_downloads_file_with_ios_helper(self, mock_open, mock_api): def test_downloads_file_but_fails_on_unreadable_with_ios_helper(self, mock_api): mock_api.return_value.ios_file_readable.return_value = False - with capture(_download_ios, '/foo', '/bar') as o: + with capture(_download_ios, '/foo', '/bar', False) as o: output = o self.assertEqual(output, 'Downloading /foo to /bar\nUnable to download file. File is not readable.\n') @@ -433,10 +442,10 @@ def test_downloads_file_but_fails_on_file_type_with_ios_helper(self, mock_api): mock_api.return_value.ios_file_readable.return_value = True mock_api.return_value.ios_file_path_is_file.return_value = False - with capture(_download_ios, '/foo', '/bar') as o: + with capture(_download_ios, '/foo', '/bar', False) as o: output = o - self.assertEqual(output, 'Downloading /foo to /bar\nUnable to download file. Target path is not a file.\n') + self.assertEqual(output, 'Downloading /foo to /bar\nTo download folders, specify --folder.\n') @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.filemanager.open', create=True) @@ -447,7 +456,7 @@ def test_downloads_file_with_android_helper(self, mock_open, mock_api): file_manager_state.cwd = '/foo' - with capture(_download_android, '/foo', '/bar') as o: + with capture(_download_android, '/foo', '/bar', False) as o: output = o expected = """Downloading /foo to /bar @@ -466,7 +475,7 @@ def test_downloads_file_but_fails_on_unreadable_with_android_helper(self, mock_o file_manager_state.cwd = '/foo' - with capture(_download_android, '/foo', '/bar') as o: + with capture(_download_android, '/foo', '/bar', False) as o: output = o self.assertFalse(mock_open.called) @@ -480,11 +489,11 @@ def test_downloads_file_but_fails_on_file_type_with_android_helper(self, mock_op file_manager_state.cwd = '/foo' - with capture(_download_android, '/foo', '/bar') as o: + with capture(_download_android, '/foo', '/bar', False) as o: output = o self.assertFalse(mock_open.called) - self.assertEqual(output, 'Downloading /foo to /bar\nUnable to download file. Target path is not a file.\n') + self.assertEqual(output, 'Downloading /foo to /bar\nTo download folders, specify --folder.\n') def test_file_upload_method_proxy_validates_arguments(self): with capture(upload, []) as o: diff --git a/tests/commands/test_jobs.py b/tests/commands/test_jobs.py index 723b405c..5907c0df 100644 --- a/tests/commands/test_jobs.py +++ b/tests/commands/test_jobs.py @@ -2,7 +2,7 @@ from unittest import mock from objection.commands.jobs import show, kill -from objection.state.jobs import job_manager_state +from objection.state.jobs import job_manager_state, Job from ..helpers import capture @@ -26,7 +26,7 @@ def setUp(self): self.mock_job = MockJob() def tearDown(self): - job_manager_state.jobs = [] + job_manager_state.jobs = {} @mock.patch('objection.state.connection.state_connection.get_api') def test_displays_empty_jobs_message(self, mock_api): @@ -34,8 +34,8 @@ def test_displays_empty_jobs_message(self, mock_api): with capture(show) as o: output = o - expected_output = """Job ID Hooks Type --------- ------- ------ + expected_output = """Job ID Type Name +------ ---- ---- """ self.assertEqual(output, expected_output) @@ -43,14 +43,14 @@ def test_displays_empty_jobs_message(self, mock_api): @mock.patch('objection.state.connection.state_connection.get_api') def test_displays_list_of_jobs(self, mock_api): mock_api.return_value.jobs_get.return_value = [ - {'identifier': 'rdcjq16g8xi', 'invocations': [{}], 'type': 'ios-jailbreak-disable'}] + {'identifier': '123456', 'invocations': [{}], 'type': 'ios-jailbreak-disable'}] with capture(show, []) as o: output = o - expected_outut = """Job ID Hooks Type ------------ ------- --------------------- -rdcjq16g8xi 1 ios-jailbreak-disable + expected_outut = """Job ID Type Name +------ ---- --------------------- +123456 hook ios-jailbreak-disable """ self.assertEqual(output, expected_outut) @@ -61,14 +61,22 @@ def test_kill_validates_arguments(self): self.assertEqual(output, 'Usage: jobs kill \n') - @mock.patch('objection.state.connection.state_connection.get_api') - def test_cant_find_job_by_uuid(self, mock_api): - kill('foo') - - self.assertTrue(mock_api.return_value.jobs_kill.called) + def test_cant_find_job_by_uuid(self): + # Attempting to kill a job that doesn't exist just removes it from state + # If it wasn't there, nothing happens + kill(['123']) + # Job was not in manager, so nothing happened + self.assertEqual(len(job_manager_state.jobs), 0) @mock.patch('objection.state.connection.state_connection.get_api') def test_kills_job_by_uuid(self, mock_api): - kill('foo') + # Add a job and then kill it + mock_handle = mock.MagicMock() + job = Job('test', 'hook', mock_handle, 123) + job_manager_state.add_job(job) + self.assertEqual(len(job_manager_state.jobs), 1) + + kill(['123']) + + self.assertEqual(len(job_manager_state.jobs), 0) - self.assertTrue(mock_api.return_value.jobs_kill.called) diff --git a/tests/commands/test_memory.py b/tests/commands/test_memory.py index 997faabc..fa5175b5 100644 --- a/tests/commands/test_memory.py +++ b/tests/commands/test_memory.py @@ -3,7 +3,7 @@ from objection.commands.memory import _is_string_input, dump_all, dump_from_base, list_modules, list_exports, \ find_pattern, replace_pattern -from ..helpers import capture +from ..helpers import capture, normalize_table_whitespace class MockRange: @@ -84,12 +84,12 @@ def test_list_modules_without_errors_without_json_flag(self, mock_api): output = o expected_outut = """Save the output by adding `--json modules.json` to this command -Name Base Size Path ------- ------ ------------- ------ -test 32768 200 (200.0 B) /foo +Name Base Size Path +---- ----- ------------- ---- +test 32768 200 (200.0 B) /foo """ - self.assertEqual(output, expected_outut) + self.assertEqual(normalize_table_whitespace(output), normalize_table_whitespace(expected_outut)) @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.memory.open', create=True) @@ -139,12 +139,12 @@ def test_dump_exports_without_error(self, mock_api): output = o expected_outut = """Save the output by adding `--json exports.json` to this command -Type Name Address --------- ------ --------- -function test 32768 +Type Name Address +-------- ---- ------- +function test 32768 """ - self.assertEqual(output, expected_outut) + self.assertEqual(normalize_table_whitespace(output), normalize_table_whitespace(expected_outut)) @mock.patch('objection.state.connection.state_connection.get_api') @mock.patch('objection.commands.memory.open', create=True) diff --git a/tests/commands/test_ui.py b/tests/commands/test_ui.py index 574a7821..cc71faaa 100644 --- a/tests/commands/test_ui.py +++ b/tests/commands/test_ui.py @@ -13,19 +13,19 @@ def tearDown(self): @mock.patch('objection.commands.ui._alert_ios') def test_alert_helper_method_proxy_calls_ios(self, mock_alert_ios): - device_state.platform = Ios() + device_state.platform = Ios alert([]) - self.assertTrue(mock_alert_ios.called_with('objection!')) + mock_alert_ios.assert_called_with('objection!') @mock.patch('objection.commands.ui._alert_ios') def test_alert_helper_method_proxy_calls_ios_custom_message(self, mock_alert_ios): - device_state.platform = Ios() + device_state.platform = Ios alert(['foo']) - self.assertTrue(mock_alert_ios.called_with('foo')) + mock_alert_ios.assert_called_with('foo') @mock.patch('objection.state.connection.state_connection.get_api') def test_alert_ios_helper_method(self, mock_api): diff --git a/tests/helpers.py b/tests/helpers.py index 9c3d763a..0bfe93d5 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,4 +1,5 @@ import sys +import re from contextlib import contextmanager from io import StringIO @@ -17,3 +18,29 @@ def capture(command, *args, **kwargs): finally: sys.stdout = out + + +def normalize_table_whitespace(text: str) -> str: + """ + Normalize whitespace in table output to be tolerant of spacing variations + across different terminals, OS configurations, and tabulate versions. + + Converts multiple spaces between columns to single spaces to allow for + flexibility in table formatting while preserving the actual data. + """ + lines = text.split('\n') + normalized_lines = [] + + for line in lines: + # Replace multiple spaces (2 or more) with a single space + normalized_line = re.sub(r' {2,}', ' ', line) + + # Collapse varying hyphen lengths in separator rows (e.g. -----) + stripped = normalized_line.replace(' ', '').replace('|', '').strip() + if stripped and all(char == '-' for char in stripped): + normalized_line = re.sub(r'-{2,}', '-', normalized_line) + + normalized_line = normalized_line.strip() + normalized_lines.append(normalized_line) + + return '\n'.join(normalized_lines) \ No newline at end of file diff --git a/tests/state/test_jobs.py b/tests/state/test_jobs.py index 8049aa23..bae0243e 100644 --- a/tests/state/test_jobs.py +++ b/tests/state/test_jobs.py @@ -1,25 +1,28 @@ import unittest -from objection.state.jobs import job_manager_state +from objection.state.jobs import job_manager_state, Job class TestJobManager(unittest.TestCase): def tearDown(self): - job_manager_state.jobs = [] + job_manager_state.jobs = {} def test_job_manager_starts_with_empty_jobs(self): self.assertEqual(len(job_manager_state.jobs), 0) def test_adds_jobs(self): - job_manager_state.add_job('foo') + job = Job('foo', 'test', None) + job_manager_state.add_job(job) self.assertEqual(len(job_manager_state.jobs), 1) def test_removes_jobs(self): - job_manager_state.add_job('foo') - job_manager_state.add_job('bar') + job1 = Job('foo', 'test', None) + job2 = Job('bar', 'test', None) + job_manager_state.add_job(job1) + job_manager_state.add_job(job2) - job_manager_state.remove_job('foo') - job_manager_state.remove_job('bar') + job_manager_state.remove_job(job1.uuid) + job_manager_state.remove_job(job2.uuid) self.assertEqual(len(job_manager_state.jobs), 0) diff --git a/uv.lock b/uv.lock index a3623250..86bc5a79 100644 --- a/uv.lock +++ b/uv.lock @@ -202,6 +202,110 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/c4/0679472c60052c27efa612b4cd3ddd2a23e885dcdc73461781d2c802d39e/configobj-5.0.9-py2.py3-none-any.whl", hash = "sha256:1ba10c5b6ee16229c79a05047aeda2b55eb4e80d7c7d8ecf17ec1ca600c79882", size = 35615, upload-time = "2024-11-26T14:03:32.972Z" }, ] +[[package]] +name = "coverage" +version = "7.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/2d/63e37369c8e81a643afe54f76073b020f7b97ddbe698c5c944b51b0a2bc5/coverage-7.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4af3b01763909f477ea17c962e2cca8f39b350a4e46e3a30838b2c12e31b81b", size = 218842, upload-time = "2026-01-25T12:57:15.3Z" }, + { url = "https://files.pythonhosted.org/packages/57/06/86ce882a8d58cbcb3030e298788988e618da35420d16a8c66dac34f138d0/coverage-7.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36393bd2841fa0b59498f75466ee9bdec4f770d3254f031f23e8fd8e140ffdd2", size = 219360, upload-time = "2026-01-25T12:57:17.572Z" }, + { url = "https://files.pythonhosted.org/packages/cd/84/70b0eb1ee19ca4ef559c559054c59e5b2ae4ec9af61398670189e5d276e9/coverage-7.13.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cc7573518b7e2186bd229b1a0fe24a807273798832c27032c4510f47ffdb896", size = 246123, upload-time = "2026-01-25T12:57:19.087Z" }, + { url = "https://files.pythonhosted.org/packages/35/fb/05b9830c2e8275ebc031e0019387cda99113e62bb500ab328bb72578183b/coverage-7.13.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca9566769b69a5e216a4e176d54b9df88f29d750c5b78dbb899e379b4e14b30c", size = 247930, upload-time = "2026-01-25T12:57:20.929Z" }, + { url = "https://files.pythonhosted.org/packages/81/aa/3f37858ca2eed4f09b10ca3c6ddc9041be0a475626cd7fd2712f4a2d526f/coverage-7.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c9bdea644e94fd66d75a6f7e9a97bb822371e1fe7eadae2cacd50fcbc28e4dc", size = 249804, upload-time = "2026-01-25T12:57:22.904Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b3/c904f40c56e60a2d9678a5ee8df3d906d297d15fb8bec5756c3b0a67e2df/coverage-7.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5bd447332ec4f45838c1ad42268ce21ca87c40deb86eabd59888859b66be22a5", size = 246815, upload-time = "2026-01-25T12:57:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/41/91/ddc1c5394ca7fd086342486440bfdd6b9e9bda512bf774599c7c7a0081e0/coverage-7.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7c79ad5c28a16a1277e1187cf83ea8dafdcc689a784228a7d390f19776db7c31", size = 247843, upload-time = "2026-01-25T12:57:26.544Z" }, + { url = "https://files.pythonhosted.org/packages/87/d2/cdff8f4cd33697883c224ea8e003e9c77c0f1a837dc41d95a94dd26aad67/coverage-7.13.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:76e06ccacd1fb6ada5d076ed98a8c6f66e2e6acd3df02819e2ee29fd637b76ad", size = 245850, upload-time = "2026-01-25T12:57:28.507Z" }, + { url = "https://files.pythonhosted.org/packages/f5/42/e837febb7866bf2553ab53dd62ed52f9bb36d60c7e017c55376ad21fbb05/coverage-7.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:49d49e9a5e9f4dc3d3dac95278a020afa6d6bdd41f63608a76fa05a719d5b66f", size = 246116, upload-time = "2026-01-25T12:57:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/09/b1/4a3f935d7df154df02ff4f71af8d61298d713a7ba305d050ae475bfbdde2/coverage-7.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed2bce0e7bfa53f7b0b01c722da289ef6ad4c18ebd52b1f93704c21f116360c8", size = 246720, upload-time = "2026-01-25T12:57:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/538a6fd44c515f1c5197a3f078094cbaf2ce9f945df5b44e29d95c864bff/coverage-7.13.2-cp310-cp310-win32.whl", hash = "sha256:1574983178b35b9af4db4a9f7328a18a14a0a0ce76ffaa1c1bacb4cc82089a7c", size = 221465, upload-time = "2026-01-25T12:57:33.511Z" }, + { url = "https://files.pythonhosted.org/packages/5e/09/4b63a024295f326ec1a40ec8def27799300ce8775b1cbf0d33b1790605c4/coverage-7.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:a360a8baeb038928ceb996f5623a4cd508728f8f13e08d4e96ce161702f3dd99", size = 222397, upload-time = "2026-01-25T12:57:34.927Z" }, + { url = "https://files.pythonhosted.org/packages/6c/01/abca50583a8975bb6e1c59eff67ed8e48bb127c07dad5c28d9e96ccc09ec/coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e", size = 218971, upload-time = "2026-01-25T12:57:36.953Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0e/b6489f344d99cd1e5b4d5e1be52dfd3f8a3dc5112aa6c33948da8cabad4e/coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e", size = 219473, upload-time = "2026-01-25T12:57:38.934Z" }, + { url = "https://files.pythonhosted.org/packages/17/11/db2f414915a8e4ec53f60b17956c27f21fb68fcf20f8a455ce7c2ccec638/coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508", size = 249896, upload-time = "2026-01-25T12:57:40.365Z" }, + { url = "https://files.pythonhosted.org/packages/80/06/0823fe93913663c017e508e8810c998c8ebd3ec2a5a85d2c3754297bdede/coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b", size = 251810, upload-time = "2026-01-25T12:57:42.045Z" }, + { url = "https://files.pythonhosted.org/packages/61/dc/b151c3cc41b28cdf7f0166c5fa1271cbc305a8ec0124cce4b04f74791a18/coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b", size = 253920, upload-time = "2026-01-25T12:57:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/2d/35/e83de0556e54a4729a2b94ea816f74ce08732e81945024adee46851c2264/coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f", size = 250025, upload-time = "2026-01-25T12:57:45.624Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/af2eb9c3926ce3ea0d58a0d2516fcbdacf7a9fc9559fe63076beaf3f2596/coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3", size = 251612, upload-time = "2026-01-25T12:57:47.713Z" }, + { url = "https://files.pythonhosted.org/packages/26/62/5be2e25f3d6c711d23b71296f8b44c978d4c8b4e5b26871abfc164297502/coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b", size = 249670, upload-time = "2026-01-25T12:57:49.378Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/400d1b09a8344199f9b6a6fc1868005d766b7ea95e7882e494fa862ca69c/coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1", size = 249395, upload-time = "2026-01-25T12:57:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/e0/36/f02234bc6e5230e2f0a63fd125d0a2093c73ef20fdf681c7af62a140e4e7/coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059", size = 250298, upload-time = "2026-01-25T12:57:52.287Z" }, + { url = "https://files.pythonhosted.org/packages/b0/06/713110d3dd3151b93611c9cbfc65c15b4156b44f927fced49ac0b20b32a4/coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031", size = 221485, upload-time = "2026-01-25T12:57:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/16/0c/3ae6255fa1ebcb7dec19c9a59e85ef5f34566d1265c70af5b2fc981da834/coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e", size = 222421, upload-time = "2026-01-25T12:57:55.433Z" }, + { url = "https://files.pythonhosted.org/packages/b5/37/fabc3179af4d61d89ea47bd04333fec735cd5e8b59baad44fed9fc4170d7/coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28", size = 221088, upload-time = "2026-01-25T12:57:57.41Z" }, + { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, + { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" }, + { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" }, + { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" }, + { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" }, + { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" }, + { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" }, + { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" }, + { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" }, + { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" }, + { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" }, + { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" }, + { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" }, + { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" }, + { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" }, + { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" }, + { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" }, + { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "delegator-py" version = "0.1.1" @@ -335,6 +439,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "itsdangerous" version = "2.2.0" @@ -640,7 +753,7 @@ wheels = [ [[package]] name = "objection" -version = "1.12.2" +version = "1.12.3" source = { editable = "." } dependencies = [ { name = "click" }, @@ -658,6 +771,12 @@ dependencies = [ { name = "tabulate" }, ] +[package.optional-dependencies] +test = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] + [package.metadata] requires-dist = [ { name = "click", specifier = ">=8.2.0" }, @@ -669,11 +788,14 @@ requires-dist = [ { name = "packaging", specifier = ">=23.0" }, { name = "prompt-toolkit", specifier = ">=3.0.30,<4.0.0" }, { name = "pygments", specifier = ">=2.0.0" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=7.0.0" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.0.0" }, { name = "requests", specifier = ">=2.32.0" }, { name = "semver", specifier = ">=2" }, { name = "setuptools", specifier = ">=70.0.0" }, { name = "tabulate", specifier = ">=0.9.0" }, ] +provides-extras = ["test"] [[package]] name = "openai" @@ -923,6 +1045,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, ] +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0"