diff --git a/.circleci/config.yml b/.circleci/config.yml index 47076ae2a6..0106a6f0c7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,7 +66,7 @@ jobs: command: | . env/bin/activate export PYTHONUNBUFFERED=1 - pytest -n2 --reruns 3 --durations=0 --verbose --junitxml=test-results/integration_tests.xml \ + pytest -n2 --reruns 3 --reruns-delay 15 --durations=0 --verbose --junitxml=test-results/integration_tests.xml \ --cov=. --cov-append --cov-config .coveragerc \ --splits $CIRCLE_NODE_TOTAL --group $((CIRCLE_NODE_INDEX + 1)) \ --splitting-algorithm duration_based_chunks --store-durations --durations-path .test_durations \ diff --git a/.dockerignore b/.dockerignore index a498c9ee64..eabfb03301 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,8 @@ **/data/ -*.log -*.png -*.pstats +**/*.log +**/*.png +**/*.pstats +**/*.ipynb **/bittensor.egg-info/* **/lib/* **/build/* @@ -9,6 +10,12 @@ **/runs/* **/env/* **/venv/* -./circleci/* -./github/* -.ipynb \ No newline at end of file +**/tmp/* +**/test_results/* +**/__pycache__/* +**/.circleci +**/.git +**/.github +**/.hypothesis +**/.vscode +**/.gitignore diff --git a/.test_durations b/.test_durations index 35d208dc0f..8cb7d74bff 100644 --- a/.test_durations +++ b/.test_durations @@ -1,56 +1,57 @@ { - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_delegate_stake": 21.748504499, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_inspect": 2.0752911659999995, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_metagraph": 21.305854791999998, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_nominate": 5.640199582999998, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview": 33.172121875, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_all": 12.170790751000002, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_no_wallet": 0.28718404200000336, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_not_in_first_subnet": 4.324955668000005, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_hotkeys_config": 0.9465315409999988, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_sort_by_bad_column_name": 1.2147431670000017, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_sort_by_config": 11.225620790999999, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_sort_order_config": 1.0282624160000005, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_sort_order_config_bad_sort_type": 1.1607243340000046, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_width_config": 1.2331629580000012, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_without_hotkeys_config": 1.064924750000003, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_without_sort_by_config": 11.364460167, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_without_sort_order_config": 1.0801753320000032, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_without_width_config": 1.1826907519999992, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_recycle_register": 11.044216706999993, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_register": 4.742937083000001, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_set_weights": 0.004781001000001339, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake": 6.484200791999999, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_all_hotkeys": 19.003409832999996, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_exclude_hotkeys_from_all": 19.994503167000005, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_multiple_hotkeys_max_stake": 20.006779625000007, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_multiple_hotkeys_max_stake_not_enough_balance": 32.031439082999995, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_single_hotkey_max_stake": 9.746484915999996, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_single_hotkey_max_stake_enough_stake": 9.301691583999997, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_single_hotkey_max_stake_not_enough_balance": 9.691816042, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_specific_hotkeys": 20.010645583000006, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_transfer": 9.984776208000014, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_transfer_not_enough_balance": 9.753867416000006, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_undelegate_stake": 14.217678249000002, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_unstake_with_all_hotkeys": 16.998239415999997, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_unstake_with_exclude_hotkeys_from_all": 15.007678417000008, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_unstake_with_multiple_hotkeys_max_stake": 24.411396834, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_unstake_with_specific_hotkeys": 15.797360291999993, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkUsingArgs::test_delegate": 18.01229404100002, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkUsingArgs::test_list_delegates": 0.648853165999995, - "tests/integration_tests/test_cli.py::TestCLIWithNetworkUsingArgs::test_list_subnets": 19.34342137600001, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_delegate_stake": 32.565206749999994, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_inspect": 2.0870491260000037, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_metagraph": 17.437785333, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_neuron_run_reregister_false": 35.75446520799999, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_nominate": 38.171487959, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview": 54.78253583300001, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_all": 303.709275458, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_no_wallet": 33.569985001, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_not_in_first_subnet": 7.832046707999993, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_hotkeys_config": 1.235335959000004, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_sort_by_bad_column_name": 34.20312183400001, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_sort_by_config": 1.4365408759999951, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_sort_order_config": 1.4505757079999952, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_sort_order_config_bad_sort_type": 34.18927604199999, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_width_config": 1.6561556670000002, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_without_hotkeys_config": 1.2479347909999987, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_without_sort_by_config": 34.193473041, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_without_sort_order_config": 1.436726291999996, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_without_width_config": 1.449721043000011, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_recycle_register": 48.5383515, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_register": 6.655044251, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_set_weights": 0.006143250000008038, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake": 44.89659891599999, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_all_hotkeys": 31.83300620899999, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_exclude_hotkeys_from_all": 0.0015482090000062954, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_multiple_hotkeys_max_stake": 0.0011364169999907858, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_multiple_hotkeys_max_stake_not_enough_balance": 0.0009022089999959348, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_single_hotkey_max_stake": 0.0009031669999970404, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_single_hotkey_max_stake_enough_stake": 0.0012163340000057588, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_single_hotkey_max_stake_not_enough_balance": 0.0009654589999996688, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_specific_hotkeys": 357.5746072910001, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_transfer": 16.976931332999996, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_transfer_not_enough_balance": 22.429711792000006, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_undelegate_stake": 27.56590779199999, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_unstake_with_all_hotkeys": 38.311913373, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_unstake_with_exclude_hotkeys_from_all": 0.0018990010000123903, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_unstake_with_multiple_hotkeys_max_stake": 0.0010086670000006848, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_unstake_with_specific_hotkeys": 0.0012716660000009483, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkUsingArgs::test_delegate": 0.0012134169999740152, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkUsingArgs::test_list_delegates": 12.917025874999979, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkUsingArgs::test_list_subnets": 0.32005762600000764, "tests/integration_tests/test_cli.py::TestCLIWithNetworkUsingArgs::test_run_reregister_false": 2.500768667000017, "tests/integration_tests/test_cli.py::TestCLIWithNetworkUsingArgs::test_run_synapse_all": 8.177792832999984, - "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_btcli_help": 0.06256454200001826, - "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_check_configs": 0.4856975830000039, - "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_list": 0.014231541999976116, - "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_list_no_wallet": 0.005250332999992224, - "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_new_coldkey": 0.004741165999988084, - "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_new_hotkey": 0.006868499999995947, - "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_regen_coldkey": 0.004826207999983012, - "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_regen_coldkeypub": 0.0038004160000042475, - "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_regen_hotkey": 0.005372708000010107, - "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_register_cuda_use_cuda_flag": 1.1164745840000307, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_btcli_help": 0.05371037599999795, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_check_configs": 0.5839849989999948, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_list": 0.015767583999995338, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_list_no_wallet": 0.004536540000003697, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_new_coldkey": 0.005761207000013258, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_new_hotkey": 0.003966625999993312, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_regen_coldkey": 0.00497241600000109, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_regen_coldkeypub": 0.00346216599999849, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_regen_hotkey": 0.004310167000014076, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_register_cuda_use_cuda_flag": 2.813618584000004, "tests/integration_tests/test_dataset.py::test_change_data_size": 9.975283208999997, "tests/integration_tests/test_dataset.py::test_construct_text_corpus": 5.504439667999989, "tests/integration_tests/test_dataset.py::test_fail_IPFS_server": 2.991185999999985, @@ -80,6 +81,52 @@ "tests/integration_tests/test_dendrite.py::test_dendrite_to_df": 0.6830525419999987, "tests/integration_tests/test_dendrite.py::test_failing_synapse": 0.652249334000004, "tests/integration_tests/test_dendrite.py::test_successful_synapse": 0.5847192090000135, + "tests/integration_tests/test_ipfs.py::test_ipfs_init": 0.005554707999998243, + "tests/integration_tests/test_ipfs.py::test_retrieve_directory": 0.20729179199999237, + "tests/integration_tests/test_keyfile.py::TestKeyFiles::test_create": 0.08020704100000131, + "tests/integration_tests/test_keyfile.py::TestKeyFiles::test_decrypt_keyfile_data_legacy": 3.0671192910000045, + "tests/integration_tests/test_keyfile.py::TestKeyFiles::test_keyfile_mock": 0.018454082999994625, + "tests/integration_tests/test_keyfile.py::TestKeyFiles::test_keyfile_mock_func": 0.019594999999995366, + "tests/integration_tests/test_keyfile.py::TestKeyFiles::test_legacy_coldkey": 0.030612376000000552, + "tests/integration_tests/test_keyfile.py::TestKeyFiles::test_overwriting": 0.031093917000006854, + "tests/integration_tests/test_keyfile.py::TestKeyFiles::test_user_interface": 0.017205207999992922, + "tests/integration_tests/test_keyfile.py::TestKeyFiles::test_validate_password": 0.01777775099999701, + "tests/integration_tests/test_metagraph_integration.py::TestMetagraph::test_full_sync": 3.6405804169999954, + "tests/integration_tests/test_metagraph_integration.py::TestMetagraph::test_lite_sync": 3.6356975829999953, + "tests/integration_tests/test_metagraph_integration.py::TestMetagraph::test_load_sync_save": 3.243659209999997, + "tests/integration_tests/test_metagraph_integration.py::TestMetagraph::test_parameters": 3.0838419149999936, + "tests/integration_tests/test_metagraph_integration.py::TestMetagraph::test_print_empty": 2.6707623749999954, + "tests/integration_tests/test_metagraph_integration.py::TestMetagraph::test_properties": 3.287473416999994, + "tests/integration_tests/test_metagraph_integration.py::TestMetagraph::test_state_dict": 3.296576874000003, + "tests/integration_tests/test_metagraph_integration.py::TestMetagraph::test_sync_block_0": 4.055834208, + "tests/integration_tests/test_priority_thread_pool.py::test_priority_thread_pool": 0.002472417000006999, + "tests/integration_tests/test_prometheus.py::TestPrometheus::test_init_prometheus_failed": 1.491444625000014, + "tests/integration_tests/test_prometheus.py::TestPrometheus::test_init_prometheus_success": 1.6381353319999903, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_get_balance": 2.5954937909999956, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_get_balances": 1.9654992910000004, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_get_current_block": 0.3812910839999972, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_get_uid_by_hotkey_on_subnet": 0.6584294999999969, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_hotkey_register": 0.46409241699998915, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_hotkey_register_failed": 0.3542701670000099, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_network_overrides": 0.953627209000004, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_registration_failed": 1.788183917000012, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_registration_multiprocessed_already_registered": 0.9777173749999974, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_registration_partly_failed": 1.5698486670000023, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_registration_stale_then_continue": 0.781868541999998, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_set_weights": 0.6006925410000008, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_set_weights_failed": 0.3889112079999961, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_set_weights_inclusion": 0.4296055830000114, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_stake": 0.1843938319999836, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_stake_failed": 0.3917970010000005, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_stake_inclusion": 0.38589883299999883, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_transfer": 2.0724527499999965, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_transfer_dest_as_bytes": 1.2727416259999842, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_transfer_failed": 1.2812408760000125, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_transfer_inclusion": 1.2405266240000117, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_transfer_invalid_dest": 0.4117500419999942, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_unstake": 0.4006357079999958, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_unstake_failed": 0.4873798340000093, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_unstake_inclusion": 0.3860250829999927, "tests/unit_tests/bittensor_tests/test_axon.py::TestExternalAxon::test_external_ip_not_set_dont_use_internal_ip": 0.006879416000003857, "tests/unit_tests/bittensor_tests/test_axon.py::TestExternalAxon::test_external_ip_port_set_full_address_internal": 0.004500209000006805, "tests/unit_tests/bittensor_tests/test_axon.py::TestExternalAxon::test_external_ip_set_full_address_internal": 0.08792841500000037, diff --git a/CHANGELOG.md b/CHANGELOG.md index e7667475a8..19a909cd8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## 5.3.0 / 2023-07-04 + +## What's Changed +* [BIT-351] Ask for wallet name on btcli unstake by @camfairchild in https://github.com/opentensor/bittensor/pull/1387 +* Fix tests using pure-Python MockSubtensor by @camfairchild in https://github.com/opentensor/bittensor/pull/1349 +* Update README.md by @mostimasblunderbuss in https://github.com/opentensor/bittensor/pull/1397 +* Update authint version by @ifrit98 in https://github.com/opentensor/bittensor/pull/1395 +* Fix subtensor factory integration test by @camfairchild in https://github.com/opentensor/bittensor/pull/1400 +* Remove neurons by @ifrit98 in https://github.com/opentensor/bittensor/pull/1389 +* Merge pull request #1394 from opentensor/fix_axon_requests by @ifrit98 in https://github.com/opentensor/bittensor/pull/1406 +* remove hotkey from proto and dendrite by @ifrit98 in https://github.com/opentensor/bittensor/pull/1407 +* Weight Utils fix by @mrseeker in https://github.com/opentensor/bittensor/pull/1372 +* Extract config to new package by @camfairchild in https://github.com/opentensor/bittensor/pull/1401 +* Extract wallet by @camfairchild in https://github.com/opentensor/bittensor/pull/1403 +* BTCli integration with new governance protocol by @Rubberbandits in https://github.com/opentensor/bittensor/pull/1398 +* Reverting unnecessary commits for next release. by @camfairchild in https://github.com/opentensor/bittensor/pull/1415 +* Extract wallet and config by @camfairchild in https://github.com/opentensor/bittensor/pull/1411 + +## New Contributors +* @mostimasblunderbuss made their first contribution in https://github.com/opentensor/bittensor/pull/1397 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v5.2.0...v5.3.0 + + ## 5.2.0 / 2023-06-28 ## What's Changed diff --git a/DEVELOPMENT_WORKFLOW.md b/DEVELOPMENT_WORKFLOW.md index 68a199b671..97b6394b67 100644 --- a/DEVELOPMENT_WORKFLOW.md +++ b/DEVELOPMENT_WORKFLOW.md @@ -2,97 +2,102 @@ ## Table of contents -1. [Main branches](#main-branches) -1. [Development model](#development-model) - 1. [Supporting branches](#supporting-branches) - 1. [Feature branches](#feature-branches) - 1. [Release branches](#release-branches) - 1. [Hotfix branches](#hotfix-branches) - 1. [Git operations](#git-operations) - 1. [Create a feature branch](#create-a-feature-branch) - 1. [Merge feature branch into nobunaga](#merge-feature-branch-into-nobunaga) - 1. [Create release branch](#create-release-branch) - 1. [Finish a release branch](#finish-a-release-branch) - 1. [Create a hotfix branch](#create-a-hotfix-branch) - 1. [Finishing a hotfix branch](#finishing-a-hotfix-branch) +- [Development Workflow](#development-workflow) + - [Table of contents](#table-of-contents) + - [Main branches](#main-branches) + - [Development model](#development-model) + - [Feature branches](#feature-branches) + - [Release branches](#release-branches) + - [Hotfix branches](#hotfix-branches) + - [Git operations](#git-operations) + - [Create a feature branch](#create-a-feature-branch) + - [Merge feature branch into staging](#merge-feature-branch-into-staging) + - [Create release branch](#create-release-branch) + - [Finish a release branch](#finish-a-release-branch) + - [Create the hotfix branch](#create-the-hotfix-branch) + - [Finishing a hotfix branch](#finishing-a-hotfix-branch) + - [TODO](#todo) ## Main branches -The repo holds two main branches with an infinite lifetime: -- master -- nobunaga +Bittensor is composed of TWO main branches, **master** and **staging** -We consider `origin/master` to be the main branch where the source code of HEAD always reflects a **__production-ready__** state. +**master** +- master Bittensor's live production branch. This branch should only be touched and merged into by the core develpment team. This branch is protected, but you should make no attempt to push or merge into it reguardless. -We consider `origin/nobunaga` to be the main branch where the source code of HEAD always reflects a state with the **__latest delivered development__** changes for the next release. Some would call this the `"integration branch"`. This is where any automatic nightly builds would be built from. +**staging** +- staging is Bittensor's development branch. This branch is being continuously updated and merged into. This is the branch where you will propose and merge changes. ## Development model -### Supporting branches - -Each of these branches have a specific purpose and are bound to strict rules as to which branches may be their originating branch and which branches must be their merge targets. We will walk through them in a minute - #### Feature branches -- May branch off from: `nobunaga` -- Must merge back into: `nobunaga` +- May branch off from: `staging` +- Must merge back into: `staging` - Branch naming convention: - - Anything except master, nobunaga, finney, release/* or hotfix/* + - Anything except master, staging, finney, release/* or hotfix/* - Suggested: `feature//` +When implementing new features, hotfixes, bugfixes, or upgrades, it is wise to adhere to a strict naming and merging convention, whenever possible. + +**Branch naming and merging convention:** + + Feature branches are used to develop new features for the upcoming or a distant future release. When starting development of a feature, the target release in which this feature will be incorporated may well be unknown at that point. -The essence of a feature branch is that it exists as long as the feature is in development, but will eventually be merged back into `nobunaga` (to definitely add the new feature to the upcoming release) or discarded (in case of a disappointing experiment). +The essence of a feature branch is that it exists as long as the feature is in development, but will eventually be merged into `staging` (to definitely add the new feature to the upcoming release) or discarded (in case of a disappointing experiment). + +Generally, you should try to minimize the lifespan of feature branches. As soon as you merge a feature into 'staging', you should immidiately delete the feature branch. This will be strictly enforced. Excess branches creates tech debt and confusion between development teams and parties. #### Release branches -- May branch off from: `nobunaga` -- Must merge back into: `nobunaga` and `master` +- Please branch off from: `staging` +- Please merge back into: `staging` then into: `master` - Branch naming convention: - - Suggested format `release/3.4.0/optional-descriptive-message` + - STRONGLY suggested format `release/5.1.0/descriptive-message/creator's-name` -Release branches support preparation of a new production release. Furthermore, they allow for minor bug fixes and preparing meta-data for a release (e.g.: version number, configuration, etc.). By doing all of this work on a release branch, the `nobunaga` branch is cleared to receive features for the next big release. +Release branches support preparation of a new production release. Furthermore, they allow for minor bug fixes and preparing meta-data for a release (e.g.: version number, configuration, etc.). By doing all of this work on a release branch, the `staging` branch is cleared to receive features for the next big release. -This new branch may exist there for a while, until the release may be rolled out definitely. During that time, bug fixes may be applied in this branch, rather than on the `nobunaga` branch. Adding large new features here is strictly prohibited. They must be merged into `nobunaga`, and therefore, wait for the next big release. +This new branch may exist there for a while, until the release may be rolled out definitely. During that time, bug fixes may be applied in this branch, rather than on the `staging` branch. Adding large new features here is strictly prohibited. They must be merged into `staging`, and therefore, wait for the next big release. #### Hotfix branches -- May branch off from: `master` -- Must merge back into: `nobunaga` and `master` +- Please branch off from: `master` or `staging` +- Please merge back into: `staging` then into: `master` - Branch naming convention: - - Suggested format: `hotfix/3.3.4/optional-descriptive-message` + - REQUIRED format: `hotfix/3.3.4/descriptive-message/creator's-name` Hotfix branches are very much like release branches in that they are also meant to prepare for a new production release, albeit unplanned. They arise from the necessity to act immediately upon an undesired state of a live production version. When a critical bug in a production version must be resolved immediately, a hotfix branch may be branched off from the corresponding tag on the master branch that marks the production version. -The essence is that work of team members, on the `nobunaga` branch, can continue, while another person is preparing a quick production fix. +The essence is that work of team members, on the `staging` branch, can continue, while another person is preparing a quick production fix. ### Git operations #### Create a feature branch -1. Branch from the **nobunaga** branch. - 1. Command: `git checkout -b feature/my-feature nobunaga` +1. Branch from the **staging** branch. + 1. Command: `git checkout -b feature/my-feature staging` -> Try to rebase frequently with the updated nobunaga branch so you do not face big conflicts before submitting your pull request. Remember, syncing your changes with other developers could also help you avoid big conflicts. +> Rebase frequently with the updated staging branch so you do not face big conflicts before submitting your pull request. Remember, syncing your changes with other developers could also help you avoid big conflicts. -#### Merge feature branch into nobunaga +#### Merge feature branch into staging In other words, integrate your changes into a branch that will be tested and prepared for release. -- Switch branch to nobunaga: `git checkout nobunaga` -- Merging feature branch into nobunaga: `git merge --no-ff feature/my-feature` -- Pushing changes to nobunaga: `git push origin nobunaga` -- Delete feature branch: `git branch -d feature/my-feature` +- Switch branch to staging: `git checkout staging` +- Merging feature branch into staging: `git merge --no-ff feature/my-feature` +- Pushing changes to staging: `git push origin staging` +- Delete feature branch: `git branch -d feature/my-feature` (alternatively, this can be navigated on the GitHub web UI) This operation is done by Github when merging a PR. So, what you have to keep in mind is: -- Open the PR against the `nobunaga` branch. -- After merging a PR you just have to delete your feature branch. +- Open the PR against the `staging` branch. +- After merging a PR you should delete your feature branch. This will be strictly enforced. #### Create release branch -- Create branch from nobunaga: `git checkout -b release/3.4.0/optional-descriptive-message nobunaga` +- Create branch from staging: `git checkout -b release/3.4.0/descriptive-message/creator's_name staging` - Updating version with major or minor: `./scripts/update_version.sh major|minor` - Commit file changes with new version: `git commit -a -m "Updated version to 3.4.0"` @@ -106,20 +111,20 @@ In other words, releasing stable code and generating a new version for bittensor - Pushing changes to master: `git push origin master` - Pushing tags to origin: `git push origin --tags` -To keep the changes made in the __release__ branch, we need to merge those back into `nobunaga`: +To keep the changes made in the __release__ branch, we need to merge those back into `staging`: -- Switch branch to nobunaga: `git checkout nobunaga`. -- Merging release branch into nobunaga: `git merge --no-ff release/3.4.0/optional-descriptive-message` +- Switch branch to staging: `git checkout staging`. +- Merging release branch into staging: `git merge --no-ff release/3.4.0/optional-descriptive-message` This step may well lead to a merge conflict (probably even, since we have changed the version number). If so, fix it and commit. After this the release branch may be removed, since we don’t need it anymore: -- `git branch -d release/3.4.0/optional-descriptive-message` +- `git branch -d release/3.4.0/descriptive-message/creator's-name` #### Create the hotfix branch -- Create branch from master:`git checkout -b hotfix/3.3.4/optional-descriptive-message master` +- Create branch from master:`git checkout -b hotfix/3.3.4/descriptive-message/creator's-name master` - Update patch version: `./scripts/update_version.sh patch` - Commit file changes with new version: `git commit -a -m "Updated version to 3.3.4"` @@ -128,23 +133,23 @@ Then, fix the bug and commit the fix in one or more separate commits: #### Finishing a hotfix branch -When finished, the bugfix needs to be merged back into `master`, but also needs to be merged back into `nobunaga`, in order to safeguard that the bugfix is included in the next release as well. This is completely similar to how release branches are finished. +When finished, the bugfix needs to be merged back into `master`, but also needs to be merged back into `staging`, in order to safeguard that the bugfix is included in the next release as well. This is completely similar to how release branches are finished. First, update master and tag the release. - Switch branch to master: `git checkout master` - Merge changes into master: `git merge --no-ff hotfix/3.3.4/optional-descriptive-message` -- Tag new version: `git tag -a v3.3.4 -m "Releasing v3.3.4: some comment about the hotfix"` +- Tag new version: `git tag -a v3.3.4 -m "Releasing v3.3.4: descriptive comment about the hotfix"` - Pushing changes to master: `git push origin master` - Pushing tags to origin: `git push origin --tags` -Next, include the bugfix in `nobunaga`, too: +Next, include the bugfix in `staging`, too: -- Switch branch to nobunaga: `git checkout nobunaga` -- Merge changes into nobunaga: `git merge --no-ff hotfix/3.3.4/optional-descriptive-message` -- Pushing changes to origin/nobunaga: `git push origin nobunaga` +- Switch branch to staging: `git checkout staging` +- Merge changes into staging: `git merge --no-ff hotfix/3.3.4/descriptive-message/creator's-name` +- Pushing changes to origin/staging: `git push origin staging` -The one exception to the rule here is that, **when a release branch currently exists, the hotfix changes need to be merged into that release branch, instead of** `nobunaga`. Back-merging the bugfix into the __release__ branch will eventually result in the bugfix being merged into `develop` too, when the release branch is finished. (If work in develop immediately requires this bugfix and cannot wait for the release branch to be finished, you may safely merge the bugfix into develop now already as well.) +The one exception to the rule here is that, **when a release branch currently exists, the hotfix changes need to be merged into that release branch, instead of** `staging`. Back-merging the bugfix into the __release__ branch will eventually result in the bugfix being merged into `develop` too, when the release branch is finished. (If work in develop immediately requires this bugfix and cannot wait for the release branch to be finished, you may safely merge the bugfix into develop now already as well.) Finally, we remove the temporary branch: @@ -152,13 +157,11 @@ Finally, we remove the temporary branch: ## TODO -- Changing the name of the develop branch from nobunaga to `integration` - - Because sometimes nobunaga are going to have a release branch. -- Knowing if master and nobunaga are different -- Knowing what is in nobunaga that is not merge yet +- Knowing if master and staging are different +- Knowing what is in staging that is not merge yet - Document with not released developments - - When merged into nobunaga, generate the information exposing what's merged into nobunaga but not release. + - When merged into staging, generate the information exposing what's merged into staging but not release. - When merged into master, generate github release and release notes. - CircleCI job - - Merge nobunaga into master and release version (needed to release code) - - Build and Test bittensor (needed to merge PRs) \ No newline at end of file + - Merge staging into master and release version (needed to release code) + - Build and Test Bittensor (needed to merge PRs) diff --git a/README.md b/README.md index 2386882317..c73bb75456 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This repository contains Bittensor's Python API, which can be used for the following purposes: 1. Querying the Bittensor network as a client. -2. Running and building Bittensor miners and validators. +2. Running and building Bittensor miners. (Validators are now at [openvalidators](https://github.com/opentensor/validators)). 3. Pulling network state information. 4. Managing TAO wallets, balances, transfers, etc. @@ -205,8 +205,8 @@ Registered miners are free to select from variety of pre-written miners or to wr ```bash $ git clone https://github.com/opentensor/bittensor.git bittensor/ # This repo. - neurons/ # Miners and Validators across all subnetworks. - text_prompting/ # Miners and Validators for the text_prompting subnetwork. + neurons/ # Miners across all subnetworks. + text_prompting/ # Miners for the text_prompting subnetwork. miners/ # Miners. GPT4ALL/ # The root folder for the GPT4ALL miner. neuron.py # GPT4ALL miner main script. @@ -235,18 +235,7 @@ $ btcli stake --help # To add funds to the staking account associated with your $ btcli nominate --help # to become a key available for delegated stake $ btcli delegate --help # for others to delegate stake to your wallet. ``` -Bittensor's API is designed to allow Validators to write their own validation mechanisms and express their own subjective prefrences about what the network should learn. However, going too far outside consensus reduces the rewards validators attain while performing validation. To ensure your validator remains in alignment with others this repository contains a "core" validator for each subnetwork -```bash -$ tree bittensor/neurons - bittensor/ - neurons/ - text_to_embedding/ - text_prompting/ - validators/ - core/ - neuron.py -``` -For instance you can run the core text prompting validator on subnetwork 1 as follows. Note it is also recommended that you run validators on machines with a GPU. In the future bittensor/neurons/valdidators is likely to expand into its own repository. +Bittensor's API is designed to allow Validators to write their own validation mechanisms and express their own subjective prefrences about what the network should learn. However, going too far outside consensus reduces the rewards validators attain while performing validation. To ensure your validator remains in alignment with others, please see the `openvalidators` repo [here](https://github.com/opentensor/validators). # Using the CLI @@ -343,7 +332,7 @@ wallet.coldkey.sign( data ) ``` -Subtensor: Interfaces with bittensor's blochain and can perform operations like extracting state information or sending transactions. +Subtensor: Interfaces with bittensor's blockchain and can perform operations like extracting state information or sending transactions. ```python import bittensor as bt # Bittensor's chain interface. diff --git a/VERSION b/VERSION index 7cbea073be..e230c8396d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.2.0 \ No newline at end of file +5.3.0 \ No newline at end of file diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 8783b0ac4b..0841da5e63 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -27,7 +27,7 @@ nest_asyncio.apply() # Bittensor code and protocol version. -__version__ = '5.2.0' +__version__ = '5.3.0' version_split = __version__.split(".") __version_as_int__ = (100 * int(version_split[0])) + (10 * int(version_split[1])) + (1 * int(version_split[2])) __new_signature_version__ = 360 @@ -89,6 +89,9 @@ def turn_console_off(): __rao_symbol__: str = chr(0x03C1) +# Mock Testing Constant +__GLOBAL_MOCK_STATE__ = {} + # Block Explorers map network to explorer url ## Must all be polkadotjs explorer urls __network_explorer_map__ = { @@ -97,13 +100,6 @@ def turn_console_off(): 'finney': "https://explorer.finney.opentensor.ai/#/explorer" } -# Avoid collisions with other processes -from .utils.test_utils import get_random_unused_port -mock_subtensor_port = get_random_unused_port() -__mock_entrypoint__ = f"localhost:{mock_subtensor_port}" - -__mock_chain_db__ = './tmp/mock_chain_db' - # --- Type Registry --- __type_registry__ = { 'types': { @@ -135,7 +131,7 @@ def turn_console_off(): # ---- Config ---- -from bittensor._config import config as config +from bittensor_config import config as config # ---- LOGGING ---- # Duplicate import for ease of use. @@ -159,8 +155,8 @@ def turn_console_off(): from bittensor._cli import cli as cli from bittensor._axon import axon as axon from bittensor._axon import axon_info as axon_info -from bittensor._wallet import wallet as wallet -from bittensor._keyfile import keyfile as keyfile +from bittensor_wallet import wallet as wallet +from bittensor_wallet import keyfile as keyfile from bittensor._metagraph import metagraph as metagraph from bittensor._prometheus import prometheus as prometheus from bittensor._subtensor import subtensor as subtensor @@ -173,13 +169,16 @@ def turn_console_off(): # ---- Classes ----- from bittensor._cli.cli_impl import CLI as CLI -from bittensor._config.config_impl import Config as Config +from bittensor_config.config_impl import Config as Config from bittensor._subtensor.chain_data import DelegateInfo as DelegateInfo -from bittensor._wallet.wallet_impl import Wallet as Wallet -from bittensor._keyfile.keyfile_impl import Keyfile as Keyfile +from bittensor_wallet import Wallet as Wallet +from bittensor_wallet import Keyfile as Keyfile +from bittensor_wallet import Keypair as Keypair from bittensor._subtensor.chain_data import NeuronInfo as NeuronInfo from bittensor._subtensor.chain_data import NeuronInfoLite as NeuronInfoLite from bittensor._subtensor.chain_data import PrometheusInfo as PrometheusInfo +from bittensor._subtensor.chain_data import ProposalCallData as ProposalCallData +from bittensor._subtensor.chain_data import ProposalVoteData as ProposalVoteData from bittensor._subtensor.subtensor_impl import Subtensor as Subtensor from bittensor._serializer.serializer_impl import Serializer as Serializer from bittensor._subtensor.chain_data import SubnetInfo as SubnetInfo @@ -188,12 +187,10 @@ def turn_console_off(): from bittensor._ipfs.ipfs_impl import Ipfs as Ipfs # ---- Errors and Exceptions ----- -from bittensor._keyfile.keyfile_impl import KeyFileError as KeyFileError +from bittensor_wallet import KeyFileError as KeyFileError from bittensor._proto.bittensor_pb2 import ForwardTextPromptingRequest from bittensor._proto.bittensor_pb2 import ForwardTextPromptingResponse -from bittensor._proto.bittensor_pb2 import MultiForwardTextPromptingRequest -from bittensor._proto.bittensor_pb2 import MultiForwardTextPromptingResponse from bittensor._proto.bittensor_pb2 import BackwardTextPromptingRequest from bittensor._proto.bittensor_pb2 import BackwardTextPromptingResponse @@ -210,16 +207,9 @@ def turn_console_off(): # ---- Base Miners ----- from bittensor._neuron.base_miner_neuron import BaseMinerNeuron -from bittensor._neuron.base_validator import BaseValidator from bittensor._neuron.base_prompting_miner import BasePromptingMiner from bittensor._neuron.base_huggingface_miner import HuggingFaceMiner -# ---- Errors and Exceptions ----- -from bittensor._keyfile.keyfile_impl import KeyFileError as KeyFileError - -# ---- Errors and Exceptions ----- -from bittensor._keyfile.keyfile_impl import KeyFileError as KeyFileError - # DEFAULTS defaults = Config() defaults.netuid = 1 @@ -227,12 +217,10 @@ def turn_console_off(): axon.add_defaults( defaults ) prioritythreadpool.add_defaults( defaults ) prometheus.add_defaults( defaults ) -wallet.add_defaults( defaults ) +wallet.add_defaults( defaults, prefix = 'wallet' ) dataset.add_defaults( defaults ) logging.add_defaults( defaults ) -from substrateinterface import Keypair as Keypair - # Logging helpers. def trace(): logging.set_trace(True) diff --git a/bittensor/_axon/__init__.py b/bittensor/_axon/__init__.py index d73fb3941c..2768f94350 100644 --- a/bittensor/_axon/__init__.py +++ b/bittensor/_axon/__init__.py @@ -154,7 +154,7 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None): """Accept specific arguments from parser""" prefix_str = "" if prefix is None else prefix + "." if prefix is not None: - if not hasattr(bittensor.defaults, prefix): + if bittensor.defaults.get(prefix, d=None) == None: setattr(bittensor.defaults, prefix, bittensor.Config()) getattr(bittensor.defaults, prefix).axon = bittensor.defaults.axon @@ -305,7 +305,7 @@ def parse_signature(self, metadata: Dict[str, str]) -> Tuple[int, str, str, str] version = metadata.get('bittensor-version') if signature is None: raise Exception("Request signature missing") - if int(version) < 370: + if int(version) < 510: raise Exception("Incorrect Version") parts = self.parse_signature_v2(signature) if parts is not None: diff --git a/bittensor/_cli/__init__.py b/bittensor/_cli/__init__.py index 6f7c58e9ac..91dec1aef6 100644 --- a/bittensor/_cli/__init__.py +++ b/bittensor/_cli/__init__.py @@ -92,15 +92,6 @@ def __create_parser__() -> 'argparse.ArgumentParser': VoteCommand.add_args( cmd_parsers ) - return parser - - @staticmethod - def config(args: List[str]) -> 'bittensor.config': - """ From the argument parser, add config to bittensor.executor and local config - Return: bittensor.config object - """ - parser = cli.__create_parser__() - return parser @staticmethod diff --git a/bittensor/_cli/commands/delegates.py b/bittensor/_cli/commands/delegates.py index a08460adef..a71c65a92b 100644 --- a/bittensor/_cli/commands/delegates.py +++ b/bittensor/_cli/commands/delegates.py @@ -75,7 +75,7 @@ def show_delegates( delegates: List['bittensor.DelegateInfo'], prev_delegates: O table.add_column("[overline white]Desc", style='rgb(50,163,219)') #table.add_column("[overline white]DESCRIPTION", style='white') - for i, delegate in enumerate( delegates): + for i, delegate in enumerate( delegates ): owner_stake = next( map(lambda x: x[1], # get stake filter(lambda x: x[0] == delegate.owner_ss58, delegate.nominators) # filter for owner @@ -337,6 +337,7 @@ def run( cli ): if prev_delegates is None: bittensor.__console__.print(":warning: [yellow]Could not fetch delegates history[/yellow]") + show_delegates( delegates, prev_delegates = prev_delegates, width = cli.config.get('width', None) ) @staticmethod @@ -453,6 +454,7 @@ def run( cli ): my_delegates[ delegate[0].hotkey_ss58 ] = staked delegates.sort(key=lambda delegate: delegate[0].total_stake, reverse=True) + registered_delegate_info: Optional[DelegatesDetails] = get_delegates_details(url = bittensor.__delegates_details_url__) if registered_delegate_info is None: bittensor.__console__.print( ':warning:[yellow]Could not get delegate info from chain.[/yellow]') diff --git a/bittensor/_cli/commands/inspect.py b/bittensor/_cli/commands/inspect.py index 3596f5a731..3b1b80549b 100644 --- a/bittensor/_cli/commands/inspect.py +++ b/bittensor/_cli/commands/inspect.py @@ -88,7 +88,7 @@ def run (cli): for wallet in tqdm( wallets ): delegates: List[Tuple(bittensor.DelegateInfo, bittensor.Balance)] = subtensor.get_delegated( coldkey_ss58=wallet.coldkeypub.ss58_address ) if not wallet.coldkeypub_file.exists_on_device(): continue - cold_balance = wallet.get_balance( subtensor = subtensor ) + cold_balance = subtensor.get_balance( wallet.coldkeypub.ss58_address ) table.add_row( wallet.name, str(cold_balance), diff --git a/bittensor/_cli/commands/misc.py b/bittensor/_cli/commands/misc.py index 0288fb7b42..3959be4856 100644 --- a/bittensor/_cli/commands/misc.py +++ b/bittensor/_cli/commands/misc.py @@ -24,41 +24,6 @@ from rich.table import Table console = bittensor.__console__ -class HelpCommand: - @staticmethod - def run (cli): - sys.argv = [sys.argv[0], '--help'] - # # Run miner. - # if cli.config.model == 'core_server': - # bittensor.neurons.core_server.neuron().run() - # elif cli.config.model == 'core_validator': - # bittensor.neurons.core_validator.neuron().run() - # elif cli.config.model == 'multitron_server': - # bittensor.neurons.multitron_server.neuron().run() - - @staticmethod - def check_config( config: 'bittensor.Config' ): - pass - # if config.model == 'None': - # model = Prompt.ask('Enter miner name', choices = list(bittensor.neurons.__text_neurons__.keys()), default = 'core_server') - # config.model = model - - @staticmethod - def add_args( parser: argparse.ArgumentParser ): - pass - # help_parser = parser.add_parser( - # 'help', - # add_help=False, - # help='''Displays the help ''' - # ) - # help_parser.add_argument( - # '--model', - # type=str, - # choices= list(bittensor.neurons.__text_neurons__.keys()), - # default='None', - # ) - # help_parser.add_argument( '--no_version_checking', action='store_true', help='''Set false to stop cli version checking''', default = False ) - # bittensor.subtensor.add_args( help_parser ) class UpdateCommand: @staticmethod diff --git a/bittensor/_cli/commands/senate.py b/bittensor/_cli/commands/senate.py index b2e84f991b..cb9a2cccac 100644 --- a/bittensor/_cli/commands/senate.py +++ b/bittensor/_cli/commands/senate.py @@ -21,6 +21,8 @@ from rich.prompt import Prompt, Confirm from rich.table import Table from typing import List, Union, Optional, Dict, Tuple +from .utils import get_delegates_details, DelegatesDetails + console = bittensor.__console__ class SenateCommand: @@ -34,17 +36,20 @@ def run( cli ): console.print(":satellite: Syncing with chain: [white]{}[/white] ...".format(cli.config.subtensor.network)) - senate_members = subtensor.query_module("Senate", "Members").serialize() + senate_members = subtensor.get_senate_members() + delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(url = bittensor.__delegates_details_url__) table = Table(show_footer=False) table.title = ( "[white]Senate" ) + table.add_column("[overline white]NAME", footer_style = "overline white", style="rgb(50,163,219)", no_wrap=True) table.add_column("[overline white]ADDRESS", footer_style = "overline white", style='yellow', no_wrap=True) table.show_footer = True for ss58_address in senate_members: table.add_row( + delegate_info[ss58_address].name if ss58_address in delegate_info else "", ss58_address ) @@ -79,8 +84,7 @@ def add_args( cls, parser: argparse.ArgumentParser ): bittensor.wallet.add_args( senate_parser ) bittensor.subtensor.add_args( senate_parser ) -from .utils import get_delegates_details, DelegatesDetails -def format_call_data(call_data: List) -> str: +def format_call_data(call_data: 'bittensor.ProposalCallData') -> str: human_call_data = list() for arg in call_data["call_args"]: @@ -96,7 +100,7 @@ def format_call_data(call_data: List) -> str: return "{}({})".format(call_data["call_function"], ", ".join(human_call_data)) -def display_votes(vote_data, delegate_info) -> str: +def display_votes(vote_data: 'bittensor.ProposalVoteData', delegate_info: 'bittensor.DelegateInfo') -> str: vote_list = list() for address in vote_data["ayes"]: @@ -118,15 +122,8 @@ def run( cli ): console.print(":satellite: Syncing with chain: [white]{}[/white] ...".format(cli.config.subtensor.network)) - senate_members = subtensor.query_module("SenateMembers", "Members").serialize() - proposals = dict() - proposal_hashes = subtensor.query_module("Triumvirate", "Proposals") - - for hash in proposal_hashes: - proposals[hash] = [ - subtensor.query_module("Triumvirate", "ProposalOf", None, [hash]), - subtensor.get_vote_data( hash ) - ] + senate_members = subtensor.get_senate_members() + proposals = subtensor.get_proposals() registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(url = bittensor.__delegates_details_url__) @@ -144,8 +141,7 @@ def run( cli ): table.show_footer = True for hash in proposals: - call_data = proposals[hash][0].serialize() - vote_data = proposals[hash][1] + call_data, vote_data = proposals[hash] table.add_row( hash, @@ -287,7 +283,7 @@ def run( cli ): console.print('Aborting: Hotkey {} isn\'t a delegate.'.format(wallet.hotkey.ss58_address)) return - if wallet.is_senate_member(subtensor): + if subtensor.is_senate_member( hotkey_ss58=wallet.hotkey.ss58_address ): console.print('Aborting: Hotkey {} is already a senate member.'.format(wallet.hotkey.ss58_address)) return @@ -342,7 +338,7 @@ def run( cli ): wallet.hotkey wallet.coldkey - if not wallet.is_senate_member(subtensor): + if not subtensor.is_senate_member( hotkey_ss58=wallet.hotkey.ss58_address ): console.print('Aborting: Hotkey {} isn\'t a senate member.'.format(wallet.hotkey.ss58_address)) return @@ -398,7 +394,7 @@ def run( cli ): console.print('Aborting: Proposal hash not specified. View all proposals with the "proposals" command.') return - if not wallet.is_senate_member(subtensor): + if not subtensor.is_senate_member( hotkey_ss58=wallet.hotkey.ss58_address ): console.print('Aborting: Hotkey {} isn\'t a senate member.'.format(wallet.hotkey.ss58_address)) return diff --git a/bittensor/_cli/commands/stake.py b/bittensor/_cli/commands/stake.py index 4ee784ff81..ff32732957 100644 --- a/bittensor/_cli/commands/stake.py +++ b/bittensor/_cli/commands/stake.py @@ -19,7 +19,6 @@ import argparse import bittensor from tqdm import tqdm -from rich.prompt import Confirm from rich.prompt import Confirm, Prompt from bittensor.utils.balance import Balance from typing import List, Union, Optional, Dict, Tuple diff --git a/bittensor/_cli/commands/unstake.py b/bittensor/_cli/commands/unstake.py index dfb26e369e..e077c03b50 100644 --- a/bittensor/_cli/commands/unstake.py +++ b/bittensor/_cli/commands/unstake.py @@ -20,7 +20,7 @@ from tqdm import tqdm from rich.prompt import Confirm, Prompt from bittensor.utils.balance import Balance -from typing import List, Union, Optional, Dict, Tuple +from typing import List, Union, Optional, Tuple from .utils import get_hotkey_wallets_for_wallet console = bittensor.__console__ @@ -28,11 +28,11 @@ class UnStakeCommand: @classmethod def check_config( cls, config: 'bittensor.Config' ): - if config.is_set('wallet.name') and not config.no_prompt: + if not config.is_set('wallet.name') and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default = bittensor.defaults.wallet.name) config.wallet.name = str(wallet_name) - if not config.get( 'hotkey_ss58address', d=None ) and config.is_set('wallet.hotkey') and not config.no_prompt and not config.get('all_hotkeys') and not config.get('hotkeys'): + if not config.get( 'hotkey_ss58address', d=None ) and not config.is_set('wallet.hotkey') and not config.no_prompt and not config.get('all_hotkeys') and not config.get('hotkeys'): hotkey = Prompt.ask("Enter hotkey name", default = bittensor.defaults.wallet.hotkey) config.wallet.hotkey = str(hotkey) diff --git a/bittensor/_cli/commands/utils.py b/bittensor/_cli/commands/utils.py index ee6e3b8027..f11e0c6619 100644 --- a/bittensor/_cli/commands/utils.py +++ b/bittensor/_cli/commands/utils.py @@ -157,8 +157,8 @@ def _get_delegates_details_from_github(requests_get, url: str) -> Dict[str, Dele return all_delegates_details else: return {} - -def get_delegates_details(url: str) -> Optional[Dict[str, DelegatesDetails]]: + +def get_delegates_details(url: str) -> Optional[Dict[str, DelegatesDetails]]: try: return _get_delegates_details_from_github(requests.get, url) except Exception: diff --git a/bittensor/_config/__init__.py b/bittensor/_config/__init__.py deleted file mode 100644 index e26a611a06..0000000000 --- a/bittensor/_config/__init__.py +++ /dev/null @@ -1,194 +0,0 @@ -""" -Create and init the config class, which manages the config of different bittensor modules. -""" -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -import os -import sys -from argparse import ArgumentParser, Namespace -from typing import List, Optional, Dict - -import bittensor -import yaml -from loguru import logger -import pandas as pd - -from . import config_impl - -logger = logger.opt(colors=True) - -class config: - """ - Create and init the config class, which manages the config of different bittensor modules. - """ - class InvalidConfigFile(Exception): - """ In place of YAMLError - """ - - def __new__( cls, parser: ArgumentParser = None, strict: bool = False, args: Optional[List[str]] = None ): - r""" Translates the passed parser into a nested Bittensor config. - Args: - parser (argparse.Parser): - Command line parser object. - strict (bool): - If true, the command line arguments are strictly parsed. - args (list of str): - Command line arguments. - Returns: - config (bittensor.Config): - Nested config object created from parser arguments. - """ - if parser == None: - return config_impl.Config() - - # Optionally add config specific arguments - try: - parser.add_argument('--config', type=str, help='If set, defaults are overridden by passed file.') - except: - # this can fail if the --config has already been added. - pass - try: - parser.add_argument('--strict', action='store_true', help='''If flagged, config will check that only exact arguemnts have been set.''', default=False ) - except: - # this can fail if the --config has already been added. - pass - - # Get args from argv if not passed in. - if args == None: - args = sys.argv[1:] - - # 1.1 Optionally load defaults if the --config is set. - try: - config_file_path = str(os.getcwd()) + '/' + vars(parser.parse_known_args(args)[0])['config'] - except Exception as e: - config_file_path = None - - # Parse args not strict - params = cls.__parse_args__(args=args, parser=parser, strict=False) - - # 2. Optionally check for --strict, if stict we will parse the args strictly. - strict = params.strict - - if config_file_path != None: - config_file_path = os.path.expanduser(config_file_path) - try: - with open(config_file_path) as f: - params_config = yaml.safe_load(f) - print('Loading config defaults from: {}'.format(config_file_path)) - parser.set_defaults(**params_config) - except Exception as e: - print('Error in loading: {} using default parser settings'.format(e)) - - # 2. Continue with loading in params. - params = cls.__parse_args__(args=args, parser=parser, strict=strict) - - _config = config_impl.Config() - - # Splits params on dot syntax i.e neuron.axon_port - for arg_key, arg_val in params.__dict__.items(): - split_keys = arg_key.split('.') - head = _config - keys = split_keys - while len(keys) > 1: - if hasattr(head, keys[0]): - head = getattr(head, keys[0]) - keys = keys[1:] - else: - head[keys[0]] = config_impl.Config() - head = head[keys[0]] - keys = keys[1:] - if len(keys) == 1: - head[keys[0]] = arg_val - - # Get defaults for this config - is_set_map = cls.__fill_is_set_list__(_config, bittensor.defaults) - - _config['__is_set'] = is_set_map - - _config.__fill_with_defaults__(is_set_map, bittensor.defaults) - - return _config - - @staticmethod - def __fill_is_set_list__(_config: 'bittensor.Config', defaults: 'bittensor.Config') -> Dict[str, bool]: - """Creates an is_set map - Args: - _config (bittensor.Config): - Config to generate is_set mapping. - defaults (bittensor.Config): - The bittensor defaults - Returns: - is_set_map (Dict[str, bool]): - A map from flattened param name to whether this param was set in a flag. - """ - is_set_map = {} - config_d = _config.__dict__ - # Only defaults we are concerned with - defaults_filtered = {} - for key in config_d.keys(): - if key in defaults.keys(): - defaults_filtered[key] = getattr(defaults, key) - # Avoid erroring out if defaults aren't set for a submodule - if defaults_filtered == {}: - return is_set_map - - flat_config = pd.json_normalize(config_d, sep='.').to_dict('records')[0] - flat_defaults = pd.json_normalize(defaults_filtered, sep='.').to_dict('records')[0] - for key, _ in flat_defaults.items(): - if key in flat_config: - is_set_map[key] = True - else: - is_set_map[key] = False - - return is_set_map - - - @staticmethod - def __parse_args__( args: List[str], parser: ArgumentParser = None, strict: bool = False) -> Namespace: - """Parses the passed args use the passed parser. - Args: - args (List[str]): - List of arguments to parse. - parser (argparse.ArgumentParser): - Command line parser object. - strict (bool): - If true, the command line arguments are strictly parsed. - Returns: - Namespace: - Namespace object created from parser arguments. - """ - if not strict: - params = parser.parse_known_args(args=args)[0] - else: - params = parser.parse_args(args=args) - - return params - - @staticmethod - def full(): - """ From the parser, add arguments to multiple bittensor sub-modules - """ - parser = ArgumentParser() - bittensor.wallet.add_args( parser ) - bittensor.subtensor.add_args( parser ) - bittensor.axon.add_args( parser ) - bittensor.metagraph.add_args( parser ) - bittensor.dataset.add_args( parser ) - bittensor.prometheus.add_args( parser ) - return bittensor.config( parser ) diff --git a/bittensor/_config/config_impl.py b/bittensor/_config/config_impl.py deleted file mode 100644 index 28b975d934..0000000000 --- a/bittensor/_config/config_impl.py +++ /dev/null @@ -1,187 +0,0 @@ -""" -Implementation of the config class, which manages the config of different bittensor modules. -""" -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -import yaml -import json -import pandas -import bittensor -from munch import Munch -from prometheus_client import Info -from pandas import json_normalize -from typing import Dict -import copy -import bittensor - -class Config ( Munch ): - """ - Implementation of the config class, which manages the config of different bittensor modules. - """ - __is_set: Dict[str, bool] - - def __init__(self, loaded_config = None ): - super().__init__() - if loaded_config: - raise NotImplementedError('Function load_from_relative_path is not fully implemented.') - - def __repr__(self) -> str: - return self.__str__() - - def __str__(self) -> str: - return "\n" + yaml.dump(self.toDict()) - - def to_string(self, items) -> str: - """ Get string from items - """ - return "\n" + yaml.dump(items.toDict()) - - def update_with_kwargs( self, kwargs ): - """ Add config to self - """ - for key,val in kwargs.items(): - self[key] = val - - @classmethod - def _merge( cls, a, b ): - """Merge two configurations recursively. - If there is a conflict, the value from the second configuration will take precedence. - """ - for key in b: - if key in a: - if isinstance( a[key], dict ) and isinstance( b[key], dict ): - a[key] = cls._merge( a[key], b[key] ) - else: - a[key] = b[key] - else: - a[key] = b[key] - return a - - def merge(self, b): - """ Merge two configs - """ - self = self._merge( self, b ) - - def to_prometheus(self): - """ - Sends the config to the inprocess prometheus server if it exists. - """ - try: - prometheus_info = Info('config', 'Config Values') - # Make copy, remove __is_set map - config_copy = copy.deepcopy(self) - - del config_copy['__is_set'] - - config_info = json_normalize(json.loads(json.dumps(config_copy)), sep='.').to_dict(orient='records')[0] - formatted_info = {} - for key in config_info: - config_info[key] = str(config_info[key]) - formatted_info[key.replace('.', '_')] = str(config_info[key]) - prometheus_info.info(formatted_info) - except ValueError: - # The user called this function twice in the same session. - # TODO(const): need a way of distinguishing the various config items. - bittensor.__console__.print("The config has already been added to prometheus.", highlight=True) - - def is_set(self, param_name: str) -> bool: - """ - Returns a boolean indicating whether the parameter has been set or is still the default. - """ - if param_name not in self.get('__is_set'): - return False - else: - return self.get('__is_set')[param_name] - - def __fill_with_defaults__(self, is_set_map: Dict[str, bool], defaults: 'Config') -> None: - """ - Recursively fills the config with the default values using is_set_map - """ - defaults_filtered = {} - for key in self.keys(): - if key in defaults.keys(): - defaults_filtered[key] = getattr(defaults, key) - # Avoid erroring out if defaults aren't set for a submodule - if defaults_filtered == {}: return - - flat_defaults = json_normalize(defaults_filtered, sep='.').to_dict('records')[0] - for key, val in flat_defaults.items(): - if key not in is_set_map: - continue - elif not is_set_map[key]: - # If the key is not set, set it to the default value - # Loop through flattened key to get leaf - a = self - keys = key.split('.') - for key_ in keys[:-1]: - if key_ not in a: - a[key_] = {} - a = a[key_] - # Set leaf to default value - a[keys[-1]] = val - - def to_defaults(self): - try: - if 'axon' in self.keys(): - bittensor.defaults.axon.port = self.axon.port - bittensor.defaults.axon.ip = self.axon.ip - bittensor.defaults.axon.external_port = self.axon.external_port - bittensor.defaults.axon.external_ip = self.axon.external_ip - bittensor.defaults.axon.max_workers = self.axon.max_workers - bittensor.defaults.axon.maximum_concurrent_rpcs = self.axon.maximum_concurrent_rpcs - - if 'dataset' in self.keys(): - bittensor.defaults.dataset.batch_size = self.dataset.batch_size - bittensor.defaults.dataset.block_size = self.dataset.block_size - bittensor.defaults.dataset.num_batches = self.dataset.num_batches - bittensor.defaults.dataset.num_workers = self.dataset.num_workers - bittensor.defaults.dataset.dataset_names = self.dataset.dataset_names - bittensor.defaults.dataset.data_dir = self.dataset.data_dir - bittensor.defaults.dataset.save_dataset = self.dataset.save_dataset - bittensor.defaults.dataset.max_datasets = self.dataset.max_datasets - - if 'logging' in self.keys(): - bittensor.defaults.logging.debug = self.logging.debug - bittensor.defaults.logging.trace = self.logging.trace - bittensor.defaults.logging.record_log = self.logging.record_log - bittensor.defaults.logging.logging_dir = self.logging.logging_dir - - if 'subtensor' in self.keys(): - bittensor.defaults.subtensor.network = self.subtensor.network - bittensor.defaults.subtensor.chain_endpoint = self.subtensor.chain_endpoint - - if 'threadpool' in self.keys(): - bittensor.defaults.threadpool.max_workers = self.threadpool.max_workers - bittensor.defaults.threadpool.maxsize = self.threadpool.maxsize - - if 'wallet' in self.keys(): - bittensor.defaults.wallet.name = self.wallet.name - bittensor.defaults.wallet.hotkey = self.wallet.hotkey - bittensor.defaults.wallet.path = self.wallet.path - - if 'wandb' in self.keys(): - bittensor.defaults.wandb.name = self.wandb.name - bittensor.defaults.wandb.project = self.wandb.project - bittensor.defaults.wandb.tags = self.wandb.tags - bittensor.defaults.wandb.run_group = self.wandb.run_group - bittensor.defaults.wandb.directory = self.wandb.directory - bittensor.defaults.wandb.offline = self.wandb.offline - - except Exception as e: - print('Error when loading config into defaults {}'.format(e)) \ No newline at end of file diff --git a/bittensor/_dataset/__init__.py b/bittensor/_dataset/__init__.py index 92628dfb04..b9d0286f00 100644 --- a/bittensor/_dataset/__init__.py +++ b/bittensor/_dataset/__init__.py @@ -135,7 +135,7 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None ): """ prefix_str = '' if prefix == None else prefix + '.' if prefix is not None: - if not hasattr(bittensor.defaults, prefix): + if bittensor.defaults.get(prefix, d=None) == None: setattr(bittensor.defaults, prefix, bittensor.Config()) getattr(bittensor.defaults, prefix).dataset = bittensor.defaults.dataset try: diff --git a/bittensor/_dendrite/dendrite.py b/bittensor/_dendrite/dendrite.py index 0fa0a42797..0a362d52d3 100644 --- a/bittensor/_dendrite/dendrite.py +++ b/bittensor/_dendrite/dendrite.py @@ -44,7 +44,6 @@ def __init__( self.timeout = timeout self.start_time = time.time() self.elapsed_time = 0.0 - self.src_hotkey = self.dendrite.keypair.ss58_address self.src_version = bittensor.__version_as_int__ self.dest_hotkey = self.dendrite.axon_info.hotkey self.dest_version = self.dendrite.axon_info.version @@ -73,7 +72,6 @@ def _get_request_proto(self) -> object: request_proto = self.get_request_proto() request_proto.version = self.src_version request_proto.timeout = self.timeout - request_proto.hotkey = self.src_hotkey return request_proto @abstractmethod diff --git a/bittensor/_dendrite/text_prompting/dendrite.py b/bittensor/_dendrite/text_prompting/dendrite.py index 56c25ee96e..183a3d5d5b 100644 --- a/bittensor/_dendrite/text_prompting/dendrite.py +++ b/bittensor/_dendrite/text_prompting/dendrite.py @@ -14,12 +14,10 @@ # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -import grpc import json import torch -import asyncio import bittensor -from typing import Callable, List, Dict, Union +from typing import Callable, List, Union class DendriteForwardCall( bittensor.DendriteCall ): @@ -78,45 +76,6 @@ async def async_backward( self, reward: float, timeout: float = None ) -> 'Dendr ) -class MultiDendriteForwardCall( bittensor.DendriteCall ): - - name: str = "text_prompting_multi_forward" - is_forward: bool = True - multi_completions: List[str] = [""] # To be filled. - - def __init__( - self, - dendrite: 'bittensor.TextPromptingDendrite', - messages: List[str], - roles: List[str], - timeout: float = bittensor.__blocktime__, - ): - super().__init__( dendrite = dendrite, timeout = timeout ) - self.messages = messages - self.roles = roles - self.packed_messages = [json.dumps({"role": role, "content": message}) for role, message in zip(self.roles, self.messages)] - - def __repr__(self) -> str: - return f"MultiDendriteForwardCall( {bittensor.utils.codes.code_to_string(self.return_code)}, to: {self.dest_hotkey[:4]}...{self.dest_hotkey[-4:]}, msg: {self.return_message}, n_completion: {len(self.multi_completions)})" - - def __str__(self) -> str: return self.__repr__() - - def get_callable( self ) -> Callable: - return bittensor.grpc.TextPromptingStub( self.dendrite.channel ).MultiForward - - def get_request_proto( self ) -> bittensor.proto.MultiForwardTextPromptingRequest: - return bittensor.MultiForwardTextPromptingRequest( timeout = self.timeout, messages = self.packed_messages ) - - def apply_response_proto( self, response_proto: bittensor.MultiForwardTextPromptingResponse ): - self.multi_completions = response_proto.multi_completions - - def get_inputs_shape(self) -> torch.Size: - return torch.Size( [len(message) for message in self.packed_messages] ) - - def get_outputs_shape(self) -> torch.Size: - return torch.Size([ len(self.multi_completions) ] ) - - class DendriteBackwardCall( bittensor.DendriteCall ): name: str = "text_prompting_backward" @@ -198,40 +157,6 @@ async def async_forward( if return_call: return forward_call else: return forward_call.completion - def multi_forward( - self, - roles: List[ str ] , - messages: List[ str ], - timeout: float = bittensor.__blocktime__, - return_call:bool = True, - ) -> Union[ str, DendriteForwardCall ]: - forward_call = MultiDendriteForwardCall( - dendrite = self, - messages = messages, - roles = roles, - timeout = timeout, - ) - response_call = self.loop.run_until_complete( self.apply( dendrite_call = forward_call ) ) - if return_call: return response_call - else: return response_call.multi_completions - - async def async_multi_forward( - self, - roles: List[ str ], - messages: List[ str ], - timeout: float = bittensor.__blocktime__, - return_call: bool = True, - ) -> Union[ str, DendriteForwardCall ]: - forward_call = MultiDendriteForwardCall( - dendrite = self, - messages = messages, - roles = roles, - timeout = timeout, - ) - forward_call = await self.apply( dendrite_call = forward_call ) - if return_call: return forward_call - else: return forward_call.multi_completions - def backward( self, roles: List[ str ], @@ -267,7 +192,3 @@ async def async_backward( timeout = timeout, ) return await self.apply( dendrite_call = backward_call ) - - - - diff --git a/bittensor/_keyfile/keyfile_impl.py b/bittensor/_keyfile/keyfile_impl.py deleted file mode 100644 index bfb6a6d95e..0000000000 --- a/bittensor/_keyfile/keyfile_impl.py +++ /dev/null @@ -1,557 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -import os -import base64 -import json -import stat -import getpass -import bittensor -from typing import Optional -from pathlib import Path - -from ansible_vault import Vault -from ansible.parsing.vault import AnsibleVaultError -from cryptography.exceptions import InvalidSignature, InvalidKey -from cryptography.fernet import Fernet, InvalidToken -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC -from password_strength import PasswordPolicy -from substrateinterface.utils.ss58 import ss58_encode -from termcolor import colored - -class KeyFileError(Exception): - """ Error thrown when the keyfile is corrupt, non-writable, nno-readable or the password used to decrypt is invalid. - """ - -def serialized_keypair_to_keyfile_data( keypair: 'bittensor.Keypair' ): - """ Serializes keypair object into keyfile data. - Args: - password ( str, required ): - password to verify. - Returns: - valid ( bool ): - True if the password meets validity requirements. - """ - json_data = { - 'accountId': "0x" + keypair.public_key.hex() if keypair.public_key != None else None, - 'publicKey': "0x" + keypair.public_key.hex() if keypair.public_key != None else None, - 'secretPhrase': keypair.mnemonic if keypair.mnemonic != None else None, - 'secretSeed': "0x" + \ - # If bytes -> str - ( keypair.seed_hex if isinstance(keypair.seed_hex, str) else keypair.seed_hex.hex() ) - # If None -> None - if keypair.seed_hex != None else None, - 'ss58Address': keypair.ss58_address if keypair.ss58_address != None else None - } - data = json.dumps( json_data ).encode() - return data - -def deserialize_keypair_from_keyfile_data( keyfile_data:bytes ) -> 'bittensor.Keypair': - """ Deserializes Keypair object from passed keyfile data. - Args: - keyfile_data ( bytest, required ): - Keyfile data as bytes to be loaded. - Returns: - keypair (bittensor.Keypair): - Keypair loaded from bytes. - Raises: - KeyFileError: - Raised if the passed bytest cannot construct a keypair object. - """ - # Decode from json. - keyfile_data = keyfile_data.decode() - try: - keyfile_dict = dict(json.loads( keyfile_data )) - except: - string_value = str(keyfile_data) - if string_value[:2] == "0x": - string_value = ss58_encode( string_value ) - keyfile_dict = { - 'accountId': None, - 'publicKey': None, - 'secretPhrase': None, - 'secretSeed': None, - 'ss58Address': string_value - } - else: - raise KeyFileError('Keypair could not be created from keyfile data: {}'.format( string_value )) - - if "secretSeed" in keyfile_dict and keyfile_dict['secretSeed'] != None: - return bittensor.Keypair.create_from_seed(keyfile_dict['secretSeed']) - - if "secretPhrase" in keyfile_dict and keyfile_dict['secretPhrase'] != None: - return bittensor.Keypair.create_from_mnemonic(mnemonic=keyfile_dict['secretPhrase']) - - if "ss58Address" in keyfile_dict and keyfile_dict['ss58Address'] != None: - return bittensor.Keypair( ss58_address = keyfile_dict['ss58Address'] ) - - else: - raise KeyFileError('Keypair could not be created from keyfile data: {}'.format( keyfile_dict )) - -def validate_password( password:str ) -> bool: - """ Validates the password again a password policy. - Args: - password ( str, required ): - password to verify. - Returns: - valid ( bool ): - True if the password meets validity requirements. - """ - policy = PasswordPolicy.from_names( - strength=0.20, - entropybits=10, - length=6, - ) - if not password: - return False - tested_pass = policy.password(password) - result = tested_pass.test() - if len(result) > 0: - print(colored('Password not strong enough. Try increasing the length of the password or the password complexity')) - return False - password_verification = getpass.getpass("Retype your password: ") - if password != password_verification: - print("Passwords do not match") - return False - return True - -def ask_password_to_encrypt() -> str: - """ Password from user prompt. - Returns: - password (str): - Valid password from user prompt. - """ - valid = False - while not valid: - password = getpass.getpass("Specify password for key encryption: ") - valid = validate_password(password) - return password - -def keyfile_data_is_encrypted_ansible( keyfile_data:bytes ) -> bool: - """ Returns true if the keyfile data is ansible encrypted. - Args: - keyfile_data ( bytes, required ): - Bytes to validate - Returns: - is_ansible (bool): - True if data is ansible encrypted. - """ - return keyfile_data[:14] == b'$ANSIBLE_VAULT' - -def keyfile_data_is_encrypted_legacy( keyfile_data:bytes ) -> bool: - """ Returns true if the keyfile data is legacy encrypted. - Args: - keyfile_data ( bytes, required ): - Bytes to validate - Returns: - is_legacy (bool): - True if data is legacy encrypted. - """ - return keyfile_data[:6] == b"gAAAAA" - -def keyfile_data_is_encrypted( keyfile_data:bytes ) -> bool: - """ Returns true if the keyfile data is encrypted. - Args: - keyfile_data ( bytes, required ): - Bytes to validate - Returns: - is_encrypted (bool): - True if data is encrypted. - """ - return keyfile_data_is_encrypted_ansible( keyfile_data ) or keyfile_data_is_encrypted_legacy( keyfile_data ) - -def encrypt_keyfile_data ( keyfile_data:bytes, password: str = None ) -> bytes: - """ Encrypts passed keyfile data using ansible vault. - Args: - keyfile_data ( bytes, required ): - Bytes to validate - password ( bool, optional ): - It set, uses this password to encrypt data. - Returns: - encrytped_data (bytes): - Ansible encrypted data. - """ - password = ask_password_to_encrypt() if password == None else password - console = bittensor.__console__; - with console.status(":locked_with_key: Encrypting key..."): - vault = Vault( password ) - return vault.vault.encrypt ( keyfile_data ) - - -def get_coldkey_password_from_environment(coldkey_name: str) -> Optional[str]: - - for env_var in os.environ: - if ( - env_var.upper().startswith("BT_COLD_PW_") - and env_var.upper().endswith(coldkey_name.upper()) - ): - return os.getenv(env_var) - - return None - - -def decrypt_keyfile_data(keyfile_data: bytes, password: str = None, coldkey_name: Optional[str] = None) -> bytes: - """ Decrypts passed keyfile data using ansible vault. - Args: - keyfile_data ( bytes, required ): - Bytes to validate - password ( bool, optional ): - It set, uses this password to decrypt data. - Returns: - decrypted_data (bytes): - Decrypted data. - Raises: - KeyFileError: - Raised if the file is corrupted or if the password is incorrect. - """ - if coldkey_name is not None and password is None: - password = get_coldkey_password_from_environment(coldkey_name) - - try: - password = getpass.getpass("Enter password to unlock key: ") if password is None else password - console = bittensor.__console__; - with console.status(":key: Decrypting key..."): - # Ansible decrypt. - if keyfile_data_is_encrypted_ansible( keyfile_data ): - vault = Vault( password ) - try: - decrypted_keyfile_data = vault.load( keyfile_data ) - except AnsibleVaultError: - raise KeyFileError('Invalid password') - # Legacy decrypt. - elif keyfile_data_is_encrypted_legacy( keyfile_data ): - __SALT = b"Iguesscyborgslikemyselfhaveatendencytobeparanoidaboutourorigins" - kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), salt=__SALT, length=32, iterations=10000000, backend=default_backend()) - key = base64.urlsafe_b64encode(kdf.derive(password.encode())) - cipher_suite = Fernet(key) - decrypted_keyfile_data = cipher_suite.decrypt( keyfile_data ) - # Unknown. - else: - raise KeyFileError( "Keyfile data: {} is corrupt".format( keyfile_data )) - - except (InvalidSignature, InvalidKey, InvalidToken): - raise KeyFileError('Invalid password') - - if not isinstance(decrypted_keyfile_data, bytes): - decrypted_keyfile_data = json.dumps( decrypted_keyfile_data ).encode() - return decrypted_keyfile_data - -class Keyfile( object ): - """ Defines an interface for a subtrate interface keypair stored on device. - """ - def __init__( self, path: str ): - self.path = os.path.expanduser(path) - self.name = Path(self.path).parent.stem - - def __str__(self): - if not self.exists_on_device(): - return "Keyfile (empty, {})>".format( self.path ) - if self.is_encrypted(): - return "Keyfile (encrypted, {})>".format( self.path ) - else: - return "Keyfile (decrypted, {})>".format( self.path ) - - def __repr__(self): - return self.__str__() - - @property - def keypair( self ) -> 'bittensor.Keypair': - """ Returns the keypair from path, decrypts data if the file is encrypted. - Args: - password ( str, optional ): - Optional password used to decrypt file. If None, asks for user input. - Returns: - keypair (bittensor.Keypair): - Keypair stored under path. - Raises: - KeyFileError: - Raised if the file does not exists, is not readable, writable - corrupted, or if the password is incorrect. - """ - return self.get_keypair() - - @property - def data( self ) -> bytes: - """ Returns keyfile data under path. - Returns: - keyfile_data (bytes): - Keyfile data stored under path. - Raises: - KeyFileError: - Raised if the file does not exists, is not readable, or writable. - """ - return self._read_keyfile_data_from_file() - - @property - def keyfile_data( self ) -> bytes: - """ Returns keyfile data under path. - Returns: - keyfile_data (bytes): - Keyfile data stored under path. - Raises: - KeyFileError: - Raised if the file does not exists, is not readable, or writable. - """ - return self._read_keyfile_data_from_file() - - def set_keypair ( self, keypair: 'bittensor.Keypair', encrypt: bool = True, overwrite: bool = False, password:str = None): - """ Writes the keypair to the file and optional encrypts data. - Args: - keypair (bittensor.Keypair): - Keypair to store under path. - encrypt ( bool, optional, default = True ): - If True, encrypts file under path. - overwrite ( bool, optional, default = True ): - If True, forces overwrite of current file. - password ( str, optional ): - Optional password used to encrypt file. If None, asks for user input. - Raises: - KeyFileError: - Raised if the file does not exists, is not readable, or writable. - """ - self.make_dirs() - keyfile_data = serialized_keypair_to_keyfile_data( keypair ) - if encrypt: - keyfile_data = encrypt_keyfile_data( keyfile_data, password ) - self._write_keyfile_data_to_file( keyfile_data, overwrite = overwrite ) - - def get_keypair(self, password: str = None) -> 'bittensor.Keypair': - """ Returns the keypair from path, decrypts data if the file is encrypted. - Args: - password ( str, optional ): - Optional password used to decrypt file. If None, asks for user input. - Returns: - keypair (bittensor.Keypair): - Keypair stored under path. - Raises: - KeyFileError: - Raised if the file does not exists, is not readable, writable - corrupted, or if the password is incorrect. - """ - keyfile_data = self._read_keyfile_data_from_file() - if keyfile_data_is_encrypted( keyfile_data ): - keyfile_data = decrypt_keyfile_data(keyfile_data, password, coldkey_name=self.name) - return deserialize_keypair_from_keyfile_data( keyfile_data ) - - def make_dirs( self ): - """ Makes directories for path. - """ - directory = os.path.dirname( self.path ) - if not os.path.exists( directory ): - os.makedirs( directory ) - - def exists_on_device( self ) -> bool: - """ Returns true if the file exists on the device. - Returns: - on_device (bool): - True if the file is on device. - """ - if not os.path.isfile( self.path ): - return False - return True - - def is_readable( self ) -> bool: - """ Returns true if the file under path is readable. - Returns: - readable (bool): - True if the file is readable. - """ - if not self.exists_on_device(): - return False - if not os.access( self.path , os.R_OK ): - return False - return True - - def is_writable( self ) -> bool: - """ Returns true if the file under path is writable. - Returns: - writable (bool): - True if the file is writable. - """ - if os.access(self.path, os.W_OK): - return True - return False - - def is_encrypted ( self ) -> bool: - """ Returns true if the file under path is encrypted. - Returns: - encrypted (bool): - True if the file is encrypted. - """ - if not self.exists_on_device(): - return False - if not self.is_readable(): - return False - return keyfile_data_is_encrypted( self._read_keyfile_data_from_file() ) - - def _may_overwrite ( self ) -> bool: - choice = input("File {} already exists. Overwrite ? (y/N) ".format( self.path )) - return choice == 'y' - - def encrypt( self, password: str = None): - """ Encrypts file under path. - Args: - password: (str, optional): - Optional password for encryption. Otherwise asks for user input. - Raises: - KeyFileError: - Raised if the file does not exists, is not readable, writable. - """ - if not self.exists_on_device(): - raise KeyFileError( "Keyfile at: {} is not a file".format( self.path )) - if not self.is_readable(): - raise KeyFileError( "Keyfile at: {} is not readable".format( self.path )) - if not self.is_writable(): - raise KeyFileError( "Keyfile at: {} is not writeable".format( self.path ) ) - keyfile_data = self._read_keyfile_data_from_file() - if not keyfile_data_is_encrypted( keyfile_data ): - as_keypair = deserialize_keypair_from_keyfile_data( keyfile_data ) - keyfile_data = serialized_keypair_to_keyfile_data( as_keypair ) - keyfile_data = encrypt_keyfile_data( keyfile_data, password ) - self._write_keyfile_data_to_file( keyfile_data, overwrite = True ) - - def decrypt( self, password: str = None): - """ Decrypts file under path. - Args: - password: (str, optional): - Optional password for decryption. Otherwise asks for user input. - Raises: - KeyFileError: - Raised if the file does not exists, is not readable, writable - corrupted, or if the password is incorrect. - """ - if not self.exists_on_device(): - raise KeyFileError( "Keyfile at: {} is not a file".format( self.path )) - if not self.is_readable(): - raise KeyFileError( "Keyfile at: {} is not readable".format( self.path )) - if not self.is_writable(): - raise KeyFileError( "No write access for {}".format( self.path ) ) - keyfile_data = self._read_keyfile_data_from_file() - if keyfile_data_is_encrypted( keyfile_data ): - keyfile_data = decrypt_keyfile_data(keyfile_data, password, coldkey_name=self.name) - as_keypair = deserialize_keypair_from_keyfile_data( keyfile_data ) - keyfile_data = serialized_keypair_to_keyfile_data( as_keypair ) - self._write_keyfile_data_to_file( keyfile_data, overwrite = True ) - - def _read_keyfile_data_from_file ( self ) -> bytes: - """ Reads keyfile data from path. - Returns: - keyfile_data: (bytes, required): - Keyfile data sotred under path. - Raises: - KeyFileError: - Raised if the file does not exists or is not readable. - """ - if not self.exists_on_device(): - raise KeyFileError( "Keyfile at: {} is not a file".format( self.path )) - if not self.is_readable(): - raise KeyFileError( "Keyfile at: {} is not readable".format( self.path )) - with open( self.path , 'rb') as file: - data = file.read() - return data - - def _write_keyfile_data_to_file ( self, keyfile_data:bytes, overwrite: bool = False ): - """ Writes the keyfile data to path, if overwrite is true, forces operation without asking. - Args: - keyfile_data: (bytes, required): - Byte data to store under path. - overwrite (bool, optional): - If True, overwrites data without asking for overwrite permissions from the user. - Raises: - KeyFileError: - Raised if the file is not writable or the user returns No to overwrite prompt. - """ - # Check overwrite. - if self.exists_on_device() and not overwrite: - if not self._may_overwrite(): - raise KeyFileError( "Keyfile at: {} is not writeable".format( self.path ) ) - with open(self.path, "wb") as keyfile: - keyfile.write( keyfile_data ) - # Set file permissions. - os.chmod(self.path, stat.S_IRUSR | stat.S_IWUSR) - - -class MockKeyfile( object ): - """ Defines an interface to a mocked keyfile object (nothing is created on device) keypair is treated as non encrypted and the data is just the string version. - """ - def __init__( self, path: str ): - self.path = os.path.expanduser(path) - self._mock_keypair = bittensor.Keypair.create_from_mnemonic( mnemonic = 'arrive produce someone view end scout bargain coil slight festival excess struggle' ) - self._mock_data = serialized_keypair_to_keyfile_data( self._mock_keypair ) - - def __str__(self): - if not self.exists_on_device(): - return "Keyfile (empty, {})>".format( self.path ) - if self.is_encrypted(): - return "Keyfile (encrypted, {})>".format( self.path ) - else: - return "Keyfile (decrypted, {})>".format( self.path ) - - def __repr__(self): - return self.__str__() - - @property - def keypair( self ) -> 'bittensor.Keypair': - return self._mock_keypair - - @property - def data( self ) -> bytes: - return bytes(self._mock_data) - - @property - def keyfile_data( self ) -> bytes: - return bytes( self._mock_data) - - def set_keypair ( self, keypair: 'bittensor.Keypair', encrypt: bool = True, overwrite: bool = False, password:str = None): - self._mock_keypair = keypair - self._mock_data = serialized_keypair_to_keyfile_data( self._mock_keypair ) - - def get_keypair(self, password: str = None) -> 'bittensor.Keypair': - return self._mock_keypair - - def make_dirs( self ): - return - - def exists_on_device( self ) -> bool: - return True - - def is_readable( self ) -> bool: - return True - - def is_writable( self ) -> bool: - return True - - def is_encrypted ( self ) -> bool: - return False - - def encrypt( self, password: str = None): - raise ValueError('Cannot encrypt a mock keyfile') - - def decrypt( self, password: str = None): - return - - - - - - - - - - diff --git a/bittensor/_logging/__init__.py b/bittensor/_logging/__init__.py index 9ae0b35b02..f6cfe7e80d 100644 --- a/bittensor/_logging/__init__.py +++ b/bittensor/_logging/__init__.py @@ -144,7 +144,7 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None ): """ prefix_str = '' if prefix == None else prefix + '.' if prefix is not None: - if not hasattr(bittensor.defaults, prefix): + if bittensor.defaults.get(prefix, d=None) == None: setattr(bittensor.defaults, prefix, bittensor.Config()) getattr(bittensor.defaults, prefix).logging = bittensor.defaults.logging try: diff --git a/bittensor/_metagraph/__init__.py b/bittensor/_metagraph/__init__.py index f66632e9e4..a6440eaa20 100644 --- a/bittensor/_metagraph/__init__.py +++ b/bittensor/_metagraph/__init__.py @@ -114,7 +114,8 @@ def sync ( self, block: Optional[int] = None, lite: bool = True, subtensor: Opti if lite: self.neurons = subtensor.neurons_lite( block = block, netuid = self.netuid ) else: - self.neurons = subtensor.neurons(block = block, netuid = self.netuid ) + self.neurons = subtensor.neurons( block = block, netuid = self.netuid ) + self.lite = lite self.n = torch.nn.Parameter( torch.tensor( len(self.neurons), dtype=torch.int64 ), requires_grad=False ) self.version = torch.nn.Parameter( torch.tensor( [bittensor.__version_as_int__], dtype=torch.int64 ), requires_grad=False ) diff --git a/bittensor/_neuron/base_miner_neuron.py b/bittensor/_neuron/base_miner_neuron.py index a2d25862ed..b2ff1f7f67 100644 --- a/bittensor/_neuron/base_miner_neuron.py +++ b/bittensor/_neuron/base_miner_neuron.py @@ -87,6 +87,12 @@ def add_args( cls, parser: argparse.ArgumentParser, prefix: str = None ): help = 'If True, the model does not set weights.', default = False ) + parser.add_argument( + '--' + prefix_str + 'neuron.reregister', + action = 'store_true', + help = 'If True, the miner will reregister on chain.', + default = False + ) bittensor.wallet.add_args( parser, prefix = prefix ) bittensor.axon.add_args( parser, prefix = prefix ) bittensor.subtensor.add_args( parser, prefix = prefix ) @@ -106,7 +112,9 @@ def __init__(self, netuid: int = None, config: "bittensor.Config" = None ): bittensor.logging( config = self.config, logging_dir = self.config.neuron.full_path ) self.subtensor = bittensor.subtensor( self.config ) self.wallet = bittensor.wallet( self.config ) - self.metagraph = self.subtensor.metagraph( self.config.netuid ) + self.metagraph = self.subtensor.metagraph( netuid = self.config.netuid ) + self.metagraph.sync( lite = True, subtensor=self.subtensor ) + self.axon = bittensor.axon( wallet = self.wallet, config = self.config ) self.blacklister = bittensor.blacklist( config = self.config.neuron ) self.prioritizer = bittensor.priority( config = self.config.neuron ) @@ -150,7 +158,7 @@ def run( self ): # --- Start the miner. self.is_running = True - self.wallet.reregister( netuid = self.config.netuid, subtensor = self.subtensor ) + bittensor.utils.reregister( wallet = self.wallet, subtensor = self.subtensor, netuid = self.config.netuid, reregister = self.config.neuron.reregister ) self.axon.start() self.subtensor.serve_axon( netuid = self.config.netuid, axon = self.axon, wait_for_finalization = False, wait_for_inclusion = False ) #TODO: fix finalization & inclusion @@ -169,7 +177,7 @@ def run( self ): # --- Update the metagraph with the latest network state. try: - self.metagraph.sync( lite = True ) + self.metagraph.sync( lite = True, subtensor=self.subtensor ) uid = self.metagraph.hotkeys.index( self.wallet.hotkey.ss58_address ) except: # --- If we fail to sync the metagraph, wait and try again. diff --git a/bittensor/_neuron/base_validator.py b/bittensor/_neuron/base_validator.py deleted file mode 100644 index 1542bb450f..0000000000 --- a/bittensor/_neuron/base_validator.py +++ /dev/null @@ -1,167 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2023 Yuma Rao - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -import os -import time -import torch -import threading -import argparse -import bittensor - -from rich import print -from typing import List, Dict, Union, Tuple, Optional -from datetime import datetime - -class BaseValidator: - - @classmethod - def config( cls ) -> "bittensor.Config": - parser = argparse.ArgumentParser() - cls.add_args( parser ) - return bittensor.config( parser ) - - @classmethod - def help( cls ): - parser = argparse.ArgumentParser() - cls.add_args(parser) - print( cls.__new__.__doc__ ) - parser.print_help() - - @classmethod - def check_config( cls, config: "bittensor.Config" ): - bittensor.wallet.check_config( config ) - bittensor.logging.check_config( config ) - bittensor.subtensor.check_config( config ) - full_path = os.path.expanduser( - '{}/{}/{}/{}'.format( config.logging.logging_dir, config.wallet.get('name', bittensor.defaults.wallet.name), - config.wallet.get('hotkey', bittensor.defaults.wallet.hotkey), config.neuron.name ) ) - config.neuron.full_path = os.path.expanduser( full_path ) - if not os.path.exists( config.neuron.full_path ): - os.makedirs( config.neuron.full_path ) - - @classmethod - def add_args( cls, parser: argparse.ArgumentParser, prefix: str = None ): - prefix_str = "" if prefix is None else prefix + "." - parser.add_argument( - '--' + prefix_str + 'netuid', - type = int, - help = 'Subnet netuid', - default = 1 - ) - parser.add_argument( - '--' + prefix_str + 'neuron.name', - type = str, - help = 'Trials for this miner go in miner.root / (wallet_cold - wallet_hot) / miner.name ', - default = 'openai_prompting_miner' - ) - parser.add_argument( - '--' + prefix_str + 'neuron.blocks_per_epoch', - type = str, - help = 'Blocks until the miner sets weights on chain', - default = 100 - ) - parser.add_argument( - '--' + prefix_str + 'neuron.no_set_weights', - action = 'store_true', - help = 'If True, the model does not set weights.', - default = False - ) - bittensor.wallet.add_args( parser, prefix = prefix ) - bittensor.subtensor.add_args( parser, prefix = prefix ) - bittensor.logging.add_args( parser, prefix = prefix ) - - def __init__(self, netuid: int = None, config: "bittensor.Config" = None ): - # Build config. - self.config = config if config != None else BaseValidator.config() - self.config.netuid = netuid or self.config.netuid - BaseValidator.check_config( self.config ) - - # Build objects. - bittensor.logging( config = self.config, logging_dir = self.config.neuron.full_path ) - self.subtensor = bittensor.subtensor( self.config ) - self.wallet = bittensor.wallet( self.config ) - self.metagraph = self.subtensor.metagraph( self.config.netuid ) - - # Used for backgounr process. - self.is_running = False - self.should_exit = False - self.background_thread = None - - def __enter__(self): - bittensor.logging.trace( 'BaseValidator.__enter__()' ) - self.start_in_background() - return self - - def __exit__(self, exc_type, exc_value, traceback): - bittensor.logging.trace( 'BaseValidator.__exit__()' ) - self.stop() - - def start_in_background(self): - if self.is_running: - bittensor.logging.warning( 'The base miner neuron is already running.') - else: - self.should_exit = False - self.background_thread = threading.Thread( target = self.run, daemon = True ) - self.background_thread.start() - self.is_running = True - bittensor.logging.trace( 'Starting the base miner neuron in the background.') - - def stop(self): - if self.is_running: - self.should_exit = True - else: - bittensor.logging.warning( 'The base miner neuron is not running.') - - def run( self ): - bittensor.logging.debug( 'BaseMinBaseValidatorerNeuron.run()' ) - - # --- Start the miner. - self.is_running = True - self.wallet.reregister( netuid = self.config.netuid, subtensor = self.subtensor ) - - # --- Run Forever. - last_update = self.subtensor.get_current_block() - while not self.should_exit: - - # --- Wait until next epoch. - current_block = self.subtensor.get_current_block() - while (current_block - last_update) < self.config.neuron.blocks_per_epoch: - if self.should_exit: continue - time.sleep( 12 ) - current_block = self.subtensor.get_current_block() - last_update = self.subtensor.get_current_block() - - # --- Update the metagraph with the latest network state. - self.metagraph.sync( lite = True ) - uid = self.metagraph.hotkeys.index( self.wallet.hotkey.ss58_address ) - - # --- Set weights. - if not self.config.neuron.no_set_weights: - try: - # --- query the chain for the most current number of peers on the network - chain_weights = torch.zeros( self.subtensor.subnetwork_n( netuid = self.config.netuid )) - chain_weights[uid] = 1 - did_set = self.subtensor.set_weights( - uids = torch.arange(0, len(chain_weights)), - netuid = self.config.netuid, - weights = chain_weights, - wait_for_inclusion = False, - walle = self.wallet, - version_key = 1 - ) - except: - pass \ No newline at end of file diff --git a/bittensor/_prometheus/__init__.py b/bittensor/_prometheus/__init__.py index 9d22c7a573..b84c8b920e 100644 --- a/bittensor/_prometheus/__init__.py +++ b/bittensor/_prometheus/__init__.py @@ -150,7 +150,7 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None ): """ prefix_str = '' if prefix == None else prefix + '.' if prefix is not None: - if not hasattr(bittensor.defaults, prefix): + if bittensor.defaults.get(prefix, d=None) == None: setattr(bittensor.defaults, prefix, bittensor.Config()) getattr(bittensor.defaults, prefix).prometheus = bittensor.defaults.prometheus try: diff --git a/bittensor/_proto/bittensor.proto b/bittensor/_proto/bittensor.proto index 597f5e26d2..1bd758de79 100644 --- a/bittensor/_proto/bittensor.proto +++ b/bittensor/_proto/bittensor.proto @@ -3,7 +3,6 @@ syntax = "proto3"; service TextPrompting { rpc Forward (ForwardTextPromptingRequest) returns (ForwardTextPromptingResponse) {} - rpc MultiForward (MultiForwardTextPromptingRequest) returns (MultiForwardTextPromptingResponse) {} rpc Backward (BackwardTextPromptingRequest) returns (BackwardTextPromptingResponse) {} } @@ -12,33 +11,17 @@ service TextPrompting { ///////////////////////// message ForwardTextPromptingRequest { int32 version = 1; - string hotkey = 2; repeated string messages = 3; float timeout = 4; } message ForwardTextPromptingResponse { int32 version = 1; - string hotkey = 2; string response = 3; string return_message = 4; ReturnCode return_code = 5; } -message MultiForwardTextPromptingRequest { - int32 version = 1; - string hotkey = 2; - repeated string messages = 3; - float timeout = 4; -} -message MultiForwardTextPromptingResponse { - int32 version = 1; - string hotkey = 2; - repeated string multi_completions = 3; - string return_message = 4; - ReturnCode return_code = 5; -} message BackwardTextPromptingRequest { int32 version = 1; - string hotkey = 2; repeated float rewards = 3; repeated string messages = 4; string response = 5; @@ -46,7 +29,6 @@ message BackwardTextPromptingRequest { } message BackwardTextPromptingResponse { int32 version = 1; - string hotkey = 2; string return_message = 4; ReturnCode return_code = 5; } diff --git a/bittensor/_proto/bittensor_pb2.py b/bittensor/_proto/bittensor_pb2.py index 2cc1267e25..0a98cab4c9 100644 --- a/bittensor/_proto/bittensor_pb2.py +++ b/bittensor/_proto/bittensor_pb2.py @@ -20,7 +20,7 @@ syntax='proto3', serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_pb=b'\n bittensor/_proto/bittensor.proto\"a\n\x1b\x46orwardTextPromptingRequest\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x0e\n\x06hotkey\x18\x02 \x01(\t\x12\x10\n\x08messages\x18\x03 \x03(\t\x12\x0f\n\x07timeout\x18\x04 \x01(\x02\"\x8b\x01\n\x1c\x46orwardTextPromptingResponse\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x0e\n\x06hotkey\x18\x02 \x01(\t\x12\x10\n\x08response\x18\x03 \x01(\t\x12\x16\n\x0ereturn_message\x18\x04 \x01(\t\x12 \n\x0breturn_code\x18\x05 \x01(\x0e\x32\x0b.ReturnCode\"f\n MultiForwardTextPromptingRequest\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x0e\n\x06hotkey\x18\x02 \x01(\t\x12\x10\n\x08messages\x18\x03 \x03(\t\x12\x0f\n\x07timeout\x18\x04 \x01(\x02\"\x99\x01\n!MultiForwardTextPromptingResponse\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x0e\n\x06hotkey\x18\x02 \x01(\t\x12\x19\n\x11multi_completions\x18\x03 \x03(\t\x12\x16\n\x0ereturn_message\x18\x04 \x01(\t\x12 \n\x0breturn_code\x18\x05 \x01(\x0e\x32\x0b.ReturnCode\"\x85\x01\n\x1c\x42\x61\x63kwardTextPromptingRequest\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x0e\n\x06hotkey\x18\x02 \x01(\t\x12\x0f\n\x07rewards\x18\x03 \x03(\x02\x12\x10\n\x08messages\x18\x04 \x03(\t\x12\x10\n\x08response\x18\x05 \x01(\t\x12\x0f\n\x07timeout\x18\x06 \x01(\x02\"z\n\x1d\x42\x61\x63kwardTextPromptingResponse\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x0e\n\x06hotkey\x18\x02 \x01(\t\x12\x16\n\x0ereturn_message\x18\x04 \x01(\t\x12 \n\x0breturn_code\x18\x05 \x01(\x0e\x32\x0b.ReturnCode\"\xac\x01\n\x06Tensor\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x0e\n\x06\x62uffer\x18\x02 \x01(\x0c\x12\r\n\x05shape\x18\x03 \x03(\x03\x12\x1f\n\nserializer\x18\x04 \x01(\x0e\x32\x0b.Serializer\x12 \n\x0btensor_type\x18\x05 \x01(\x0e\x32\x0b.TensorType\x12\x18\n\x05\x64type\x18\x06 \x01(\x0e\x32\t.DataType\x12\x15\n\rrequires_grad\x18\x08 \x01(\x08*\xda\x04\n\nReturnCode\x12\x0c\n\x08NoReturn\x10\x00\x12\x0b\n\x07Success\x10\x01\x12\x0b\n\x07Timeout\x10\x02\x12\x0b\n\x07\x42\x61\x63koff\x10\x03\x12\x0f\n\x0bUnavailable\x10\x04\x12\x12\n\x0eNotImplemented\x10\x05\x12\x10\n\x0c\x45mptyRequest\x10\x06\x12\x11\n\rEmptyResponse\x10\x07\x12\x13\n\x0fInvalidResponse\x10\x08\x12\x12\n\x0eInvalidRequest\x10\t\x12\x19\n\x15RequestShapeException\x10\n\x12\x1a\n\x16ResponseShapeException\x10\x0b\x12!\n\x1dRequestSerializationException\x10\x0c\x12\"\n\x1eResponseSerializationException\x10\r\x12#\n\x1fRequestDeserializationException\x10\x0e\x12$\n ResponseDeserializationException\x10\x0f\x12\x15\n\x11NotServingNucleus\x10\x10\x12\x12\n\x0eNucleusTimeout\x10\x11\x12\x0f\n\x0bNucleusFull\x10\x12\x12\x1e\n\x1aRequestIncompatibleVersion\x10\x13\x12\x1f\n\x1bResponseIncompatibleVersion\x10\x14\x12\x11\n\rSenderUnknown\x10\x15\x12\x14\n\x10UnknownException\x10\x16\x12\x13\n\x0fUnauthenticated\x10\x17\x12\x0f\n\x0b\x42\x61\x64\x45ndpoint\x10\x18\x12\x0f\n\x0b\x42lacklisted\x10\x19*&\n\nSerializer\x12\x0b\n\x07MSGPACK\x10\x00\x12\x0b\n\x07\x43MPPACK\x10\x01*2\n\nTensorType\x12\t\n\x05TORCH\x10\x00\x12\x0e\n\nTENSORFLOW\x10\x01\x12\t\n\x05NUMPY\x10\x02*h\n\x08\x44\x61taType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07\x46LOAT32\x10\x01\x12\x0b\n\x07\x46LOAT64\x10\x02\x12\t\n\x05INT32\x10\x03\x12\t\n\x05INT64\x10\x04\x12\x08\n\x04UTF8\x10\x05\x12\x0b\n\x07\x46LOAT16\x10\x06\x12\x08\n\x04\x42OOL\x10\x07\x32\xff\x01\n\rTextPrompting\x12H\n\x07\x46orward\x12\x1c.ForwardTextPromptingRequest\x1a\x1d.ForwardTextPromptingResponse\"\x00\x12W\n\x0cMultiForward\x12!.MultiForwardTextPromptingRequest\x1a\".MultiForwardTextPromptingResponse\"\x00\x12K\n\x08\x42\x61\x63kward\x12\x1d.BackwardTextPromptingRequest\x1a\x1e.BackwardTextPromptingResponse\"\x00\x62\x06proto3' + serialized_pb=b'\n bittensor/_proto/bittensor.proto\"Q\n\x1b\x46orwardTextPromptingRequest\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x10\n\x08messages\x18\x03 \x03(\t\x12\x0f\n\x07timeout\x18\x04 \x01(\x02\"{\n\x1c\x46orwardTextPromptingResponse\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x10\n\x08response\x18\x03 \x01(\t\x12\x16\n\x0ereturn_message\x18\x04 \x01(\t\x12 \n\x0breturn_code\x18\x05 \x01(\x0e\x32\x0b.ReturnCode\"u\n\x1c\x42\x61\x63kwardTextPromptingRequest\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x0f\n\x07rewards\x18\x03 \x03(\x02\x12\x10\n\x08messages\x18\x04 \x03(\t\x12\x10\n\x08response\x18\x05 \x01(\t\x12\x0f\n\x07timeout\x18\x06 \x01(\x02\"j\n\x1d\x42\x61\x63kwardTextPromptingResponse\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x16\n\x0ereturn_message\x18\x04 \x01(\t\x12 \n\x0breturn_code\x18\x05 \x01(\x0e\x32\x0b.ReturnCode\"\xac\x01\n\x06Tensor\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x0e\n\x06\x62uffer\x18\x02 \x01(\x0c\x12\r\n\x05shape\x18\x03 \x03(\x03\x12\x1f\n\nserializer\x18\x04 \x01(\x0e\x32\x0b.Serializer\x12 \n\x0btensor_type\x18\x05 \x01(\x0e\x32\x0b.TensorType\x12\x18\n\x05\x64type\x18\x06 \x01(\x0e\x32\t.DataType\x12\x15\n\rrequires_grad\x18\x08 \x01(\x08*\xda\x04\n\nReturnCode\x12\x0c\n\x08NoReturn\x10\x00\x12\x0b\n\x07Success\x10\x01\x12\x0b\n\x07Timeout\x10\x02\x12\x0b\n\x07\x42\x61\x63koff\x10\x03\x12\x0f\n\x0bUnavailable\x10\x04\x12\x12\n\x0eNotImplemented\x10\x05\x12\x10\n\x0c\x45mptyRequest\x10\x06\x12\x11\n\rEmptyResponse\x10\x07\x12\x13\n\x0fInvalidResponse\x10\x08\x12\x12\n\x0eInvalidRequest\x10\t\x12\x19\n\x15RequestShapeException\x10\n\x12\x1a\n\x16ResponseShapeException\x10\x0b\x12!\n\x1dRequestSerializationException\x10\x0c\x12\"\n\x1eResponseSerializationException\x10\r\x12#\n\x1fRequestDeserializationException\x10\x0e\x12$\n ResponseDeserializationException\x10\x0f\x12\x15\n\x11NotServingNucleus\x10\x10\x12\x12\n\x0eNucleusTimeout\x10\x11\x12\x0f\n\x0bNucleusFull\x10\x12\x12\x1e\n\x1aRequestIncompatibleVersion\x10\x13\x12\x1f\n\x1bResponseIncompatibleVersion\x10\x14\x12\x11\n\rSenderUnknown\x10\x15\x12\x14\n\x10UnknownException\x10\x16\x12\x13\n\x0fUnauthenticated\x10\x17\x12\x0f\n\x0b\x42\x61\x64\x45ndpoint\x10\x18\x12\x0f\n\x0b\x42lacklisted\x10\x19*&\n\nSerializer\x12\x0b\n\x07MSGPACK\x10\x00\x12\x0b\n\x07\x43MPPACK\x10\x01*2\n\nTensorType\x12\t\n\x05TORCH\x10\x00\x12\x0e\n\nTENSORFLOW\x10\x01\x12\t\n\x05NUMPY\x10\x02*h\n\x08\x44\x61taType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07\x46LOAT32\x10\x01\x12\x0b\n\x07\x46LOAT64\x10\x02\x12\t\n\x05INT32\x10\x03\x12\t\n\x05INT64\x10\x04\x12\x08\n\x04UTF8\x10\x05\x12\x0b\n\x07\x46LOAT16\x10\x06\x12\x08\n\x04\x42OOL\x10\x07\x32\xa6\x01\n\rTextPrompting\x12H\n\x07\x46orward\x12\x1c.ForwardTextPromptingRequest\x1a\x1d.ForwardTextPromptingResponse\"\x00\x12K\n\x08\x42\x61\x63kward\x12\x1d.BackwardTextPromptingRequest\x1a\x1e.BackwardTextPromptingResponse\"\x00\x62\x06proto3' ) _RETURNCODE = _descriptor.EnumDescriptor( @@ -163,8 +163,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=973, - serialized_end=1575, + serialized_start=647, + serialized_end=1249, ) _sym_db.RegisterEnumDescriptor(_RETURNCODE) @@ -189,8 +189,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=1577, - serialized_end=1615, + serialized_start=1251, + serialized_end=1289, ) _sym_db.RegisterEnumDescriptor(_SERIALIZER) @@ -220,8 +220,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=1617, - serialized_end=1667, + serialized_start=1291, + serialized_end=1341, ) _sym_db.RegisterEnumDescriptor(_TENSORTYPE) @@ -276,8 +276,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=1669, - serialized_end=1773, + serialized_start=1343, + serialized_end=1447, ) _sym_db.RegisterEnumDescriptor(_DATATYPE) @@ -340,21 +340,14 @@ is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='hotkey', full_name='ForwardTextPromptingRequest.hotkey', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='messages', full_name='ForwardTextPromptingRequest.messages', index=2, + name='messages', full_name='ForwardTextPromptingRequest.messages', index=1, number=3, type=9, cpp_type=9, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='timeout', full_name='ForwardTextPromptingRequest.timeout', index=3, + name='timeout', full_name='ForwardTextPromptingRequest.timeout', index=2, number=4, type=2, cpp_type=6, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, @@ -373,7 +366,7 @@ oneofs=[ ], serialized_start=36, - serialized_end=133, + serialized_end=117, ) @@ -393,28 +386,21 @@ is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='hotkey', full_name='ForwardTextPromptingResponse.hotkey', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='response', full_name='ForwardTextPromptingResponse.response', index=2, + name='response', full_name='ForwardTextPromptingResponse.response', index=1, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='return_message', full_name='ForwardTextPromptingResponse.return_message', index=3, + name='return_message', full_name='ForwardTextPromptingResponse.return_message', index=2, number=4, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='return_code', full_name='ForwardTextPromptingResponse.return_code', index=4, + name='return_code', full_name='ForwardTextPromptingResponse.return_code', index=3, number=5, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, @@ -432,121 +418,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=136, - serialized_end=275, -) - - -_MULTIFORWARDTEXTPROMPTINGREQUEST = _descriptor.Descriptor( - name='MultiForwardTextPromptingRequest', - full_name='MultiForwardTextPromptingRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='version', full_name='MultiForwardTextPromptingRequest.version', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='hotkey', full_name='MultiForwardTextPromptingRequest.hotkey', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='messages', full_name='MultiForwardTextPromptingRequest.messages', index=2, - number=3, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='timeout', full_name='MultiForwardTextPromptingRequest.timeout', index=3, - number=4, type=2, cpp_type=6, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=277, - serialized_end=379, -) - - -_MULTIFORWARDTEXTPROMPTINGRESPONSE = _descriptor.Descriptor( - name='MultiForwardTextPromptingResponse', - full_name='MultiForwardTextPromptingResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='version', full_name='MultiForwardTextPromptingResponse.version', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='hotkey', full_name='MultiForwardTextPromptingResponse.hotkey', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='multi_completions', full_name='MultiForwardTextPromptingResponse.multi_completions', index=2, - number=3, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='return_message', full_name='MultiForwardTextPromptingResponse.return_message', index=3, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='return_code', full_name='MultiForwardTextPromptingResponse.return_code', index=4, - number=5, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=382, - serialized_end=535, + serialized_start=119, + serialized_end=242, ) @@ -566,35 +439,28 @@ is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='hotkey', full_name='BackwardTextPromptingRequest.hotkey', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='rewards', full_name='BackwardTextPromptingRequest.rewards', index=2, + name='rewards', full_name='BackwardTextPromptingRequest.rewards', index=1, number=3, type=2, cpp_type=6, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='messages', full_name='BackwardTextPromptingRequest.messages', index=3, + name='messages', full_name='BackwardTextPromptingRequest.messages', index=2, number=4, type=9, cpp_type=9, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='response', full_name='BackwardTextPromptingRequest.response', index=4, + name='response', full_name='BackwardTextPromptingRequest.response', index=3, number=5, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='timeout', full_name='BackwardTextPromptingRequest.timeout', index=5, + name='timeout', full_name='BackwardTextPromptingRequest.timeout', index=4, number=6, type=2, cpp_type=6, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, @@ -612,8 +478,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=538, - serialized_end=671, + serialized_start=244, + serialized_end=361, ) @@ -633,21 +499,14 @@ is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='hotkey', full_name='BackwardTextPromptingResponse.hotkey', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='return_message', full_name='BackwardTextPromptingResponse.return_message', index=2, + name='return_message', full_name='BackwardTextPromptingResponse.return_message', index=1, number=4, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='return_code', full_name='BackwardTextPromptingResponse.return_code', index=3, + name='return_code', full_name='BackwardTextPromptingResponse.return_code', index=2, number=5, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, @@ -665,8 +524,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=673, - serialized_end=795, + serialized_start=363, + serialized_end=469, ) @@ -739,20 +598,17 @@ extension_ranges=[], oneofs=[ ], - serialized_start=798, - serialized_end=970, + serialized_start=472, + serialized_end=644, ) _FORWARDTEXTPROMPTINGRESPONSE.fields_by_name['return_code'].enum_type = _RETURNCODE -_MULTIFORWARDTEXTPROMPTINGRESPONSE.fields_by_name['return_code'].enum_type = _RETURNCODE _BACKWARDTEXTPROMPTINGRESPONSE.fields_by_name['return_code'].enum_type = _RETURNCODE _TENSOR.fields_by_name['serializer'].enum_type = _SERIALIZER _TENSOR.fields_by_name['tensor_type'].enum_type = _TENSORTYPE _TENSOR.fields_by_name['dtype'].enum_type = _DATATYPE DESCRIPTOR.message_types_by_name['ForwardTextPromptingRequest'] = _FORWARDTEXTPROMPTINGREQUEST DESCRIPTOR.message_types_by_name['ForwardTextPromptingResponse'] = _FORWARDTEXTPROMPTINGRESPONSE -DESCRIPTOR.message_types_by_name['MultiForwardTextPromptingRequest'] = _MULTIFORWARDTEXTPROMPTINGREQUEST -DESCRIPTOR.message_types_by_name['MultiForwardTextPromptingResponse'] = _MULTIFORWARDTEXTPROMPTINGRESPONSE DESCRIPTOR.message_types_by_name['BackwardTextPromptingRequest'] = _BACKWARDTEXTPROMPTINGREQUEST DESCRIPTOR.message_types_by_name['BackwardTextPromptingResponse'] = _BACKWARDTEXTPROMPTINGRESPONSE DESCRIPTOR.message_types_by_name['Tensor'] = _TENSOR @@ -776,20 +632,6 @@ }) _sym_db.RegisterMessage(ForwardTextPromptingResponse) -MultiForwardTextPromptingRequest = _reflection.GeneratedProtocolMessageType('MultiForwardTextPromptingRequest', (_message.Message,), { - 'DESCRIPTOR' : _MULTIFORWARDTEXTPROMPTINGREQUEST, - '__module__' : 'bittensor._proto.bittensor_pb2' - # @@protoc_insertion_point(class_scope:MultiForwardTextPromptingRequest) - }) -_sym_db.RegisterMessage(MultiForwardTextPromptingRequest) - -MultiForwardTextPromptingResponse = _reflection.GeneratedProtocolMessageType('MultiForwardTextPromptingResponse', (_message.Message,), { - 'DESCRIPTOR' : _MULTIFORWARDTEXTPROMPTINGRESPONSE, - '__module__' : 'bittensor._proto.bittensor_pb2' - # @@protoc_insertion_point(class_scope:MultiForwardTextPromptingResponse) - }) -_sym_db.RegisterMessage(MultiForwardTextPromptingResponse) - BackwardTextPromptingRequest = _reflection.GeneratedProtocolMessageType('BackwardTextPromptingRequest', (_message.Message,), { 'DESCRIPTOR' : _BACKWARDTEXTPROMPTINGREQUEST, '__module__' : 'bittensor._proto.bittensor_pb2' @@ -820,8 +662,8 @@ index=0, serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_start=1776, - serialized_end=2031, + serialized_start=1450, + serialized_end=1616, methods=[ _descriptor.MethodDescriptor( name='Forward', @@ -833,20 +675,10 @@ serialized_options=None, create_key=_descriptor._internal_create_key, ), - _descriptor.MethodDescriptor( - name='MultiForward', - full_name='TextPrompting.MultiForward', - index=1, - containing_service=None, - input_type=_MULTIFORWARDTEXTPROMPTINGREQUEST, - output_type=_MULTIFORWARDTEXTPROMPTINGRESPONSE, - serialized_options=None, - create_key=_descriptor._internal_create_key, - ), _descriptor.MethodDescriptor( name='Backward', full_name='TextPrompting.Backward', - index=2, + index=1, containing_service=None, input_type=_BACKWARDTEXTPROMPTINGREQUEST, output_type=_BACKWARDTEXTPROMPTINGRESPONSE, diff --git a/bittensor/_proto/bittensor_pb2_grpc.py b/bittensor/_proto/bittensor_pb2_grpc.py index 4af9882962..e9ea07e3a2 100644 --- a/bittensor/_proto/bittensor_pb2_grpc.py +++ b/bittensor/_proto/bittensor_pb2_grpc.py @@ -19,11 +19,6 @@ def __init__(self, channel): request_serializer=bittensor_dot___proto_dot_bittensor__pb2.ForwardTextPromptingRequest.SerializeToString, response_deserializer=bittensor_dot___proto_dot_bittensor__pb2.ForwardTextPromptingResponse.FromString, ) - self.MultiForward = channel.unary_unary( - '/TextPrompting/MultiForward', - request_serializer=bittensor_dot___proto_dot_bittensor__pb2.MultiForwardTextPromptingRequest.SerializeToString, - response_deserializer=bittensor_dot___proto_dot_bittensor__pb2.MultiForwardTextPromptingResponse.FromString, - ) self.Backward = channel.unary_unary( '/TextPrompting/Backward', request_serializer=bittensor_dot___proto_dot_bittensor__pb2.BackwardTextPromptingRequest.SerializeToString, @@ -40,12 +35,6 @@ def Forward(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') - def MultiForward(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - def Backward(self, request, context): """Missing associated documentation comment in .proto file.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) @@ -60,11 +49,6 @@ def add_TextPromptingServicer_to_server(servicer, server): request_deserializer=bittensor_dot___proto_dot_bittensor__pb2.ForwardTextPromptingRequest.FromString, response_serializer=bittensor_dot___proto_dot_bittensor__pb2.ForwardTextPromptingResponse.SerializeToString, ), - 'MultiForward': grpc.unary_unary_rpc_method_handler( - servicer.MultiForward, - request_deserializer=bittensor_dot___proto_dot_bittensor__pb2.MultiForwardTextPromptingRequest.FromString, - response_serializer=bittensor_dot___proto_dot_bittensor__pb2.MultiForwardTextPromptingResponse.SerializeToString, - ), 'Backward': grpc.unary_unary_rpc_method_handler( servicer.Backward, request_deserializer=bittensor_dot___proto_dot_bittensor__pb2.BackwardTextPromptingRequest.FromString, @@ -97,23 +81,6 @@ def Forward(request, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) - @staticmethod - def MultiForward(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary(request, target, '/TextPrompting/MultiForward', - bittensor_dot___proto_dot_bittensor__pb2.MultiForwardTextPromptingRequest.SerializeToString, - bittensor_dot___proto_dot_bittensor__pb2.MultiForwardTextPromptingResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) - @staticmethod def Backward(request, target, diff --git a/bittensor/_subtensor/__init__.py b/bittensor/_subtensor/__init__.py index 3810f4ac07..951338d4eb 100644 --- a/bittensor/_subtensor/__init__.py +++ b/bittensor/_subtensor/__init__.py @@ -68,7 +68,7 @@ def __new__( config.subtensor._mock = _mock if _mock != None else config.subtensor._mock if config.subtensor._mock == True or network == 'mock' or config.subtensor.get('network', bittensor.defaults.subtensor.network) == 'mock': config.subtensor._mock = True - return subtensor_mock.mock_subtensor.mock() + return subtensor_mock.MockSubtensor() # Determine config.subtensor.chain_endpoint and config.subtensor.network config. # If chain_endpoint is set, we override the network flag, otherwise, the chain_endpoint is assigned by the network. @@ -142,11 +142,11 @@ def help(cls): def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None ): prefix_str = '' if prefix == None else prefix + '.' if prefix is not None: - if not hasattr(bittensor.defaults, prefix): + if bittensor.defaults.get(prefix, d=None) == None: setattr(bittensor.defaults, prefix, bittensor.Config()) getattr(bittensor.defaults, prefix).subtensor = bittensor.defaults.subtensor try: - parser.add_argument('--' + prefix_str + 'subtensor.network', default = argparse.SUPPRESS, type=str, + parser.add_argument('--' + prefix_str + 'subtensor.network', default = bittensor.defaults.subtensor.network, type=str, help='''The subtensor network flag. The likely choices are: -- finney (main network) -- local (local running network) @@ -165,10 +165,10 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None ): parser.add_argument('--' + prefix_str + 'subtensor.register.verbose', help="Whether to ouput the registration statistics verbosely.", action='store_true', required=False, default=bittensor.defaults.subtensor.register.verbose) ## Registration args for CUDA registration. - parser.add_argument( '--' + prefix_str + 'subtensor.register.cuda.use_cuda', '--' + prefix_str + 'cuda', '--' + prefix_str + 'cuda.use_cuda', default=argparse.SUPPRESS, help='''Set flag to use CUDA to register.''', action="store_true", required=False ) - parser.add_argument( '--' + prefix_str + 'subtensor.register.cuda.no_cuda', '--' + prefix_str + 'no_cuda', '--' + prefix_str + 'cuda.no_cuda', dest=prefix_str + 'subtensor.register.cuda.use_cuda', default=argparse.SUPPRESS, help='''Set flag to not use CUDA for registration''', action="store_false", required=False ) + parser.add_argument( '--' + prefix_str + 'subtensor.register.cuda.use_cuda', '--' + prefix_str + 'cuda', '--' + prefix_str + 'cuda.use_cuda', default=bittensor.defaults.subtensor.register.cuda.use_cuda, help='''Set flag to use CUDA to register.''', action="store_true", required=False ) + parser.add_argument( '--' + prefix_str + 'subtensor.register.cuda.no_cuda', '--' + prefix_str + 'no_cuda', '--' + prefix_str + 'cuda.no_cuda', dest=prefix_str + 'subtensor.register.cuda.use_cuda', default=not bittensor.defaults.subtensor.register.cuda.use_cuda, help='''Set flag to not use CUDA for registration''', action="store_false", required=False ) - parser.add_argument( '--' + prefix_str + 'subtensor.register.cuda.dev_id', '--' + prefix_str + 'cuda.dev_id', type=int, nargs='+', default=argparse.SUPPRESS, help='''Set the CUDA device id(s). Goes by the order of speed. (i.e. 0 is the fastest).''', required=False ) + parser.add_argument( '--' + prefix_str + 'subtensor.register.cuda.dev_id', '--' + prefix_str + 'cuda.dev_id', type=int, nargs='+', default=bittensor.defaults.subtensor.register.cuda.dev_id, help='''Set the CUDA device id(s). Goes by the order of speed. (i.e. 0 is the fastest).''', required=False ) parser.add_argument( '--' + prefix_str + 'subtensor.register.cuda.TPB', '--' + prefix_str + 'cuda.TPB', type=int, default=bittensor.defaults.subtensor.register.cuda.TPB, help='''Set the number of Threads Per Block for CUDA.''', required=False ) except argparse.ArgumentError: diff --git a/bittensor/_subtensor/chain_data.py b/bittensor/_subtensor/chain_data.py index 97e6174ca8..054d494257 100644 --- a/bittensor/_subtensor/chain_data.py +++ b/bittensor/_subtensor/chain_data.py @@ -16,16 +16,16 @@ # DEALINGS IN THE SOFTWARE. from dataclasses import dataclass -from typing import List, Tuple, Dict, Optional, Any +from typing import List, Tuple, Dict, Optional, Any, TypedDict import bittensor -from bittensor import Balance +from bittensor import Balance, axon_info import torch +from scalecodec.types import GenericCall from scalecodec.base import RuntimeConfiguration, ScaleBytes from scalecodec.type_registry import load_type_registry_preset from scalecodec.utils.ss58 import ss58_encode from enum import Enum - custom_rpc_type_registry = { "types": { "SubnetInfo": { @@ -288,6 +288,14 @@ def _null_neuron() -> 'NeuronInfo': pruning_score = 0, ) return neuron + + @classmethod + def from_weights_bonds_and_neuron_lite( cls, neuron_lite: 'NeuronInfoLite', weights_as_dict: Dict[int, List[Tuple[int, int]]], bonds_as_dict: Dict[int, List[Tuple[int, int]]] ) -> 'NeuronInfo': + n_dict = neuron_lite.__dict__ + n_dict['weights'] = weights_as_dict.get(neuron_lite.uid, []) + n_dict['bonds'] = bonds_as_dict.get(neuron_lite.uid, []) + + return cls( **n_dict ) @staticmethod def _neuron_dict_to_namespace(neuron_dict) -> 'NeuronInfo': @@ -360,7 +368,7 @@ def fix_decoded_values(cls, neuron_info_decoded: Any) -> 'NeuronInfoLite': neuron_info_decoded['validator_trust'] = bittensor.utils.U16_NORMALIZED_FLOAT(neuron_info_decoded['validator_trust']) neuron_info_decoded['dividends'] = bittensor.utils.U16_NORMALIZED_FLOAT(neuron_info_decoded['dividends']) neuron_info_decoded['prometheus_info'] = PrometheusInfo.fix_decoded_values(neuron_info_decoded['prometheus_info']) - neuron_info_decoded['axon_info'] = bittensor.axon_info.from_neuron_info(neuron_info_decoded) + neuron_info_decoded['axon_info'] = axon_info.from_neuron_info(neuron_info_decoded) return cls(**neuron_info_decoded) @classmethod @@ -440,28 +448,6 @@ def _neuron_dict_to_namespace(neuron_dict) -> 'NeuronInfoLite': return neuron -@dataclass -class axon_info: - r""" - Dataclass for axon info. - """ - block: int - version: int - ip: str - port: int - ip_type: int - protocol: int - placeholder1: int # placeholder for future use - placeholder2: int - - @classmethod - def fix_decoded_values(cls, axon_info_decoded: Dict) -> 'axon_info': - r""" Returns an axon_info object from an axon_info_decoded dictionary. - """ - axon_info_decoded['ip'] = bittensor.utils.networking.int_to_ip(int(axon_info_decoded['ip'])) - - return cls(**axon_info_decoded) - @dataclass class PrometheusInfo: r""" @@ -657,3 +643,15 @@ def from_parameter_dict( cls, parameter_dict: 'torch.nn.ParameterDict' ) -> 'Sub r""" Returns a SubnetInfo object from a torch parameter_dict. """ return cls( **dict(parameter_dict) ) + + +# Senate / Proposal data + +class ProposalVoteData(TypedDict): + index: int + threshold: int + ayes: List[str] + nays: List[str] + end: int + +ProposalCallData = GenericCall diff --git a/bittensor/_subtensor/errors.py b/bittensor/_subtensor/errors.py index 7e775c1f08..0bead4f9c9 100644 --- a/bittensor/_subtensor/errors.py +++ b/bittensor/_subtensor/errors.py @@ -50,6 +50,11 @@ class UnstakeError(ChainTransactionError): """ pass +class NominationError(ChainTransactionError): + r""" Error raised when a nomination transaction fails. + """ + pass + class TransferError(ChainTransactionError): r""" Error raised when a transfer transaction fails. diff --git a/bittensor/_subtensor/extrinsics/delegation.py b/bittensor/_subtensor/extrinsics/delegation.py index b5d98590a7..6cca049586 100644 --- a/bittensor/_subtensor/extrinsics/delegation.py +++ b/bittensor/_subtensor/extrinsics/delegation.py @@ -53,95 +53,28 @@ def nominate_extrinsic( with bittensor.__console__.status(":satellite: Sending nominate call on [white]{}[/white] ...".format(subtensor.network)): try: - with subtensor.substrate as substrate: - call = substrate.compose_call( - call_module='SubtensorModule', - call_function='become_delegate', - call_params = { - 'hotkey': wallet.hotkey.ss58_address - } - ) - extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) # sign with coldkey - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") - return True - - response.process_events() - if response.is_success: - bittensor.__console__.print(":white_heavy_check_mark: [green]Finalized[/green]") - bittensor.logging.success( prefix = 'Become Delegate', sufix = 'Finalized: ' + str(response.is_success) ) - else: - bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(response.error_message)) - bittensor.logging.warning( prefix = 'Set weights', sufix = 'Failed: ' + str(response.error_message) ) + success = subtensor._do_nominate( + wallet = wallet, + wait_for_inclusion = wait_for_inclusion, + wait_for_finalization = wait_for_finalization + ) + + if success == True: + bittensor.__console__.print(":white_heavy_check_mark: [green]Finalized[/green]") + bittensor.logging.success( prefix = 'Become Delegate', sufix = 'Finalized: ' + str(success) ) + + # Raises NominationError if False + return success except Exception as e: bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) bittensor.logging.warning( prefix = 'Set weights', sufix = 'Failed: ' + str(e) ) - return False - - if response.is_success: - return True + except NominationError as e: + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) + bittensor.logging.warning( prefix = 'Set weights', sufix = 'Failed: ' + str(e) ) return False -def do_delegation( - subtensor: 'bittensor.Subtensor', - wallet: 'bittensor.wallet', - delegate_ss58: str, - amount: 'bittensor.Balance', - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - with subtensor.substrate as substrate: - call = substrate.compose_call( - call_module='SubtensorModule', - call_function='add_stake', - call_params={ - 'hotkey': delegate_ss58, - 'amount_staked': amount.rao - } - ) - extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - response.process_events() - if response.is_success: - return True - else: - raise StakeError(response.error_message) - -def do_undelegation( - subtensor: 'bittensor.Subtensor', - wallet: 'bittensor.wallet', - delegate_ss58: str, - amount: 'bittensor.Balance', - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - with subtensor.substrate as substrate: - call = substrate.compose_call( - call_module='SubtensorModule', - call_function='remove_stake', - call_params={ - 'hotkey': delegate_ss58, - 'amount_unstaked': amount.rao - } - ) - extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - response.process_events() - if response.is_success: - return True - else: - raise StakeError(response.error_message) - def delegate_extrinsic( subtensor: 'bittensor.Subtensor', @@ -217,8 +150,7 @@ def delegate_extrinsic( try: with bittensor.__console__.status(":satellite: Staking to: [bold white]{}[/bold white] ...".format(subtensor.network)): - staking_response: bool = do_delegation( - subtensor = subtensor, + staking_response: bool = subtensor._do_delegation( wallet = wallet, delegate_ss58 = delegate_ss58, amount = staking_balance, @@ -226,7 +158,7 @@ def delegate_extrinsic( wait_for_finalization = wait_for_finalization, ) - if staking_response: # If we successfully staked. + if staking_response == True: # If we successfully staked. # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") @@ -326,8 +258,7 @@ def undelegate_extrinsic( try: with bittensor.__console__.status(":satellite: Unstaking from: [bold white]{}[/bold white] ...".format(subtensor.network)): - staking_response: bool = do_undelegation( - subtensor = subtensor, + staking_response: bool = subtensor._do_undelegation( wallet = wallet, delegate_ss58 = delegate_ss58, amount = unstaking_balance, @@ -335,7 +266,7 @@ def undelegate_extrinsic( wait_for_finalization = wait_for_finalization, ) - if staking_response: # If we successfully staked. + if staking_response == True: # If we successfully staked. # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") diff --git a/bittensor/_subtensor/extrinsics/prometheus.py b/bittensor/_subtensor/extrinsics/prometheus.py index b27ff850fa..4908b42b7e 100644 --- a/bittensor/_subtensor/extrinsics/prometheus.py +++ b/bittensor/_subtensor/extrinsics/prometheus.py @@ -21,6 +21,7 @@ from rich.prompt import Confirm import bittensor.utils.networking as net from ..errors import * +from ..types import PrometheusServeCallParams def prometheus_extrinsic( subtensor: 'bittensor.Subtensor', @@ -66,7 +67,7 @@ def prometheus_extrinsic( else: external_ip = ip - call_params={ + call_params: 'PrometheusServeCallParams' = { 'version': bittensor.__version_as_int__, 'ip': net.ip_to_int(external_ip), 'port': port, @@ -100,23 +101,21 @@ def prometheus_extrinsic( call_params['netuid'] = netuid with bittensor.__console__.status(":satellite: Serving prometheus on: [white]{}:{}[/white] ...".format(subtensor.network, netuid)): - with subtensor.substrate as substrate: - call = substrate.compose_call( - call_module='SubtensorModule', - call_function='serve_prometheus', - call_params = call_params - ) - extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.hotkey) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) - if wait_for_inclusion or wait_for_finalization: - response.process_events() - if response.is_success: - bittensor.__console__.print(':white_heavy_check_mark: [green]Served prometheus[/green]\n [bold white]{}[/bold white]'.format( - json.dumps(call_params, indent=4, sort_keys=True) - )) - return True - else: - bittensor.__console__.print(':cross_mark: [green]Failed to serve prometheus[/green] error: {}'.format(response.error_message)) - return False - else: + success, err = subtensor._do_serve_prometheus( + wallet=wallet, + call_params = call_params, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion + ) + + if wait_for_inclusion or wait_for_finalization: + if success == True: + bittensor.__console__.print(':white_heavy_check_mark: [green]Served prometheus[/green]\n [bold white]{}[/bold white]'.format( + json.dumps(call_params, indent=4, sort_keys=True) + )) return True + else: + bittensor.__console__.print(':cross_mark: [green]Failed to serve prometheus[/green] error: {}'.format(err)) + return False + else: + return True diff --git a/bittensor/_subtensor/extrinsics/registration.py b/bittensor/_subtensor/extrinsics/registration.py index 5fe86aee45..49d1645dab 100644 --- a/bittensor/_subtensor/extrinsics/registration.py +++ b/bittensor/_subtensor/extrinsics/registration.py @@ -22,7 +22,7 @@ import torch import time from rich.prompt import Confirm -from typing import List, Dict, Union, Optional +from typing import List, Dict, Union, Optional, Tuple import bittensor.utils.networking as net from bittensor.utils.registration import POWSolution, create_pow from ..errors import * @@ -113,7 +113,11 @@ def register_extrinsic ( # pow failed if not pow_result: # might be registered already on this subnet - if (wallet.is_registered( subtensor = subtensor, netuid = netuid )): + is_registered = subtensor.is_hotkey_registered( + netuid = netuid, + hotkey_ss58 = wallet.hotkey.ss58_address, + ) + if is_registered: bittensor.__console__.print(f":white_heavy_check_mark: [green]Already registered on netuid:{netuid}[/green]") return True @@ -122,50 +126,38 @@ def register_extrinsic ( with bittensor.__console__.status(":satellite: Submitting POW..."): # check if pow result is still valid while not pow_result.is_stale(subtensor=subtensor): - with subtensor.substrate as substrate: - # create extrinsic call - call = substrate.compose_call( - call_module='SubtensorModule', - call_function='register', - call_params={ - 'netuid': netuid, - 'block_number': pow_result.block_number, - 'nonce': pow_result.nonce, - 'work': [int(byte_) for byte_ in pow_result.seal], - 'hotkey': wallet.hotkey.ss58_address, - 'coldkey': wallet.coldkeypub.ss58_address, - } - ) - extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.hotkey ) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization ) + result: Tuple[bool, Optional[str]] = subtensor._do_pow_register( + netuid = netuid, + wallet = wallet, + pow_result = pow_result, + wait_for_inclusion = wait_for_inclusion, + wait_for_finalization = wait_for_finalization, + ) + success, err_msg = result - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") + if success != True or success == False: + if 'key is already registered' in err_msg: + # Error meant that the key is already registered. + bittensor.__console__.print(f":white_heavy_check_mark: [green]Already Registered on [bold]subnet:{netuid}[/bold][/green]") return True - # process if registration successful, try again if pow is still valid - response.process_events() - if not response.is_success: - if 'key is already registered' in response.error_message: - # Error meant that the key is already registered. - bittensor.__console__.print(f":white_heavy_check_mark: [green]Already Registered on [bold]subnet:{netuid}[/bold][/green]") - return True + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(err_msg)) + time.sleep(0.5) - bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(response.error_message)) - time.sleep(0.5) - - # Successful registration, final check for neuron and pubkey + # Successful registration, final check for neuron and pubkey + else: + bittensor.__console__.print(":satellite: Checking Balance...") + is_registered = subtensor.is_hotkey_registered( + netuid = netuid, + hotkey_ss58 = wallet.hotkey.ss58_address, + ) + if is_registered: + bittensor.__console__.print(":white_heavy_check_mark: [green]Registered[/green]") + return True else: - bittensor.__console__.print(":satellite: Checking Balance...") - is_registered = wallet.is_registered( subtensor = subtensor, netuid = netuid ) - if is_registered: - bittensor.__console__.print(":white_heavy_check_mark: [green]Registered[/green]") - return True - else: - # neuron not found, try again - bittensor.__console__.print(":cross_mark: [red]Unknown error. Neuron not found.[/red]") - continue + # neuron not found, try again + bittensor.__console__.print(":cross_mark: [red]Unknown error. Neuron not found.[/red]") + continue else: # Exited loop because pow is no longer valid. bittensor.__console__.print( "[red]POW is stale.[/red]" ) @@ -236,41 +228,31 @@ def burned_register_extrinsic ( return False with bittensor.__console__.status(":satellite: Recycling TAO for Registration..."): - with subtensor.substrate as substrate: - # create extrinsic call - call = substrate.compose_call( - call_module='SubtensorModule', - call_function='burned_register', - call_params={ - 'netuid': netuid, - 'hotkey': wallet.hotkey.ss58_address - } - ) - extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization ) + success, err_msg = subtensor._do_burned_register( + netuid = netuid, + wallet = wallet, + wait_for_inclusion = wait_for_inclusion, + wait_for_finalization = wait_for_finalization, + ) + + if success != True or success == False: + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(err_msg)) + time.sleep(0.5) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") - return True - - # process if registration successful, try again if pow is still valid - response.process_events() - if not response.is_success: - bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(response.error_message)) - time.sleep(0.5) + # Successful registration, final check for neuron and pubkey + else: + bittensor.__console__.print(":satellite: Checking Balance...") + block = subtensor.get_current_block() + new_balance = subtensor.get_balance( wallet.coldkeypub.ss58_address, block = block ) - # Successful registration, final check for neuron and pubkey + bittensor.__console__.print("Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( old_balance, new_balance )) + is_registered = subtensor.is_hotkey_registered( + netuid = netuid, + hotkey_ss58 = wallet.hotkey.ss58_address, + ) + if is_registered: + bittensor.__console__.print(":white_heavy_check_mark: [green]Registered[/green]") + return True else: - bittensor.__console__.print(":satellite: Checking Balance...") - block = subtensor.get_current_block() - new_balance = subtensor.get_balance( wallet.coldkeypub.ss58_address, block = block ) - - bittensor.__console__.print("Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( old_balance, new_balance )) - is_registered = wallet.is_registered( subtensor = subtensor, netuid = netuid ) - if is_registered: - bittensor.__console__.print(":white_heavy_check_mark: [green]Registered[/green]") - return True - else: - # neuron not found, try again - bittensor.__console__.print(":cross_mark: [red]Unknown error. Neuron not found.[/red]") + # neuron not found, try again + bittensor.__console__.print(":cross_mark: [red]Unknown error. Neuron not found.[/red]") diff --git a/bittensor/_subtensor/extrinsics/serving.py b/bittensor/_subtensor/extrinsics/serving.py index 4f8b6e61ef..8fca4e8925 100644 --- a/bittensor/_subtensor/extrinsics/serving.py +++ b/bittensor/_subtensor/extrinsics/serving.py @@ -21,6 +21,7 @@ from rich.prompt import Confirm import bittensor.utils.networking as net from ..errors import * +from ..types import AxonServeCallParams def serve_extrinsic ( subtensor: 'bittensor.Subtensor', @@ -66,7 +67,7 @@ def serve_extrinsic ( """ # Decrypt hotkey wallet.hotkey - params = { + params: 'AxonServeCallParams' = { 'version': bittensor.__version_as_int__, 'ip': net.ip_to_int(ip), 'port': port, @@ -119,26 +120,24 @@ def serve_extrinsic ( return False with bittensor.__console__.status(":satellite: Serving axon on: [white]{}:{}[/white] ...".format(subtensor.network, netuid)): - with subtensor.substrate as substrate: - call = substrate.compose_call( - call_module='SubtensorModule', - call_function='serve_axon', - call_params=params - ) - extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.hotkey) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) - if wait_for_inclusion or wait_for_finalization: - response.process_events() - if response.is_success: - bittensor.__console__.print(':white_heavy_check_mark: [green]Served[/green]\n [bold white]{}[/bold white]'.format( - json.dumps(params, indent=4, sort_keys=True) - )) - return True - else: - bittensor.__console__.print(':cross_mark: [green]Failed to Serve axon[/green] error: {}'.format(response.error_message)) - return False - else: + success, error_message = subtensor._do_serve_axon( + wallet = wallet, + call_params = params, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + ) + + if wait_for_inclusion or wait_for_finalization: + if success == True: + bittensor.__console__.print(':white_heavy_check_mark: [green]Served[/green]\n [bold white]{}[/bold white]'.format( + json.dumps(params, indent=4, sort_keys=True) + )) return True + else: + bittensor.__console__.print(':cross_mark: [green]Failed to Serve axon[/green] error: {}'.format(error_message)) + return False + else: + return True def serve_axon_extrinsic ( subtensor: 'bittensor.Subtensor', diff --git a/bittensor/_subtensor/extrinsics/set_weights.py b/bittensor/_subtensor/extrinsics/set_weights.py index 271942e9dc..259ba5a8fd 100644 --- a/bittensor/_subtensor/extrinsics/set_weights.py +++ b/bittensor/_subtensor/extrinsics/set_weights.py @@ -79,34 +79,26 @@ def set_weights_extrinsic( with bittensor.__console__.status(":satellite: Setting weights on [white]{}[/white] ...".format(subtensor.network)): try: - with subtensor.substrate as substrate: - call = substrate.compose_call( - call_module='SubtensorModule', - call_function='set_weights', - call_params = { - 'dests': weight_uids, - 'weights': weight_vals, - 'netuid': netuid, - 'version_key': version_key, - } - ) - # Period dictates how long the extrinsic will stay as part of waiting pool - extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.hotkey, era={'period':100}) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") - return True - - response.process_events() - if response.is_success: - bittensor.__console__.print(":white_heavy_check_mark: [green]Finalized[/green]") - bittensor.logging.success( prefix = 'Set weights', sufix = 'Finalized: ' + str(response.is_success) ) - return True - else: - bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(response.error_message)) - bittensor.logging.warning( prefix = 'Set weights', sufix = 'Failed: ' + str(response.error_message) ) - return False + success, error_message = subtensor._do_set_weights( + wallet = wallet, + netuid = netuid, + uids = weight_uids, + vals = weight_vals, + version_key = version_key, + ) + + if not wait_for_finalization and not wait_for_inclusion: + bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") + return True + + if success == True: + bittensor.__console__.print(":white_heavy_check_mark: [green]Finalized[/green]") + bittensor.logging.success( prefix = 'Set weights', sufix = 'Finalized: ' + str(success) ) + return True + else: + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(error_message)) + bittensor.logging.warning( prefix = 'Set weights', sufix = 'Failed: ' + str(error_message) ) + return False except Exception as e: diff --git a/bittensor/_subtensor/extrinsics/staking.py b/bittensor/_subtensor/extrinsics/staking.py index c9af6ed83b..99e69b2976 100644 --- a/bittensor/_subtensor/extrinsics/staking.py +++ b/bittensor/_subtensor/extrinsics/staking.py @@ -128,7 +128,7 @@ def add_stake_extrinsic( wait_for_finalization = wait_for_finalization, ) - if staking_response: # If we successfully staked. + if staking_response == True: # If we successfully staked. # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") @@ -273,7 +273,7 @@ def add_stake_multiple_extrinsic ( wait_for_finalization = wait_for_finalization, ) - if staking_response: # If we successfully staked. + if staking_response == True: # If we successfully staked. # We only wait here if we expect finalization. if idx < len(hotkey_ss58s) - 1: @@ -365,6 +365,7 @@ def __do_add_stake_single( """ # Decrypt keys, wallet.coldkey + hotkey_owner = subtensor.get_hotkey_owner( hotkey_ss58 ) own_hotkey = (wallet.coldkeypub.ss58_address == hotkey_owner) if not own_hotkey: @@ -373,23 +374,13 @@ def __do_add_stake_single( if not subtensor.is_hotkey_delegate( hotkey_ss58 = hotkey_ss58 ): raise NotDelegateError("Hotkey: {} is not a delegate.".format(hotkey_ss58)) - with subtensor.substrate as substrate: - call = substrate.compose_call( - call_module='SubtensorModule', - call_function='add_stake', - call_params={ - 'hotkey': hotkey_ss58, - 'amount_staked': amount.rao - } - ) - extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True + success = subtensor._do_stake( + wallet = wallet, + hotkey_ss58 = hotkey_ss58, + amount = amount, + wait_for_inclusion = wait_for_inclusion, + wait_for_finalization = wait_for_finalization, + ) - response.process_events() - if response.is_success: - return True - else: - raise StakeError(response.error_message) \ No newline at end of file + return success + \ No newline at end of file diff --git a/bittensor/_subtensor/extrinsics/transfer.py b/bittensor/_subtensor/extrinsics/transfer.py index ad38fe2abb..c6e4a918ab 100644 --- a/bittensor/_subtensor/extrinsics/transfer.py +++ b/bittensor/_subtensor/extrinsics/transfer.py @@ -82,25 +82,11 @@ def transfer_extrinsic( existential_deposit = subtensor.get_existential_deposit() with bittensor.__console__.status(":satellite: Transferring..."): - with subtensor.substrate as substrate: - call = substrate.compose_call( - call_module='Balances', - call_function='transfer', - call_params={ - 'dest': dest, - 'value': transfer_balance.rao - } - ) - - try: - payment_info = substrate.get_payment_info( call = call, keypair = wallet.coldkey ) - except Exception as e: - bittensor.__console__.print(":cross_mark: [red]Failed to get payment info[/red]:[bold white]\n {}[/bold white]".format(e)) - payment_info = { - 'partialFee': 2e7, # assume 0.02 Tao - } - - fee = bittensor.Balance.from_rao( payment_info['partialFee'] ) + fee = subtensor.get_transfer_fee( + wallet=wallet, + dest = dest, + value = transfer_balance.rao + ) if not keep_alive: # Check if the transfer should keep_alive the account @@ -117,37 +103,25 @@ def transfer_extrinsic( return False with bittensor.__console__.status(":satellite: Transferring..."): - with subtensor.substrate as substrate: - call = substrate.compose_call( - call_module='Balances', - call_function='transfer', - call_params={ - 'dest': dest, - 'value': transfer_balance.rao - } - ) - extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") - return True - - # Otherwise continue with finalization. - response.process_events() - if response.is_success: - bittensor.__console__.print(":white_heavy_check_mark: [green]Finalized[/green]") - block_hash = response.block_hash - bittensor.__console__.print("[green]Block Hash: {}[/green]".format( block_hash )) - - explorer_url = bittensor.utils.get_explorer_url_for_network( subtensor.network, block_hash, bittensor.__network_explorer_map__ ) - if explorer_url is not None: - bittensor.__console__.print("[green]Explorer Link: {}[/green]".format( explorer_url )) - - else: - bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(response.error_message)) - - if response.is_success: + success, block_hash, err_msg = subtensor._do_transfer( + wallet, + dest, + transfer_balance, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + ) + + if success: + bittensor.__console__.print(":white_heavy_check_mark: [green]Finalized[/green]") + bittensor.__console__.print("[green]Block Hash: {}[/green]".format( block_hash )) + + explorer_url = bittensor.utils.get_explorer_url_for_network( subtensor.network, block_hash, bittensor.__network_explorer_map__ ) + if explorer_url is not None: + bittensor.__console__.print("[green]Explorer Link: {}[/green]".format( explorer_url )) + else: + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(err_msg)) + + if success: with bittensor.__console__.status(":satellite: Checking Balance..."): new_balance = subtensor.get_balance( wallet.coldkey.ss58_address ) bittensor.__console__.print("Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format(account_balance, new_balance)) diff --git a/bittensor/_subtensor/extrinsics/unstaking.py b/bittensor/_subtensor/extrinsics/unstaking.py index 6b5e6a702b..8db5943b03 100644 --- a/bittensor/_subtensor/extrinsics/unstaking.py +++ b/bittensor/_subtensor/extrinsics/unstaking.py @@ -63,26 +63,15 @@ def __do_remove_stake_single( # Decrypt keys, wallet.coldkey - with subtensor.substrate as substrate: - call = substrate.compose_call( - call_module='SubtensorModule', - call_function='remove_stake', - call_params={ - 'hotkey': hotkey_ss58, - 'amount_unstaked': amount.rao - } - ) - extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True + success = subtensor._do_unstake( + wallet = wallet, + hotkey_ss58 = hotkey_ss58, + amount = amount, + wait_for_inclusion = wait_for_inclusion, + wait_for_finalization = wait_for_finalization, + ) - response.process_events() - if response.is_success: - return True - else: - raise StakeError(response.error_message) + return success def unstake_extrinsic ( subtensor: 'bittensor.Subtensor', @@ -157,7 +146,7 @@ def unstake_extrinsic ( wait_for_finalization = wait_for_finalization, ) - if staking_response: # If we successfully unstaked. + if staking_response == True: # If we successfully unstaked. # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") @@ -278,7 +267,7 @@ def unstake_multiple_extrinsic ( wait_for_finalization = wait_for_finalization, ) - if staking_response: # If we successfully unstaked. + if staking_response == True: # If we successfully unstaked. # We only wait here if we expect finalization. if idx < len(hotkey_ss58s) - 1: diff --git a/bittensor/_subtensor/subtensor_impl.py b/bittensor/_subtensor/subtensor_impl.py index 08dba2ec6a..9fd2db12b7 100644 --- a/bittensor/_subtensor/subtensor_impl.py +++ b/bittensor/_subtensor/subtensor_impl.py @@ -22,12 +22,14 @@ import scalecodec from retry import retry from typing import List, Dict, Union, Optional, Tuple -from substrateinterface import SubstrateInterface +from substrateinterface.base import QueryMapResult, SubstrateInterface + from bittensor.utils.balance import Balance from bittensor.utils import U16_NORMALIZED_FLOAT, U64_MAX, RAOPERTAO, U16_MAX +from bittensor.utils.registration import POWSolution # Local imports. -from .chain_data import NeuronInfo, axon_info, DelegateInfo, PrometheusInfo, SubnetInfo, NeuronInfoLite +from .chain_data import NeuronInfo, DelegateInfo, PrometheusInfo, SubnetInfo, NeuronInfoLite, axon_info, ProposalVoteData, ProposalCallData from .errors import * from .extrinsics.staking import add_stake_extrinsic, add_stake_multiple_extrinsic from .extrinsics.unstaking import unstake_extrinsic, unstake_multiple_extrinsic @@ -38,6 +40,7 @@ from .extrinsics.prometheus import prometheus_extrinsic from .extrinsics.delegation import delegate_extrinsic, nominate_extrinsic,undelegate_extrinsic from .extrinsics.senate import register_senate_extrinsic, leave_senate_extrinsic, vote_senate_extrinsic +from .types import AxonServeCallParams, PrometheusServeCallParams # Logging from loguru import logger @@ -164,6 +167,40 @@ def set_weights( wait_for_finalization=wait_for_finalization, prompt=prompt, ) + + def _do_set_weights( + self, + wallet: 'bittensor.wallet', + uids: List[int], + vals: List[int], + netuid: int, + version_key: int = bittensor.__version_as_int__, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> Tuple[bool, Optional[str]]: # (success, error_message) + with self.substrate as substrate: + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='set_weights', + call_params = { + 'dests': uids, + 'weights': vals, + 'netuid': netuid, + 'version_key': version_key, + } + ) + # Period dictates how long the extrinsic will stay as part of waiting pool + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.hotkey, era={'period':100}) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True, None + + response.process_events() + if response.is_success: + return True, None + else: + return False, response.error_message ###################### #### Registration #### @@ -219,6 +256,88 @@ def burned_register ( wait_for_finalization = wait_for_finalization, prompt = prompt ) + + def _do_pow_register( + self, + netuid: int, + wallet: 'bittensor.Wallet', + pow_result: POWSolution, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> Tuple[bool, Optional[str]]: + """ Sends a (POW) register extrinsic to the chain. + Args: + netuid (int): the subnet to register on. + wallet (bittensor.Wallet): the wallet to register. + pow_result (POWSolution): the pow result to register. + wait_for_inclusion (bool): if true, waits for the extrinsic to be included in a block. + wait_for_finalization (bool): if true, waits for the extrinsic to be finalized. + Returns: + success (bool): True if the extrinsic was included in a block. + error (Optional[str]): None on success or not waiting for inclusion/finalization, otherwise the error message. + """ + with self.substrate as substrate: + # create extrinsic call + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='register', + call_params={ + 'netuid': netuid, + 'block_number': pow_result.block_number, + 'nonce': pow_result.nonce, + 'work': [int(byte_) for byte_ in pow_result.seal], + 'hotkey': wallet.hotkey.ss58_address, + 'coldkey': wallet.coldkeypub.ss58_address, + } + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.hotkey ) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization ) + + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") + return True, None + + # process if registration successful, try again if pow is still valid + response.process_events() + if not response.is_success: + return False, response.error_message + # Successful registration + else: + return True, None + + def _do_burned_register( + self, + netuid: int, + wallet: 'bittensor.Wallet', + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> Tuple[bool, Optional[str]]: + with self.substrate as substrate: + # create extrinsic call + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='burned_register', + call_params={ + 'netuid': netuid, + 'hotkey': wallet.hotkey.ss58_address + } + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization ) + + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") + return True + + # process if registration successful, try again if pow is still valid + response.process_events() + if not response.is_success: + return False, response.error_message + # Successful registration + else: + return True, None ################## #### Transfer #### @@ -242,6 +361,83 @@ def transfer( wait_for_finalization = wait_for_finalization, prompt = prompt ) + + def get_transfer_fee( + self, + wallet: 'bittensor.Wallet', + dest: str, + value: Union[Balance, float, int], + ) -> Balance: + if isinstance(value, float): + transfer_balance = bittensor.Balance.from_tao(value) + elif isinstance(value, int): + transfer_balance = bittensor.Balance.from_rao(value) + + with self.substrate as substrate: + call = substrate.compose_call( + call_module='Balances', + call_function='transfer', + call_params={ + 'dest': dest, + 'value': transfer_balance.rao + } + ) + + try: + payment_info = substrate.get_payment_info( call = call, keypair = wallet.coldkeypub ) + except Exception as e: + bittensor.__console__.print(":cross_mark: [red]Failed to get payment info[/red]:[bold white]\n {}[/bold white]".format(e)) + payment_info = { + 'partialFee': 2e7, # assume 0.02 Tao + } + + fee = bittensor.Balance.from_rao( payment_info['partialFee'] ) + return fee + + def _do_transfer( + self, + wallet: 'bittensor.wallet', + dest: str, + transfer_balance: Balance, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> Tuple[bool, Optional[str], Optional[str]]: + """ Sends a transfer extrinsic to the chain. + Args: + wallet (:obj:`bittensor.wallet`): Wallet object. + dest (:obj:`str`): Destination public key address. + transfer_balance (:obj:`bittensor.Balance`): Amount to transfer. + wait_for_inclusion (:obj:`bool`): If true, waits for inclusion. + wait_for_finalization (:obj:`bool`): If true, waits for finalization. + Returns: + success (:obj:`bool`): True if transfer was successful. + block_hash (:obj:`str`): Block hash of the transfer. + (On success and if wait_for_ finalization/inclusion is True) + error (:obj:`str`): Error message if transfer failed. + """ + with self.substrate as substrate: + call = substrate.compose_call( + call_module='Balances', + call_function='transfer', + call_params={ + 'dest': dest, + 'value': transfer_balance.rao + } + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") + return True, None, None + + # Otherwise continue with finalization. + response.process_events() + if response.is_success: + block_hash = response.block_hash + return True, block_hash, None + else: + return False, None, response.error_message def get_existential_deposit( self, @@ -287,6 +483,30 @@ def serve_axon ( prompt: bool = False, ) -> bool: return serve_axon_extrinsic( self, netuid, axon, use_upnpc, wait_for_inclusion, wait_for_finalization) + + def _do_serve_axon( + self, + wallet: 'bittensor.wallet', + call_params: AxonServeCallParams, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> Tuple[bool, Optional[str]]: + with self.substrate as substrate: + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='serve_axon', + call_params=call_params + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.hotkey) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + if wait_for_inclusion or wait_for_finalization: + response.process_events() + if response.is_success: + return True, None + else: + return False, response.error_message + else: + return True, None def serve_prometheus ( self, @@ -297,6 +517,42 @@ def serve_prometheus ( wait_for_finalization: bool = True, ) -> bool: return prometheus_extrinsic( self, wallet = wallet, port = port, netuid = netuid, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization) + + def _do_serve_prometheus( + self, + wallet: 'bittensor.wallet', + call_params: PrometheusServeCallParams, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> Tuple[bool, Optional[str]]: + """ + Sends a serve prometheus extrinsic to the chain. + Args: + wallet (:obj:`bittensor.wallet`): Wallet object. + call_params (:obj:`PrometheusServeCallParams`): Prometheus serve call parameters. + wait_for_inclusion (:obj:`bool`): If true, waits for inclusion. + wait_for_finalization (:obj:`bool`): If true, waits for finalization. + Returns: + success (:obj:`bool`): True if serve prometheus was successful. + error (:obj:`Optional[str]`): Error message if serve prometheus failed, None otherwise. + """ + with self.substrate as substrate: + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='serve_prometheus', + call_params = call_params + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.hotkey) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + if wait_for_inclusion or wait_for_finalization: + response.process_events() + if response.is_success: + return True, None + else: + return False, response.error_message + else: + return True, None + ################# #### Staking #### ################# @@ -322,7 +578,7 @@ def add_stake( def add_stake_multiple ( self, - wallet: 'bittensor.wallet', + wallet: 'bittensor.Wallet', hotkey_ss58s: List[str], amounts: List[Union[Balance, float]] = None, wait_for_inclusion: bool = True, @@ -331,6 +587,47 @@ def add_stake_multiple ( ) -> bool: """ Adds stake to each hotkey_ss58 in the list, using each amount, from a common coldkey.""" return add_stake_multiple_extrinsic( self, wallet, hotkey_ss58s, amounts, wait_for_inclusion, wait_for_finalization, prompt) + + def _do_stake( + self, + wallet: 'bittensor.Wallet', + hotkey_ss58: str, + amount: Balance, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: + """ Sends a stake extrinsic to the chain. + Args: + wallet (:obj:`bittensor.Wallet`): Wallet object that can sign the extrinsic. + hotkey_ss58 (:obj:`str`): Hotkey ss58 address to stake to. + amount (:obj:`bittensor.Balance`): Amount to stake. + wait_for_inclusion (:obj:`bool`): If true, waits for inclusion before returning. + wait_for_finalization (:obj:`bool`): If true, waits for finalization before returning. + Returns: + success (:obj:`bool`): True if the extrinsic was successful. + Raises: + StakeError: If the extrinsic failed. + """ + with self.substrate as substrate: + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='add_stake', + call_params={ + 'hotkey': hotkey_ss58, + 'amount_staked': amount.rao + } + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True + + response.process_events() + if response.is_success: + return True + else: + raise StakeError(response.error_message) ################### #### Unstaking #### @@ -358,6 +655,47 @@ def unstake ( ) -> bool: """ Removes stake into the wallet coldkey from the specified hotkey uid.""" return unstake_extrinsic( self, wallet, hotkey_ss58, amount, wait_for_inclusion, wait_for_finalization, prompt ) + + def _do_unstake( + self, + wallet: 'bittensor.Wallet', + hotkey_ss58: str, + amount: Balance, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: + """ Sends an unstake extrinsic to the chain. + Args: + wallet (:obj:`bittensor.Wallet`): Wallet object that can sign the extrinsic. + hotkey_ss58 (:obj:`str`): Hotkey ss58 address to unstake from. + amount (:obj:`bittensor.Balance`): Amount to unstake. + wait_for_inclusion (:obj:`bool`): If true, waits for inclusion before returning. + wait_for_finalization (:obj:`bool`): If true, waits for finalization before returning. + Returns: + success (:obj:`bool`): True if the extrinsic was successful. + Raises: + StakeError: If the extrinsic failed. + """ + with self.substrate as substrate: + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='remove_stake', + call_params={ + 'hotkey': hotkey_ss58, + 'amount_unstaked': amount.rao + } + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True + + response.process_events() + if response.is_success: + return True + else: + raise StakeError(response.error_message) ################ #### Senate #### @@ -395,17 +733,61 @@ def vote_senate( def is_senate_member( self, - hotkey_ss58: str + hotkey_ss58: str, + block: Optional[int] = None, ) -> bool: - senate_members = self.query_module("Senate", "Members").serialize() + senate_members = self.query_module(module="Senate", name="Members", block=block ).serialize() return senate_members.count( hotkey_ss58 ) > 0 def get_vote_data( self, - proposal_hash: str - ) -> Optional[dict]: - vote_data = self.query_module("Triumvirate", "Voting", None, [proposal_hash]) + proposal_hash: str, + block: Optional[int] = None, + ) -> Optional[ProposalVoteData]: + vote_data = self.query_module(module="Triumvirate", name="Voting", block=block, params=[proposal_hash]) return vote_data.serialize() if vote_data != None else None + + get_proposal_vote_data = get_vote_data + + def get_senate_members( + self, + block: Optional[int] = None, + ) -> Optional[List[str]]: + senate_members = self.query_module("SenateMembers", "Members", block=block ) + + return senate_members.serialize() if senate_members != None else None + + def get_proposal_call_data( + self, + proposal_hash: str, + block: Optional[int] = None, + ) -> Optional['bittensor.ProposalCallData']: + proposal_data = self.query_module(module="Triumvirate", name="ProposalOf", block=block, params=[proposal_hash]) + + return proposal_data.serialize() if proposal_data != None else None + + def get_proposal_hashes( + self, + block: Optional[int] = None, + ) -> Optional[List[str]]: + proposal_hashes = self.query_module(module="Triumvirate", name="Proposals", block=block) + + return proposal_hashes.serialize() if proposal_hashes != None else None + + def get_proposals( + self, + block: Optional[int] = None, + ) -> Optional[Dict[str, Tuple['bittensor.ProposalCallData', 'bittensor.ProposalVoteData']]]: + proposals = {} + proposal_hashes: List = self.get_proposal_hashes( block=block ) + + for proposal_hash in proposal_hashes: + proposals[proposal_hash] = ( + self.get_proposal_call_data( proposal_hash, block=block ), + self.get_proposal_vote_data( proposal_hash, block=block ) + ) + + return proposals ######################## #### Standard Calls #### @@ -425,7 +807,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() """ Queries subtensor map storage with params and block. """ - def query_map_subtensor( self, name: str, block: Optional[int] = None, params: Optional[List[object]] = [] ) -> Optional[object]: + def query_map_subtensor( self, name: str, block: Optional[int] = None, params: Optional[List[object]] = [] ) -> QueryMapResult: @retry(delay=2, tries=3, backoff=2, max_delay=4) def make_substrate_call_with_retry(): with self.substrate as substrate: @@ -670,9 +1052,6 @@ def serving_rate_limit (self, block: Optional[int] = None ) -> Optional[int]: def tx_rate_limit (self, block: Optional[int] = None ) -> Optional[int]: return self.query_subtensor( "TxRateLimit", block ).value - def tx_rate_limit (self, block: Optional[int] = None ) -> Optional[int]: - return self.query_subtensor( "TxRateLimit", block ).value - ##################################### #### Network Parameters #### ##################################### @@ -766,8 +1145,8 @@ def make_substrate_call_with_retry(): #################### #### Nomination #### #################### - def is_hotkey_delegate( self, hotkey_ss58: str ) -> bool: - return hotkey_ss58 in [ info.hotkey_ss58 for info in self.get_delegates() ] + def is_hotkey_delegate( self, hotkey_ss58: str, block: Optional[int] = None ) -> bool: + return hotkey_ss58 in [ info.hotkey_ss58 for info in self.get_delegates( block = block ) ] def get_delegate_take( self, hotkey_ss58: str, block: Optional[int] = None ) -> Optional[float]: return U16_NORMALIZED_FLOAT( self.query_subtensor( 'Delegates', block, [ hotkey_ss58 ] ).value ) @@ -858,10 +1237,13 @@ def is_hotkey_registered_any( self, hotkey_ss58: str, block: Optional[int] = Non def is_hotkey_registered_on_subnet( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None) -> bool: return self.get_uid_for_hotkey_on_subnet( hotkey_ss58, netuid, block ) != None - def is_hotkey_registered( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None) -> bool: - return self.get_uid_for_hotkey_on_subnet( hotkey_ss58, netuid, block ) != None + def is_hotkey_registered( self, hotkey_ss58: str, netuid: Optional[int] = None, block: Optional[int] = None) -> bool: + if netuid == None: + return self.is_hotkey_registered_any( hotkey_ss58, block ) + else: + return self.is_hotkey_registered_on_subnet( hotkey_ss58, netuid, block ) - def get_uid_for_hotkey_on_subnet( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None) -> int: + def get_uid_for_hotkey_on_subnet( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None) -> Optional[int]: return self.query_subtensor( 'Uids', block, [ netuid, hotkey_ss58 ] ).value def get_all_uids_for_hotkey( self, hotkey_ss58: str, block: Optional[int] = None) -> List[int]: @@ -933,25 +1315,23 @@ def neurons(self, netuid: int, block: Optional[int] = None ) -> List[NeuronInfo] neuron (List[NeuronInfo]): List of neuron metadata objects. """ - @retry(delay=2, tries=3, backoff=2, max_delay=4) - def make_substrate_call_with_retry(): - with self.substrate as substrate: - block_hash = None if block == None else substrate.get_block_hash( block ) - params = [netuid] - if block_hash: - params = params + [block_hash] - return substrate.rpc_request( - method="neuronInfo_getNeurons", # custom rpc method - params=params - ) + neurons_lite = self.neurons_lite( netuid = netuid, block = block ) + weights = self.weights( block = block, netuid = netuid ) + bonds = self.bonds( block = block, netuid = netuid ) - json_body = make_substrate_call_with_retry() - result = json_body['result'] + weights_as_dict = { + uid: w for uid, w in weights + } + bonds_as_dict = { + uid: b for uid, b in bonds + } - if result in (None, []): - return [] + neurons = [ + NeuronInfo.from_weights_bonds_and_neuron_lite( neuron_lite, weights_as_dict, bonds_as_dict ) for neuron_lite in neurons_lite + ] - return NeuronInfo.list_from_vec_u8( result ) + return neurons + def neuron_for_uid_lite( self, uid: int, netuid: int, block: Optional[int] = None ) -> Optional[NeuronInfoLite]: r""" Returns a list of neuron lite from the chain. @@ -1018,7 +1398,7 @@ def make_substrate_call_with_retry(): return NeuronInfoLite.list_from_vec_u8( result ) def metagraph( self, netuid: int, lite: bool = True, block: Optional[int] = None ) -> 'bittensor.Metagraph': - r""" Returns the metagraph for the subnet. + r""" Returns a synced metagraph for the subnet. Args: netuid ( int ): The network uid of the subnet to query. @@ -1031,9 +1411,112 @@ def metagraph( self, netuid: int, lite: bool = True, block: Optional[int] = None The metagraph for the subnet at the block. """ metagraph_ = bittensor.metagraph( network = self.network, netuid = netuid, lite = lite, sync = False ) - metagraph_.sync( block = block, lite = lite, subtensor = self) + metagraph_.sync( block = block, lite = lite, subtensor = self ) return metagraph_ + + def weights(self, netuid: int, block: Optional[int] = None) -> List[Tuple[int, List[Tuple[int, int]]]]: + w_map = [] + w_map_encoded = self.query_map_subtensor(name="Weights", block=block, params = [netuid]) + if w_map_encoded.records: + for uid, w in w_map_encoded: + w_map.append((uid.serialize(), w.serialize())) + + return w_map + + def bonds(self, netuid: int, block: Optional[int] = None) -> List[Tuple[int, List[Tuple[int, int]]]]: + b_map = [] + b_map_encoded = self.query_map_subtensor(name="Bonds", block=block, params = [netuid]) + if b_map_encoded.records: + for uid, b in b_map_encoded: + b_map.append((uid.serialize(), b.serialize())) + + return b_map + + ################ + ## Extrinsics ## + ################ + + def _do_delegation( + self, + wallet: 'bittensor.wallet', + delegate_ss58: str, + amount: 'bittensor.Balance', + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: + with self.substrate as substrate: + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='add_stake', + call_params={ + 'hotkey': delegate_ss58, + 'amount_staked': amount.rao + } + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True + response.process_events() + if response.is_success: + return True + else: + raise StakeError(response.error_message) + + def _do_undelegation( + self, + wallet: 'bittensor.wallet', + delegate_ss58: str, + amount: 'bittensor.Balance', + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: + with self.substrate as substrate: + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='remove_stake', + call_params={ + 'hotkey': delegate_ss58, + 'amount_unstaked': amount.rao + } + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True + response.process_events() + if response.is_success: + return True + else: + raise StakeError(response.error_message) + + def _do_nominate( + self, + wallet: 'bittensor.wallet', + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: + with self.substrate as substrate: + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='become_delegate', + call_params = { + 'hotkey': wallet.hotkey.ss58_address + } + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) # sign with coldkey + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True + response.process_events() + if response.is_success: + return True + else: + raise NominationError(response.error_message) ################ #### Legacy #### @@ -1117,3 +1600,6 @@ def _null_neuron() -> NeuronInfo: hotkey = "000000000000000000000000000000000000000000000000" ) return neuron + + def get_block_hash(self, block_id: int) -> str: + return self.substrate.get_block_hash( block_id = block_id ) diff --git a/bittensor/_subtensor/subtensor_mock.py b/bittensor/_subtensor/subtensor_mock.py index f0bedacaf6..f31a890001 100644 --- a/bittensor/_subtensor/subtensor_mock.py +++ b/bittensor/_subtensor/subtensor_mock.py @@ -1,5 +1,5 @@ # The MIT License (MIT) -# Copyright © 2022 Opentensor Foundation +# Copyright © 2022-2023 Opentensor Foundation # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated # documentation files (the “Software”), to deal in the Software without restriction, including without limitation @@ -15,429 +15,1359 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from substrateinterface import SubstrateInterface, Keypair -from scalecodec import GenericCall -import psutil -import subprocess -from sys import platform +from random import randint +from types import SimpleNamespace +from typing import Any, Dict, List, Optional, Tuple, TypedDict, Union +from unittest.mock import MagicMock +from dataclasses import dataclass +from abc import ABC, abstractclassmethod +from collections.abc import Mapping + import bittensor -import time -import os -from typing import Optional, Tuple, Dict, Union -import requests - -from . import subtensor_impl - -__type_registery__ = { - "runtime_id": 2, - "types": { - "Balance": "u64", - "NeuronMetadataOf": { - "type": "struct", - "type_mapping": [ - ["version", "u32"], - ["ip", "u128"], - ["port", "u16"], - ["ip_type", "u8"], - ["uid", "u32"], - ["modality", "u8"], - ["hotkey", "AccountId"], - ["coldkey", "AccountId"], - ["active", "bool"], - ["last_update", "u64"], - ["validator_permit", "bool"], - ["stake", "u64"], - ["rank", "u16"], - ["trust", "u16"], - ["consensus", "u16"], - ["validator_trust", "u16"], - ["incentive", "u16"], - ["dividends", "u16"], - ["emission", "u64"], - ["bonds", "Vec<(u16, u16)>"], - ["weights", "Vec<(u16, u16)>"] - ] - } - } -} +from bittensor.utils import RAOPERTAO, U16_NORMALIZED_FLOAT +from bittensor.utils.registration import POWSolution +from hashlib import sha256 + +from .chain_data import (NeuronInfo, NeuronInfoLite, PrometheusInfo, DelegateInfo, + SubnetInfo, axon_info) +from .errors import * +from .subtensor_impl import Subtensor, AxonServeCallParams, PrometheusServeCallParams + +BlockNumber = int + +class InfoDict(Mapping): + @abstractclassmethod + def default(cls): + raise NotImplementedError + + def __getitem__(self, key): + return getattr(self, key) + + def __setitem__(self, key, value): + return setattr(self, key, value) + + def __iter__(self): + return iter(self.__dict__) + + def __len__(self): + return len(self.__dict__) + +@dataclass +class AxonInfoDict(InfoDict): + block: int + version: int + ip: int # integer representation of ip address + port: int + ip_type: int + protocol: int + placeholder1: int # placeholder for future use + placeholder2: int + + @classmethod + def default(cls): + return cls( + block=0, + version=0, + ip=0, + port=0, + ip_type=0, + protocol=0, + placeholder1=0, + placeholder2=0, + ) + +@dataclass +class PrometheusInfoDict(InfoDict): + block: int + version: int + ip: int # integer representation of ip address + port: int + ip_type: int -GLOBAL_SUBTENSOR_MOCK_PROCESS_NAME = "node-subtensor" + @classmethod + def default(cls): + return cls( + block=0, + version=0, + ip=0, + port=0, + ip_type=0, + ) -class mock_subtensor(): - r""" Returns a subtensor connection interface to a mocked subtensor process running in the background. - Optionall creates the background process if it does not exist. +@dataclass +class MockSubtensorValue: + value: Optional[Any] + +class MockMapResult: + records: Optional[List[Tuple[MockSubtensorValue, MockSubtensorValue]]] + + def __init__(self, records: Optional[List[Tuple[Union[Any, MockSubtensorValue], Union[Any, MockSubtensorValue]]]] = None): + _records = [ + (MockSubtensorValue( value=record[0] ), MockSubtensorValue( value=record[1] )) + # Make sure record is a tuple of MockSubtensorValue (dict with value attr) + if not (isinstance(record, tuple) and all(isinstance(item, dict) and hasattr(item, 'value') for item in record)) + else record + for record in records + ] + + self.records = _records + + def __iter__(self): + return iter(self.records) + +class MockSystemState(TypedDict): + Account: Dict[str, Dict[int, int]] # address -> block -> balance + +class MockSubtensorState(TypedDict): + Rho: Dict[int, Dict[BlockNumber, int]] # netuid -> block -> rho + Kappa: Dict[int, Dict[BlockNumber, int]] # netuid -> block -> kappa + Difficulty: Dict[int, Dict[BlockNumber, int]] # netuid -> block -> difficulty + ImmunityPeriod: Dict[int, Dict[BlockNumber, int]] # netuid -> block -> immunity_period + ValidatorBatchSize: Dict[int, Dict[BlockNumber, int]] # netuid -> block -> validator_batch_size + Active: Dict[int, Dict[BlockNumber, bool]] # (netuid, uid), block -> active + Stake: Dict[str, Dict[str, Dict[int, int]]] # (hotkey, coldkey) -> block -> stake + + Delegates: Dict[str, Dict[int, float]] # address -> block -> delegate_take + + NetworksAdded: Dict[int, Dict[BlockNumber, bool]] # netuid -> block -> added + +class MockChainState(TypedDict): + System: MockSystemState + SubtensorModule: MockSubtensorState + +class MockSubtensor(Subtensor): """ + A Mock Subtensor class for running tests. + This should mock only methods that make queries to the chain. + e.g. We mock `Subtensor.query_subtensor` instead of all query methods. + + This class will also store a local (mock) state of the chain. + """ + chain_state: MockChainState + block_number: int @classmethod - def mock(cls): - - if not cls.global_mock_process_is_running(): - # Remove any old chain db - if os.path.exists(f'{bittensor.__mock_chain_db__}_{os.getpid()}'): - # Name mock chain db using pid to avoid conflicts while multiple processes are running. - os.system(f'rm -rf {bittensor.__mock_chain_db__}_{os.getpid()}') - _owned_mock_subtensor_process = cls.create_global_mock_process(os.getpid()) + def reset(cls) -> None: + bittensor.__GLOBAL_MOCK_STATE__.clear() + + _ = cls() + + def setup(self) -> None: + if not hasattr(self, 'chain_state') or getattr(self, 'chain_state') is None: + self.chain_state = { + 'System': { + 'Account': {} + }, + 'Balances': { + 'ExistentialDeposit': { + 0: 500 + }, + }, + 'SubtensorModule': { + 'NetworksAdded': {}, + 'Rho': {}, + 'Kappa': {}, + 'Difficulty': {}, + 'ImmunityPeriod': {}, + 'ValidatorBatchSize': {}, + 'ValidatorSequenceLength': {}, + 'ValidatorEpochsPerReset': {}, + 'ValidatorEpochLength': {}, + 'MaxAllowedValidators': {}, + 'MinAllowedWeights': {}, + 'MaxWeightLimit': {}, + 'SynergyScalingLawPower': {}, + 'ScalingLawPower': {}, + 'SubnetworkN': {}, + 'MaxAllowedUids': {}, + 'NetworkModality': {}, + 'BlocksSinceLastStep': {}, + 'Tempo': {}, + 'NetworkConnect': {}, + 'EmissionValues': {}, + 'Burn': {}, + + 'Active': {}, + + 'Uids': {}, + 'Keys': {}, + 'Owner': {}, + 'IsNetworkMember': {}, + 'LastUpdate': {}, + + 'Rank': {}, + 'Emission': {}, + 'Incentive': {}, + 'Consensus': {}, + 'Trust': {}, + 'ValidatorTrust': {}, + 'Dividends': {}, + 'PruningScores': {}, + 'ValidatorPermit': {}, + + 'Weights': {}, + 'Bonds': {}, + + 'Stake': {}, + 'TotalStake': { + 0: 0 + }, + 'TotalIssuance': { + 0: 0 + }, + 'TotalHotkeyStake': {}, + 'TotalColdkeyStake': {}, + + 'TxRateLimit': { + 0: 0 # No limit + }, + + 'Delegates': {}, + + 'Axons': {}, + 'Prometheus': {}, + }, + } + + self.block_number = 0 + + self.network = 'mock' + self.chain_endpoint = 'mock_endpoint' + self.substrate = MagicMock() + + def __init__(self) -> None: + self.__dict__ = bittensor.__GLOBAL_MOCK_STATE__ + + if not hasattr(self, 'chain_state') or getattr(self, 'chain_state') is None: + self.setup() + + def get_block_hash(self, block_id: int) -> str: + return '0x' + sha256(str(block_id).encode()).hexdigest()[:64] + + + def create_subnet( self, netuid: int ) -> None: + subtensor_state = self.chain_state['SubtensorModule'] + if netuid not in subtensor_state['NetworksAdded']: + # Per Subnet + subtensor_state['Rho'][netuid] = {} + subtensor_state['Rho'][netuid][0] = 10 + subtensor_state['Kappa'][netuid] = {} + subtensor_state['Kappa'][netuid][0] = 32_767 + subtensor_state['Difficulty'][netuid] = {} + subtensor_state['Difficulty'][netuid][0] = 10_000_000 + subtensor_state['ImmunityPeriod'][netuid] = {} + subtensor_state['ImmunityPeriod'][netuid][0] = 4096 + subtensor_state['ValidatorBatchSize'][netuid] = {} + subtensor_state['ValidatorBatchSize'][netuid][0] = 32 + subtensor_state['ValidatorSequenceLength'][netuid] = {} + subtensor_state['ValidatorSequenceLength'][netuid][0] = 256 + subtensor_state['ValidatorEpochsPerReset'][netuid] = {} + subtensor_state['ValidatorEpochsPerReset'][netuid][0] = 60 + subtensor_state['ValidatorEpochLength'][netuid] = {} + subtensor_state['ValidatorEpochLength'][netuid][0] = 100 + subtensor_state['MaxAllowedValidators'][netuid] = {} + subtensor_state['MaxAllowedValidators'][netuid][0] = 128 + subtensor_state['MinAllowedWeights'][netuid] = {} + subtensor_state['MinAllowedWeights'][netuid][0] = 1024 + subtensor_state['MaxWeightLimit'][netuid] = {} + subtensor_state['MaxWeightLimit'][netuid][0] = 1_000 + subtensor_state['SynergyScalingLawPower'][netuid] = {} + subtensor_state['SynergyScalingLawPower'][netuid][0] = 50 + subtensor_state['ScalingLawPower'][netuid] = {} + subtensor_state['ScalingLawPower'][netuid][0] = 50 + subtensor_state['SubnetworkN'][netuid] = {} + subtensor_state['SubnetworkN'][netuid][0] = 0 + subtensor_state['MaxAllowedUids'][netuid] = {} + subtensor_state['MaxAllowedUids'][netuid][0] = 4096 + subtensor_state['NetworkModality'][netuid] = {} + subtensor_state['NetworkModality'][netuid][0] = 0 + subtensor_state['BlocksSinceLastStep'][netuid] = {} + subtensor_state['BlocksSinceLastStep'][netuid][0] = 0 + subtensor_state['Tempo'][netuid] = {} + subtensor_state['Tempo'][netuid][0] = 99 + # subtensor_state['NetworkConnect'][netuid] = {} + # subtensor_state['NetworkConnect'][netuid][0] = {} + subtensor_state['EmissionValues'][netuid] = {} + subtensor_state['EmissionValues'][netuid][0] = 0 + subtensor_state['Burn'][netuid] = {} + subtensor_state['Burn'][netuid][0] = 0 + + # Per-UID/Hotkey + + subtensor_state['Uids'][netuid] = {} + subtensor_state['Keys'][netuid] = {} + subtensor_state['Owner'][netuid] = {} + + subtensor_state['LastUpdate'][netuid] = {} + subtensor_state['Active'][netuid] = {} + subtensor_state['Rank'][netuid] = {} + subtensor_state['Emission'][netuid] = {} + subtensor_state['Incentive'][netuid] = {} + subtensor_state['Consensus'][netuid] = {} + subtensor_state['Trust'][netuid] = {} + subtensor_state['ValidatorTrust'][netuid] = {} + subtensor_state['Dividends'][netuid] = {} + subtensor_state['PruningScores'][netuid] = {} + subtensor_state['PruningScores'][netuid][0] = {} + subtensor_state['ValidatorPermit'][netuid] = {} + + subtensor_state['Weights'][netuid] = {} + subtensor_state['Bonds'][netuid] = {} + + subtensor_state['Axons'][netuid] = {} + subtensor_state['Prometheus'][netuid] = {} + + subtensor_state['NetworksAdded'][netuid] = {} + subtensor_state['NetworksAdded'][netuid][0] = True + else: - _owned_mock_subtensor_process = None - print ('Mock subtensor already running.') - - endpoint = bittensor.__mock_entrypoint__ - port = int(endpoint.split(':')[1]) - substrate = SubstrateInterface( - ss58_format = bittensor.__ss58_format__, - type_registry_preset='substrate-node-template', - type_registry = __type_registery__, - url = "ws://{}".format('localhost:{}'.format(port)), - use_remote_preset=True - ) - subtensor = Mock_Subtensor( - substrate = substrate, - network = 'mock', - chain_endpoint = 'localhost:{}'.format(port), - - # Is mocked, optionally has owned process for ref counting. - _is_mocked = True, - _owned_mock_subtensor_process = _owned_mock_subtensor_process - ) - return subtensor + raise Exception("Subnet already exists") - @classmethod - def global_mock_process_is_running(cls) -> bool: - r""" Check if the global mocked subtensor process is running under a process with the same name as this one. - """ - this_process = psutil.Process(os.getpid()) - for p in psutil.process_iter(): - if p.name() == GLOBAL_SUBTENSOR_MOCK_PROCESS_NAME and p.status() != psutil.STATUS_ZOMBIE and p.status() != psutil.STATUS_DEAD: - if p.parent().name == this_process.name: - print(f"Found process with name {p.name()}, parent {p.parent().pid} status {p.status()} and pid {p.pid}") - return True - return False + def set_difficulty( self, netuid: int, difficulty: int ) -> None: + subtensor_state = self.chain_state['SubtensorModule'] + if netuid not in subtensor_state['NetworksAdded']: + raise Exception("Subnet does not exist") - @classmethod - def kill_global_mock_process(self): - r""" Kills the global mocked subtensor process even if not owned. - """ - for p in psutil.process_iter(): - if p.name() == GLOBAL_SUBTENSOR_MOCK_PROCESS_NAME and p.parent().pid == os.getpid() : - p.terminate() - p.kill() - time.sleep(2) # Buffer to ensure the processes actually die + subtensor_state['Difficulty'][netuid][self.block_number] = difficulty - @classmethod - def create_global_mock_process(self, pid: int) -> 'subprocess.Popen[bytes]': - r""" Creates a global mocked subtensor process running in the backgroun with name GLOBAL_SUBTENSOR_MOCK_PROCESS_NAME. - """ - try: - operating_system = "OSX" if platform == "darwin" else "Linux" - path_root = "./tests/mock_subtensor" - path = "{}/bin/{}/{}".format(path_root, operating_system, GLOBAL_SUBTENSOR_MOCK_PROCESS_NAME) - path_to_spec = "{}/specs/local_raw.json".format(path_root) - - ws_port = int(bittensor.__mock_entrypoint__.split(':')[1]) - print(f'MockSub ws_port: {ws_port}') - - command_args = [ path ] + f'--chain {path_to_spec} --base-path {bittensor.__mock_chain_db__}_{pid} --execution native --ws-max-connections 1000 --no-mdns --rpc-cors all'.split(' ') + \ - f'--port {int(bittensor.get_random_unused_port())} --rpc-port {int(bittensor.get_random_unused_port())} --ws-port {ws_port}'.split(' ') + \ - '--validator --alice'.split(' ') - - print ('Starting subtensor process with command: {}'.format(command_args)) - - _mock_subtensor_process = subprocess.Popen( - command_args, - close_fds=True, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) - - # Wait for the process to start. Check for errors. - try: - # Timeout is okay. - error_code = _mock_subtensor_process.wait(timeout=12) - except subprocess.TimeoutExpired: - error_code = None - - if error_code is not None: - # Get the error message. - error_message = _mock_subtensor_process.stderr.read().decode('utf-8') - raise RuntimeError( 'Failed to start mocked subtensor process: {}'.format(error_code), error_message ) - - print ('Starting subtensor process with pid {} and name {}'.format(_mock_subtensor_process.pid, GLOBAL_SUBTENSOR_MOCK_PROCESS_NAME)) - - errored: bool = True - while errored: - errored = False - try: - _ = requests.get('http://localhost:{}'.format(ws_port)) - except requests.exceptions.ConnectionError as e: - errored = True - time.sleep(0.5) # Wait for the process to start. - - return _mock_subtensor_process - except Exception as e: - raise RuntimeError( 'Failed to start mocked subtensor process: {}'.format(e) ) - - -class Mock_Subtensor(subtensor_impl.Subtensor): - """ - Handles interactions with the subtensor chain. - """ - sudo_keypair: Keypair = Keypair.create_from_uri('//Alice') # Alice is the sudo keypair for the mock chain. + def _register_neuron( + self, + netuid: int, + hotkey: str, + coldkey: str, + ) -> int: + subtensor_state = self.chain_state['SubtensorModule'] + if netuid not in subtensor_state['NetworksAdded']: + raise Exception("Subnet does not exist") + + subnetwork_n = self._get_most_recent_storage(subtensor_state['SubnetworkN'][netuid]) + + if subnetwork_n > 0 and any(self._get_most_recent_storage(subtensor_state['Keys'][netuid][uid]) == hotkey for uid in range(subnetwork_n)): + # already_registered + raise Exception("Hotkey already registered") + else: + # Not found + if subnetwork_n >= self._get_most_recent_storage(subtensor_state['MaxAllowedUids'][netuid]): + # Subnet full, replace neuron randomly + uid = randint(0, subnetwork_n-1) + else: + # Subnet not full, add new neuron + # Append as next uid and increment subnetwork_n + uid = subnetwork_n + subtensor_state['SubnetworkN'][netuid][self.block_number] = subnetwork_n + 1 + + subtensor_state['Stake'][hotkey] = {} + subtensor_state['Stake'][hotkey][coldkey] = {} + subtensor_state['Stake'][hotkey][coldkey][self.block_number] = 0 + + subtensor_state['Uids'][netuid][hotkey] = {} + subtensor_state['Uids'][netuid][hotkey][self.block_number] = uid + + subtensor_state['Keys'][netuid][uid] = {} + subtensor_state['Keys'][netuid][uid][self.block_number] = hotkey + + subtensor_state['Owner'][hotkey] = {} + subtensor_state['Owner'][hotkey][self.block_number] = coldkey + + subtensor_state['Active'][netuid][uid] = {} + subtensor_state['Active'][netuid][uid][self.block_number] = True + + subtensor_state['LastUpdate'][netuid][uid] = {} + subtensor_state['LastUpdate'][netuid][uid][self.block_number] = self.block_number + + subtensor_state['Rank'][netuid][uid] = {} + subtensor_state['Rank'][netuid][uid][self.block_number] = 0.0 + + subtensor_state['Emission'][netuid][uid] = {} + subtensor_state['Emission'][netuid][uid][self.block_number] = 0.0 + + subtensor_state['Incentive'][netuid][uid] = {} + subtensor_state['Incentive'][netuid][uid][self.block_number] = 0.0 + + subtensor_state['Consensus'][netuid][uid] = {} + subtensor_state['Consensus'][netuid][uid][self.block_number] = 0.0 + + subtensor_state['Trust'][netuid][uid] = {} + subtensor_state['Trust'][netuid][uid][self.block_number] = 0.0 - def __init__( + subtensor_state['ValidatorTrust'][netuid][uid] = {} + subtensor_state['ValidatorTrust'][netuid][uid][self.block_number] = 0.0 + + subtensor_state['Dividends'][netuid][uid] = {} + subtensor_state['Dividends'][netuid][uid][self.block_number] = 0.0 + + subtensor_state['PruningScores'][netuid][uid] = {} + subtensor_state['PruningScores'][netuid][uid][self.block_number] = 0.0 + + subtensor_state['ValidatorPermit'][netuid][uid] = {} + subtensor_state['ValidatorPermit'][netuid][uid][self.block_number] = False + + subtensor_state['Weights'][netuid][uid] = {} + subtensor_state['Weights'][netuid][uid][self.block_number] = [] + + subtensor_state['Bonds'][netuid][uid] = {} + subtensor_state['Bonds'][netuid][uid][self.block_number] = [] + + subtensor_state['Axons'][netuid][hotkey] = {} + subtensor_state['Axons'][netuid][hotkey][self.block_number] = {} + + subtensor_state['Prometheus'][netuid][hotkey] = {} + subtensor_state['Prometheus'][netuid][hotkey][self.block_number] = {} + + if hotkey not in subtensor_state['IsNetworkMember']: + subtensor_state['IsNetworkMember'][hotkey] = {} + subtensor_state['IsNetworkMember'][hotkey][netuid] = {} + subtensor_state['IsNetworkMember'][hotkey][netuid][self.block_number] = True + + return uid + + @staticmethod + def _convert_to_balance( + balance: Union['bittensor.Balance', float, int] + ) -> 'bittensor.Balance': + if isinstance(balance, float): + balance = bittensor.Balance.from_tao(balance) + + if isinstance(balance, int): + balance = bittensor.Balance.from_rao(balance) + + return balance + + + def force_register_neuron( self, - _is_mocked: bool, - _owned_mock_subtensor_process: object, - **kwargs, - ): - r""" Initializes a subtensor chain interface. - Args: - _owned_mock_subtensor_process (Used for testing): - a subprocess where a mock chain is running. + netuid: int, + hotkey: str, + coldkey: str, + stake: Union['bittensor.Balance', float, int] = bittensor.Balance(0), + balance: Union['bittensor.Balance', float, int] = bittensor.Balance(0), + ) -> int: + """ + Force register a neuron on the mock chain, returning the UID. """ - super().__init__(**kwargs) - # Exclusively used to mock a connection to our chain. - self._owned_mock_subtensor_process = _owned_mock_subtensor_process - self._is_mocked = _is_mocked + stake = self._convert_to_balance(stake) + balance = self._convert_to_balance(balance) - print("---- MOCKED SUBTENSOR INITIALIZED ----") + subtensor_state = self.chain_state['SubtensorModule'] + if netuid not in subtensor_state['NetworksAdded']: + raise Exception("Subnet does not exist") - def __str__(self) -> str: - if self._is_mocked == True and self._owned_mock_subtensor_process != None: - # Mocked and owns background process. - return "MockSubtensor({}, PID:{})".format( self.chain_endpoint, self._owned_mock_subtensor_process.pid) - else: - # Mocked but does not own process. - return "MockSubtensor({})".format( self.chain_endpoint) + uid = self._register_neuron( + netuid=netuid, + hotkey=hotkey, + coldkey=coldkey, + ) + + subtensor_state['TotalStake'][self.block_number] = self._get_most_recent_storage(subtensor_state['TotalStake']) + stake.rao + subtensor_state['Stake'][hotkey][coldkey][self.block_number] = stake.rao - def __del__(self): - self.optionally_kill_owned_mock_instance() + if balance.rao > 0: + self.force_set_balance(coldkey, balance) + self.force_set_balance(coldkey, balance) - def __exit__(self): - pass + return uid - def optionally_kill_owned_mock_instance(self): - r""" If this subtensor instance owns the mock process, it kills the process. + def force_set_balance( + self, + ss58_address: str, + balance: Union['bittensor.Balance', float, int] = bittensor.Balance(0), + ) -> Tuple[bool, Optional[str]]: """ - if self._owned_mock_subtensor_process != None: - try: - self._owned_mock_subtensor_process.terminate() - self._owned_mock_subtensor_process.kill() - os.system("kill %i" % self._owned_mock_subtensor_process.pid) - time.sleep(2) # Buffer to ensure the processes actually die - except Exception as e: - print(f"failed to kill owned mock instance: {e}") - # Occasionally - pass - - def wrap_sudo(self, call: GenericCall) -> GenericCall: - r""" Wraps a call in a sudo call. + Returns: + Tuple[bool, Optional[str]]: (success, err_msg) """ - return self.substrate.compose_call( - call_module='Sudo', - call_function='sudo', - call_params = { - 'call': call.value + balance = self._convert_to_balance(balance) + + if ss58_address not in self.chain_state['System']['Account']: + self.chain_state['System']['Account'][ss58_address] = { + 'data': { + 'free': { + 0: 0, + }, + }, } - ) - def sudo_force_set_balance(self, ss58_address: str, balance: Union['bittensor.Balance', int, float], ) -> Tuple[bool, Optional[str]]: - r""" Sets the balance of an account using the sudo key. - """ - if isinstance(balance, bittensor.Balance): - balance = balance.rao - elif isinstance(balance, float): - balance = int(balance * bittensor.utils.RAOPERTAO) - elif isinstance(balance, int): - pass + old_balance = self.get_balance(ss58_address, self.block_number) + diff = balance.rao - old_balance.rao + + # Update total issuance + self.chain_state['SubtensorModule']['TotalIssuance'][self.block_number] = self._get_most_recent_storage(self.chain_state['SubtensorModule']['TotalIssuance']) + diff + + self.chain_state['System']['Account'][ss58_address] = { + 'data': { + 'free': { + self.block_number: balance.rao + } + } + } + + return True, None + + # Alias for force_set_balance + sudo_force_set_balance = force_set_balance + + def do_block_step( self ) -> None: + self.block_number += 1 + + # Doesn't do epoch + subtensor_state = self.chain_state['SubtensorModule'] + for subnet in subtensor_state['NetworksAdded']: + subtensor_state['BlocksSinceLastStep'][subnet][self.block_number] = self._get_most_recent_storage(subtensor_state['BlocksSinceLastStep'][subnet]) + 1 + + def _handle_type_default( self, name: str, params: List[object] ) -> object: + defaults_mapping = { + 'TotalStake': 0, + 'TotalHotkeyStake': 0, + 'TotalColdkeyStake': 0, + 'Stake': 0, + } + + return defaults_mapping.get(name, None) + + def query_subtensor( self, name: str, block: Optional[int] = None, params: Optional[List[object]] = [] ) -> MockSubtensorValue: + if block: + if self.block_number < block: + raise Exception("Cannot query block in the future") + else: - raise ValueError('Invalid type for balance: {}'.format(type(balance))) - - with self.substrate as substrate: - call = substrate.compose_call( - call_module='Balances', - call_function='set_balance', - call_params = { - 'who': ss58_address, - 'new_free': balance, - 'new_reserved': 0 - } + block = self.block_number + + state = self.chain_state['SubtensorModule'][name] + if state is not None: + # Use prefix + if len(params) > 0: + while state is not None and len(params) > 0: + state = state.get(params.pop(0), None) + if state is None: + return SimpleNamespace( + value = self._handle_type_default(name, params) + ) + + # Use block + state_at_block = state.get(block, None) + while state_at_block is None and block > 0: + block -= 1 + state_at_block = self.state.get(block, None) + if state_at_block is not None: + return SimpleNamespace( + value=state_at_block ) - wrapped_call = self.wrap_sudo(call) + return SimpleNamespace( + value = self._handle_type_default(name, params) + ) + else: + return SimpleNamespace( + value = self._handle_type_default(name, params) + ) + + def query_map_subtensor( self, name: str, block: Optional[int] = None, params: Optional[List[object]] = [] ) -> Optional[MockMapResult]: + """ + Note: Double map requires one param + """ + if block: + if self.block_number < block: + raise Exception("Cannot query block in the future") + + else: + block = self.block_number + + state = self.chain_state['SubtensorModule'][name] + if state is not None: + # Use prefix + if len(params) > 0: + while state is not None and len(params) > 0: + state = state.get(params.pop(0), None) + if state is None: + return MockMapResult([]) + + # Check if single map or double map + if len(state.keys()) == 0: + return MockMapResult([]) + + inner = list(state.values())[0] + # Should have at least one key + if len(inner.keys()) == 0: + raise Exception("Invalid state") + + # Check if double map + if isinstance(list(inner.values())[0], dict): + # is double map + raise ChainQueryError("Double map requires one param") + + # Iterate over each key and add value to list, max at block + records = [] + for key in state: + result = self._get_most_recent_storage(state[key], block) + if result is None: + continue # Skip if no result for this key at `block` or earlier - extrinsic = substrate.create_signed_extrinsic( call = wrapped_call, keypair = self.sudo_keypair ) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = True, wait_for_finalization = True ) + records.append((key, result)) + + return MockMapResult(records) + else: + return MockMapResult([]) + + def query_constant( self, module_name: str, constant_name: str, block: Optional[int] = None ) -> Optional[object]: + if block: + if self.block_number < block: + raise Exception("Cannot query block in the future") + + else: + block = self.block_number - response.process_events() - if response.is_success: - return True, None + state = self.chain_state.get(module_name, None) + if state is not None: + if constant_name in state: + state = state[constant_name] else: - return False, response.error_message - def sudo_set_tx_rate_limit(self, netuid: int, tx_rate_limit: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = True ) -> Tuple[bool, Optional[str]]: - r""" Sets the tx rate limit of the subnet in the mock chain using the sudo key. - """ - with self.substrate as substrate: - call = substrate.compose_call( - call_module='SubtensorModule', - call_function='sudo_set_tx_rate_limit', - call_params = { - 'netuid': netuid, - 'tx_rate_limit': tx_rate_limit - } - ) + return None + + # Use block + state_at_block = self._get_most_recent_storage(state, block) + if state_at_block is not None: + return SimpleNamespace(value=state_at_block) + + return state_at_block # Can be None + else: + return None + + def get_current_block( self ) -> int: + return self.block_number - wrapped_call = self.wrap_sudo(call) + # ==== Balance RPC methods ==== - extrinsic = substrate.create_signed_extrinsic( call = wrapped_call, keypair = self.sudo_keypair ) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + def get_balance(self, address: str, block: int = None) -> 'bittensor.Balance': + if block: + if self.block_number < block: + raise Exception("Cannot query block in the future") + + else: + block = self.block_number - if not wait_for_finalization: - return True, None - response.process_events() - if response.is_success: - return True, None + state = self.chain_state['System']['Account'] + if state is not None: + if address in state: + state = state[address] else: - return False, response.error_message - def sudo_set_difficulty(self, netuid: int, difficulty: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = True ) -> Tuple[bool, Optional[str]]: - r""" Sets the difficulty of the mock chain using the sudo key. - """ - with self.substrate as substrate: - call = substrate.compose_call( - call_module='SubtensorModule', - call_function='sudo_set_difficulty', - call_params = { - 'netuid': netuid, - 'difficulty': difficulty - } - ) + return bittensor.Balance(0) + + # Use block + balance_state = state['data']['free'] + state_at_block = self._get_most_recent_storage(balance_state, block) # Can be None + if state_at_block is not None: + bal_as_int = state_at_block + return bittensor.Balance.from_rao(bal_as_int) + else: + return bittensor.Balance(0) + else: + return bittensor.Balance(0) - wrapped_call = self.wrap_sudo(call) + def get_balances(self, block: int = None) -> Dict[str, 'bittensor.Balance']: + balances = {} + for address in self.chain_state['System']['Account']: + balances[address] = self.get_balance(address, block) - extrinsic = substrate.create_signed_extrinsic( call = wrapped_call, keypair = self.sudo_keypair ) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + return balances - if not wait_for_finalization: - return True, None + # ==== Neuron RPC methods ==== - if not wait_for_finalization: - return True, None + def neuron_for_uid( self, uid: int, netuid: int, block: Optional[int] = None ) -> Optional[NeuronInfo]: + if uid is None: + return NeuronInfo._null_neuron() + + if block: + if self.block_number < block: + raise Exception("Cannot query block in the future") - response.process_events() - if response.is_success: - return True, None - else: - return False, response.error_message + else: + block = self.block_number + + if netuid not in self.chain_state['SubtensorModule']['NetworksAdded']: + return None + + neuron_info = self._neuron_subnet_exists( uid, netuid, block ) + if neuron_info is None: + return None + + else: + return neuron_info + + def neurons(self, netuid: int, block: Optional[int] = None ) -> List[NeuronInfo]: + if netuid not in self.chain_state['SubtensorModule']['NetworksAdded']: + raise Exception("Subnet does not exist") + + neurons = [] + subnet_n = self._get_most_recent_storage( self.chain_state['SubtensorModule']['SubnetworkN'][netuid], block ) + for uid in range( subnet_n ): + neuron_info = self.neuron_for_uid( uid, netuid, block ) + if neuron_info is not None: + neurons.append(neuron_info) + + return neurons + + @staticmethod + def _get_most_recent_storage( storage: Dict[BlockNumber, Any], block_number: Optional[int] = None ) -> Any: + if block_number is None: + items = list(storage.items()) + items.sort(key=lambda x: x[0], reverse=True) + if len(items) == 0: + return None + + return items[0][1] + + else: + while block_number >= 0: + if block_number in storage: + return storage[block_number] + + block_number -= 1 + + return None + + def _get_axon_info( self, netuid: int, hotkey: str, block: Optional[int] = None ) -> AxonInfoDict: + # Axons [netuid][hotkey][block_number] + subtensor_state = self.chain_state['SubtensorModule'] + if netuid not in subtensor_state['Axons']: + return AxonInfoDict.default() + + if hotkey not in subtensor_state['Axons'][netuid]: + return AxonInfoDict.default() + + result = self._get_most_recent_storage(subtensor_state['Axons'][netuid][hotkey], block) + if not result: + return AxonInfoDict.default() + + return result + + def _get_prometheus_info( self, netuid: int, hotkey: str, block: Optional[int] = None ) -> PrometheusInfoDict: + subtensor_state = self.chain_state['SubtensorModule'] + if netuid not in subtensor_state['Prometheus']: + return PrometheusInfoDict.default() + + if hotkey not in subtensor_state['Prometheus'][netuid]: + return PrometheusInfoDict.default() + + result = self._get_most_recent_storage(subtensor_state['Prometheus'][netuid][hotkey], block) + if not result: + return PrometheusInfoDict.default() + + return result + + def _neuron_subnet_exists( self, uid: int, netuid: int, block: Optional[int] = None ) -> Optional[NeuronInfo]: + subtensor_state = self.chain_state['SubtensorModule'] + if netuid not in subtensor_state['NetworksAdded']: + return None + + if self._get_most_recent_storage(subtensor_state['SubnetworkN'][netuid]) <= uid: + return None + + hotkey = self._get_most_recent_storage(subtensor_state['Keys'][netuid][uid]) + if hotkey is None: + return None + + + axon_info_ = self._get_axon_info( netuid, hotkey, block ) + + prometheus_info = self._get_prometheus_info( netuid, hotkey, block ) + + + coldkey = self._get_most_recent_storage(subtensor_state['Owner'][hotkey], block) + active = self._get_most_recent_storage(subtensor_state['Active'][netuid][uid], block) + rank = self._get_most_recent_storage(subtensor_state['Rank'][netuid][uid], block) + emission = self._get_most_recent_storage(subtensor_state['Emission'][netuid][uid], block) + incentive = self._get_most_recent_storage(subtensor_state['Incentive'][netuid][uid], block) + consensus = self._get_most_recent_storage(subtensor_state['Consensus'][netuid][uid], block) + trust = self._get_most_recent_storage(subtensor_state['Trust'][netuid][uid], block) + validator_trust = self._get_most_recent_storage(subtensor_state['ValidatorTrust'][netuid][uid], block) + dividends = self._get_most_recent_storage(subtensor_state['Dividends'][netuid][uid], block) + pruning_score = self._get_most_recent_storage(subtensor_state['PruningScores'][netuid][uid], block) + last_update = self._get_most_recent_storage(subtensor_state['LastUpdate'][netuid][uid], block) + validator_permit = self._get_most_recent_storage(subtensor_state['ValidatorPermit'][netuid][uid], block) + + weights = self._get_most_recent_storage(subtensor_state['Weights'][netuid][uid], block) + bonds = self._get_most_recent_storage(subtensor_state['Bonds'][netuid][uid], block) + + stake_dict = {coldkey: bittensor.Balance.from_rao(self._get_most_recent_storage( + subtensor_state['Stake'][hotkey][coldkey], block + )) for coldkey in subtensor_state['Stake'][hotkey]} + + stake = sum(stake_dict.values()) + + + weights = [[int(weight[0]), int(weight[1])] for weight in weights] + bonds = [[int(bond[0]), int(bond[1])] for bond in bonds] + rank = U16_NORMALIZED_FLOAT(rank) + emission = emission / RAOPERTAO + incentive = U16_NORMALIZED_FLOAT(incentive) + consensus = U16_NORMALIZED_FLOAT(consensus) + trust = U16_NORMALIZED_FLOAT(trust) + validator_trust = U16_NORMALIZED_FLOAT(validator_trust) + dividends = U16_NORMALIZED_FLOAT(dividends) + prometheus_info = PrometheusInfo.fix_decoded_values(prometheus_info) + axon_info_ = axon_info.from_neuron_info( { + 'hotkey': hotkey, + 'coldkey': coldkey, + 'axon_info': axon_info_, + }) + + neuron_info = NeuronInfo( + hotkey = hotkey, + coldkey = coldkey, + uid = uid, + netuid = netuid, + active = active, + rank = rank, + emission = emission, + incentive = incentive, + consensus = consensus, + trust = trust, + validator_trust = validator_trust, + dividends = dividends, + pruning_score = pruning_score, + last_update = last_update, + validator_permit = validator_permit, + stake = stake, + stake_dict = stake_dict, + total_stake=stake, + prometheus_info=prometheus_info, + axon_info=axon_info_, + weights = weights, + bonds = bonds, + is_null=False, + ) - def sudo_set_max_difficulty(self, netuid: int, max_difficulty: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = True ) -> Tuple[bool, Optional[str]]: - r""" Sets the max difficulty of the mock chain using the sudo key. - """ - with self.substrate as substrate: - call = substrate.compose_call( - call_module='SubtensorModule', - call_function='sudo_set_max_difficulty', - call_params = { - 'netuid': netuid, - 'max_difficulty': max_difficulty - } - ) + return neuron_info - wrapped_call = self.wrap_sudo(call) - extrinsic = substrate.create_signed_extrinsic( call = wrapped_call, keypair = self.sudo_keypair ) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + def neuron_for_uid_lite( self, uid: int, netuid: int, block: Optional[int] = None ) -> Optional[NeuronInfoLite]: + if block: + if self.block_number < block: + raise Exception("Cannot query block in the future") + + else: + block = self.block_number + + if netuid not in self.chain_state['SubtensorModule']['NetworksAdded']: + raise Exception("Subnet does not exist") + + neuron_info = self._neuron_subnet_exists( uid, netuid, block ) + if neuron_info is None: + return None + + else: + neuron_info_dict = neuron_info.__dict__ + del neuron_info + del neuron_info_dict['weights'] + del neuron_info_dict['bonds'] + + neuron_info_lite = NeuronInfoLite( + **neuron_info_dict + ) + return neuron_info_lite + + def neurons_lite(self, netuid: int, block: Optional[int] = None ) -> List[NeuronInfoLite]: + if netuid not in self.chain_state['SubtensorModule']['NetworksAdded']: + raise Exception("Subnet does not exist") + + neurons = [] + subnet_n = self._get_most_recent_storage( self.chain_state['SubtensorModule']['SubnetworkN'][netuid] ) + for uid in range(subnet_n): + neuron_info = self.neuron_for_uid_lite( uid, netuid, block ) + if neuron_info is not None: + neurons.append(neuron_info) + + return neurons + + # Extrinsics + def _do_delegation( + self, + wallet: 'bittensor.Wallet', + delegate_ss58: str, + amount: 'bittensor.Balance', + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: + # Check if delegate + if not self.is_hotkey_delegate( + hotkey_ss58 = delegate_ss58 + ): + raise StakeError("Not a delegate") + + # do stake + success = self._do_stake( + wallet = wallet, + hotkey_ss58 = delegate_ss58, + amount = amount, + wait_for_inclusion = wait_for_inclusion, + wait_for_finalization = wait_for_finalization, + ) - if not wait_for_finalization: - return True, None + return success + - response.process_events() - if response.is_success: - return True, None - else: - return False, response.error_message + def _do_undelegation( + self, + wallet: 'bittensor.Wallet', + delegate_ss58: str, + amount: 'bittensor.Balance', + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: + # Check if delegate + if not self.is_hotkey_delegate( + hotkey_ss58 = delegate_ss58 + ): + raise StakeError("Not a delegate") + + # do unstake + self._do_unstake( + wallet = wallet, + hotkey_ss58 = delegate_ss58, + amount = amount, + wait_for_inclusion = wait_for_inclusion, + wait_for_finalization = wait_for_finalization, + ) + + def _do_nominate( + self, + wallet: 'bittensor.Wallet', + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: + hotkey_ss58 = wallet.hotkey.ss58_address + coldkey_ss58 = wallet.coldkeypub.ss58_address + + subtensor_state = self.chain_state['SubtensorModule'] + if self.is_hotkey_delegate( + hotkey_ss58=hotkey_ss58 + ): + return True + + else: + subtensor_state['Delegates'][hotkey_ss58] = {} + subtensor_state['Delegates'][hotkey_ss58][self.block_number] = 0.18 # Constant for now - def sudo_set_min_difficulty(self, netuid: int, min_difficulty: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = True ) -> Tuple[bool, Optional[str]]: - r""" Sets the min difficulty of the mock chain using the sudo key. - """ - with self.substrate as substrate: - call = substrate.compose_call( - call_module='SubtensorModule', - call_function='sudo_set_min_difficulty', - call_params = { - 'netuid': netuid, - 'min_difficulty': min_difficulty - } - ) + return True + + def get_transfer_fee( + self, + wallet: 'bittensor.Wallet', + dest: str, + value: Union['bittensor.Balance', float, int], + ) -> 'bittensor.Balance': + return bittensor.Balance( 700 ) + + def _do_transfer( + self, + wallet: 'bittensor.Wallet', + dest: str, + transfer_balance: 'bittensor.Balance', + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> Tuple[bool, Optional[str], Optional[str]]: + bal = self.get_balance(wallet.coldkeypub.ss58_address) + dest_bal = self.get_balance(dest) + transfer_fee = self.get_transfer_fee(wallet, dest, transfer_balance) + + existential_deposit = self.get_existential_deposit() + + if bal < transfer_balance + existential_deposit + transfer_fee: + raise TransferError("Insufficient balance") + + # Remove from the free balance + self.chain_state['System']['Account'][wallet.coldkeypub.ss58_address]['data']['free'][self.block_number] = (bal - transfer_balance - transfer_fee).rao + + # Add to the free balance + if dest not in self.chain_state['System']['Account']: + self.chain_state['System']['Account'][dest] = { + 'data': { + 'free': {}, + } + } - wrapped_call = self.wrap_sudo(call) + self.chain_state['System']['Account'][dest]['data']['free'][self.block_number] = (dest_bal + transfer_balance).rao - extrinsic = substrate.create_signed_extrinsic( call = wrapped_call, keypair = self.sudo_keypair ) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + return True, None, None - if not wait_for_finalization: - return True, None + def _do_pow_register( + self, + netuid: int, + wallet: 'bittensor.Wallet', + pow_result: 'POWSolution', + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> Tuple[bool, Optional[str]]: + # Assume pow result is valid + + subtensor_state = self.chain_state['SubtensorModule'] + if netuid not in subtensor_state['NetworksAdded']: + raise RegistrationError("Subnet does not exist") + + self._register_neuron( + netuid=netuid, + hotkey=wallet.hotkey.ss58_address, + coldkey=wallet.coldkeypub.ss58_address, + ) - response.process_events() - if response.is_success: - return True, None - else: - return False, response.error_message + return True, None - def sudo_add_network(self, netuid: int, tempo: int = 0, modality: int = 0, wait_for_inclusion: bool = True, wait_for_finalization: bool = True ) -> Tuple[bool, Optional[str]]: - r""" Adds a network to the mock chain using the sudo key. - """ - with self.substrate as substrate: - call = substrate.compose_call( - call_module='SubtensorModule', - call_function='sudo_add_network', - call_params = { - 'netuid': netuid, - 'tempo': tempo, - 'modality': modality - } - ) + def _do_burned_register( + self, + netuid: int, + wallet: 'bittensor.Wallet', + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> Tuple[bool, Optional[str]]: + subtensor_state = self.chain_state['SubtensorModule'] + if netuid not in subtensor_state['NetworksAdded']: + raise RegistrationError("Subnet does not exist") + + bal = self.get_balance( wallet.coldkeypub.ss58_address ) + burn = self.burn( netuid=netuid ) + existential_deposit = self.get_existential_deposit( ) + + if bal < burn + existential_deposit: + raise RegistrationError("Insufficient funds") + + self._register_neuron( + netuid=netuid, + hotkey=wallet.hotkey.ss58_address, + coldkey=wallet.coldkeypub.ss58_address, + ) - wrapped_call = self.wrap_sudo(call) + # Burn the funds + self.chain_state['System']['Account'][wallet.coldkeypub.ss58_address]['data']['free'][self.block_number] = (bal - burn).rao - extrinsic = substrate.create_signed_extrinsic( call = wrapped_call, keypair = self.sudo_keypair ) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + return True, None - if not wait_for_finalization: - return True, None + def _do_stake( + self, + wallet: 'bittensor.Wallet', + hotkey_ss58: str, + amount: 'bittensor.Balance', + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: + subtensor_state = self.chain_state['SubtensorModule'] + + bal = self.get_balance( wallet.coldkeypub.ss58_address ) + curr_stake = self.get_stake_for_coldkey_and_hotkey( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + ) + if curr_stake is None: + curr_stake = bittensor.Balance(0) + existential_deposit = self.get_existential_deposit( ) + + if bal < amount + existential_deposit: + raise StakeError("Insufficient funds") + + stake_state = subtensor_state['Stake'] + + # Stake the funds + if not hotkey_ss58 in stake_state: + stake_state[hotkey_ss58] = {} + if not wallet.coldkeypub.ss58_address in stake_state[hotkey_ss58]: + stake_state[hotkey_ss58][wallet.coldkeypub.ss58_address] = {} + + stake_state[hotkey_ss58][wallet.coldkeypub.ss58_address][self.block_number] = amount.rao + + # Add to total_stake storage + subtensor_state['TotalStake'][self.block_number] = self._get_most_recent_storage(subtensor_state['TotalStake']) + amount.rao + + total_hotkey_stake_state = subtensor_state['TotalHotkeyStake'] + if not hotkey_ss58 in total_hotkey_stake_state: + total_hotkey_stake_state[hotkey_ss58] = {} + + total_coldkey_stake_state = subtensor_state['TotalColdkeyStake'] + if not wallet.coldkeypub.ss58_address in total_coldkey_stake_state: + total_coldkey_stake_state[wallet.coldkeypub.ss58_address] = {} + + curr_total_hotkey_stake = self.query_subtensor( + name='TotalHotkeyStake', + params=[hotkey_ss58], + block=min(self.block_number - 1, 0), + ) + curr_total_coldkey_stake = self.query_subtensor( + name='TotalColdkeyStake', + params=[wallet.coldkeypub.ss58_address], + block=min(self.block_number - 1, 0), + ) - if not wait_for_finalization: - return True, None - - response.process_events() - if response.is_success: - return True, None - else: - return False, response.error_message - def sudo_register(self, netuid: int, hotkey: str, coldkey: str, stake: int = 0, balance: int = 0, wait_for_inclusion: bool = True, wait_for_finalization: bool = True ) -> Tuple[bool, Optional[str]]: - r""" Registers a neuron to the subnet using sudo. - """ - with self.substrate as substrate: - call = substrate.compose_call( - call_module='SubtensorModule', - call_function='sudo_register', - call_params = { - 'netuid': netuid, - 'hotkey': hotkey, - 'coldkey': coldkey, - 'stake': stake, - 'balance': balance - } - ) + total_hotkey_stake_state[hotkey_ss58][self.block_number] = curr_total_hotkey_stake.value + amount.rao + total_coldkey_stake_state[wallet.coldkeypub.ss58_address][self.block_number] = curr_total_coldkey_stake.value + amount.rao + + # Remove from free balance + self.chain_state['System']['Account'][wallet.coldkeypub.ss58_address]['data']['free'][self.block_number] = (bal - amount).rao - wrapped_call = self.wrap_sudo(call) + return True - extrinsic = substrate.create_signed_extrinsic( call = wrapped_call, keypair = self.sudo_keypair ) - response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + def _do_unstake( + self, + wallet: 'bittensor.Wallet', + hotkey_ss58: str, + amount: 'bittensor.Balance', + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: + subtensor_state = self.chain_state['SubtensorModule'] + + bal = self.get_balance( wallet.coldkeypub.ss58_address ) + curr_stake = self.get_stake_for_coldkey_and_hotkey( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + ) + if curr_stake is None: + curr_stake = bittensor.Balance(0) + + if curr_stake < amount: + raise StakeError("Insufficient funds") + + stake_state = subtensor_state['Stake'] + + if curr_stake.rao == 0: + return True + + # Unstake the funds + # We know that the hotkey has stake, so we can just remove it + stake_state[hotkey_ss58][wallet.coldkeypub.ss58_address][self.block_number] = (curr_stake - amount).rao + # Add to the free balance + if wallet.coldkeypub.ss58_address not in self.chain_state['System']['Account']: + self.chain_state['System']['Account'][wallet.coldkeypub.ss58_address] = { + 'data': { + 'free': {}, + } + } - if not wait_for_finalization: - return True, None + # Remove from total stake storage + subtensor_state['TotalStake'][self.block_number] = self._get_most_recent_storage(subtensor_state['TotalStake']) - amount.rao - if not wait_for_finalization: - return True, None - - response.process_events() - if response.is_success: - return True, None - else: - return False, response.error_message \ No newline at end of file + total_hotkey_stake_state = subtensor_state['TotalHotkeyStake'] + if not hotkey_ss58 in total_hotkey_stake_state: + total_hotkey_stake_state[hotkey_ss58] = {} + total_hotkey_stake_state[hotkey_ss58][self.block_number] = 0 # Shouldn't happen + + total_coldkey_stake_state = subtensor_state['TotalColdkeyStake'] + if not wallet.coldkeypub.ss58_address in total_coldkey_stake_state: + total_coldkey_stake_state[wallet.coldkeypub.ss58_address] = {} + total_coldkey_stake_state[wallet.coldkeypub.ss58_address][self.block_number] = amount.rao # Shouldn't happen + + total_hotkey_stake_state[hotkey_ss58][self.block_number] = self._get_most_recent_storage(subtensor_state['TotalHotkeyStake'][hotkey_ss58]) - amount.rao + total_coldkey_stake_state[wallet.coldkeypub.ss58_address][self.block_number] = self._get_most_recent_storage(subtensor_state['TotalColdkeyStake'][wallet.coldkeypub.ss58_address]) - amount.rao + + self.chain_state['System']['Account'][wallet.coldkeypub.ss58_address]['data']['free'][self.block_number] = (bal + amount).rao + + return True + + + def get_delegate_by_hotkey( self, hotkey_ss58: str, block: Optional[int] = None ) -> Optional['bittensor.DelegateInfo']: + subtensor_state = self.chain_state['SubtensorModule'] + + if hotkey_ss58 not in subtensor_state['Delegates']: + return None + + newest_state = self._get_most_recent_storage( + subtensor_state['Delegates'][hotkey_ss58], + block + ) + if newest_state is None: + return None + + nom_result = [] + nominators = subtensor_state['Stake'][hotkey_ss58] + for nominator in nominators: + nom_amount = self.get_stake_for_coldkey_and_hotkey( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=nominator, + block=block, + ) + if nom_amount is not None and nom_amount.rao > 0: + nom_result.append((nominator, nom_amount)) + + registered_subnets = [] + for subnet in self.get_all_subnet_netuids(block=block): + uid = self.get_uid_for_hotkey_on_subnet( + hotkey_ss58=hotkey_ss58, + netuid=subnet, + block=block, + ) + + if uid is not None: + registered_subnets.append((subnet, uid)) + + info = DelegateInfo( + hotkey_ss58=hotkey_ss58, + total_stake=self.get_total_stake_for_hotkey( + ss58_address=hotkey_ss58, + ) or bittensor.Balance(0), + nominators=nom_result, + owner_ss58=self.get_hotkey_owner( + hotkey_ss58=hotkey_ss58, + block=block, + ), + take=0.18, + validator_permits=[subnet for subnet, uid in registered_subnets if self.neuron_has_validator_permit( uid = uid, netuid = subnet, block=block )], + registrations=[subnet for subnet, _ in registered_subnets], + return_per_1000=bittensor.Balance.from_tao(1234567), # Doesn't matter for mock? + total_daily_return=bittensor.Balance.from_tao(1234567), # Doesn't matter for mock? + ) + + return info + + + def get_delegates( self, block: Optional[int] = None ) -> List['bittensor.DelegateInfo']: + subtensor_state = self.chain_state['SubtensorModule'] + delegates_info = [] + for hotkey in subtensor_state['Delegates']: + info = self.get_delegate_by_hotkey( + hotkey_ss58=hotkey, + block=block, + ) + if info is not None: + delegates_info.append(info) + + return delegates_info + + def get_delegated( self, coldkey_ss58: str, block: Optional[int] = None ) -> List[Tuple['bittensor.DelegateInfo', 'bittensor.Balance']]: + """ Returns the list of delegates that a given coldkey is staked to. + """ + delegates = self.get_delegates(block=block) + + result = [] + for delegate in delegates: + if coldkey_ss58 in delegate.nominators: + result.append((delegate, delegate.nominators[coldkey_ss58])) + + return result + + + def get_all_subnets_info( self, block: Optional[int] = None ) -> List[SubnetInfo]: + subtensor_state = self.chain_state['SubtensorModule'] + result = [] + for subnet in subtensor_state['NetworksAdded']: + info = self.get_subnet_info( + netuid=subnet, + block=block, + ) + if info is not None: + result.append(info) + + return result + + def get_subnet_info( self, netuid: int, block: Optional[int] = None ) -> Optional[SubnetInfo]: + if not self.subnet_exists( + netuid=netuid, + block=block, + ): + return None + + def query_subnet_info( name: str ) -> Optional[object]: + return self.query_subtensor( + name=name, + block=block, + params=[netuid] + ).value + + info = SubnetInfo( + netuid=netuid, + rho = query_subnet_info( + name = 'Rho', + ), + kappa=query_subnet_info( + name = 'Kappa', + ), + difficulty=query_subnet_info( + name = 'Difficulty', + ), + immunity_period=query_subnet_info( + name = 'ImmunityPeriod', + ), + validator_batch_size=query_subnet_info( + name = 'ValidatorBatchSize', + ), + validator_sequence_length=query_subnet_info( + name = 'ValidatorSequenceLength', + ), + validator_epochs_per_reset=query_subnet_info( + name = 'ValidatorEpochsPerReset', + ), + validator_epoch_length=query_subnet_info( + name = 'ValidatorEpochLength', + ), + max_allowed_validators=query_subnet_info( + name = 'MaxAllowedValidators', + ), + min_allowed_weights=query_subnet_info( + name = 'MinAllowedWeights', + ), + max_weight_limit=query_subnet_info( + name = 'MaxWeightLimit', + ), + scaling_law_power=query_subnet_info( + name = 'ScalingLawPower', + ), + synergy_scaling_law_power=query_subnet_info( + name = 'SynergyScalingLawPower', + ), + subnetwork_n=query_subnet_info( + name = 'SubnetworkN', + ), + max_n=query_subnet_info( + name = 'MaxAllowedUids', + ), + blocks_since_epoch=query_subnet_info( + name = 'BlocksSinceLastStep', + ), + tempo=query_subnet_info( + name = 'Tempo', + ), + modality=query_subnet_info( + name = 'NetworkModality', + ), + connection_requirements={ + str(netuid_.value): percentile.value for netuid_, percentile in self.query_map_subtensor( + name = 'NetworkConnect', + block = block, + params = [netuid] + ).records + }, + emission_value=query_subnet_info( + name = 'EmissionValues', + ), + burn=query_subnet_info( + name = 'Burn', + ), + ) + + return info + + def _do_serve_prometheus( + self, + wallet: 'bittensor.wallet', + call_params: 'PrometheusServeCallParams', + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> Tuple[bool, Optional[str]]: + return True, None + + def _do_set_weights( + self, + wallet: 'bittensor.wallet', + netuid: int, + uids: int, + vals: List[int], + version_key: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> Tuple[bool, Optional[str]]: + return True, None + + def _do_serve_axon( + self, + wallet: 'bittensor.wallet', + call_params: 'AxonServeCallParams', + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> Tuple[bool, Optional[str]]: + return True, None \ No newline at end of file diff --git a/bittensor/_keyfile/__init__.py b/bittensor/_subtensor/types.py similarity index 57% rename from bittensor/_keyfile/__init__.py rename to bittensor/_subtensor/types.py index 9c34fcda5b..63040abcbd 100644 --- a/bittensor/_keyfile/__init__.py +++ b/bittensor/_subtensor/types.py @@ -1,5 +1,5 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao +# Copyright © 2023 Opentensor Technologies Inc # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated # documentation files (the “Software”), to deal in the Software without restriction, including without limitation @@ -15,29 +15,24 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -import bittensor -from . import keyfile_impl +from typing import TypedDict -class keyfile (object): - """ Factory for a bittensor on device keypair +class AxonServeCallParams(TypedDict): """ - def __new__( - cls, - path: str = None, - _mock: bool = False - ) -> 'bittensor.Keyfile': - r""" Initialize a bittensor on device keypair interface. - - Args: - path (required=False, default: ~/.bittensor/wallets/default/coldkey ): - Path where this keypair is stored. - """ - path = '~/.bittensor/wallets/default/coldkey' if path == None else path - if _mock: - return keyfile_impl.MockKeyfile( path = path ) - else: - return keyfile_impl.Keyfile( path = path ) + Axon serve chain call parameters. + """ + version: int + ip: int + port: int + ip_type: int + netuid: int - @classmethod - def mock(cls): - return keyfile(_mock=True) +class PrometheusServeCallParams(TypedDict): + """ + Prometheus serve chain call parameters. + """ + version: int + ip: int + port: int + ip_type: int + netuid: int \ No newline at end of file diff --git a/bittensor/_synapse/text_prompting/synapse.py b/bittensor/_synapse/text_prompting/synapse.py index 77af418f22..779e4d2b0c 100644 --- a/bittensor/_synapse/text_prompting/synapse.py +++ b/bittensor/_synapse/text_prompting/synapse.py @@ -20,44 +20,10 @@ import bittensor from typing import List, Dict, Union, Callable -from abc import ABC, abstractmethod +from abc import abstractmethod import json -class SynapseForwardMulti( bittensor.SynapseCall ): - name: str = "text_prompting_forward_multi" - is_forward: bool = True - multi_completions: List[ str ] = [""] - - def __init__( - self, - synapse: "bittensor.TextPromptingSynapseMulti", - request_proto: bittensor.proto.MultiForwardTextPromptingRequest, - multi_forward_callback: Callable, - context: grpc.ServicerContext - ): - super().__init__( synapse = synapse, request_proto = request_proto, context = context ) - self.messages: List[ Dict[str, str] ] = request_proto.messages - self.formatted_messages = [ json.loads(message) for message in self.messages ] - self.multi_forward_callback = multi_forward_callback - - def apply( self ): - bittensor.logging.trace( "SynapseForward.apply()" ) - self.multi_completions = self.multi_forward_callback( messages = self.formatted_messages ) - bittensor.logging.trace( "SynapseForward.apply() = ", self.multi_completions ) - - def get_response_proto( self ) -> bittensor.proto.MultiForwardTextPromptingResponse: - bittensor.logging.trace( "SynapseForward.get_response_proto()") - return bittensor.MultiForwardTextPromptingResponse( multi_completions = self.multi_completions ) - - def get_inputs_shape(self) -> Union[torch.Size, None]: - bittensor.logging.trace( "SynapseForward.get_inputs_shape()" ) - return torch.Size( [ len(message) for message in self.messages ] ) - - def get_outputs_shape(self) -> Union[torch.Size, None]: - bittensor.logging.trace( "SynapseForward.get_outputs_shape()" ) - return torch.Size( [ len(self.multi_completions) ] ) - class SynapseForward( bittensor.SynapseCall ): name: str = "text_prompting_forward" is_forward: bool = True @@ -137,8 +103,6 @@ def __init__(self, axon: "bittensor.axon" ): @abstractmethod def forward( self, messages: List[Dict[str, str]] ) -> str: ... - def multi_forward( self, messages: List[Dict[str, str]] ) -> List[ str ]: ... - @abstractmethod def backward( self, messages: List[Dict[str, str]], response: str, rewards: torch.FloatTensor ) -> str: ... @@ -147,11 +111,6 @@ def Forward( self, request: bittensor.proto.ForwardTextPromptingRequest, context bittensor.logging.trace( 'Forward: {} '.format( call ) ) return self.apply( call = call ) - def MultiForward( self, request: bittensor.proto.MultiForwardTextPromptingRequest, context: grpc.ServicerContext ) -> bittensor.proto.MultiForwardTextPromptingResponse: - call = SynapseForwardMulti( self, request, self.multi_forward, context ) - bittensor.logging.trace( 'MultiForward: {} '.format( call ) ) - return self.apply( call = call ) - def Backward( self, request: bittensor.proto.BackwardTextPromptingRequest, context: grpc.ServicerContext ) -> bittensor.proto.BackwardTextPromptingResponse: call = SynapseBackward( self, request, self.backward, context ) bittensor.logging.trace( 'Backward: {}'.format( call ) ) diff --git a/bittensor/_threadpool/__init__.py b/bittensor/_threadpool/__init__.py index ac76d4812d..fe47e14a4a 100644 --- a/bittensor/_threadpool/__init__.py +++ b/bittensor/_threadpool/__init__.py @@ -56,7 +56,7 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None ): """ prefix_str = '' if prefix == None else prefix + '.' if prefix is not None: - if not hasattr(bittensor.defaults, prefix): + if bittensor.defaults.get(prefix, d=None) == None: setattr(bittensor.defaults, prefix, bittensor.Config()) getattr(bittensor.defaults, prefix).priority = bittensor.defaults.priority try: diff --git a/bittensor/_wallet/__init__.py b/bittensor/_wallet/__init__.py deleted file mode 100644 index 3bb036de3a..0000000000 --- a/bittensor/_wallet/__init__.py +++ /dev/null @@ -1,145 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -import argparse -import copy -from distutils.util import strtobool -import os - -import bittensor -from bittensor.utils import strtobool -from . import wallet_impl, wallet_mock - -class wallet: - """ Create and init wallet that stores hot and coldkey - """ - @classmethod - def mock(cls) -> 'bittensor.Wallet': - return wallet( name='mock' ) - - def __new__( - cls, - config: 'bittensor.Config' = None, - name: str = None, - hotkey: str = None, - path: str = None, - _mock: bool = None - ) -> 'bittensor.Wallet': - r""" Init bittensor wallet object containing a hot and coldkey. - - Args: - config (:obj:`bittensor.Config`, `optional`): - bittensor.wallet.config() - name (required=False, default='default'): - The name of the wallet to unlock for running bittensor - hotkey (required=False, default='default'): - The name of hotkey used to running the miner. - path (required=False, default='~/.bittensor/wallets/'): - The path to your bittensor wallets - _mock (required=False, default=False): - If true creates a mock wallet with random keys. - """ - if config == None: - config = wallet.config() - config = copy.deepcopy( config ) - config.wallet.name = name if name != None else config.wallet.name - config.wallet.hotkey = hotkey if hotkey != None else config.wallet.hotkey - config.wallet.path = path if path != None else config.wallet.path - config.wallet._mock = _mock if _mock != None else config.wallet._mock - wallet.check_config( config ) - # Allows mocking from the command line. - if config.wallet.get('name', bittensor.defaults.wallet.name) == 'mock' or config.wallet._mock: - config.wallet._mock = True - _mock = True - - return wallet_mock.Wallet_mock( - name = config.wallet.get('name', bittensor.defaults.wallet.name), - hotkey = config.wallet.get('hotkey', bittensor.defaults.wallet.hotkey), - path = config.wallet.path, - _mock = True, - config = config - ) - - network = config.get('subtensor.network', bittensor.defaults.subtensor.network) - - # Default to finney. - return wallet_impl.Wallet( - name = config.wallet.get('name', bittensor.defaults.wallet.name), - hotkey = config.wallet.get('hotkey', bittensor.defaults.wallet.hotkey), - path = config.wallet.path, - config = config - ) - - @classmethod - def config(cls) -> 'bittensor.Config': - """ Get config from the argument parser - Return: bittensor.config object - """ - parser = argparse.ArgumentParser() - wallet.add_args( parser ) - return bittensor.config( parser ) - - @classmethod - def help(cls): - """ Print help to stdout - """ - parser = argparse.ArgumentParser() - cls.add_args( parser ) - print (cls.__new__.__doc__) - parser.print_help() - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None ): - """ Accept specific arguments from parser - """ - prefix_str = '' if prefix == None else prefix + '.' - if prefix is not None: - if not hasattr(bittensor.defaults, prefix): - setattr(bittensor.defaults, prefix, bittensor.Config()) - getattr(bittensor.defaults, prefix).wallet = bittensor.defaults.wallet - try: - parser.add_argument('--' + prefix_str + 'wallet.name', required=False, default=argparse.SUPPRESS, help='''The name of the wallet to unlock for running bittensor (name mock is reserved for mocking this wallet)''') - parser.add_argument('--' + prefix_str + 'wallet.hotkey', required=False, default=argparse.SUPPRESS, help='''The name of wallet's hotkey.''') - parser.add_argument('--' + prefix_str + 'wallet.path', required=False, default=bittensor.defaults.wallet.path, help='''The path to your bittensor wallets''') - parser.add_argument('--' + prefix_str + 'wallet._mock', action='store_true', default=bittensor.defaults.wallet._mock, help='To turn on wallet mocking for testing purposes.') - - parser.add_argument('--' + prefix_str + 'wallet.reregister', required=False, action='store', default=bittensor.defaults.wallet.reregister, type=strtobool, help='''Whether to reregister the wallet if it is not already registered.''') - - except argparse.ArgumentError as e: - pass - - @classmethod - def add_defaults(cls, defaults): - """ Adds parser defaults to object from enviroment variables. - """ - defaults.wallet = bittensor.Config() - defaults.wallet.name = os.getenv('BT_WALLET_NAME') if os.getenv('BT_WALLET_NAME') != None else 'default' - defaults.wallet.hotkey = os.getenv('BT_WALLET_HOTKEY') if os.getenv('BT_WALLET_HOTKEY') != None else 'default' - defaults.wallet.path = os.getenv('BT_WALLET_PATH') if os.getenv('BT_WALLET_PATH') != None else '~/.bittensor/wallets/' - defaults.wallet._mock = os.getenv('BT_WALLET_MOCK') if os.getenv('BT_WALLET_MOCK') != None else False - # Defaults for registration - defaults.wallet.reregister = True - - @classmethod - def check_config(cls, config: 'bittensor.Config' ): - """ Check config for wallet name/hotkey/path/hotkeys/sort_by - """ - assert 'wallet' in config - assert isinstance(config.wallet.get('name', bittensor.defaults.wallet.name), str) - assert isinstance(config.wallet.get('hotkey', bittensor.defaults.wallet.hotkey), str ) or config.wallet.get('hotkey', bittensor.defaults.wallet.hotkey) == None - assert isinstance(config.wallet.path, str) - assert isinstance(config.wallet.reregister, bool) diff --git a/bittensor/_wallet/wallet_impl.py b/bittensor/_wallet/wallet_impl.py deleted file mode 100644 index af91191065..0000000000 --- a/bittensor/_wallet/wallet_impl.py +++ /dev/null @@ -1,887 +0,0 @@ -""" Implementation of the wallet class, which manages balances with staking and transfer. Also manages hotkey and coldkey. -""" -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -import os -import sys -from types import SimpleNamespace -from typing import Optional, Union, List, Tuple, Dict, overload - -import bittensor -from bittensor.utils import is_valid_bittensor_address_or_public_key -from substrateinterface import Keypair -from substrateinterface.base import is_valid_ss58_address -from termcolor import colored - - -def display_mnemonic_msg( keypair : Keypair, key_type : str ): - """ Displaying the mnemonic and warning message to keep mnemonic safe - """ - mnemonic = keypair.mnemonic - mnemonic_green = colored(mnemonic, 'green') - print (colored("\nIMPORTANT: Store this mnemonic in a secure (preferable offline place), as anyone " \ - "who has possesion of this mnemonic can use it to regenerate the key and access your tokens. \n", "red")) - print ("The mnemonic to the new {} is:\n\n{}\n".format(key_type, mnemonic_green)) - print ("You can use the mnemonic to recreate the key in case it gets lost. The command to use to regenerate the key using this mnemonic is:") - print("btcli regen_{} --mnemonic {}".format(key_type, mnemonic)) - print('') - -class Wallet(): - """ - Bittensor wallet maintenance class. Each wallet contains a coldkey and a hotkey. - The coldkey is the user's primary key for holding stake in their wallet - and is the only way that users can access Tao. Coldkeys can hold tokens and should be encrypted on your device. - The coldkey must be used to stake and unstake funds from a running node. The hotkey, on the other hand, is only used - for suscribing and setting weights from running code. Hotkeys are linked to coldkeys through the metagraph. - """ - def __init__( - self, - name:str, - path:str, - hotkey:str, - config: 'bittensor.Config' = None, - ): - r""" Init bittensor wallet object containing a hot and coldkey. - Args: - name (required=True, default='default): - The name of the wallet to unlock for running bittensor - hotkey (required=True, default='default): - The name of hotkey used to running the miner. - path (required=True, default='~/.bittensor/wallets/'): - The path to your bittensor wallets - config (:obj:`bittensor.Config`, `optional`): - bittensor.wallet.config() - """ - self.name = name - self.path = path - self.hotkey_str = hotkey - self._hotkey = None - self._coldkey = None - self._coldkeypub = None - self.config = config - - def __str__(self): - return "Wallet ({}, {}, {})".format(self.name, self.hotkey_str, self.path) - - def __repr__(self): - return self.__str__() - - def neuron(self, netuid: int) -> Optional['bittensor.NeuronInfo']: - return self.get_neuron(netuid=netuid) - - def trust(self, netuid: int) -> Optional[float]: - neuron = self.get_neuron(netuid=netuid) - if neuron is None: - return None - return neuron.trust - - def validator_trust(self, netuid: int) -> Optional[float]: - neuron = self.get_neuron(netuid=netuid) - if neuron is None: - return None - return neuron.validator_trust - - def rank(self, netuid: int) -> Optional[float]: - neuron = self.get_neuron(netuid=netuid) - if neuron is None: - return None - return neuron.rank - - def incentive(self, netuid: int) -> Optional[float]: - neuron = self.get_neuron(netuid=netuid) - if neuron is None: - return None - return neuron.incentive - - def dividends(self, netuid: int) -> Optional[float]: - neuron = self.get_neuron(netuid=netuid) - if neuron is None: - return None - return neuron.dividends - - def consensus(self, netuid: int) -> Optional[float]: - neuron = self.get_neuron(netuid=netuid) - if neuron is None: - return None - return neuron.consensus - - def last_update(self, netuid: int) -> Optional[int]: - neuron = self.get_neuron(netuid=netuid) - if neuron is None: - return None - return neuron.last_update - - def validator_permit(self, netuid: int) -> Optional[bool]: - neuron = self.get_neuron(netuid=netuid) - if neuron is None: - return None - return neuron.validator_permit - - def weights(self, netuid: int) -> Optional[List[List[int]]]: - neuron = self.get_neuron(netuid=netuid) - if neuron is None: - return None - return neuron.weights - - def bonds(self, netuid: int) -> Optional[List[List[int]]]: - neuron = self.get_neuron(netuid=netuid) - if neuron is None: - return None - return neuron.bonds - - def uid(self, netuid: int) -> int: - return self.get_uid(netuid=netuid) - - @property - def stake(self) -> 'bittensor.Balance': - return self.get_stake() - - @property - def balance(self) -> 'bittensor.Balance': - return self.get_balance() - - def is_registered( self, subtensor: Optional['bittensor.Subtensor'] = None, netuid: Optional[int] = None ) -> bool: - """ Returns true if this wallet is registered. - Args: - subtensor( Optional['bittensor.Subtensor'] ): - Bittensor subtensor connection. Overrides with defaults if None. - Determines which network we check for registration. - netuid ( Optional[int] ): - The network uid to check for registration. - Default is None, which checks any subnetwork. - Return: - is_registered (bool): - Is the wallet registered on the chain. - """ - if subtensor == None: subtensor = bittensor.subtensor(self.config) - - # default to finney - if netuid == None: - return subtensor.is_hotkey_registered_any( self.hotkey.ss58_address ) - else: - return subtensor.is_hotkey_registered_on_subnet( self.hotkey.ss58_address, netuid = netuid ) - - def is_senate_member( self, subtensor: Optional['bittensor.Subtensor'] = None ) -> bool: - """ Returns true if this wallet is registered as a senate member. - Args: - subtensor( Optional['bittensor.Subtensor'] ): - Bittensor subtensor connection. Overrides with defaults if None. - Determines which network we check for senate membership. - Return: - is_registered (bool): - Is the wallet apart of the senate. - """ - if subtensor == None: subtensor = bittensor.subtensor(self.config) - - # default to finney - return subtensor.is_senate_member( self.hotkey.ss58_address ) - - - def get_neuron ( self, netuid: int, subtensor: Optional['bittensor.Subtensor'] = None ) -> Optional['bittensor.NeuronInfo'] : - """ Returns this wallet's neuron information from subtensor. - Args: - netuid (int): - The network uid of the subnet to query. - subtensor( Optional['bittensor.Subtensor'] ): - Bittensor subtensor connection. Overrides with defaults if None. - Return: - neuron (Union[ NeuronInfo, None ]): - neuron account on the chain or None if you are not registered. - """ - if subtensor == None: subtensor = bittensor.subtensor() - if not self.is_registered(netuid = netuid, subtensor=subtensor): - print(colored('This wallet is not registered. Call wallet.register() before this function.','red')) - return None - neuron = subtensor.neuron_for_wallet( self, netuid = netuid ) - return neuron - - def get_uid ( self, netuid: int, subtensor: Optional['bittensor.Subtensor'] = None ) -> int: - """ Returns this wallet's hotkey uid or -1 if the hotkey is not subscribed. - Args: - netuid (int): - The network uid of the subnet to query. - subtensor( Optional['bittensor.Subtensor'] ): - Bittensor subtensor connection. Overrides with defaults if None. - Return: - uid (int): - Network uid or -1 if you are not registered. - """ - if subtensor == None: subtensor = bittensor.subtensor() - if not self.is_registered(netuid = netuid, subtensor=subtensor): - print(colored('This wallet is not registered. Call wallet.register() before this function.','red')) - return -1 - neuron = self.get_neuron(netuid = netuid, subtensor = subtensor) - if neuron.is_null: - return -1 - else: - return neuron.uid - - def get_stake ( self, subtensor: Optional['bittensor.Subtensor'] = None ) -> 'bittensor.Balance': - """ Returns this wallet's staking balance from passed subtensor connection. - Args: - subtensor( Optional['bittensor.Subtensor'] ): - Bittensor subtensor connection. Overrides with defaults if None. - Return: - balance (bittensor.utils.balance.Balance): - Stake account balance. - """ - if subtensor == None: subtensor = bittensor.subtensor() - stake = subtensor.get_stake_for_coldkey_and_hotkey( hotkey_ss58 = self.hotkey.ss58_address, coldkey_ss58 = self.coldkeypub.ss58_address ) - if not stake: # Not registered. - print(colored('This wallet is not registered. Call wallet.register() before this function.','red')) - return bittensor.Balance(0) - - return stake - - def get_balance( self, subtensor: Optional['bittensor.Subtensor'] = None ) -> 'bittensor.Balance': - """ Returns this wallet's coldkey balance from passed subtensor connection. - Args: - subtensor( Optional['bittensor.Subtensor'] ): - Bittensor subtensor connection. Overrides with defaults if None. - Return: - balance (bittensor.utils.balance.Balance): - Coldkey balance. - """ - if subtensor == None: subtensor = bittensor.subtensor() - return subtensor.get_balance(address = self.coldkeypub.ss58_address) - - def reregister( - self, - netuid: int, - subtensor: Optional['bittensor.Subtensor'] = None, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False - ) -> Optional['bittensor.Wallet']: - """ Re-register this wallet on the chain. - Args: - netuid (int): - The network uid of the subnet to register on. - subtensor( Optional['bittensor.Subtensor'] ): - Bittensor subtensor connection. Overrides with defaults if None. - wait_for_inclusion (bool): - if set, waits for the extrinsic to enter a block before returning true, - or returns false if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): - if set, waits for the extrinsic to be finalized on the chain before returning true, - or returns false if the extrinsic fails to be finalized within the timeout. - prompt (bool): - If true, the call waits for confirmation from the user before proceeding. - - Return: - wallet (bittensor.Wallet): - This wallet. - """ - if subtensor == None: - subtensor = bittensor.subtensor() - if not self.is_registered(netuid = netuid, subtensor=subtensor): - # Check if the wallet should reregister - if not self.config.wallet.get('reregister'): - sys.exit(0) - - self.register( - subtensor = subtensor, - netuid = netuid, - prompt = prompt, - TPB = self.config.subtensor.register.cuda.get('TPB', None), - update_interval = self.config.subtensor.register.cuda.get('update_interval', None), - num_processes = self.config.subtensor.register.get('num_processes', None), - cuda = self.config.subtensor.register.cuda.get('use_cuda', bittensor.defaults.subtensor.register.cuda.use_cuda), - dev_id = self.config.subtensor.register.cuda.get('dev_id', None), - wait_for_inclusion = wait_for_inclusion, - wait_for_finalization = wait_for_finalization, - output_in_place = self.config.subtensor.register.get('output_in_place', bittensor.defaults.subtensor.register.output_in_place), - log_verbose = self.config.subtensor.register.get('verbose', bittensor.defaults.subtensor.register.verbose), - ) - - return self - - def register ( - self, - netuid: int, - subtensor: Optional['bittensor.Subtensor'] = None, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, - max_allowed_attempts: int = 3, - cuda: bool = False, - dev_id: int = 0, - TPB: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - output_in_place: bool = True, - log_verbose: bool = False, - ) -> 'bittensor.Wallet': - """ Registers the wallet to chain. - Args: - netuid (int): - The network uid of the subnet to register on. - subtensor( Optional['bittensor.Subtensor'] ): - Bittensor subtensor connection. Overrides with defaults if None. - wait_for_inclusion (bool): - If set, waits for the extrinsic to enter a block before returning true, - or returns false if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): - If set, waits for the extrinsic to be finalized on the chain before returning true, - or returns false if the extrinsic fails to be finalized within the timeout. - prompt (bool): - If true, the call waits for confirmation from the user before proceeding. - max_allowed_attempts (int): - Maximum number of attempts to register the wallet. - cuda (bool): - If true, the wallet should be registered on the cuda device. - dev_id (int): - The cuda device id. - TPB (int): - The number of threads per block (cuda). - num_processes (int): - The number of processes to use to register. - update_interval (int): - The number of nonces to solve between updates. - output_in_place (bool): - If true, the registration output is printed in-place. - log_verbose (bool): - If true, the registration output is more verbose. - Returns: - success (bool): - flag is true if extrinsic was finalized or uncluded in the block. - If we did not wait for finalization / inclusion, the response is true. - """ - # Get chain connection. - if subtensor == None: subtensor = bittensor.subtensor() - subtensor.register( - wallet = self, - wait_for_inclusion = wait_for_inclusion, - wait_for_finalization = wait_for_finalization, - prompt=prompt, max_allowed_attempts=max_allowed_attempts, - output_in_place = output_in_place, - cuda=cuda, - dev_id=dev_id, - TPB=TPB, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - netuid = netuid, - ) - - return self - - def add_stake( self, - amount: Union[float, bittensor.Balance] = None, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - subtensor: Optional['bittensor.Subtensor'] = None, - prompt: bool = False - ) -> bool: - """ Stakes tokens from this wallet's coldkey onto it's hotkey. - Args: - amount_tao (float): - amount of tao to stake or bittensor balance object. If None, stakes all available balance. - wait_for_inclusion (bool): - if set, waits for the extrinsic to enter a block before returning true, - or returns false if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): - if set, waits for the extrinsic to be finalized on the chain before returning true, - or returns false if the extrinsic fails to be finalized within the timeout. - subtensor( `bittensor.Subtensor` ): - Bittensor subtensor connection. Overrides with defaults if None. - prompt (bool): - If true, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): - flag is true if extrinsic was finalized or uncluded in the block. - If we did not wait for finalization / inclusion, the response is true. - """ - if subtensor == None: subtensor = bittensor.subtensor() - return subtensor.add_stake( wallet = self, amount = amount, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization, prompt=prompt ) - - def remove_stake( self, - amount: Union[float, bittensor.Balance] = None, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - subtensor: Optional['bittensor.Subtensor'] = None, - prompt: bool = False, - ) -> bool: - """ Removes stake from this wallet's hotkey and moves them onto it's coldkey balance. - Args: - amount_tao (float): - amount of tao to unstake or bittensor balance object. If None, unstakes all available hotkey balance. - wait_for_inclusion (bool): - if set, waits for the extrinsic to enter a block before returning true, - or returns false if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): - if set, waits for the extrinsic to be finalized on the chain before returning true, - or returns false if the extrinsic fails to be finalized within the timeout. - subtensor( `bittensor.Subtensor` ): - Bittensor subtensor connection. Overrides with defaults if None. - prompt (bool): - If true, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): - flag is true if extrinsic was finalized or uncluded in the block. - If we did not wait for finalization / inclusion, the response is true. - """ - if subtensor == None: subtensor = bittensor.subtensor() - return subtensor.unstake( wallet = self, amount = amount, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization, prompt=prompt ) - - def transfer( - self, - dest:str, - amount: Union[float, bittensor.Balance] , - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - subtensor: Optional['bittensor.Subtensor'] = None, - prompt: bool = False, - ) -> bool: - """ Transfers Tao from this wallet's coldkey to the destination address. - Args: - dest (`type`:str, required): - The destination address either encoded as a ss58 or ed255 public-key string of - secondary account. - amount (float, required): - amount of tao to transfer or a bittensor balance object. - wait_for_inclusion (bool): - if set, waits for the extrinsic to enter a block before returning true, - or returns false if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): - if set, waits for the extrinsic to be finalized on the chain before returning true, - or returns false if the extrinsic fails to be finalized within the timeout. - subtensor( `bittensor.Subtensor` ): - Bittensor subtensor connection. Overrides with defaults if None. - prompt (bool): - If true, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): - flag is true if extrinsic was finalized or uncluded in the block. - If we did not wait for finalization / inclusion, the response is true. - """ - if subtensor == None: subtensor = bittensor.subtensor() - return subtensor.transfer( wallet = self, dest = dest, amount = amount, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization, prompt=prompt ) - - def create_if_non_existent( self, coldkey_use_password:bool = True, hotkey_use_password:bool = False) -> 'Wallet': - """ Checks for existing coldkeypub and hotkeys and creates them if non-existent. - """ - return self.create(coldkey_use_password, hotkey_use_password) - - def create (self, coldkey_use_password:bool = True, hotkey_use_password:bool = False ) -> 'Wallet': - """ Checks for existing coldkeypub and hotkeys and creates them if non-existent. - """ - # ---- Setup Wallet. ---- - if not self.coldkey_file.exists_on_device() and not self.coldkeypub_file.exists_on_device(): - self.create_new_coldkey( n_words = 12, use_password = coldkey_use_password ) - if not self.hotkey_file.exists_on_device(): - self.create_new_hotkey( n_words = 12, use_password = hotkey_use_password ) - return self - - def recreate (self, coldkey_use_password:bool = True, hotkey_use_password:bool = False ) -> 'Wallet': - """ Checks for existing coldkeypub and hotkeys and creates them if non-existent. - """ - # ---- Setup Wallet. ---- - self.create_new_coldkey( n_words = 12, use_password = coldkey_use_password ) - self.create_new_hotkey( n_words = 12, use_password = hotkey_use_password ) - return self - - @property - def hotkey_file(self) -> 'bittensor.Keyfile': - - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - hotkey_path = os.path.join(wallet_path, "hotkeys", self.hotkey_str) - return bittensor.keyfile( path = hotkey_path ) - - @property - def coldkey_file(self) -> 'bittensor.Keyfile': - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - coldkey_path = os.path.join(wallet_path, "coldkey") - return bittensor.keyfile( path = coldkey_path ) - - @property - def coldkeypub_file(self) -> 'bittensor.Keyfile': - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - coldkeypub_path = os.path.join(wallet_path, "coldkeypub.txt") - return bittensor.Keyfile( path = coldkeypub_path ) - - def set_hotkey(self, keypair: 'bittensor.Keypair', encrypt: bool = False, overwrite: bool = False) -> 'bittensor.Keyfile': - self._hotkey = keypair - self.hotkey_file.set_keypair( keypair, encrypt = encrypt, overwrite = overwrite ) - - def set_coldkeypub(self, keypair: 'bittensor.Keypair', encrypt: bool = False, overwrite: bool = False) -> 'bittensor.Keyfile': - self._coldkeypub = Keypair(ss58_address=keypair.ss58_address) - self.coldkeypub_file.set_keypair( self._coldkeypub, encrypt = encrypt, overwrite = overwrite ) - - def set_coldkey(self, keypair: 'bittensor.Keypair', encrypt: bool = True, overwrite: bool = False) -> 'bittensor.Keyfile': - self._coldkey = keypair - self.coldkey_file.set_keypair( self._coldkey, encrypt = encrypt, overwrite = overwrite ) - - def get_coldkey(self, password: str = None ) -> 'bittensor.Keypair': - self.coldkey_file.get_keypair( password = password ) - - def get_hotkey(self, password: str = None ) -> 'bittensor.Keypair': - self.hotkey_file.get_keypair( password = password ) - - def get_coldkeypub(self, password: str = None ) -> 'bittensor.Keypair': - self.coldkeypub_file.get_keypair( password = password ) - - @property - def hotkey(self) -> 'bittensor.Keypair': - r""" Loads the hotkey from wallet.path/wallet.name/hotkeys/wallet.hotkey or raises an error. - Returns: - hotkey (Keypair): - hotkey loaded from config arguments. - Raises: - KeyFileError: Raised if the file is corrupt of non-existent. - CryptoKeyError: Raised if the user enters an incorrec password for an encrypted keyfile. - """ - if self._hotkey == None: - self._hotkey = self.hotkey_file.keypair - return self._hotkey - - @property - def coldkey(self) -> 'bittensor.Keypair': - r""" Loads the hotkey from wallet.path/wallet.name/coldkey or raises an error. - Returns: - coldkey (Keypair): - colkey loaded from config arguments. - Raises: - KeyFileError: Raised if the file is corrupt of non-existent. - CryptoKeyError: Raised if the user enters an incorrec password for an encrypted keyfile. - """ - if self._coldkey == None: - self._coldkey = self.coldkey_file.keypair - return self._coldkey - - @property - def coldkeypub(self) -> 'bittensor.Keypair': - r""" Loads the coldkeypub from wallet.path/wallet.name/coldkeypub.txt or raises an error. - Returns: - coldkeypub (Keypair): - colkeypub loaded from config arguments. - Raises: - KeyFileError: Raised if the file is corrupt of non-existent. - CryptoKeyError: Raised if the user enters an incorrect password for an encrypted keyfile. - """ - if self._coldkeypub == None: - self._coldkeypub = self.coldkeypub_file.keypair - return self._coldkeypub - - def create_coldkey_from_uri(self, uri:str, use_password: bool = True, overwrite:bool = False) -> 'Wallet': - """ Creates coldkey from suri string, optionally encrypts it with the user's inputed password. - Args: - uri: (str, required): - URI string to use i.e. /Alice or /Bob - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Will this operation overwrite the coldkey under the same path //coldkey - Returns: - wallet (bittensor.Wallet): - this object with newly created coldkey. - """ - keypair = Keypair.create_from_uri( uri ) - display_mnemonic_msg( keypair, "coldkey" ) - self.set_coldkey( keypair, encrypt = use_password, overwrite = overwrite) - self.set_coldkeypub( keypair, overwrite = overwrite) - return self - - def create_hotkey_from_uri( self, uri:str, use_password: bool = False, overwrite:bool = False) -> 'Wallet': - """ Creates hotkey from suri string, optionally encrypts it with the user's inputed password. - Args: - uri: (str, required): - URI string to use i.e. /Alice or /Bob - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Will this operation overwrite the hotkey under the same path //hotkeys/ - Returns: - wallet (bittensor.Wallet): - this object with newly created hotkey. - """ - keypair = Keypair.create_from_uri( uri ) - display_mnemonic_msg( keypair, "hotkey" ) - self.set_hotkey( keypair, encrypt=use_password, overwrite = overwrite) - return self - - def new_coldkey( self, n_words:int = 12, use_password: bool = True, overwrite:bool = False) -> 'Wallet': - """ Creates a new coldkey, optionally encrypts it with the user's inputed password and saves to disk. - Args: - n_words: (int, optional): - Number of mnemonic words to use. - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Will this operation overwrite the coldkey under the same path //coldkey - Returns: - wallet (bittensor.Wallet): - this object with newly created coldkey. - """ - self.create_new_coldkey( n_words, use_password, overwrite ) - - def create_new_coldkey( self, n_words:int = 12, use_password: bool = True, overwrite:bool = False) -> 'Wallet': - """ Creates a new coldkey, optionally encrypts it with the user's inputed password and saves to disk. - Args: - n_words: (int, optional): - Number of mnemonic words to use. - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Will this operation overwrite the coldkey under the same path //coldkey - Returns: - wallet (bittensor.Wallet): - this object with newly created coldkey. - """ - mnemonic = Keypair.generate_mnemonic( n_words) - keypair = Keypair.create_from_mnemonic(mnemonic) - display_mnemonic_msg( keypair, "coldkey" ) - self.set_coldkey( keypair, encrypt = use_password, overwrite = overwrite) - self.set_coldkeypub( keypair, overwrite = overwrite) - return self - - def new_hotkey( self, n_words:int = 12, use_password: bool = False, overwrite:bool = False) -> 'Wallet': - """ Creates a new hotkey, optionally encrypts it with the user's inputed password and saves to disk. - Args: - n_words: (int, optional): - Number of mnemonic words to use. - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Will this operation overwrite the hotkey under the same path //hotkeys/ - Returns: - wallet (bittensor.Wallet): - this object with newly created hotkey. - """ - self.create_new_hotkey( n_words, use_password, overwrite ) - - def create_new_hotkey( self, n_words:int = 12, use_password: bool = False, overwrite:bool = False) -> 'Wallet': - """ Creates a new hotkey, optionally encrypts it with the user's inputed password and saves to disk. - Args: - n_words: (int, optional): - Number of mnemonic words to use. - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Will this operation overwrite the hotkey under the same path //hotkeys/ - Returns: - wallet (bittensor.Wallet): - this object with newly created hotkey. - """ - mnemonic = Keypair.generate_mnemonic( n_words) - keypair = Keypair.create_from_mnemonic(mnemonic) - display_mnemonic_msg( keypair, "hotkey" ) - self.set_hotkey( keypair, encrypt=use_password, overwrite = overwrite) - return self - - def regenerate_coldkeypub( self, ss58_address: Optional[str] = None, public_key: Optional[Union[str, bytes]] = None, overwrite: bool = False ) -> 'Wallet': - """ Regenerates the coldkeypub from passed ss58_address or public_key and saves the file - Requires either ss58_address or public_key to be passed. - Args: - ss58_address: (str, optional): - Address as ss58 string. - public_key: (str | bytes, optional): - Public key as hex string or bytes. - overwrite (bool, optional) (default: False): - Will this operation overwrite the coldkeypub (if exists) under the same path //coldkeypub - Returns: - wallet (bittensor.Wallet): - newly re-generated Wallet with coldkeypub. - - """ - if ss58_address is None and public_key is None: - raise ValueError("Either ss58_address or public_key must be passed") - - if not is_valid_bittensor_address_or_public_key( ss58_address if ss58_address is not None else public_key ): - raise ValueError(f"Invalid {'ss58_address' if ss58_address is not None else 'public_key'}") - - if ss58_address is not None: - ss58_format = bittensor.utils.get_ss58_format( ss58_address ) - keypair = Keypair(ss58_address=ss58_address, public_key=public_key, ss58_format=ss58_format) - else: - keypair = Keypair(ss58_address=ss58_address, public_key=public_key, ss58_format=bittensor.__ss58_format__) - - # No need to encrypt the public key - self.set_coldkeypub( keypair, overwrite = overwrite) - - return self - - # Short name for regenerate_coldkeypub - regen_coldkeypub = regenerate_coldkeypub - - @overload - def regenerate_coldkey( - self, - mnemonic: Optional[Union[list, str]] = None, - use_password: bool = True, - overwrite: bool = False - ) -> 'Wallet': - ... - - @overload - def regenerate_coldkey( - self, - seed: Optional[str] = None, - use_password: bool = True, - overwrite: bool = False - ) -> 'Wallet': - ... - - @overload - def regenerate_coldkey( - self, - json: Optional[Tuple[Union[str, Dict], str]] = None, - use_password: bool = True, - overwrite: bool = False - ) -> 'Wallet': - ... - - - def regenerate_coldkey( - self, - use_password: bool = True, - overwrite: bool = False, - **kwargs - ) -> 'Wallet': - """ Regenerates the coldkey from passed mnemonic, seed, or json encrypts it with the user's password and saves the file - Args: - mnemonic: (Union[list, str], optional): - Key mnemonic as list of words or string space separated words. - seed: (str, optional): - Seed as hex string. - json: (Tuple[Union[str, Dict], str], optional): - Restore from encrypted JSON backup as (json_data: Union[str, Dict], passphrase: str) - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Will this operation overwrite the coldkey under the same path //coldkey - Returns: - wallet (bittensor.Wallet): - this object with newly created coldkey. - - Note: uses priority order: mnemonic > seed > json - """ - if len(kwargs) == 0: - raise ValueError("Must pass either mnemonic, seed, or json") - - # Get from kwargs - mnemonic = kwargs.get('mnemonic', None) - seed = kwargs.get('seed', None) - json = kwargs.get('json', None) - - if mnemonic is None and seed is None and json is None: - raise ValueError("Must pass either mnemonic, seed, or json") - if mnemonic is not None: - if isinstance( mnemonic, str): mnemonic = mnemonic.split() - if len(mnemonic) not in [12,15,18,21,24]: - raise ValueError("Mnemonic has invalid size. This should be 12,15,18,21 or 24 words") - keypair = Keypair.create_from_mnemonic(" ".join(mnemonic), ss58_format=bittensor.__ss58_format__ ) - display_mnemonic_msg( keypair, "coldkey" ) - elif seed is not None: - keypair = Keypair.create_from_seed(seed, ss58_format=bittensor.__ss58_format__ ) - else: - # json is not None - if not isinstance(json, tuple) or len(json) != 2 or not isinstance(json[0], (str, dict)) or not isinstance(json[1], str): - raise ValueError("json must be a tuple of (json_data: str | Dict, passphrase: str)") - - json_data, passphrase = json - keypair = Keypair.create_from_encrypted_json( json_data, passphrase, ss58_format=bittensor.__ss58_format__ ) - - self.set_coldkey( keypair, encrypt = use_password, overwrite = overwrite) - self.set_coldkeypub( keypair, overwrite = overwrite) - return self - - # Short name for regenerate_coldkey - regen_coldkey = regenerate_coldkey - - @overload - def regenerate_hotkey( - self, - mnemonic: Optional[Union[list, str]] = None, - use_password: bool = True, - overwrite: bool = False - ) -> 'Wallet': - ... - - @overload - def regenerate_hotkey( - self, - seed: Optional[str] = None, - use_password: bool = True, - overwrite: bool = False - ) -> 'Wallet': - ... - - @overload - def regenerate_hotkey( - self, - json: Optional[Tuple[Union[str, Dict], str]] = None, - use_password: bool = True, - overwrite: bool = False - ) -> 'Wallet': - ... - - def regenerate_hotkey( - self, - use_password: bool = True, - overwrite: bool = False, - **kwargs - ) -> 'Wallet': - """ Regenerates the hotkey from passed mnemonic, encrypts it with the user's password and save the file - Args: - mnemonic: (Union[list, str], optional): - Key mnemonic as list of words or string space separated words. - seed: (str, optional): - Seed as hex string. - json: (Tuple[Union[str, Dict], str], optional): - Restore from encrypted JSON backup as (json_data: Union[str, Dict], passphrase: str) - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Will this operation overwrite the hotkey under the same path //hotkeys/ - Returns: - wallet (bittensor.Wallet): - this object with newly created hotkey. - """ - if len(kwargs) == 0: - raise ValueError("Must pass either mnemonic, seed, or json") - - # Get from kwargs - mnemonic = kwargs.get('mnemonic', None) - seed = kwargs.get('seed', None) - json = kwargs.get('json', None) - - if mnemonic is None and seed is None and json is None: - raise ValueError("Must pass either mnemonic, seed, or json") - if mnemonic is not None: - if isinstance( mnemonic, str): mnemonic = mnemonic.split() - if len(mnemonic) not in [12,15,18,21,24]: - raise ValueError("Mnemonic has invalid size. This should be 12,15,18,21 or 24 words") - keypair = Keypair.create_from_mnemonic(" ".join(mnemonic), ss58_format=bittensor.__ss58_format__ ) - display_mnemonic_msg( keypair, "hotkey" ) - elif seed is not None: - keypair = Keypair.create_from_seed(seed, ss58_format=bittensor.__ss58_format__ ) - else: - # json is not None - if not isinstance(json, tuple) or len(json) != 2 or not isinstance(json[0], (str, dict)) or not isinstance(json[1], str): - raise ValueError("json must be a tuple of (json_data: str | Dict, passphrase: str)") - - json_data, passphrase = json - keypair = Keypair.create_from_encrypted_json( json_data, passphrase, ss58_format=bittensor.__ss58_format__ ) - - - self.set_hotkey( keypair, encrypt=use_password, overwrite = overwrite) - return self - - # Short name for regenerate_hotkey - regen_hotkey = regenerate_hotkey diff --git a/bittensor/_wallet/wallet_mock.py b/bittensor/_wallet/wallet_mock.py deleted file mode 100644 index f2720fcae3..0000000000 --- a/bittensor/_wallet/wallet_mock.py +++ /dev/null @@ -1,59 +0,0 @@ - -from . import wallet_impl -import os -import bittensor - -class Wallet_mock(wallet_impl.Wallet): - """ - Mocked Version of the bittensor wallet class, meant to be used for testing - """ - def __init__( - self, - _mock:bool, - **kwargs, - ): - r""" Init bittensor wallet object containing a hot and coldkey. - Args: - _mock (required=True, default=False): - If true creates a mock wallet with random keys. - """ - super().__init__(**kwargs) - # For mocking. - self._is_mock = _mock - self._mocked_coldkey_keyfile = None - self._mocked_hotkey_keyfile = None - - print("---- MOCKED WALLET INITIALIZED- ---") - - @property - def hotkey_file(self) -> 'bittensor.Keyfile': - if self._is_mock: - if self._mocked_hotkey_keyfile == None: - self._mocked_hotkey_keyfile = bittensor.keyfile(path='MockedHotkey', _mock = True) - return self._mocked_hotkey_keyfile - else: - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - hotkey_path = os.path.join(wallet_path, "hotkeys", self.hotkey_str) - return bittensor.keyfile( path = hotkey_path ) - - @property - def coldkey_file(self) -> 'bittensor.Keyfile': - if self._is_mock: - if self._mocked_coldkey_keyfile == None: - self._mocked_coldkey_keyfile = bittensor.keyfile(path='MockedColdkey', _mock = True) - return self._mocked_coldkey_keyfile - else: - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - coldkey_path = os.path.join(wallet_path, "coldkey") - return bittensor.keyfile( path = coldkey_path ) - - @property - def coldkeypub_file(self) -> 'bittensor.Keyfile': - if self._is_mock: - if self._mocked_coldkey_keyfile == None: - self._mocked_coldkey_keyfile = bittensor.keyfile(path='MockedColdkeyPub', _mock = True) - return self._mocked_coldkey_keyfile - else: - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - coldkeypub_path = os.path.join(wallet_path, "coldkeypub.txt") - return bittensor.Keyfile( path = coldkeypub_path ) \ No newline at end of file diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 05d7c2f6bc..41dfec589f 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -1,14 +1,16 @@ import numbers -from typing import Callable, Union, List, Optional, Dict +from typing import Callable, Union, List, Optional, Dict, Literal, Type, Any import bittensor import pandas import requests import torch import scalecodec -from substrateinterface import Keypair +import argparse from substrateinterface.utils import ss58 -from .registration import create_pow +from bittensor_wallet.utils import * + +from .registration import create_pow, __reregister_wallet as reregister RAOPERTAO = 1e9 U16_MAX = 65535 @@ -72,83 +74,8 @@ def version_checking(): if latest_version_as_int > bittensor.__version_as_int__: print('\u001b[33mBittensor Version: Current {}/Latest {}\nPlease update to the latest version at your earliest convenience\u001b[0m'.format(bittensor.__version__,latest_version)) -def is_valid_ss58_address( address: str ) -> bool: - """ - Checks if the given address is a valid ss58 address. - - Args: - address(str): The address to check. - - Returns: - True if the address is a valid ss58 address for Bittensor, False otherwise. - """ - try: - return ss58.is_valid_ss58_address( address, valid_ss58_format=bittensor.__ss58_format__ ) or \ - ss58.is_valid_ss58_address( address, valid_ss58_format=42 ) # Default substrate ss58 format (legacy) - except (IndexError): - return False - -def is_valid_ed25519_pubkey( public_key: Union[str, bytes] ) -> bool: - """ - Checks if the given public_key is a valid ed25519 key. - - Args: - public_key(Union[str, bytes]): The public_key to check. - - Returns: - True if the public_key is a valid ed25519 key, False otherwise. - - """ - try: - if isinstance( public_key, str ): - if len(public_key) != 64 and len(public_key) != 66: - raise ValueError( "a public_key should be 64 or 66 characters" ) - elif isinstance( public_key, bytes ): - if len(public_key) != 32: - raise ValueError( "a public_key should be 32 bytes" ) - else: - raise ValueError( "public_key must be a string or bytes" ) - - keypair = Keypair( - public_key=public_key, - ss58_format=bittensor.__ss58_format__ - ) - - ss58_addr = keypair.ss58_address - return ss58_addr is not None - - except (ValueError, IndexError): - return False - -def is_valid_bittensor_address_or_public_key( address: Union[str, bytes] ) -> bool: - """ - Checks if the given address is a valid destination address. - - Args: - address(Union[str, bytes]): The address to check. - - Returns: - True if the address is a valid destination address, False otherwise. - """ - if isinstance( address, str ): - # Check if ed25519 - if address.startswith('0x'): - return is_valid_ed25519_pubkey( address ) - else: - # Assume ss58 address - return is_valid_ss58_address( address ) - elif isinstance( address, bytes ): - # Check if ed25519 - return is_valid_ed25519_pubkey( address ) - else: - # Invalid address type - return False - -def get_ss58_format( ss58_address: str ) -> int: - """Returns the ss58 format of the given ss58 address.""" - return ss58.get_ss58_format( ss58_address ) -def strtobool_with_default( default: bool ) -> Callable[[str], bool]: +def strtobool_with_default( default: bool ) -> Callable[[str], Union[bool, Literal['==SUPRESS==']]]: """ Creates a strtobool function with a default value. @@ -161,7 +88,7 @@ def strtobool_with_default( default: bool ) -> Callable[[str], bool]: return lambda x: strtobool(x) if x != "" else default -def strtobool(val: str) -> bool: +def strtobool(val: str) -> Union[bool, Literal['==SUPRESS==']]: """ Converts a string to a boolean value. @@ -241,3 +168,9 @@ def u8_key_to_ss58(u8_key: List[int]) -> str: """ # First byte is length, then 32 bytes of key. return scalecodec.ss58_encode( bytes(u8_key).hex(), bittensor.__ss58_format__) + +def type_or_suppress(type_func: Callable[[str], Any]) -> Callable[[str], Union[Any, Literal['==SUPRESS==']]]: + def _type_or_suppress(value: str) -> Union[Any, Literal['==SUPRESS==']]: + return value if value == argparse.SUPPRESS else type_func(value) + + return _type_or_suppress diff --git a/bittensor/utils/registration.py b/bittensor/utils/registration.py index 947422cd2c..8def93288f 100644 --- a/bittensor/utils/registration.py +++ b/bittensor/utils/registration.py @@ -4,6 +4,7 @@ import multiprocessing import os import random +import sys import time from dataclasses import dataclass from datetime import timedelta @@ -443,7 +444,10 @@ def _solve_for_difficulty_fast( subtensor, wallet: 'bittensor.Wallet', netuid: i hash_rates = [0] * n_samples # The last n true hash_rates weights = [alpha_ ** i for i in range(n_samples)] # weights decay by alpha - while not wallet.is_registered(netuid = netuid, subtensor = subtensor): + while not subtensor.is_hotkey_registered( + netuid = netuid, + hotkey_ss58 = wallet.hotkey.ss58_address, + ): # Wait until a solver finds a solution try: solution = solution_queue.get(block=True, timeout=0.25) @@ -543,7 +547,7 @@ def _get_block_with_retry(subtensor: 'bittensor.Subtensor', netuid: int) -> Tupl """ block_number = subtensor.get_current_block() difficulty = subtensor.difficulty(netuid = netuid) - block_hash = subtensor.substrate.get_block_hash( block_number ) + block_hash = subtensor.get_block_hash( block_number ) if block_hash is None: raise Exception("Network error. Could not connect to substrate to get block hash") if difficulty is None: @@ -732,7 +736,10 @@ def _solve_for_difficulty_fast_cuda( subtensor: 'bittensor.Subtensor', wallet: ' weights = [alpha_ ** i for i in range(n_samples)] # weights decay by alpha solution = None - while not wallet.is_registered(netuid = netuid, subtensor = subtensor): + while not subtensor.is_hotkey_registered( + netuid = netuid, + hotkey_ss58 = wallet.hotkey.ss58_address, + ): # Wait until a solver finds a solution try: solution = solution_queue.get(block=True, timeout=0.15) @@ -869,3 +876,55 @@ def create_pow( ) return solution + + +def __reregister_wallet( + netuid: int, + wallet: 'bittensor.Wallet', + subtensor: 'bittensor.Subtensor', + reregister: bool = False, + prompt: bool = False, + **registration_args: Any + ) -> Optional['bittensor.Wallet']: + """ Re-register this a Wallet on the chain, or exits. + Exits if the wallet is not registered on the chain AND + reregister is set to False. + Args: + netuid (int): + The network uid of the subnet to register on. + wallet( 'bittensor.Wallet' ): + Bittensor Wallet to re-register + reregister (bool, default=False): + If true, re-registers the wallet on the chain. + Exits if False and the wallet is not registered on the chain. + prompt (bool): + If true, the call waits for confirmation from the user before proceeding. + **registration_args (Any): + The registration arguments to pass to the subtensor register function. + Return: + wallet (bittensor.Wallet): + The wallet + + Raises: + SytemExit(0): + If the wallet is not registered on the chain AND + the config.subtensor.reregister flag is set to False. + """ + wallet.hotkey + + if not subtensor.is_hotkey_registered_on_subnet( + hotkey_ss58=wallet.hotkey.ss58_address, + netuid=netuid + ): + # Check if the wallet should reregister + if not reregister: + sys.exit(0) + + subtensor.register( + wallet = wallet, + netuid = netuid, + prompt = prompt, + **registration_args, + ) + + return wallet \ No newline at end of file diff --git a/bittensor/utils/registratrion_old.py b/bittensor/utils/registratrion_old.py index 384503db93..bb35b549cb 100644 --- a/bittensor/utils/registratrion_old.py +++ b/bittensor/utils/registratrion_old.py @@ -377,7 +377,7 @@ def update( self, stats: RegistrationStatistics, verbose: bool = False ) -> None self.console.log( self.get_status_message(stats, verbose=verbose), ) -def solve_for_difficulty_fast( subtensor, wallet, output_in_place: bool = True, num_processes: Optional[int] = None, update_interval: Optional[int] = None, n_samples: int = 10, alpha_: float = 0.80, log_verbose: bool = False ) -> Optional[POWSolution]: +def solve_for_difficulty_fast( subtensor: 'bittensor.Subtensor', wallet, output_in_place: bool = True, num_processes: Optional[int] = None, update_interval: Optional[int] = None, n_samples: int = 10, alpha_: float = 0.80, log_verbose: bool = False ) -> Optional[POWSolution]: """ Solves the POW for registration using multiprocessing. Args: @@ -474,7 +474,9 @@ def solve_for_difficulty_fast( subtensor, wallet, output_in_place: bool = True, hash_rates = [0] * n_samples # The last n true hash_rates weights = [alpha_ ** i for i in range(n_samples)] # weights decay by alpha - while not wallet.is_registered(subtensor): + while not subtensor.is_hotkey_registered( + hotkey_ss58 = wallet.hotkey.ss58_address, + ): # Wait until a solver finds a solution try: solution = solution_queue.get(block=True, timeout=0.25) @@ -732,7 +734,9 @@ def solve_for_difficulty_fast_cuda( subtensor: 'bittensor.Subtensor', wallet: 'b weights = [alpha_ ** i for i in range(n_samples)] # weights decay by alpha solution = None - while not wallet.is_registered(subtensor): + while not subtensor.is_hotkey_registered( + hotkey_ss58 = wallet.hotkey.ss58_address, + ): # Wait until a solver finds a solution try: solution = solution_queue.get(block=True, timeout=0.15) diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index 73c8891e01..7aff40f722 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -182,14 +182,14 @@ def process_weights_for_netuid( non_zero_weights = weights[ non_zero_weight_idx ] if non_zero_weights.numel() == 0 or metagraph.n < min_allowed_weights: bittensor.logging.warning( 'No non-zero weights returning all ones.' ) - final_weights = torch.ones( ( metagraph.n ) ) / metagraph.n + final_weights = torch.ones( ( metagraph.n ) ).to( metagraph.n ) / metagraph.n bittensor.logging.debug( 'final_weights', final_weights ) return torch.tensor( list( range( len( final_weights ) ) ) ), final_weights elif non_zero_weights.numel() < min_allowed_weights: bittensor.logging.warning( 'No non-zero weights less then min allowed weight, returning all ones.' ) # ( const ): Should this be torch.zeros( ( metagraph.n ) ) to reset everyone to build up weight? - weights = torch.ones( ( metagraph.n ) ) * 1e-5 # creating minimum even non-zero weights + weights = torch.ones( ( metagraph.n ) ).to( metagraph.n ) * 1e-5 # creating minimum even non-zero weights weights[non_zero_weight_idx] += non_zero_weights bittensor.logging.debug( 'final_weights', weights ) normalized_weights = bittensor.utils.weight_utils.normalize_max_weight( @@ -221,4 +221,4 @@ def process_weights_for_netuid( ) bittensor.logging.debug( 'final_weights', normalized_weights ) - return non_zero_weight_uids, normalized_weights \ No newline at end of file + return non_zero_weight_uids, normalized_weights diff --git a/neurons/text/prompting/miners/self_hosted/neuron.py b/neurons/text/prompting/miners/self_hosted/neuron.py index 1f8123435f..db3d9895fc 100644 --- a/neurons/text/prompting/miners/self_hosted/neuron.py +++ b/neurons/text/prompting/miners/self_hosted/neuron.py @@ -104,7 +104,7 @@ def main(): wallet.register(netuid=config.netuid, subtensor=subtensor) # --- Create our network state cache - metagraph = bittensor.metagraph(config=config, netuid=config.netuid, ) + metagraph = bittensor.metagraph(config=config, netuid=config.netuid, sync=False ) metagraph.sync(netuid=config.netuid, subtensor=subtensor).save() uid = metagraph.hotkeys.index(wallet.hotkey.ss58_address) diff --git a/neurons/text/prompting/validators/constitution/neuron.py b/neurons/text/prompting/validators/constitution/neuron.py deleted file mode 100644 index 3c36300ac2..0000000000 --- a/neurons/text/prompting/validators/constitution/neuron.py +++ /dev/null @@ -1,97 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -import os -import time -import json -import math -import copy -import queue -import torch -import random -import bittensor -import argparse -import bittensor as bt - -from loguru import logger -from types import SimpleNamespace -from typing import List, Optional, Tuple, Dict - -class neuron: - @classmethod - def check_config( cls, config: 'bt.Config' ): - r""" Checks/validates the config namespace object. - """ - bt.logging.check_config( config ) - bt.wallet.check_config( config ) - bt.subtensor.check_config( config ) - full_path = os.path.expanduser('{}/{}/{}/netuid{}/{}'.format( config.logging.logging_dir, config.wallet.name, config.wallet.hotkey, config.netuid, config.neuron.name )) - config.neuron.full_path = os.path.expanduser( full_path ) - if not os.path.exists( config.neuron.full_path ): - os.makedirs( config.neuron.full_path, exist_ok = True) - - @classmethod - def config ( cls ): - parser = argparse.ArgumentParser() - parser.add_argument( '--netuid', type = int, help = 'Prompting network netuid', default = 1 ) - parser.add_argument( '--neuron.name', type = str, help = 'Trials for this miner go in miner.root / (wallet_cold - wallet_hot) / miner.name ', default = 'core_prompting_validator') - parser.add_argument( '--neuron.device', type = str, help = 'Device to run the validator on.', default = "cuda" if torch.cuda.is_available() else "cpu" ) - bt.wallet.add_args( parser ) - bt.subtensor.add_args( parser ) - bt.logging.add_args( parser ) - bt.axon.add_args( parser ) - return bt.config( parser ) - - def __init__( self ): - self.config = neuron.config() - self.check_config( self.config ) - bt.logging( config = self.config, logging_dir = self.config.neuron.full_path ) - print( self.config ) - self.subtensor = bt.subtensor ( config = self.config ) - self.wallet = bt.wallet ( config = self.config ) - self.metagraph = bt.metagraph( netuid = self.config.netuid, network = self.subtensor.network ) - print ('done init') - - def train( self ): - while True: - uids = torch.tensor( random.sample( self.metagraph.uids.tolist(), 2 ), dtype = torch.int64 ) - A = bittensor.text_prompting( keypair = self.wallet.hotkey, axon = self.metagraph.axons[uids[0]] ) - B = bittensor.text_prompting( keypair = self.wallet.hotkey, axon = self.metagraph.axons[uids[1]] ) - resp_A = A.forward( - roles = ['user'], - messages = ['ask me a random question?'], - timeout = 5, - ) - resp_B = B.forward( - roles = ['user'], - messages = ['ask me a random question?'], - timeout = 5, - ) - bittensor.logging.info(str(resp_A)) - bittensor.logging.info(str(resp_B)) - - if resp_A.is_success and resp_B.is_success: - bittensor.logging.info('success') - break - else: - bittensor.logging.info('failure') - continue - - -if __name__ == '__main__': - bittensor.logging.info( 'neuron().train()' ) - neuron().train() diff --git a/neurons/text/prompting/validators/core/README.md b/neurons/text/prompting/validators/core/README.md deleted file mode 100644 index 50a01bbb28..0000000000 --- a/neurons/text/prompting/validators/core/README.md +++ /dev/null @@ -1,95 +0,0 @@ -# Bittensor Prompting Validator -This repository the the core validator for the bittensor prompting network. - -## Prerequisites -- Python 3.8+ -- Bittensor - -## Installation -1. Clone the repository -2. Install the required packages with `pip install -r neurons/text/prompting/validators/core/requirements.txt` -For more configuration options related to the wallet, axon, subtensor, logging, and metagraph, please refer to the Bittensor documentation. - -## Example Usage -To run the Core Bittensor Prompting Validator with default settings, use the following command: - -``` -python3 -m pip install -r neurons/text/prompting/validators/core/requirements.txt -python3 neurons/text/prompting/validators/core/neuron.py -``` - -# Full Usage -``` -usage: neuron.py [-h] [--netuid NETUID] [--neuron.name NEURON.NAME] [--neuron.reward_model_name NEURON.REWARD_MODEL_NAME] [--neuron.inference_topk NEURON.INFERENCE_TOPK] [--neuron.training_topk NEURON.TRAINING_TOPK] - [--prompting.model_name PROMPTING.MODEL_NAME] [--prompting.min_tokens PROMPTING.MIN_TOKENS] [--prompting.max_tokens PROMPTING.MAX_TOKENS] [--prompting.temperature PROMPTING.TEMPERATURE] - [--prompting.top_p PROMPTING.TOP_P] [--prompting.logprobs PROMPTING.LOGPROBS] [--prompting.repetition_penalty PROMPTING.REPETITION_PENALTY] [--wallet.name WALLET.NAME] [--wallet.hotkey WALLET.HOTKEY] - [--wallet.path WALLET.PATH] [--wallet._mock] [--wallet.reregister WALLET.REREGISTER] [--subtensor.network SUBTENSOR.NETWORK] [--subtensor.chain_endpoint SUBTENSOR.CHAIN_ENDPOINT] [--subtensor._mock] - [--subtensor.register.num_processes SUBTENSOR.REGISTER.NUM_PROCESSES] [--subtensor.register.update_interval SUBTENSOR.REGISTER.UPDATE_INTERVAL] [--subtensor.register.no_output_in_place] [--subtensor.register.verbose] - [--subtensor.register.cuda.use_cuda] [--subtensor.register.cuda.no_cuda] [--subtensor.register.cuda.dev_id SUBTENSOR.REGISTER.CUDA.DEV_ID [SUBTENSOR.REGISTER.CUDA.DEV_ID ...]] - [--subtensor.register.cuda.TPB SUBTENSOR.REGISTER.CUDA.TPB] [--metagraph._mock] [--logging.debug] [--logging.trace] [--logging.record_log] [--logging.logging_dir LOGGING.LOGGING_DIR] [--config CONFIG] [--strict] - -optional arguments: - -h, --help show this help message and exit - --netuid NETUID Prompting network netuid - --neuron.name NEURON.NAME - Trials for this miner go in miner.root / (wallet_cold - wallet_hot) / miner.name - --neuron.reward_model_name NEURON.REWARD_MODEL_NAME - GPTRewardModel name - --neuron.inference_topk NEURON.INFERENCE_TOPK - At inference time, how many miners to we query and return the top rewarded. - --neuron.training_topk NEURON.TRAINING_TOPK - During training time, how many miners to we query for each batch based on scores from gating network. - --prompting.model_name PROMPTING.MODEL_NAME - Name of the model to use - --prompting.min_tokens PROMPTING.MIN_TOKENS - Minimum number of tokens to generate - --prompting.max_tokens PROMPTING.MAX_TOKENS - Maximum number of tokens to generate - --prompting.temperature PROMPTING.TEMPERATURE - Temperature for sampling - --prompting.top_p PROMPTING.TOP_P - Top p for sampling - --prompting.logprobs PROMPTING.LOGPROBS - Number of logprobs to return - --prompting.repetition_penalty PROMPTING.REPETITION_PENALTY - Repetition penalty for sampling - --wallet.name WALLET.NAME - The name of the wallet to unlock for running bittensor (name mock is reserved for mocking this wallet) - --wallet.hotkey WALLET.HOTKEY - The name of wallet's hotkey. - --wallet.path WALLET.PATH - The path to your bittensor wallets - --wallet._mock To turn on wallet mocking for testing purposes. - --wallet.reregister WALLET.REREGISTER - Whether to reregister the wallet if it is not already registered. - --subtensor.network SUBTENSOR.NETWORK - The subtensor network flag. The likely choices are: -- finney (main network) -- local (local running network) -- mock (creates a mock connection (for testing)) If this option is set it overloads - subtensor.chain_endpoint with an entry point node from that network. - --subtensor.chain_endpoint SUBTENSOR.CHAIN_ENDPOINT - The subtensor endpoint flag. If set, overrides the --network flag. - --subtensor._mock To turn on subtensor mocking for testing purposes. - --subtensor.register.num_processes SUBTENSOR.REGISTER.NUM_PROCESSES, -n SUBTENSOR.REGISTER.NUM_PROCESSES - Number of processors to use for registration - --subtensor.register.update_interval SUBTENSOR.REGISTER.UPDATE_INTERVAL, --subtensor.register.cuda.update_interval SUBTENSOR.REGISTER.UPDATE_INTERVAL, --cuda.update_interval SUBTENSOR.REGISTER.UPDATE_INTERVAL, -u SUBTENSOR.REGISTER.UPDATE_INTERVAL - The number of nonces to process before checking for next block during registration - --subtensor.register.no_output_in_place, --no_output_in_place - Whether to not ouput the registration statistics in-place. Set flag to disable output in-place. - --subtensor.register.verbose - Whether to ouput the registration statistics verbosely. - --subtensor.register.cuda.use_cuda, --cuda, --cuda.use_cuda - Set flag to use CUDA to register. - --subtensor.register.cuda.no_cuda, --no_cuda, --cuda.no_cuda - Set flag to not use CUDA for registration - --subtensor.register.cuda.dev_id SUBTENSOR.REGISTER.CUDA.DEV_ID [SUBTENSOR.REGISTER.CUDA.DEV_ID ...], --cuda.dev_id SUBTENSOR.REGISTER.CUDA.DEV_ID [SUBTENSOR.REGISTER.CUDA.DEV_ID ...] - Set the CUDA device id(s). Goes by the order of speed. (i.e. 0 is the fastest). - --subtensor.register.cuda.TPB SUBTENSOR.REGISTER.CUDA.TPB, --cuda.TPB SUBTENSOR.REGISTER.CUDA.TPB - Set the number of Threads Per Block for CUDA. - --metagraph._mock To turn on metagraph mocking for testing purposes. - --logging.debug Turn on bittensor debugging information - --logging.trace Turn on bittensor trace level information - --logging.record_log Turns on logging to file. - --logging.logging_dir LOGGING.LOGGING_DIR - Logging default root directory. - --config CONFIG If set, defaults are overridden by passed file. - --strict If flagged, config will check that only exact arguemnts have been set. -``` \ No newline at end of file diff --git a/neurons/text/prompting/validators/core/gating.py b/neurons/text/prompting/validators/core/gating.py deleted file mode 100644 index 5966258192..0000000000 --- a/neurons/text/prompting/validators/core/gating.py +++ /dev/null @@ -1,120 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -import torch -import argparse -import bittensor -from transformers import AutoModel, AutoTokenizer, AutoConfig - -class GatingModel( torch.nn.Module ): - """ - This class is a PyTorch module that encapsulates the gating model functionality. - - - The backward method runs a backward pass through the model using the mean squared error between the normalized scores and the normalized rewards as the loss function. - - The forward method runs a forward pass through the model, encoding the input message and generating scores for each uid in the network. The scores are returned as a tensor. - """ - - @classmethod - def add_args( cls, parser: argparse.ArgumentParser ): - """ - Adds command line arguments to the parser that are used to configure the gating model. - The arguments added are: - - `--gating.model_name`: Name of the pre-trained transformer-based language model to use as the encoding layer for the gating model. (default: 'EleutherAI/gpt-neo-125m') - - `--gating.num_uids`: Number of uids to gate on. (default: 4096) - - `--gating.learning_rate`: Learning rate for the gating model optimizer. (default: 0.01) - - `--gating.momentum`: Momentum for the gating model optimizer. (default: 0.9) - """ - parser.add_argument('--gating.model_name', type=str, default='EleutherAI/gpt-neo-125m', help='Name of the model to use as the encoding layer for the gating model') - parser.add_argument('--gating.num_uids', type=int, default=4096, help='Number of uids to gate on') - parser.add_argument('--gating.learning_rate', type=float, default=0.01, help='Learning rate for the gating model') - parser.add_argument('--gating.momentum', type=float, default=0.9, help='Momentum for the gating model') - - @classmethod - def config ( cls ): - """ - Returns a configuration object that contains the command line arguments for the gating model. - """ - parser = argparse.ArgumentParser() - cls.add_args( parser ) - return bittensor.config( parser ) - - @classmethod - def check_config( cls, config: 'bittensor.Config' ): - """ - Validates the configuration object for the gating model. - """ - pass - - def __init__( - self, - metagraph: 'bittensor.metagraph.Metagraph', - config: 'bittensor.config' = None, - model_name: str = None, - num_uids: int = None - ): - """ - Initializes the gating model. - - `metagraph`: A reference to the Bittensor metagraph object. - - `config`: Configuration object for the gating model. If `None`, the default configuration is used. - - `model_name`: Name of the pre-trained transformer-based language model to use as the encoding layer for the gating model. If `None`, the default model name specified in the configuration is used. - - `num_uids`: Number of uids to gate on. If `None`, the default number specified in the configuration is used. - """ - super(GatingModel, self).__init__() - if config is None: config = GatingModel.config() - if model_name is not None: config.gating.model_name = model_name - config.gating.num_uids = num_uids if num_uids is not None else metagraph.n - self.config = config - self.num_uids = config.gating.num_uids - self.device = torch.device( self.config.neuron.device ) - self.tokenizer = AutoTokenizer.from_pretrained( self.config.gating.model_name ) - self.model = AutoModel.from_pretrained( self.config.gating.model_name) - self.linear = torch.nn.Linear( self.model.config.hidden_size, config.gating.num_uids ) - self.optimizer = torch.optim.SGD( - [ {"params": self.parameters()} ], - lr = self.config.gating.learning_rate, - momentum = self.config.gating.momentum, - ) - - def backward( self, scores: torch.FloatTensor, rewards: torch.FloatTensor ): - """ Runs a backward pass through the model. - Args: - scores (:obj:`torch.FloatTensor` of shape :obj:`(metagraph.n)`): - Scores for each uids as output by the gating model. - rewards (:obj:`torch.FloatTensor` of shape :obj:`(metagraph.n)`): - Rewards for each uids as output by the reward model. - """ - normalized_scores = torch.nn.functional.softmax( scores, dim=0 ).to( self.device ) - nomralized_rewards = torch.nn.functional.softmax( rewards, dim=0 ).to( self.device ) - loss = torch.nn.functional.mse_loss( normalized_scores, nomralized_rewards.detach() ) - loss.backward() - self.optimizer.step() - - def forward( self, message: str ) -> 'torch.FloatTensor': - """ Runs a forward pass through the model. - Args: - message (:obj:`str`): - text message to be encoded. - Returns: - scores (:obj:`torch.FloatTensor` of shape :obj:`(network_size)`): - Scores for each uids as output by the gating model. - """ - inputs = self.tokenizer( message, return_tensors="pt" ,truncation=True, max_length=2048).to( self.device ) - with torch.no_grad(): - hidden_states = self.model( **inputs ).last_hidden_state[0, -1, :] - return self.linear( hidden_states ) - - diff --git a/neurons/text/prompting/validators/core/neuron.py b/neurons/text/prompting/validators/core/neuron.py deleted file mode 100644 index 3b88248c24..0000000000 --- a/neurons/text/prompting/validators/core/neuron.py +++ /dev/null @@ -1,803 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -import os -import time -import math -import copy -import queue -import torch -import random -import bittensor -import argparse -import bittensor as bt -import traceback - -from loguru import logger -from types import SimpleNamespace -from typing import List, Optional, Tuple, Dict -from reward import RewardModel -from gating import GatingModel -from transformers import AutoTokenizer, AutoModelForSequenceClassification -from datasets import load_dataset -from datetime import datetime - -__default_question_prompt__ = ''' -Ask me a random question about anything. Make the question very domain specific. Do not include the answer in the question. -''' - -__default_base_prompt__ = ''' -You are designed to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. -''' - -__default_follow_up_prompt__ = ''' -Ask a follow up question. -''' -class neuron: - @classmethod - def check_config( cls, config: 'bt.Config' ): - r""" Checks/validates the config namespace object. - """ - bt.logging.check_config( config ) - bt.wallet.check_config( config ) - bt.subtensor.check_config( config ) - full_path = os.path.expanduser('{}/{}/{}/netuid{}/{}'.format( config.logging.logging_dir, config.wallet.name, config.wallet.hotkey, config.netuid, config.neuron.name )) - config.neuron.full_path = os.path.expanduser( full_path ) - config.neuron.reward_path = os.path.expanduser( config.neuron.reward_path ) - if not os.path.exists( config.neuron.full_path ): - os.makedirs( config.neuron.full_path, exist_ok = True) - if not os.path.exists( config.neuron.reward_path + '/hf_ckpt.pt' ): - os.makedirs( config.neuron.reward_path, exist_ok = True ) - os.system( - f"wget -O { config.neuron.reward_path + '/hf_ckpt.pt'} \ - https://huggingface.co/Dahoas/gptj-rm-static/resolve/main/hf_ckpt.pt" - ) - if not config.neuron.dont_save_events: - # Add custom event logger for the events. - logger.level("EVENTS", no=38, icon="📝") - logger.add( - config.neuron.full_path + "/" + "completions.log", - rotation=config.neuron.events_retention_size, serialize=True, enqueue=True, backtrace=False, diagnose=False, level="EVENTS", - format = "{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message} | {extra[prompt]} {extra[completion]} {extra[uids]} {extra[all_uids]} {extra[rewards]}{extra[all_completions]} {extra[block]}" - ) - - def record_event( self, event: SimpleNamespace ): - self.history.put( event ) - if not self.config.neuron.dont_save_events: - logger.log( - "EVENTS", - "events", - prompt = event.message, - completion = event.completion, - uids = event.uids.tolist(), - all_uids = event.all_uids.tolist(), - rewards = event.rewards.tolist(), - all_completions = event.all_completions, - block = event.block.item(), - ) - - @classmethod - def add_args( cls, parser ): - # Netuid Arg - parser.add_argument( '--netuid', type = int, help = 'Prompting network netuid', default = 1 ) - parser.add_argument( '--neuron.name', type = str, help = 'Trials for this miner go in miner.root / (wallet_cold - wallet_hot) / miner.name ', default = 'core_prompting_validator') - parser.add_argument( '--neuron.base_prompt', type=str, help = 'Prompt injected before a question is completed by miners on the network', default = __default_base_prompt__ ) - parser.add_argument( '--neuron.follow_up_prompt', type=str, help = 'Follow up prompt that is completed by miners on the network.', default = __default_follow_up_prompt__ ) - parser.add_argument( '--neuron.reset_bootstrap_prompt_frequency', type=int, help = 'How frequent to use the base follow up question.', default = 3 ) - parser.add_argument( '--neuron.question_prompt', type=str, help = 'Prompt used to generate questions from the network whicha are used to evaluate other miners.', default = __default_question_prompt__ ) - parser.add_argument( '--neuron.reward_model_name', type = str, help = 'GPTRewardModel name', default = 'Dahoas/gpt2-rm-static') - parser.add_argument( '--neuron.length_timeout_multiplier', type = int, help = 'Base timeout for all requests.', default = 0.01 ) - parser.add_argument( '--neuron.inference_topk', type = int, help = 'At inference time, how many miners to we query and return the top rewarded.', default = 10 ) - parser.add_argument( '--neuron.training_topk', type = int, help = 'During training time, how many miners to we query for each batch based on scores from gating network.', default = 50 ) - parser.add_argument( '--neuron.training_timeout', type = int, help = 'Query timeout during training', default = 4 ) - parser.add_argument( '--neuron.inference_timeout', type = int, help = 'Query timeout during inference', default = 10 ) - parser.add_argument( '--neuron.inference_only', action = 'store_true', help = 'If set, training off and only inference will be served via axon.', default = False ) - parser.add_argument( '--neuron.axon_off', action = 'store_true', help = 'If set, the axon will be turned off.', default = False ) - parser.add_argument( '--neuron.reward_path', type = str, help = 'Path to reward model.', default = '~/.bittensor/reward_models' ) - parser.add_argument( '--neuron.max_history', type = int, help = 'Maximum number history values to store at any time.', default = 100000 ) - parser.add_argument( '--neuron.device', type = str, help = 'Device to run the validator on.', default = "cuda" if torch.cuda.is_available() else "cpu" ) - parser.add_argument( '--neuron.epoch_length_override', type = int, help = 'Override the default timeout', default = -1 ) - parser.add_argument( '--neuron.dont_save_events', action = 'store_true', help = 'If set, we dont save events to a log file.', default = False ) - parser.add_argument( '--neuron.events_retention_size', type = str, help = 'Events retention size.', default = "2 GB" ) - parser.add_argument( '--neuron.no_reward_model', action = 'store_true', help = 'If set, we dont load the reward model instead use just the scores.', default = False ) - parser.add_argument( '--neuron.question_random_sample_uids', action = 'store_true', help = 'If set, random sample uids to get question.', default = False ) - parser.add_argument( '--neuron.reward_shift', type = int, help = 'The value to shift rewards for calculation.', default = 3 ) - parser.add_argument( '--neuron.no_nsfw_filter', action = 'store_true', help = 'If set, allow handling of not-safe-for-work messages.', default = False ) - parser.add_argument( '--neuron.vpermit_tao_limit', type = int, help = 'The maximum number of TAO allowed to query a validator with a vpermit.', default = 1024 ) - - @classmethod - def config ( cls ): - parser = argparse.ArgumentParser() - bt.wallet.add_args( parser ) - bt.subtensor.add_args( parser ) - bt.logging.add_args( parser ) - bt.axon.add_args( parser ) - GatingModel.add_args( parser ) - cls.add_args( parser ) - return bt.config( parser ) - - def __init__( self ): - self.config = neuron.config() - self.check_config( self.config ) - bt.logging( config = self.config, logging_dir = self.config.neuron.full_path ) - print( self.config ) - - self.subtensor = bt.subtensor ( config = self.config ) - self.device = torch.device( self.config.neuron.device ) - self.wallet = bt.wallet ( config = self.config ) - self.metagraph = bt.metagraph( netuid = self.config.netuid, network = self.subtensor.network ) - self.wallet.create_if_non_existent() - self.wallet.reregister( subtensor = self.subtensor, netuid = self.config.netuid ) - self.uid = self.wallet.get_uid( subtensor = self.subtensor, netuid = self.config.netuid ) - self.tokenizer = AutoTokenizer.from_pretrained( 'EleutherAI/gpt-j-6b' ) - - # check if invoking iter() is indeed necessary - self.dataset = iter(load_dataset('squad_v2', split='train', streaming=True).shuffle(buffer_size=10000)) - - self.moving_averaged_scores = torch.zeros((self.metagraph.n)).to( self.device ) - self.alpha = 0.99 - self.hotkeys = self.metagraph.hotkeys - # Reward model - if not self.config.neuron.no_reward_model: - bittensor.logging.info('Loading reward model') - self.reward_model = RewardModel( model_path = 'EleutherAI/gpt-j-6b', device = self.config.neuron.device ) - for fpath in os.listdir( self.config.neuron.reward_path ): - if fpath.endswith(".pt") or fpath.endswith(".bin"): - checkpoint = os.path.join( self.config.neuron.reward_path, fpath ) - break - ckpt_state = torch.load( checkpoint ) - self.reward_model.load_state_dict( ckpt_state ) - self.reward_model.eval() - self.reward_model.half() - self.reward_model.requires_grad_( False ) - self.reward_model.to( self.device ) - bittensor.logging.info('done loading reward model') - - # Init the gating model which learns which miners to select for each query. - self.gating_model = GatingModel( metagraph = self.metagraph, config = self.config ).to( self.device ) - # Denddrite pool for querying the network. - self.dendrite_pool = bt.text_prompting_pool( keypair = self.wallet.hotkey, metagraph = self.metagraph ) - self.inference_pool = bt.text_prompting_pool( keypair = self.wallet.hotkey, metagraph = self.metagraph ) - # History of forward events. - self.history = queue.Queue( maxsize = self.config.neuron.max_history ) - # Get a list of peers delegating to me - delegated = self.subtensor.get_delegated( self.wallet.coldkeypub.ss58_address ) - self.my_nominators = { nomin[0]: nomin[1] for nomin in delegated[0][0].nominators } if len(delegated) else {} - - self.load() - self.check_weights() - - # set up filter model - filter_model_path = 'facebook/roberta-hate-speech-dynabench-r4-target' - self.filter_model = AutoModelForSequenceClassification.from_pretrained(filter_model_path).to(self.device) - self.filter_tokenizer = AutoTokenizer.from_pretrained(filter_model_path) - self.filter_tokenizer.pad_token = self.filter_tokenizer.eos_token - self.filter_message_count = 0 - - # Axon set and served for inference requests, unless --neuron.axon_off flag is set. - if not self.config.neuron.axon_off: - # Build synapse entrypoint. - class Synapse( bittensor.TextPromptingSynapse ): - def priority( _, forward_call: "bittensor.TextPromptingForwardCall" ) -> float: - if forward_call.src_hotkey == self.wallet.hotkey.ss58_address: return math.inf # myself. - elif forward_call.src_hotkey in self.my_nominators: return self.my_nominators[ forward_call.src_hotkey ].tao # Delegates. - else: return 0.0 # Everyone else. - - def blacklist( _, forward_call: "bittensor.TextPromptingForwardCall" ) -> bool: - if forward_call.src_hotkey == self.wallet.hotkey.ss58_address: - return True - - elif forward_call.src_hotkey in self.metagraph.hotkeys: - uid = self.metagraph.hotkeys.index(forward_call.src_hotkey) - if self.metagraph.validator_permit[uid]: - return True - return False # Non Validator miners - - elif forward_call.src_hotkey in self.my_nominators: - return False # Delegates, dont blacklist. - else: - return False # Everyone else, dont blacklist. - - def backward( self, messages: List[Dict[str, str]], response: str, rewards: torch.FloatTensor ) -> str: pass - - def forward( _, messages: List[Dict[str, str]] ) -> str: - return self.inference( - messages = messages, - timeout = self.config.neuron.inference_timeout - ) - - def multi_forward( _, messages: List[Dict[str, str]] ) -> str: - return self.inference( - messages = messages, - timeout = self.config.neuron.inference_timeout, - return_all = True - ) - - # Serve axon. - self.axon = bittensor.axon( - wallet = self.wallet, - metagraph = self.metagraph, - config = self.config, - ) - self.synapse = Synapse( axon = self.axon ) - self.axon.start() - self.subtensor.serve_axon( self.config.netuid, self.axon ) - - def filter_message( - self, - message - ) -> bool: - """ Check if the message is related to any sexual content. - - Args: - message (str): - The message that we check if we should filter out. - Returns: - result (bool): - True indicates we should filter out the result, false indicates the result is safe. - """ - # If no filter needed, then just return false withough checking. - if self.config.neuron.no_nsfw_filter: - return False - - now = datetime.now() - dt_string = now.strftime("%d/%m/%Y %H:%M:%S") - tokenized = self.filter_tokenizer(message) - input_ids = tokenized['input_ids'] - bound_score1 = 0.5 - bound_score2 = 0.5 - - while len(input_ids) > 0: - _input_ids = input_ids[:512] - - with torch.no_grad(): - output = self.filter_model(torch.tensor([_input_ids]).to(self.device)) - - filter_out = output.logits[0, 0] < bound_score1 or output.logits[0, 1] > bound_score2 - - if filter_out: - bittensor.logging.debug( 'filtered message', message ) - break - else: - bittensor.logging.debug( 'safe message', message ) - - input_ids = input_ids[512:] - - self.filter_message_count += 1 - return filter_out - - def forward( - self, - roles: List[ str ], - messages: List[ str ], - topk: Optional[int] = None, - random_sample_uids: Optional[ bool ] = False, - train_gating_model: Optional[ bool ] = False, - train_network: Optional[ bool ] = False, - timeout: float = None, - question: bool = False, - ) -> SimpleNamespace: - """ - Queries the network for a response to the passed message using a gating model to select the best uids. - Trains the gating model based on the rewards calculated for the successful completions and passes rewards - backward for potential PPO. - - Args: - roles ( List[ str ] ): - roles associated with messages. - message ( List[ str ] ): - messages content for each role. - topk (Optional[int]): - The number of uids to consider for the query. If None or -1, all uids will be considered. - If provided, selects the top k uids based on the gating model scores. - random_sample_uids( bool, default = False ): - If True, randomly samples the uids to query rather than using topk. - train_gating_model ( bool, default = False ): - If True, trains the gating model based on the rewards calculated for the successful completions. - train_network ( bool, default = False ): - If True, sends backward messages to the network. - Returns: - result (SimpleNamespace): - A namespace containing the completion with the highest reward, message, uids, - rewards, scores, and all completions. - """ - bittensor.logging.info( 'forward()' ) - bittensor.logging.debug( 'roles', roles ) - bittensor.logging.debug( 'message', messages ) - - # Format the messages for the query. - unravelled_message = '' - for role, message in list(zip( roles, messages )): - if role == 'system': unravelled_message += 'system: ' + message + '\n' - if role== 'assistant': unravelled_message += 'assistant: ' + message + '\n' - if role == 'user': unravelled_message += 'user: ' + message + '\n' - - # Set `topk` to the number of items in `self.metagraph.n` if `topk` is not provided or is -1. - # Find the available `uids` that are currently serving. - # If `topk` is larger than the number of available `uids`, set `topk` to the number of available `uids`. - # Check if we have vpermit and if we do, ensure query only UIDs with less than vpermit_tao_limit. - def available( uid ) -> bool: - # Filter non serving axons. - if not self.metagraph.axons[uid].is_serving: - return False - # Filter validator permit > 1024 stake. - if self.metagraph.validator_permit[uid]: - if self.metagraph.S[uid] > self.config.neuron.vpermit_tao_limit: - return False - # Available otherwise. - return True - candidate_uids = [uid for uid, ax in enumerate(self.metagraph.axons) if available( uid )] - available_uids = torch.tensor( candidate_uids, dtype = torch.int64 ).to( self.device ) - if topk is None or topk == -1: topk = self.metagraph.n.item() - if topk > len( available_uids ): topk = len( available_uids ) - if len( available_uids ) == 0: bittensor.logging.error( 'no available uids' ); return None - bittensor.logging.trace( 'available_uids', available_uids ) - bittensor.logging.trace( 'topk', topk ) - - # We run the gating network here to get the best uids - # Use the gating model to generate scores for each `uid`. - scores = self.gating_model( unravelled_message ).to( self.device ) - bittensor.logging.trace( 'scores', scores ) - - # Select the top `topk` `uids` based on the highest `scores`. - # Use the selected `uids` to query the dendrite pool. - # Print the `completions`. - if random_sample_uids: - topk_uids = torch.tensor( random.sample( available_uids.tolist(), topk ), dtype = torch.int64 ).to( self.device ) - else: - topk_uids = available_uids[ scores[ available_uids ].sort()[ 1 ][ -topk: ]] - forward_calls = self.dendrite_pool( - roles = roles, - messages = messages, - uids = topk_uids, - timeout = timeout, - ) - bittensor.logging.trace( 'topk_uids', topk_uids ) - - # Filter out any `None` `completions`. - successful_uids = torch.tensor([uid for uid, call in list(zip(topk_uids, forward_calls)) if call is not None and call.completion is not None and len(call.completion)>10], dtype=torch.int64).to(self.device) - successful_completions = [call.completion for call in forward_calls if call is not None and call.completion is not None and len(call.completion)>10] - unsuccessful_uids = torch.tensor([uid for uid in topk_uids if uid not in successful_uids]) - bittensor.logging.debug( 'successful_uids', successful_uids ) - if len( successful_completions ) == 0: bittensor.logging.error('no successful completions'); return None - - # Calculate the rewards for the successful `completions` using the reward model. - # Print the rewards for all `uids`.` - flattened_message_for_reward = '' - if not self.config.neuron.no_reward_model: - for role_i, message_i in list(zip(roles, messages)): - if role_i != 'system': flattened_message_for_reward += message_i.strip() + '\n' - full_completions_for_reward = [ 'Question: ' + flattened_message_for_reward + 'Answer: ' + comp.strip() for comp in successful_completions ] - completions_for_reward = [comp.strip() for comp in successful_completions] - rewards = self.reward_model.reward( full_completions_for_reward, completions_for_reward, difference = True, shift = self.config.neuron.reward_shift).detach().to( self.device ) - bittensor.logging.trace( 'rewards', rewards ) - else: - rewards = scores[ successful_uids ] - - # Train the gating model using the scores and rewards of the successful `completions`. - if train_gating_model: - self.gating_model.backward( scores = scores[ successful_uids ], rewards = rewards ) - bittensor.logging.trace( 'Apply backward to gating model' ) - - # Pass rewards backward for potential PPO. - if train_network: - self.dendrite_pool.backward( - forward_calls = forward_calls, - rewards = rewards, - timeout = timeout, - ) - bittensor.logging.trace( 'Applied backward to network.' ) - - best_idx = rewards.detach().argmax() - bittensor.logging.trace( 'rewards', rewards ) - bittensor.logging.trace('successful_completions', len(successful_completions)) - bittensor.logging.trace('best_idx', best_idx) - best_completion = successful_completions[best_idx] - - - # Save the query history in a `result` object. - # Return the `completion` with the highest reward. - event = SimpleNamespace( - completion = successful_completions[ rewards.argmax( dim = 0 ) ], - message = message, - uids = successful_uids, - rewards = rewards, - all_uids = topk_uids, - all_completions = successful_completions, - block = self.metagraph.block, - is_question = message == self.config.neuron.question_prompt, - best_completion = best_completion - ) - self.record_event( event ) - - # First we normalize the rewards with a softmax. - normalized_rewards = torch.nn.functional.softmax( event.rewards.to( self.device ), dim=0 ) - - # We scatter the normalized onto the moving scores (updating them but not changing the source) - scattered_rewards = self.moving_averaged_scores.scatter(0, event.uids.to( self.device ), normalized_rewards.to( self.device ) ) - scattered_rewards = scattered_rewards.scatter(0, unsuccessful_uids.to( self.device ) , torch.zeros_like(unsuccessful_uids, dtype=torch.float).to( self.device ) ) - - # We now perform a moving average of the scattered rewards. - self.moving_averaged_scores = self.alpha * self.moving_averaged_scores + ( 1 - self.alpha ) * scattered_rewards - bittensor.logging.trace( 'normalized_rewards', normalized_rewards ) - bittensor.logging.trace( 'scattered_rewards', scattered_rewards ) - bittensor.logging.trace( 'moving_averaged_scores', self.moving_averaged_scores ) - print("===== Best Completion =====") - print(f"\n===== {successful_uids[best_idx], rewards[best_idx]} =====\n") - - print('flattened_message_for_reward:\n', flattened_message_for_reward) - print('completion:\n', best_completion.strip()) - - return event - - def inference( - self, - messages: List[Dict[str, str]], - timeout: float, - dont_use_reward_model: bool = True, - return_all = False - ) -> str: - bittensor.logging.info( 'inference()') - - # Pre-process messages. - roles = []; contents = []; unravelled_message = ''; user_message = None - for message_dict in messages: - roles.append( message_dict['role'] ) - contents.append( message_dict['content'] ) - if message_dict['role'] == 'system': unravelled_message += 'system: ' + message_dict['content'] + '\n' - if message_dict['role'] == 'assistant': unravelled_message += 'assistant: ' + message_dict['content'] + '\n' - if message_dict['role'] == 'user': - unravelled_message += 'user: ' + message_dict['content'] + '\n' - user_message = message_dict['content'] - - bittensor.logging.info( 'inference message', str(unravelled_message) ) - - if user_message and self.filter_message(user_message): - if return_all: - return ['Received possible explicit content.'] - else: - return 'Received possible explicit content.' - - # Get scores for query. - scores = self.gating_model( unravelled_message ).to( self.device ) - bittensor.logging.info( 'inference scores', str(scores) ) - - # Get uids for query. - uids = scores.sort()[ 1 ][ -self.config.neuron.inference_topk: ] - bittensor.logging.info( 'inference uids', str(uids) ) - - # Query using dendrite pool - forward_start = time.time() - bittensor.logging.trace( 'applying dendrite forward' ) - forward_calls = self.inference_pool( - roles = roles, - messages = contents, - uids = uids, - timeout = timeout, - ) - bittensor.logging.trace( 'finished dendrite forward ', time.time() - forward_start ) - - # Return longest completion. - if dont_use_reward_model or self.config.neuron.no_reward_model: - bittensor.logging.info('not applying the reward model taking the best completed response') - # Return first best from scores. - forward_calls.reverse() - - if return_all: - completions = [] - for call in forward_calls: - if len( call.completion ) > 0 and not self.filter_message(call.completion): - completions.append(call.completion) - if len(completions) > 0: - return completions - - else: - for call in forward_calls: - if len( call.completion ) > 0 and not self.filter_message(call.completion): - bittensor.logging.info( 'best completion', call.completion ) - return call.completion - - if return_all: - return ['no valid completions'] - - else: - return 'no valid completions' - - - else: - # Format messages for reward model. - flattened_message_for_reward = '' - for role_i, message_i in list(zip(roles, messages)): - if role_i != 'system': flattened_message_for_reward += message_i.strip() + '\n\n' - completions = [ call.completion for call in forward_calls if len(call.completion) > 0 and not self.filter_message(call.completion) ] - flattened_completions_for_reward = [ flattened_message_for_reward + comp.strip() for comp in completions ] - - # Return best via reward model. - reward_model_start = time.time() - completions_for_reward = [comp.strip() for comp in completions] - rewards = self.reward_model.reward( flattened_completions_for_reward, completions_for_reward, difference =False ).to( self.device ) - best_completion = completions[ rewards.argmax( dim = 0 ) ] - bittensor.logging.info('finished applying the reward model ', time.time() - reward_model_start ) - - if return_all: - return completions - else: - return best_completion - - def get_question(self, uids, bootstrap_prompt, reset_bootstrap_prompt = False, random_sample_uids = False): - - def _get_question(uids, bootstrap_prompt, reset_bootstrap_prompt = False): - # retrieve the answer - # sample = next(self.dataset) - # google_ai_dataset_place_holder = sample['answers']['text'][0] - - if reset_bootstrap_prompt: - bootstrap_prompt = next(self.dataset)['context'] # google_ai_dataset_place_holder - self.base_prompt = bootstrap_prompt - with open('prompt_history.txt', 'a') as file: - file.write("============== reset ==================" + '\n') - file.write(f"bootstrap prompt: {bootstrap_prompt}" + '\n') - - else: - bootstrap_prompt = bootstrap_prompt.replace('As an AI language model, ', '') - - question_prompt = f"{bootstrap_prompt}\n\n{self.config.neuron.follow_up_prompt}" - - questions = self.dendrite_pool( - roles = ['user'], - messages = [ question_prompt ], - uids = uids, - timeout = 12, - ) - - successful_questions = [question.completion for question in questions if question is not None and question.completion is not None and len(question.completion) > 10 and not self.filter_message(question.completion) ] - full_completions_for_reward = [ 'Question: ' + bootstrap_prompt + 'Answer: ' + comp.strip() for comp in successful_questions ] - completions_for_reward = [comp.strip() for comp in successful_questions] - reward_diffs = torch.zeros(len(successful_questions)) - if not self.config.neuron.no_reward_model: - reward_diffs = self.reward_model.reward( full_completions_for_reward, completions_for_reward, difference = True, shift = self.config.neuron.reward_shift ).to( self.device ) - for question, reward_diff in zip(successful_questions, reward_diffs.tolist()): - print(f"\n=== Question score: {reward_diff}===\n") - print(question) - if reward_diff > 0 : - return question, reward_diff - - return None, None - - def _get_random_uids(): - available_uids = torch.tensor( [ uid for uid, ax in enumerate( self.metagraph.axons ) if ax.is_serving ], dtype = torch.int64 ) - uids = torch.tensor( random.sample( available_uids.tolist(), self.config.neuron.training_topk ), dtype = torch.int64 ) - return uids - - question = None - - if random_sample_uids: - uids = _get_random_uids() - - while question is None: - question, reward_diff = _get_question(uids, bootstrap_prompt, reset_bootstrap_prompt) - reset_bootstrap_prompt = True - uids = _get_random_uids() - - return question, reward_diff - - def train( self ): - """ Training - The function uses an infinite loop to repeatedly generate a random question, - ask the network to complete the question, and train the gating network using - the question and the resulting completions. - """ - # Store the current epoch block number for comparison later. - last_epoch_block = self.subtensor.block - steps = 0 - - # grab the question from the current sample - prompt = next(self.dataset)['context'] - self.base_prompt = self.config.neuron.base_prompt - reward_diff = 0 - self.last_sync = self.subtensor.block - - # Start an infinite loop for training. - try: - while True: - # Ask the network to complete the random question, training the gating network. - with open('prompt_history.txt', 'a') as file: - file.write(f"{steps} | Q score({round(reward_diff , 4)}): {prompt}" + '\n') - - forward_result = self.forward( - roles = ['system', 'user' ], - messages = [ self.base_prompt, prompt ], - topk = self.config.neuron.training_topk, - random_sample_uids = True, - train_gating_model = True, - timeout = self.config.neuron.inference_timeout, - question = False - ) - - if forward_result is not None: - with open('prompt_history.txt', 'a') as file: - file.write(f"{steps} | A score({round(forward_result.rewards.sort(descending = True)[0][0].item(), 4)}): {forward_result.best_completion}" + '\n') - - idx_reward_sorted = forward_result.rewards.sort(descending = True)[1] - prompt, reward_diff = self.get_question( - uids = forward_result.uids[idx_reward_sorted], - bootstrap_prompt = forward_result.best_completion, - reset_bootstrap_prompt = (steps % self.config.neuron.reset_bootstrap_prompt_frequency == 0), - random_sample_uids = self.config.neuron.question_random_sample_uids - ) - - # Resync metagraph before returning. (sync every 15 min or ~75 blocks) - if self.subtensor.block - self.last_sync > 100: - self.metagraph.sync() - self.last_sync = self.subtensor.block - self.save() - delegates = self.subtensor.get_delegated( self.wallet.coldkeypub.ss58_address ) - - # Recreate pools here to ensure sizing is correct. - self.dendrite_pool = bt.text_prompting_pool( keypair = self.wallet.hotkey, metagraph = self.metagraph ) - self.inference_pool = bt.text_prompting_pool( keypair = self.wallet.hotkey, metagraph = self.metagraph ) - - self.my_nominators = { nomin[0]: nomin[1] for nomin in delegates[0][0].nominators } if len(delegates) else {} - self.check_weights() - - if self.metagraph.n > self.gating_model.num_uids: - self.gating_model = GatingModel( metagraph = self.metagraph, config = self.config ).to( self.device ) - - # Check if enough epoch blocks have elapsed since the last epoch. - epoch_length = self.subtensor.validator_epoch_length(self.config.netuid) if self.config.neuron.epoch_length_override == -1 else self.config.neuron.epoch_length_override - blocks_until_epoch = epoch_length - ( self.subtensor.block - last_epoch_block ) - bittensor.logging.debug( 'blocks_until_epoch', blocks_until_epoch ) - if blocks_until_epoch <= 0: - bittensor.logging.trace( 'epoch()' ) - bittensor.logging.info( 'block', self.subtensor.block ) - - # Update the last epoch block to the current epoch block. - last_epoch_block = self.subtensor.block - - # Computes the average reward for each uid across non-zero values - # using the rewards history stored in the self.history list. - uids, weights = self.compute_weights() - bittensor.logging.info( 'weights', weights ) - - # Set the weights on chain via our subtensor connection. - self.subtensor.set_weights( - wallet = self.wallet, - netuid = self.config.netuid, - uids = uids, - weights = weights, - wait_for_finalization = False, - ) - steps += 1 - - except Exception as e: - bittensor.logging.info( 'Error in training loop', str( e ) ) - print(traceback.format_exc()) - - def compute_weights( self ) -> Tuple[ torch.LongTensor, torch.FloatTensor ]: - """ - Computes the average reward for each uid across non-zero values - using the rewards history stored in the self.history list. - - Returns: - uids ( torch.LongTensor, shape = (n) ): - Uid to set weights on. - weights ( torch.FloatTensor, shape = (n) ): - The weights for each uid. - """ - bittensor.logging.info( 'compute_weights()' ) - - # Return zeros weights if there is no history. - if self.history.qsize() == 0: - bittensor.logging.warning( 'No history to compute weights returning all ones.' ) - return torch.ones((self.metagraph.n)) / self.metagraph.n - - # Calculate the average reward for each uid across non-zero values. - # Replace any NaN values with 0. - raw_weights = torch.nn.functional.normalize( self.moving_averaged_scores, p=1, dim=0 ) - bittensor.logging.trace( 'raw_weights', raw_weights ) - bittensor.logging.trace( 'top10 values', raw_weights.sort()[0] ) - bittensor.logging.trace( 'top10 uids', raw_weights.sort()[1] ) - - # Process the raw weights to final_weights via subtensor limitations. - processed_weight_uids, processed_weights = bittensor.utils.weight_utils.process_weights_for_netuid( - uids = self.metagraph.uids.to( "cpu" ), - weights = raw_weights.to( "cpu" ), - netuid = self.config.netuid, - subtensor = self.subtensor, - metagraph = self.metagraph - ) - bittensor.logging.trace( 'processed_weights', processed_weights ) - bittensor.logging.trace( 'processed_weight_uids', processed_weight_uids ) - return processed_weight_uids, processed_weights - - def run(self): - if self.config.neuron.inference_only: - # Start an infinite loop, allows axon to service inference requests. - last_sync = self.subtensor.block - while True: - time.sleep(12) - if self.subtensor.block -last_sync > 100: - self.metagraph.sync() - self.last_sync = self.subtensor.block - self.load(inference_only = True) - - else: - # Normal validator train operation for validation. - self.train() - - def save(self, path=None): - r""" Save hotkeys and moving average scores to filesystem. """ - try: - if path is None: - path = self.config.neuron.full_path - state_dict = { - 'neuron_weights': self.moving_averaged_scores, - 'neuron_hotkeys': self.hotkeys - } - - torch.save(state_dict, f'{path}/model.torch') - bittensor.logging.success(prefix='Saved model', sufix=f'{path}/model.torch') - - gating_state_dict = { - 'model_state_dict':self.gating_model.state_dict(), - 'num_hotkeys': self.gating_model.num_uids - } - torch.save(gating_state_dict, f'{path}/gating.torch') - bittensor.logging.success(prefix='Saved gating model', sufix=f'{path}/gating.torch') - except Exception as e: - logger.warning(f'Failed to save model with error: {e}') - - def load(self, path=None, inference_only=False): - r""" Load hotkeys and moving average scores from filesystem. """ - try: - if path is None: - path = self.config.neuron.full_path - state_dict = torch.load(f'{path}/model.torch') - self.moving_averaged_scores = state_dict['neuron_weights'].clone().detach() - self.hotkeys = state_dict['neuron_hotkeys'] - bittensor.logging.success(prefix='Reloaded model', sufix=f'{path}/model.torch') - - gating_state_dict = torch.load(f'{path}/gating.torch') - if self.gating_model.num_uids == gating_state_dict['num_hotkeys']: - self.gating_model.load_state_dict(gating_state_dict['model_state_dict'], strict=False) - bittensor.logging.success(prefix='Reloaded Gating model', sufix=f'{path}/gating.torch') - - elif inference_only: - self.gating_model = GatingModel( metagraph = self.metagraph, config = self.config, num_uids=gating_state_dict['num_hotkeys']).to( self.device ) - self.gating_model.load_state_dict(gating_state_dict['model_state_dict'], strict=False) - bittensor.logging.success(prefix='Reloaded Gating model', sufix=f'{path}/gating.torch') - - except Exception as e: - logger.warning(f'Failed to load model with error: {e}') - - def check_weights(self): - """ Checks current hotkeys with the current version of the metagraph """ - for uid, hotkey in enumerate( self.hotkeys ): - if hotkey != self.metagraph.hotkeys[ uid ]: - self.moving_averaged_scores[ uid ] = 0 #hotkey has been replaced - if self.metagraph.validator_permit[ uid ] and self.metagraph.S[ uid ] > self.config.neuron.vpermit_tao_limit: - self.moving_averaged_scores[ uid ] = 0 # hotkey has validation rights and is below the tao limit - if len(self.hotkeys) < len(self.metagraph.hotkeys): - new_moving_average = torch.zeros((self.metagraph.n)).to( self.device ) - new_moving_average[:len(self.hotkeys)] = self.moving_averaged_scores - self.moving_averaged_scores = new_moving_average - self.hotkeys = copy.deepcopy(self.metagraph.hotkeys) - - -if __name__ == '__main__': - bittensor.logging.info( 'neuron().train()' ) - neuron().run() diff --git a/neurons/text/prompting/validators/core/requirements.txt b/neurons/text/prompting/validators/core/requirements.txt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/neurons/text/prompting/validators/core/reward.py b/neurons/text/prompting/validators/core/reward.py deleted file mode 100644 index 95c5a7f77c..0000000000 --- a/neurons/text/prompting/validators/core/reward.py +++ /dev/null @@ -1,163 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -#### NOTE(carro): This code is modified from trlX - -import torch -import argparse -import bittensor - -from torch import nn -from typing import List -from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig - -class RewardModel(nn.Module): - - def __init__( self, model_path: str, device: str, config: 'bittensor.config' = None): - super().__init__() - config = AutoConfig.from_pretrained( model_path ) - self.model = AutoModelForCausalLM.from_config( config ) - self.config = self.model.config - # `gpt-neo(x)` models use `hidden_size` attribute names instead of `n_embd`` - if config is None: config = RewardModel.config() - - self.config.n_embd = self.config.hidden_size if hasattr(self.config, "hidden_size") else self.config.n_embd - self.device = torch.device( device ) - self.transformer = self.model.transformer - self.v_head = nn.Linear(self.config.n_embd, 1, bias=False) - self.tokenizer = AutoTokenizer.from_pretrained('EleutherAI/gpt-j-6b') - self.tokenizer.pad_token = self.tokenizer.eos_token - self.PAD_ID = self.tokenizer(self.tokenizer.pad_token)["input_ids"][0] - - def reward( self, full_completions: List[str], comp: List[str], difference=False, shift =3) -> torch.FloatTensor: - def reward_fn( samples ): - if samples is None: return 0 - scores_list = [] - batch_size = 1 - for i in range(0, len(samples), batch_size): - sub_samples = samples[i : i + batch_size] - sub_samples = [ - "<|startoftext|>" + chosen + "<|endoftext|>" for chosen in sub_samples - ] - encodings_dict = self.tokenizer( - sub_samples, - truncation=False, - max_length=550, - padding="max_length", - return_tensors="pt", - ) - input_ids = encodings_dict["input_ids"].to( self.device ) - attn_masks = encodings_dict["attention_mask"].to( self.device ) - input_ids = input_ids.repeat(2, 1) - attn_masks = attn_masks.repeat(2, 1) - with torch.no_grad(): - sub_scores = self.forward(input_ids=input_ids.to( self.device ), attention_mask=attn_masks.to( self.device )) - scores_list.append(sub_scores["chosen_end_scores"]) - scores = torch.cat(scores_list, dim=0).mean().item() - return scores - - with torch.no_grad(): - full_rewards = [reward_fn([completion]) for completion in full_completions] - if difference: - comp_rewards = [reward_fn([completion]) for completion in comp] - return torch.nn.functional.relu(torch.tensor(full_rewards, dtype=torch.float32)+shift) - torch.nn.functional.relu(torch.tensor(comp_rewards, dtype=torch.float32)+shift) - else: - for completion, f_reward in zip(full_completions, full_rewards): - print(completion) - print(f_reward) - return torch.tensor(full_rewards, dtype=torch.float32) - def forward( - self, - input_ids=None, - past_key_values=None, - attention_mask=None, - token_type_ids=None, - position_ids=None, - head_mask=None, - inputs_embeds=None, - mc_token_ids=None, - labels=None, - return_dict=False, - output_attentions=False, - output_hidden_states=False, - ): - loss = None - transformer_outputs = self.transformer( - input_ids, - attention_mask=attention_mask, - ) - - hidden_states = transformer_outputs[0] - - rewards = self.v_head(hidden_states).squeeze(-1) - chosen_end_scores = [] - rejected_end_scores = [] - - # Split the inputs and rewards into two parts, chosen and rejected - assert len(input_ids.shape) == 2 - bs = input_ids.shape[0] // 2 - chosen = input_ids[:bs] - rejected = input_ids[bs:] - chosen_rewards = rewards[:bs] - rejected_rewards = rewards[bs:] - - loss = 0 - inference = False - for i in range(bs): - if torch.all(torch.eq(chosen[i], rejected[i])).item(): - c_inds = (chosen[i] == self.PAD_ID).nonzero() - c_ind = c_inds[0].item() if len(c_inds) > 0 else chosen.shape[1] - chosen_end_scores.append(chosen_rewards[i, c_ind - 1]) - inference = True - continue - - # Check if there is any padding otherwise take length of sequence - c_inds = (chosen[i] == self.PAD_ID).nonzero() - c_ind = c_inds[0].item() if len(c_inds) > 0 else chosen.shape[1] - r_inds = (rejected[i] == self.PAD_ID).nonzero() - r_ind = r_inds[0].item() if len(r_inds) > 0 else rejected.shape[1] - end_ind = max(c_ind, r_ind) - - # Retrieve first index where trajectories diverge - divergence_ind = (chosen[i] != rejected[i]).nonzero()[0] - assert divergence_ind > 0 - - # Index into the correct rewards - c_truncated_reward = chosen_rewards[i][divergence_ind:end_ind] - r_truncated_reward = rejected_rewards[i][divergence_ind:end_ind] - - # Append the last rewards to the list of end scores - chosen_end_scores.append(c_truncated_reward[-1]) - rejected_end_scores.append(r_truncated_reward[-1]) - - # Compute loss based on truncated rewards (ignore padding) - loss += -torch.log(torch.sigmoid(c_truncated_reward - r_truncated_reward)).mean() - loss = loss / bs - - if not inference: - chosen_end_scores = torch.stack(chosen_end_scores) - rejected_end_scores = torch.stack(rejected_end_scores) - - if inference: - chosen_end_scores = torch.stack(chosen_end_scores) - return {"chosen_end_scores": chosen_end_scores} - - return { - "loss": loss, - "chosen_end_scores": chosen_end_scores, - "rejected_end_scores": rejected_end_scores, - } diff --git a/requirements/prod.txt b/requirements/prod.txt index 63df59bce5..86bb295205 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -2,6 +2,8 @@ ansible_vault==2.1 argparse==1.4.0 base58==2.0.1 backoff==2.1.0 +bittensor-config==0.0.0 +bittensor-wallet==0.0.1 cryptography==39.0.0 datasets==2.12.0 fuzzywuzzy==0.18.0 diff --git a/scripts/release/add_notes_changelog.sh b/scripts/release/add_notes_changelog.sh index 008dc99a1d..d0f7594f55 100755 --- a/scripts/release/add_notes_changelog.sh +++ b/scripts/release/add_notes_changelog.sh @@ -33,6 +33,11 @@ while [[ $# -gt 0 ]]; do shift # past argument shift # past value ;; + -B|--release-branch) + RELEASE_BRANCH="$2" + shift # past argument + shift # past value + ;; -*|--*) echo "Unknown option $1" exit 1 @@ -59,6 +64,11 @@ if [[ -z $VERSION ]]; then exit 1 fi +if [[ -z $RELEASE_BRANCH ]]; then + echo_warning "Release branch not specified with (-B, --release-branch) assuming: release/$VERSION" + RELEASE_BRANCH=release/$VERSION +fi + DATE=$(date +"%Y-%m-%d") RELEASE_NAME="$VERSION / $DATE" TAG_NAME=v$VERSION @@ -67,7 +77,7 @@ PREV_TAG_NAME=v$PREV_TAG_VERSION # 2.2. Generate release notes if [[ $APPLY == "true" ]]; then echo_info "Generating Github release notes" - RESPONSE=$(generate_github_release_notes $GITHUB_TOKEN) + RESPONSE=$(generate_github_release_notes_for_changelog $GITHUB_TOKEN) DESCRIPTION=$(echo $RESPONSE | jq '.body' | tail -1 | sed "s/\"//g") if [ $(echo $RESPONSE | jq '.body' | wc -l) -eq 1 ]; then diff --git a/scripts/release/github_utils.sh b/scripts/release/github_utils.sh index fcd3a6b45c..e10cb96166 100644 --- a/scripts/release/github_utils.sh +++ b/scripts/release/github_utils.sh @@ -59,6 +59,26 @@ function generate_github_release_notes_post_data() EOF } +# +# Needs: +# - TAG_NAME +# - RELEASE_BRANCH +# - RELEASE_NAME +# +function generate_github_release_notes_for_changelog_post_data() +{ + cat </hotkeys/ ) : ") -descriptive_name = input("Your validator's descriptive name (i.e. Opentensor Foundation): ") -url = input("Your validator url (i.e. www.opentensor.org ): ") -description = input("A short description for your validator ( i.e. Build, maintain and advance Bittensor): ") -keypair = bittensor.Keypair.create_from_mnemonic(mnemonic) -dictionary = {} -dictionary[ keypair.ss58_address ] = { - 'name': descriptive_name, - 'url': url, - 'description': description, -} -message = json.dumps( dictionary ) -signature = keypair.sign( data = message ) -print('\n\n\tVerified', bittensor.Keypair(ss58_address=keypair.ss58_address).verify( data = message, signature = signature) ) -print ( - "\tValidator information: {}\n".format(message), - "\tValidator signature: {}\n\n".format(signature.hex()), -) \ No newline at end of file diff --git a/scripts/validator_info_signature/verify.py b/scripts/validator_info_signature/verify.py deleted file mode 100644 index ad7b705352..0000000000 --- a/scripts/validator_info_signature/verify.py +++ /dev/null @@ -1,9 +0,0 @@ -import json -import bittensor -import binascii -information_str = input("Validator information: ") -signature_hex = input("Validator signature: ").encode() -information_dict = json.loads(information_str) -print (str(list(information_dict.keys())[0])) -keypair = bittensor.Keypair(ss58_address=str(list(information_dict.keys())[0])) -print ('Verified', keypair.verify( data = information_str, signature = binascii.unhexlify( signature_hex ) )) \ No newline at end of file diff --git a/tests/helpers.py b/tests/helpers.py index 5e68aeb3b7..771880dfa3 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -21,6 +21,8 @@ from rich.console import Console from rich.text import Text +from tests.mocks.wallet_mock import MockWallet + from Crypto.Hash import keccak class CLOSE_IN_VALUE(): @@ -127,6 +129,24 @@ def get_mock_neuron_by_uid( uid: int, **kwargs ) -> NeuronInfo: **kwargs ) +def get_mock_wallet(coldkey: "Keypair" = None, hotkey: "Keypair" = None): + wallet = MockWallet( + name = 'mock_wallet', + hotkey = 'mock', + path = '/tmp/mock_wallet', + ) + + if not coldkey: + coldkey = Keypair.create_from_mnemonic(Keypair.generate_mnemonic()) + if not hotkey: + hotkey = Keypair.create_from_mnemonic(Keypair.generate_mnemonic()) + + wallet.set_coldkey(coldkey, encrypt=False, overwrite=True) + wallet.set_coldkeypub(coldkey, encrypt=False, overwrite=True) + wallet.set_hotkey(hotkey, encrypt=False, overwrite=True) + + return wallet + class MockStatus: def __enter__(self): return self diff --git a/tests/integration_tests/test_cli.py b/tests/integration_tests/test_cli.py index 64740c731c..9a18d22403 100644 --- a/tests/integration_tests/test_cli.py +++ b/tests/integration_tests/test_cli.py @@ -21,94 +21,52 @@ from copy import deepcopy from types import SimpleNamespace from typing import Dict -from unittest.mock import ANY, MagicMock, call, patch +from unittest.mock import MagicMock, patch import random import pytest -import substrateinterface from substrateinterface.base import Keypair import bittensor -from bittensor._subtensor.subtensor_mock import Mock_Subtensor, mock_subtensor from bittensor.utils.balance import Balance -from tests.helpers import MockConsole, get_mock_keypair +from tests.helpers import MockConsole, get_mock_keypair, get_mock_wallet as generate_wallet +from bittensor._subtensor.subtensor_mock import MockSubtensor -_subtensor_mock: Mock_Subtensor = None +_subtensor_mock: MockSubtensor = bittensor.subtensor( network = 'mock', _mock = True ) -def setupMockSubtensor(): - global _subtensor_mock - # Start a mock instance of subtensor. - _subtensor_mock = bittensor.subtensor( _mock = True, network='finney' ) +def setUpModule(): + _subtensor_mock.reset() -# Only run once per session. -# Runs before all tests and only once. -# @pytest.fixture(scope="session", autouse=True) -# def setupSubnets(request): -# # Setup first mock subtensor -# setupMockSubtensor() + _subtensor_mock.create_subnet( + netuid = 1 + ) -# def killMockSubtensorProcess(): -# _subtensor_mock.optionally_kill_owned_mock_instance() + _subtensor_mock.create_subnet( + netuid = 2 + ) -# # Setup mock subtensor networks. -# try: -# # create mock subnet 2 -# created_subnet, err = _subtensor_mock.sudo_add_network( netuid = 2, tempo = 90, modality = 0, wait_for_finalization=False ) -# if err != None: raise Exception(err) + _subtensor_mock.create_subnet( + netuid = 3 + ) -# # create mock subnet 3 -# created_subnet, err = _subtensor_mock.sudo_add_network( netuid = 3, tempo = 90, modality = 0, wait_for_finalization=False ) -# if err != None: raise Exception(err) + # Set diff 0 + _subtensor_mock.set_difficulty( + netuid = 1, + difficulty = 0 + ) -# # create a mock subnet 1 -# created_subnet, err = _subtensor_mock.sudo_add_network( netuid = 1, tempo = 99, modality = 0, wait_for_finalization=False ) -# if err != None: raise Exception(err) + _subtensor_mock.set_difficulty( + netuid = 2, + difficulty = 0 + ) -# # Make registration difficulty 0. Instant registration. -# set_diff, err = _subtensor_mock.sudo_set_difficulty( netuid = 1, difficulty = 0, wait_for_finalization=False ) -# if err != None: raise Exception(err) + _subtensor_mock.set_difficulty( + netuid = 3, + difficulty = 0 + ) -# # Make registration min difficulty 0. -# set_min_diff, err = _subtensor_mock.sudo_set_min_difficulty( netuid = 1, min_difficulty = 0, wait_for_finalization=False ) -# if err != None: raise Exception(err) - -# # Make registration max difficulty 1. -# set_max_diff, err = _subtensor_mock.sudo_set_max_difficulty( netuid = 1, max_difficulty = 1, wait_for_finalization=False ) -# if err != None: raise Exception(err) - -# set_tx_limit, err = _subtensor_mock.sudo_set_tx_rate_limit( netuid = 1, tx_rate_limit = 0, wait_for_finalization=False ) # No tx limit -# if err != None: raise Exception(err) - -# except Exception as e: -# print("Error in setup: ", e) - -# else: -# # Seems to be the process owner of the mock instance. -# # Setup mock kill to run after all tests. -# request.addfinalizer(killMockSubtensorProcess) - -# yield - -# def setUpModule(): -# setupMockSubtensor() - -def generate_wallet(coldkey : 'Keypair' = None, hotkey: 'Keypair' = None): - wallet = bittensor.wallet(_mock=True).create() - - if not coldkey: - coldkey = Keypair.create_from_mnemonic(Keypair.generate_mnemonic()) - if not hotkey: - hotkey = Keypair.create_from_mnemonic(Keypair.generate_mnemonic()) - - wallet.set_coldkey(coldkey, encrypt=False, overwrite=True) - wallet.set_coldkeypub(coldkey, encrypt=False, overwrite=True) - wallet.set_hotkey(hotkey, encrypt=False, overwrite=True) - - return wallet - -@unittest.skip("") class TestCLIWithNetworkAndConfig(unittest.TestCase): def setUp(self): self._config = TestCLIWithNetworkAndConfig.construct_config() @@ -121,48 +79,50 @@ def config(self): @staticmethod def construct_config(): defaults = bittensor.Config() + defaults.netuid = 1 - bittensor.subtensor.add_defaults( defaults ) + bittensor.subtensor.add_defaults(defaults) # Always use mock subtensor. - defaults.subtensor.network = 'finney' + defaults.subtensor.network = "finney" defaults.subtensor._mock = True # Skip version checking. defaults.no_version_checking = True - bittensor.dendrite.add_defaults( defaults ) - bittensor.axon.add_defaults( defaults ) - bittensor.wallet.add_defaults( defaults ) - bittensor.dataset.add_defaults( defaults ) + bittensor.axon.add_defaults(defaults) + bittensor.wallet.add_defaults(defaults) + bittensor.dataset.add_defaults(defaults) + bittensor.logging.add_defaults(defaults) + bittensor.prometheus.add_defaults(defaults) return defaults - def test_overview( self ): + + def test_overview(self): config = self.config - config.wallet.path = '/tmp/test_cli_test_overview' - config.wallet.name = 'mock_wallet' + config.wallet.path = "/tmp/test_cli_test_overview" + config.wallet.name = "mock_wallet" config.command = "overview" config.no_prompt = True config.all = False - config.netuid = [] # Don't set, so it tries all networks. + config.netuid = [] # Don't set, so it tries all networks. cli = bittensor.cli(config) - mock_hotkeys = ['hk0', 'hk1', 'hk2', 'hk3', 'hk4'] + mock_hotkeys = ["hk0", "hk1", "hk2", "hk3", "hk4"] mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( - name = config.wallet.name, - coldkey = mock_coldkey_kp, - coldkeypub = mock_coldkey_kp, - hotkey_str = hk, - hotkey = get_mock_keypair(idx + 100, self.id()), - coldkeypub_file = MagicMock( - exists_on_device=MagicMock( - return_value=True # Wallet exists - ) + name=config.wallet.name, + coldkey=mock_coldkey_kp, + coldkeypub=mock_coldkey_kp, + hotkey_str=hk, + hotkey=get_mock_keypair(idx + 100, self.id()), + coldkeypub_file=MagicMock( + exists_on_device=MagicMock(return_value=True) # Wallet exists ), - ) for idx, hk in enumerate(mock_hotkeys) + ) + for idx, hk in enumerate(mock_hotkeys) ] mock_registrations = [ @@ -174,27 +134,28 @@ def test_overview( self ): (2, mock_wallets[2]), (3, mock_wallets[0]), (3, mock_wallets[1]), - (3, mock_wallets[2]), # All registered on netuid 3 (but hk3) - (3, mock_wallets[4]) # hk4 is only on netuid 3 - ] # hk3 is not registered on any network + (3, mock_wallets[2]), # All registered on netuid 3 (but hk3) + (3, mock_wallets[4]), # hk4 is only on netuid 3 + ] # hk3 is not registered on any network # Register each wallet to it's subnet. + print("Registering wallets to mock subtensor...") + for netuid, wallet in mock_registrations: - result, err = _subtensor_mock.sudo_register( - netuid = netuid, - coldkey = wallet.coldkey.ss58_address, - hotkey = wallet.hotkey.ss58_address + _ = _subtensor_mock.force_register_neuron( + netuid=netuid, + coldkey=wallet.coldkey.ss58_address, + hotkey=wallet.hotkey.ss58_address, ) - self.assertTrue(result, err) - + def mock_get_wallet(*args, **kwargs): - hk = kwargs.get('hotkey') - name_ = kwargs.get('name') + hk = kwargs.get("hotkey") + name_ = kwargs.get("name") - if not hk and kwargs.get('config'): - hk = kwargs.get('config').wallet.hotkey - if not name_ and kwargs.get('config'): - name_ = kwargs.get('config').wallet.name + if not hk and kwargs.get("config"): + hk = kwargs.get("config").wallet.hotkey + if not name_ and kwargs.get("config"): + name_ = kwargs.get("config").wallet.name for wallet in mock_wallets: if wallet.name == name_ and wallet.hotkey_str == hk: @@ -207,26 +168,32 @@ def mock_get_wallet(*args, **kwargs): return mock_wallets[0] mock_console = MockConsole() - with patch('bittensor._cli.commands.overview.get_hotkey_wallets_for_wallet') as mock_get_all_wallets: + with patch( + "bittensor._cli.commands.overview.get_hotkey_wallets_for_wallet" + ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet - with patch('bittensor.__console__', mock_console): + with patch("bittensor.__console__", mock_console): cli.run() # Check that the overview was printed. self.assertIsNotNone(mock_console.captured_print) - output_no_syntax = mock_console.remove_rich_syntax(mock_console.captured_print) + output_no_syntax = mock_console.remove_rich_syntax( + mock_console.captured_print + ) # Check that each subnet was printed. - self.assertIn('Subnet: 1', output_no_syntax) - self.assertIn('Subnet: 2', output_no_syntax) - self.assertIn('Subnet: 3', output_no_syntax) + self.assertIn("Subnet: 1", output_no_syntax) + self.assertIn("Subnet: 2", output_no_syntax) + self.assertIn("Subnet: 3", output_no_syntax) # Check that only registered hotkeys are printed once for each subnet. for wallet in mock_wallets: - expected = [wallet.hotkey_str for _, wallet in mock_registrations].count(wallet.hotkey_str) + expected = [ + wallet.hotkey_str for _, wallet in mock_registrations + ].count(wallet.hotkey_str) occurrences = output_no_syntax.count(wallet.hotkey_str) self.assertEqual(occurrences, expected) @@ -235,59 +202,63 @@ def mock_get_wallet(*args, **kwargs): if wallet not in [w for _, w in mock_registrations]: self.assertNotIn(wallet.hotkey_str, output_no_syntax) - def test_overview_not_in_first_subnet( self ): + + def test_overview_not_in_first_subnet(self): config = self.config - config.wallet.path = '/tmp/test_cli_test_overview' - config.wallet.name = 'mock_wallet' + config.wallet.path = "/tmp/test_cli_test_overview" + config.wallet.name = "mock_wallet" config.command = "overview" config.no_prompt = True config.all = False - config.netuid = [] # Don't set, so it tries all networks. + config.netuid = [] # Don't set, so it tries all networks. cli = bittensor.cli(config) - mock_hotkeys = ['hk0', 'hk1', 'hk2', 'hk3', 'hk4'] + mock_hotkeys = ["hk0", "hk1", "hk2", "hk3", "hk4"] mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( - name = config.wallet.name, - coldkey = mock_coldkey_kp, - coldkeypub = mock_coldkey_kp, - hotkey_str = hk, - hotkey = get_mock_keypair(idx + 100, self.id()), - coldkeypub_file = MagicMock( - exists_on_device=MagicMock( - return_value=True # Wallet exists - ) + name=config.wallet.name, + coldkey=mock_coldkey_kp, + coldkeypub=mock_coldkey_kp, + hotkey_str=hk, + hotkey=get_mock_keypair(idx + 100, self.id()), + coldkeypub_file=MagicMock( + exists_on_device=MagicMock(return_value=True) # Wallet exists ), - ) for idx, hk in enumerate(mock_hotkeys) + ) + for idx, hk in enumerate(mock_hotkeys) ] mock_registrations = [ # No registrations in subnet 1 or 2 - (3, mock_wallets[4]) # hk4 is on netuid 3 + (3, mock_wallets[4]) # hk4 is on netuid 3 ] # Register each wallet to it's subnet print("Registering mock wallets to subnets...") + + for netuid, wallet in mock_registrations: - print("Registering wallet {} to subnet {}".format(wallet.hotkey_str, netuid)) - _subtensor_mock.sudo_register( - netuid = netuid, - coldkey = wallet.coldkey.ss58_address, - hotkey = wallet.hotkey.ss58_address + print( + "Registering wallet {} to subnet {}".format(wallet.hotkey_str, netuid) + ) + _ = _subtensor_mock.force_register_neuron( + netuid=netuid, + coldkey=wallet.coldkey.ss58_address, + hotkey=wallet.hotkey.ss58_address, ) def mock_get_wallet(*args, **kwargs): - hk = kwargs.get('hotkey') - name_ = kwargs.get('name') + hk = kwargs.get("hotkey") + name_ = kwargs.get("name") - if not hk and kwargs.get('config'): - hk = kwargs.get('config').wallet.hotkey - if not name_ and kwargs.get('config'): - name_ = kwargs.get('config').wallet.name + if not hk and kwargs.get("config"): + hk = kwargs.get("config").wallet.hotkey + if not name_ and kwargs.get("config"): + name_ = kwargs.get("config").wallet.name for wallet in mock_wallets: if wallet.name == name_ and wallet.hotkey_str == hk: @@ -300,27 +271,33 @@ def mock_get_wallet(*args, **kwargs): return mock_wallets[0] mock_console = MockConsole() - with patch('bittensor._cli.commands.overview.get_hotkey_wallets_for_wallet') as mock_get_all_wallets: + with patch( + "bittensor._cli.commands.overview.get_hotkey_wallets_for_wallet" + ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet - with patch('bittensor.__console__', mock_console): + with patch("bittensor.__console__", mock_console): cli.run() # Check that the overview was printed. self.assertIsNotNone(mock_console.captured_print) - output_no_syntax = mock_console.remove_rich_syntax(mock_console.captured_print) + output_no_syntax = mock_console.remove_rich_syntax( + mock_console.captured_print + ) # Check that each subnet was printed except subnet 1 and 2. # Subnet 1 and 2 are not printed because no wallet is registered to them. - self.assertNotIn('Subnet: 1', output_no_syntax) - self.assertNotIn('Subnet: 2', output_no_syntax) - self.assertIn('Subnet: 3', output_no_syntax) + self.assertNotIn("Subnet: 1", output_no_syntax) + self.assertNotIn("Subnet: 2", output_no_syntax) + self.assertIn("Subnet: 3", output_no_syntax) # Check that only registered hotkeys are printed once for each subnet. for wallet in mock_wallets: - expected = [wallet.hotkey_str for _, wallet in mock_registrations].count(wallet.hotkey_str) + expected = [ + wallet.hotkey_str for _, wallet in mock_registrations + ].count(wallet.hotkey_str) occurrences = output_no_syntax.count(wallet.hotkey_str) self.assertEqual(occurrences, expected) @@ -329,202 +306,212 @@ def mock_get_wallet(*args, **kwargs): if wallet not in [w for _, w in mock_registrations]: self.assertNotIn(wallet.hotkey_str, output_no_syntax) - - def test_overview_no_wallet( self ): + + def test_overview_no_wallet(self): # Mock IO for wallet - with patch('bittensor.Wallet.coldkeypub_file', MagicMock( - exists_on_device=MagicMock( - return_value=False - ) - )): - bittensor.subtensor.register = MagicMock(return_value = True) + with patch( + "bittensor.Wallet.coldkeypub_file", + MagicMock(exists_on_device=MagicMock(return_value=False)), + ): + bittensor.subtensor.register = MagicMock(return_value=True) config = self.config config.command = "overview" config.no_prompt = True config.all = False - config.netuid = [] # Don't set, so it tries all networks. + config.netuid = [] # Don't set, so it tries all networks. cli = bittensor.cli(config) cli.run() - def test_overview_with_hotkeys_config( self ): + + def test_overview_with_hotkeys_config(self): config = self.config config.command = "overview" config.no_prompt = True - config.hotkeys = ['some_hotkey'] + config.hotkeys = ["some_hotkey"] config.all = False - config.netuid = [] # Don't set, so it tries all networks. + config.netuid = [] # Don't set, so it tries all networks. cli = bittensor.cli(config) cli.run() - def test_overview_without_hotkeys_config( self ): + + def test_overview_without_hotkeys_config(self): config = self.config config.command = "overview" config.no_prompt = True config.all = False - config.netuid = [] # Don't set, so it tries all networks. + config.netuid = [] # Don't set, so it tries all networks. cli = bittensor.cli(config) cli.run() - def test_overview_with_sort_by_config( self ): + + def test_overview_with_sort_by_config(self): config = self.config config.command = "overview" config.no_prompt = True config.wallet.sort_by = "rank" config.all = False - config.netuid = [] # Don't set, so it tries all networks. + config.netuid = [] # Don't set, so it tries all networks. cli = bittensor.cli(config) cli.run() - def test_overview_with_sort_by_bad_column_name( self ): + + def test_overview_with_sort_by_bad_column_name(self): config = self.config config.command = "overview" config.no_prompt = True config.wallet.sort_by = "totallynotmatchingcolumnname" config.all = False - config.netuid = [] # Don't set, so it tries all networks. + config.netuid = [] # Don't set, so it tries all networks. cli = bittensor.cli(config) cli.run() - def test_overview_without_sort_by_config( self ): + + def test_overview_without_sort_by_config(self): config = self.config config.command = "overview" config.no_prompt = True config.all = False - config.netuid = [] # Don't set, so it tries all networks. + config.netuid = [] # Don't set, so it tries all networks. cli = bittensor.cli(config) cli.run() - def test_overview_with_sort_order_config( self ): + + def test_overview_with_sort_order_config(self): config = self.config config.command = "overview" - config.wallet.sort_order = "desc" # Set descending sort order + config.wallet.sort_order = "desc" # Set descending sort order config.no_prompt = True config.all = False - config.netuid = [] # Don't set, so it tries all networks. + config.netuid = [] # Don't set, so it tries all networks. cli = bittensor.cli(config) cli.run() - def test_overview_with_sort_order_config_bad_sort_type( self ): + + def test_overview_with_sort_order_config_bad_sort_type(self): config = self.config config.command = "overview" config.wallet.sort_order = "nowaythisshouldmatchanyorderingchoice" config.no_prompt = True config.all = False - config.netuid = [] # Don't set, so it tries all networks. + config.netuid = [] # Don't set, so it tries all networks. cli = bittensor.cli(config) cli.run() - def test_overview_without_sort_order_config( self ): + + def test_overview_without_sort_order_config(self): config = self.config config.command = "overview" # Don't specify sort_order in config config.no_prompt = True config.all = False - config.netuid = [] # Don't set, so it tries all networks. + config.netuid = [] # Don't set, so it tries all networks. cli = bittensor.cli(config) cli.run() - def test_overview_with_width_config( self ): + + def test_overview_with_width_config(self): config = self.config config.command = "overview" config.width = 100 config.no_prompt = True config.all = False - config.netuid = [] # Don't set, so it tries all networks. + config.netuid = [] # Don't set, so it tries all networks. cli = bittensor.cli(config) cli.run() - def test_overview_without_width_config( self ): + + def test_overview_without_width_config(self): config = self.config config.command = "overview" # Don't specify width in config config.no_prompt = True config.all = False - config.netuid = [] # Don't set, so it tries all networks. + config.netuid = [] # Don't set, so it tries all networks. cli = bittensor.cli(config) cli.run() - def test_overview_all( self ): + + def test_overview_all(self): config = self.config config.command = "overview" config.no_prompt = True - config.netuid = [] # Don't set, so it tries all networks. + config.netuid = [] # Don't set, so it tries all networks. config.all = True cli = bittensor.cli(config) cli.run() - def test_unstake_with_specific_hotkeys( self ): + def test_unstake_with_specific_hotkeys(self): config = self.config config.command = "unstake" config.no_prompt = True config.amount = 5.0 config.wallet.name = "fake_wallet" - config.hotkeys = [ - 'hk0', 'hk1', 'hk2' - ] - config.all_hotkeys =False + config.hotkeys = ["hk0", "hk1", "hk2"] + config.all_hotkeys = False # Notice no max_stake specified mock_stakes: Dict[str, bittensor.Balance] = { # All have more than 5.0 stake - 'hk0': bittensor.Balance.from_float(10.0), - 'hk1': bittensor.Balance.from_float(11.1), - 'hk2': bittensor.Balance.from_float(12.2), + "hk0": bittensor.Balance.from_float(10.0), + "hk1": bittensor.Balance.from_float(11.1), + "hk2": bittensor.Balance.from_float(12.2), } mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( - name = config.wallet.name, - coldkey = mock_coldkey_kp, - coldkeypub = mock_coldkey_kp, - hotkey_str = hk, - hotkey = get_mock_keypair(idx + 100, self.id()), - ) for idx, hk in enumerate(config.hotkeys) + name=config.wallet.name, + coldkey=mock_coldkey_kp, + coldkeypub=mock_coldkey_kp, + hotkey_str=hk, + hotkey=get_mock_keypair(idx + 100, self.id()), + ) + for idx, hk in enumerate(config.hotkeys) ] # Register mock wallets and give them stakes + + for wallet in mock_wallets: - success, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = wallet.hotkey.ss58_address, - coldkey = wallet.coldkey.ss58_address, - stake = mock_stakes[wallet.hotkey_str].rao, + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=wallet.hotkey.ss58_address, + coldkey=wallet.coldkey.ss58_address, + stake=mock_stakes[wallet.hotkey_str].rao, ) - self.assertTrue(success, err) - + cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - if kwargs.get('hotkey'): + if kwargs.get("hotkey"): for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get('hotkey'): + if wallet.hotkey_str == kwargs.get("hotkey"): return wallet else: return mock_wallets[0] - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before unstaking for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao) @@ -534,69 +521,77 @@ def mock_get_wallet(*args, **kwargs): for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, + ) + self.assertAlmostEqual( + stake.tao, + mock_stakes[wallet.hotkey_str].tao - config.amount, + places=4, ) - self.assertAlmostEqual(stake.tao, mock_stakes[wallet.hotkey_str].tao - config.amount, places=4) - def test_unstake_with_all_hotkeys( self ): + + def test_unstake_with_all_hotkeys(self): config = self.config config.command = "unstake" config.no_prompt = True config.amount = 5.0 config.wallet.name = "fake_wallet" # Notice wallet.hotkeys not specified - config.all_hotkeys =True + config.all_hotkeys = True # Notice no max_stake specified mock_stakes: Dict[str, bittensor.Balance] = { # All have more than 5.0 stake - 'hk0': bittensor.Balance.from_float(10.0), - 'hk1': bittensor.Balance.from_float(11.1), - 'hk2': bittensor.Balance.from_float(12.2), + "hk0": bittensor.Balance.from_float(10.0), + "hk1": bittensor.Balance.from_float(11.1), + "hk2": bittensor.Balance.from_float(12.2), } mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( - name = config.wallet.name, - coldkey = mock_coldkey_kp, - coldkeypub = mock_coldkey_kp, - hotkey_str = hk, - hotkey = get_mock_keypair(idx + 100, self.id()), - ) for idx, hk in enumerate(list(mock_stakes.keys())) + name=config.wallet.name, + coldkey=mock_coldkey_kp, + coldkeypub=mock_coldkey_kp, + hotkey_str=hk, + hotkey=get_mock_keypair(idx + 100, self.id()), + ) + for idx, hk in enumerate(list(mock_stakes.keys())) ] # Register mock wallets and give them stakes + for wallet in mock_wallets: - success, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = wallet.hotkey.ss58_address, - coldkey = wallet.coldkey.ss58_address, - stake = mock_stakes[wallet.hotkey_str].rao, + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=wallet.hotkey.ss58_address, + coldkey=wallet.coldkey.ss58_address, + stake=mock_stakes[wallet.hotkey_str].rao, ) - self.assertTrue(success, err) - + cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - if kwargs.get('hotkey'): + if kwargs.get("hotkey"): for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get('hotkey'): + if wallet.hotkey_str == kwargs.get("hotkey"): return wallet else: return mock_wallets[0] - with patch('bittensor._cli.commands.unstake.get_hotkey_wallets_for_wallet') as mock_get_all_wallets: + with patch( + "bittensor._cli.commands.unstake.get_hotkey_wallets_for_wallet" + ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before unstaking for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao) @@ -606,68 +601,75 @@ def mock_get_wallet(*args, **kwargs): for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, + ) + self.assertAlmostEqual( + stake.tao, + mock_stakes[wallet.hotkey_str].tao - config.amount, + places=4, ) - self.assertAlmostEqual(stake.tao, mock_stakes[wallet.hotkey_str].tao - config.amount, places=4) - def test_unstake_with_exclude_hotkeys_from_all( self ): + def test_unstake_with_exclude_hotkeys_from_all(self): config = self.config config.command = "unstake" config.no_prompt = True config.amount = 5.0 config.wallet.name = "fake_wallet" - config.hotkeys = ["hk1"] # Exclude hk1 - config.all_hotkeys =True + config.hotkeys = ["hk1"] # Exclude hk1 + config.all_hotkeys = True mock_stakes: Dict[str, bittensor.Balance] = { # All have more than 5.0 stake - 'hk0': bittensor.Balance.from_float(10.0), - 'hk1': bittensor.Balance.from_float(11.1), - 'hk2': bittensor.Balance.from_float(12.2), + "hk0": bittensor.Balance.from_float(10.0), + "hk1": bittensor.Balance.from_float(11.1), + "hk2": bittensor.Balance.from_float(12.2), } mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( - name = config.wallet.name, - coldkey = mock_coldkey_kp, - coldkeypub = mock_coldkey_kp, - hotkey_str = hk, - hotkey = get_mock_keypair(idx + 100, self.id()), - ) for idx, hk in enumerate(list(mock_stakes.keys())) + name=config.wallet.name, + coldkey=mock_coldkey_kp, + coldkeypub=mock_coldkey_kp, + hotkey_str=hk, + hotkey=get_mock_keypair(idx + 100, self.id()), + ) + for idx, hk in enumerate(list(mock_stakes.keys())) ] # Register mock wallets and give them stakes + for wallet in mock_wallets: - success, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = wallet.hotkey.ss58_address, - coldkey = wallet.coldkey.ss58_address, - stake = mock_stakes[wallet.hotkey_str].rao, + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=wallet.hotkey.ss58_address, + coldkey=wallet.coldkey.ss58_address, + stake=mock_stakes[wallet.hotkey_str].rao, ) - self.assertTrue(success, err) - + cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - if kwargs.get('hotkey'): + if kwargs.get("hotkey"): for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get('hotkey'): + if wallet.hotkey_str == kwargs.get("hotkey"): return wallet else: return mock_wallets[0] - with patch('bittensor._cli.commands.unstake.get_hotkey_wallets_for_wallet') as mock_get_all_wallets: + with patch( + "bittensor._cli.commands.unstake.get_hotkey_wallets_for_wallet" + ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before unstaking for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao) @@ -677,77 +679,84 @@ def mock_get_wallet(*args, **kwargs): for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) - if wallet.hotkey_str == 'hk1': + if wallet.hotkey_str == "hk1": # hk1 should not have been unstaked - self.assertAlmostEqual(stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4) + self.assertAlmostEqual( + stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4 + ) else: - self.assertAlmostEqual(stake.tao, mock_stakes[wallet.hotkey_str].tao - config.amount, places=4) + self.assertAlmostEqual( + stake.tao, + mock_stakes[wallet.hotkey_str].tao - config.amount, + places=4, + ) - def test_unstake_with_multiple_hotkeys_max_stake( self ): + def test_unstake_with_multiple_hotkeys_max_stake(self): config = self.config config.command = "unstake" config.no_prompt = True # Notie amount is not specified - config.max_stake = 5.0 # The keys should have at most 5.0 tao staked after + config.max_stake = 5.0 # The keys should have at most 5.0 tao staked after config.wallet.name = "fake_wallet" - config.hotkeys = [ - 'hk0', 'hk1', 'hk2' - ] - config.all_hotkeys =False + config.hotkeys = ["hk0", "hk1", "hk2"] + config.all_hotkeys = False mock_stakes: Dict[str, bittensor.Balance] = { # All have more than 5.0 stake - 'hk0': bittensor.Balance.from_float(10.0), - 'hk1': bittensor.Balance.from_float(4.9), - 'hk2': bittensor.Balance.from_float(12.2), + "hk0": bittensor.Balance.from_float(10.0), + "hk1": bittensor.Balance.from_float(4.9), + "hk2": bittensor.Balance.from_float(12.2), } mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( - name = config.wallet.name, - coldkey = mock_coldkey_kp, - coldkeypub = mock_coldkey_kp, - hotkey_str = hk, - hotkey = get_mock_keypair(idx + 100, self.id()), - ) for idx, hk in enumerate(list(mock_stakes.keys())) + name=config.wallet.name, + coldkey=mock_coldkey_kp, + coldkeypub=mock_coldkey_kp, + hotkey_str=hk, + hotkey=get_mock_keypair(idx + 100, self.id()), + ) + for idx, hk in enumerate(list(mock_stakes.keys())) ] # Register mock wallets and give them stakes print("Registering mock wallets...") + for wallet in mock_wallets: print("Registering mock wallet {}".format(wallet.hotkey_str)) - success, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = wallet.hotkey.ss58_address, - coldkey = wallet.coldkey.ss58_address, - stake = mock_stakes[wallet.hotkey_str].rao, + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=wallet.hotkey.ss58_address, + coldkey=wallet.coldkey.ss58_address, + stake=mock_stakes[wallet.hotkey_str].rao, ) - self.assertTrue(success, err) - + cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - if kwargs.get('hotkey'): + if kwargs.get("hotkey"): for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get('hotkey'): + if wallet.hotkey_str == kwargs.get("hotkey"): return wallet else: return mock_wallets[0] - with patch('bittensor._cli.commands.unstake.get_hotkey_wallets_for_wallet') as mock_get_all_wallets: + with patch( + "bittensor._cli.commands.unstake.get_hotkey_wallets_for_wallet" + ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before unstaking for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao) @@ -757,25 +766,28 @@ def mock_get_wallet(*args, **kwargs): for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) # All should have been unstaked below or equal to max_stake - self.assertLessEqual(stake.tao, config.max_stake + 0.0001) # Add a small buffer for fp errors + self.assertLessEqual( + stake.tao, config.max_stake + 0.0001 + ) # Add a small buffer for fp errors - if wallet.hotkey_str == 'hk1': + if wallet.hotkey_str == "hk1": # hk1 should not have been unstaked because it was already below max_stake - self.assertAlmostEqual(stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4) - - def test_stake_with_specific_hotkeys( self ): + self.assertAlmostEqual( + stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4 + ) + + + def test_stake_with_specific_hotkeys(self): config = self.config config.command = "stake" config.no_prompt = True config.amount = 5.0 config.wallet.name = "fake_wallet" - config.hotkeys = [ - 'hk0', 'hk1', 'hk2' - ] - config.all_hotkeys =False + config.hotkeys = ["hk0", "hk1", "hk2"] + config.all_hotkeys = False # Notice no max_stake specified mock_balance = bittensor.Balance.from_float(22.2) @@ -784,49 +796,49 @@ def test_stake_with_specific_hotkeys( self ): mock_wallets = [ SimpleNamespace( - name = config.wallet.name, - coldkey = mock_coldkey_kp, - coldkeypub = mock_coldkey_kp, - hotkey_str = hk, - hotkey = get_mock_keypair(idx + 100, self.id()), - ) for idx, hk in enumerate(config.hotkeys) + name=config.wallet.name, + coldkey=mock_coldkey_kp, + coldkeypub=mock_coldkey_kp, + hotkey_str=hk, + hotkey=get_mock_keypair(idx + 100, self.id()), + ) + for idx, hk in enumerate(config.hotkeys) ] # Register mock wallets and give them balances print("Registering mock wallets...") + for wallet in mock_wallets: print("Registering mock wallet {}".format(wallet.hotkey_str)) - success, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = wallet.hotkey.ss58_address, - coldkey = wallet.coldkey.ss58_address + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=wallet.hotkey.ss58_address, + coldkey=wallet.coldkey.ss58_address, ) - self.assertTrue(success, err) - - success, err = _subtensor_mock.sudo_force_set_balance( + + success, err = _subtensor_mock.force_set_balance( ss58_address=mock_coldkey_kp.ss58_address, - balance=mock_balance.rao + balance=mock_balance.rao, ) - self.assertTrue(success, err) - + cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - if kwargs.get('hotkey'): + if kwargs.get("hotkey"): for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get('hotkey'): + if wallet.hotkey_str == kwargs.get("hotkey"): return wallet else: return mock_wallets[0] - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before staking for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) self.assertEqual(stake.rao, 0) @@ -836,11 +848,12 @@ def mock_get_wallet(*args, **kwargs): for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) self.assertAlmostEqual(stake.tao, config.amount, places=4) - def test_stake_with_all_hotkeys( self ): + + def test_stake_with_all_hotkeys(self): config = self.config config.command = "stake" config.no_prompt = True @@ -850,7 +863,7 @@ def test_stake_with_all_hotkeys( self ): config.all_hotkeys = True # Notice no max_stake specified - mock_hotkeys = ['hk0', 'hk1', 'hk2'] + mock_hotkeys = ["hk0", "hk1", "hk2"] mock_balance = bittensor.Balance.from_float(22.0) @@ -858,52 +871,54 @@ def test_stake_with_all_hotkeys( self ): mock_wallets = [ SimpleNamespace( - name = config.wallet.name, - coldkey = mock_coldkey_kp, - coldkeypub = mock_coldkey_kp, - hotkey_str = hk, - hotkey = get_mock_keypair(idx + 100, self.id()), - ) for idx, hk in enumerate(mock_hotkeys) + name=config.wallet.name, + coldkey=mock_coldkey_kp, + coldkeypub=mock_coldkey_kp, + hotkey_str=hk, + hotkey=get_mock_keypair(idx + 100, self.id()), + ) + for idx, hk in enumerate(mock_hotkeys) ] # Register mock wallets and give them no stake print("Registering mock wallets...") + for wallet in mock_wallets: print("Registering mock wallet {}".format(wallet.hotkey_str)) - success, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = wallet.hotkey.ss58_address, - coldkey = wallet.coldkeypub.ss58_address + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=wallet.hotkey.ss58_address, + coldkey=wallet.coldkeypub.ss58_address, ) - self.assertTrue(success, err) - + # Set the coldkey balance - success, err = _subtensor_mock.sudo_force_set_balance( + success, err = _subtensor_mock.force_set_balance( ss58_address=mock_coldkey_kp.ss58_address, - balance=mock_balance.rao + balance=mock_balance.rao, ) - self.assertTrue(success, err) - + cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - if kwargs.get('hotkey'): + if kwargs.get("hotkey"): for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get('hotkey'): + if wallet.hotkey_str == kwargs.get("hotkey"): return wallet else: return mock_wallets[0] - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet - with patch('bittensor._cli.commands.stake.get_hotkey_wallets_for_wallet') as mock_get_hotkey_wallets_for_wallet: + with patch( + "bittensor._cli.commands.stake.get_hotkey_wallets_for_wallet" + ) as mock_get_hotkey_wallets_for_wallet: mock_get_hotkey_wallets_for_wallet.return_value = mock_wallets # Check stakes before staking for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) # Check that all stakes are 0 self.assertEqual(stake.rao, 0) @@ -915,14 +930,13 @@ def mock_get_wallet(*args, **kwargs): self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - cli.run() # Check stakes after staking for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) # Check that all stakes are 5.0 self.assertAlmostEqual(stake.tao, config.amount, places=4) @@ -931,19 +945,23 @@ def mock_get_wallet(*args, **kwargs): balance = _subtensor_mock.get_balance( address=wallet.coldkeypub.ss58_address ) - self.assertAlmostEqual(balance.tao, mock_balance.tao - (config.amount * len(mock_wallets)), places=4) + self.assertAlmostEqual( + balance.tao, + mock_balance.tao - (config.amount * len(mock_wallets)), + places=4, + ) - def test_stake_with_exclude_hotkeys_from_all( self ): + def test_stake_with_exclude_hotkeys_from_all(self): config = self.config config.command = "stake" config.no_prompt = True config.amount = 5.0 config.wallet.name = "fake_wallet" - config.hotkeys = ['hk1'] # exclude hk1 - config.all_hotkeys =True + config.hotkeys = ["hk1"] # exclude hk1 + config.all_hotkeys = True # Notice no max_stake specified - mock_hotkeys = ['hk0', 'hk1', 'hk2'] + mock_hotkeys = ["hk0", "hk1", "hk2"] mock_balance = bittensor.Balance.from_float(25.0) @@ -951,52 +969,54 @@ def test_stake_with_exclude_hotkeys_from_all( self ): mock_wallets = [ SimpleNamespace( - name = config.wallet.name, - coldkey = mock_coldkey_kp, - coldkeypub = mock_coldkey_kp, - hotkey_str = hk, - hotkey = get_mock_keypair(idx + 100, self.id()), - ) for idx, hk in enumerate(mock_hotkeys) + name=config.wallet.name, + coldkey=mock_coldkey_kp, + coldkeypub=mock_coldkey_kp, + hotkey_str=hk, + hotkey=get_mock_keypair(idx + 100, self.id()), + ) + for idx, hk in enumerate(mock_hotkeys) ] # Register mock wallets and give them balances print("Registering mock wallets...") + for wallet in mock_wallets: print("Registering mock wallet {}".format(wallet.hotkey_str)) - success, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = wallet.hotkey.ss58_address, - coldkey = wallet.coldkeypub.ss58_address + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=wallet.hotkey.ss58_address, + coldkey=wallet.coldkeypub.ss58_address, ) - self.assertTrue(success, err) - + # Set the coldkey balance - success, err = _subtensor_mock.sudo_force_set_balance( + _subtensor_mock.force_set_balance( ss58_address=mock_coldkey_kp.ss58_address, - balance=mock_balance.rao + balance=mock_balance.rao, ) - self.assertTrue(success, err) - + cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - if kwargs.get('hotkey'): + if kwargs.get("hotkey"): for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get('hotkey'): + if wallet.hotkey_str == kwargs.get("hotkey"): return wallet else: return mock_wallets[0] - with patch('bittensor._cli.commands.stake.get_hotkey_wallets_for_wallet') as mock_get_all_wallets: + with patch( + "bittensor._cli.commands.stake.get_hotkey_wallets_for_wallet" + ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before staking for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) # Check that all stakes are 0 self.assertEqual(stake.rao, 0) @@ -1014,10 +1034,10 @@ def mock_get_wallet(*args, **kwargs): for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) - if wallet.hotkey_str == 'hk1': + if wallet.hotkey_str == "hk1": # Check that hk1 stake is 0 # We excluded it from staking self.assertEqual(stake.tao, 0) @@ -1029,88 +1049,87 @@ def mock_get_wallet(*args, **kwargs): balance = _subtensor_mock.get_balance( address=wallet.coldkeypub.ss58_address ) - self.assertAlmostEqual(balance.tao, mock_balance.tao - (config.amount * 2), places=4) + self.assertAlmostEqual( + balance.tao, mock_balance.tao - (config.amount * 2), places=4 + ) - def test_stake_with_multiple_hotkeys_max_stake( self ): + def test_stake_with_multiple_hotkeys_max_stake(self): config = self.config config.command = "stake" config.no_prompt = True # Notie amount is not specified - config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after + config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after config.wallet.name = "fake_wallet" - config.hotkeys = [ - 'hk0', 'hk1', 'hk2' - ] - config.all_hotkeys =False + config.hotkeys = ["hk0", "hk1", "hk2"] + config.all_hotkeys = False mock_balance = bittensor.Balance.from_float(config.max_stake * 3) mock_stakes: Dict[str, bittensor.Balance] = { - 'hk0': bittensor.Balance.from_float(0.0), - 'hk1': bittensor.Balance.from_float(config.max_stake * 2), - 'hk2': bittensor.Balance.from_float(0.0), + "hk0": bittensor.Balance.from_float(0.0), + "hk1": bittensor.Balance.from_float(config.max_stake * 2), + "hk2": bittensor.Balance.from_float(0.0), } mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( - name = config.wallet.name, - coldkey = mock_coldkey_kp, - coldkeypub = mock_coldkey_kp, - hotkey_str = hk, - hotkey = get_mock_keypair(idx + 100, self.id()), - ) for idx, hk in enumerate(config.hotkeys) + name=config.wallet.name, + coldkey=mock_coldkey_kp, + coldkeypub=mock_coldkey_kp, + hotkey_str=hk, + hotkey=get_mock_keypair(idx + 100, self.id()), + ) + for idx, hk in enumerate(config.hotkeys) ] # Register mock wallets and give them balances print("Registering mock wallets...") + for wallet in mock_wallets: print("Registering mock wallet {}".format(wallet.hotkey_str)) - if wallet.hotkey_str == 'hk1': + if wallet.hotkey_str == "hk1": # Set the stake for hk1 - success, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = wallet.hotkey.ss58_address, - coldkey = wallet.coldkeypub.ss58_address, - stake = mock_stakes[wallet.hotkey_str].rao + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=wallet.hotkey.ss58_address, + coldkey=wallet.coldkeypub.ss58_address, + stake=mock_stakes[wallet.hotkey_str].rao, ) - self.assertTrue(success, err) else: - success, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = wallet.hotkey.ss58_address, - coldkey = wallet.coldkeypub.ss58_address + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=wallet.hotkey.ss58_address, + coldkey=wallet.coldkeypub.ss58_address, ) - self.assertTrue(success, err) - - success, err = _subtensor_mock.sudo_force_set_balance( + + _subtensor_mock.force_set_balance( ss58_address=mock_coldkey_kp.ss58_address, - balance=mock_balance.rao + balance=mock_balance.rao, ) - self.assertTrue(success, err) - + cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - if kwargs.get('hotkey'): + if kwargs.get("hotkey"): for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get('hotkey'): + if wallet.hotkey_str == kwargs.get("hotkey"): return wallet else: return mock_wallets[0] - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before staking for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) # Check that all stakes are correct - if wallet.hotkey_str == 'hk1': + if wallet.hotkey_str == "hk1": self.assertAlmostEqual(stake.tao, config.max_stake * 2, places=4) else: self.assertEqual(stake.rao, 0) @@ -1128,7 +1147,7 @@ def mock_get_wallet(*args, **kwargs): for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) # Check that all stakes at least 15.0 @@ -1137,7 +1156,9 @@ def mock_get_wallet(*args, **kwargs): if wallet.hotkey_str == "hk1": # Check that hk1 stake was not changed # It had more than max_stake already - self.assertAlmostEqual(stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4) + self.assertAlmostEqual( + stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4 + ) # Check that the balance decreased balance = _subtensor_mock.get_balance( @@ -1145,67 +1166,67 @@ def mock_get_wallet(*args, **kwargs): ) self.assertLessEqual(balance.tao, mock_balance.tao) - def test_stake_with_multiple_hotkeys_max_stake_not_enough_balance( self ): + def test_stake_with_multiple_hotkeys_max_stake_not_enough_balance(self): config = self.config config.command = "stake" config.no_prompt = True # Notie amount is not specified - config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after + config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after config.wallet.name = "fake_wallet" - config.hotkeys = [ - 'hk0', 'hk1', 'hk2' - ] - config.all_hotkeys =False + config.hotkeys = ["hk0", "hk1", "hk2"] + config.all_hotkeys = False - mock_balance = bittensor.Balance.from_float(15.0 * 2) # Not enough for all hotkeys + mock_balance = bittensor.Balance.from_float( + 15.0 * 2 + ) # Not enough for all hotkeys mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( - name = config.wallet.name, - coldkey = mock_coldkey_kp, - coldkeypub = mock_coldkey_kp, - hotkey_str = hk, - hotkey = get_mock_keypair(idx + 100, self.id()), - ) for idx, hk in enumerate(config.hotkeys) + name=config.wallet.name, + coldkey=mock_coldkey_kp, + coldkeypub=mock_coldkey_kp, + hotkey_str=hk, + hotkey=get_mock_keypair(idx + 100, self.id()), + ) + for idx, hk in enumerate(config.hotkeys) ] # Register mock wallets and give them balances print("Registering mock wallets...") + for wallet in mock_wallets: print("Registering mock wallet {}".format(wallet.hotkey_str)) - success, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = wallet.hotkey.ss58_address, - coldkey = wallet.coldkeypub.ss58_address + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=wallet.hotkey.ss58_address, + coldkey=wallet.coldkeypub.ss58_address, ) - self.assertTrue(success, err) - - success, err = _subtensor_mock.sudo_force_set_balance( + + _subtensor_mock.force_set_balance( ss58_address=mock_coldkey_kp.ss58_address, - balance=mock_balance.rao + balance=mock_balance.rao, ) - self.assertTrue(success, err) - + cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - if kwargs.get('hotkey'): + if kwargs.get("hotkey"): for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get('hotkey'): + if wallet.hotkey_str == kwargs.get("hotkey"): return wallet else: return mock_wallets[0] - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before staking for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) # Check that all stakes are 0 self.assertEqual(stake.rao, 0) @@ -1223,10 +1244,10 @@ def mock_get_wallet(*args, **kwargs): for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) - if wallet.hotkey_str == 'hk2': + if wallet.hotkey_str == "hk2": # Check that the stake is still 0 self.assertEqual(stake.tao, 0) @@ -1240,17 +1261,15 @@ def mock_get_wallet(*args, **kwargs): ) self.assertLessEqual(balance.tao, mock_balance.tao) - def test_stake_with_single_hotkey_max_stake( self ): + def test_stake_with_single_hotkey_max_stake(self): config = self.config config.command = "stake" config.no_prompt = True # Notie amount is not specified - config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after + config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after config.wallet.name = "fake_wallet" - config.hotkeys = [ - 'hk0' - ] - config.all_hotkeys =False + config.hotkeys = ["hk0"] + config.all_hotkeys = False mock_balance = bittensor.Balance.from_float(15.0 * 3) @@ -1258,49 +1277,49 @@ def test_stake_with_single_hotkey_max_stake( self ): mock_wallets = [ SimpleNamespace( - name = config.wallet.name, - coldkey = mock_coldkey_kp, - coldkeypub = mock_coldkey_kp, - hotkey_str = hk, - hotkey = get_mock_keypair(idx + 100, self.id()), - ) for idx, hk in enumerate(config.hotkeys) + name=config.wallet.name, + coldkey=mock_coldkey_kp, + coldkeypub=mock_coldkey_kp, + hotkey_str=hk, + hotkey=get_mock_keypair(idx + 100, self.id()), + ) + for idx, hk in enumerate(config.hotkeys) ] # Register mock wallets and give them balances print("Registering mock wallets...") + for wallet in mock_wallets: print("Registering mock wallet {}".format(wallet.hotkey_str)) - success, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = wallet.hotkey.ss58_address, - coldkey = wallet.coldkeypub.ss58_address + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=wallet.hotkey.ss58_address, + coldkey=wallet.coldkeypub.ss58_address, ) - self.assertTrue(success, err) - - success, err = _subtensor_mock.sudo_force_set_balance( + + _subtensor_mock.force_set_balance( ss58_address=mock_coldkey_kp.ss58_address, - balance=mock_balance.rao + balance=mock_balance.rao, ) - self.assertTrue(success, err) - + cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - if kwargs.get('hotkey'): + if kwargs.get("hotkey"): for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get('hotkey'): + if wallet.hotkey_str == kwargs.get("hotkey"): return wallet else: return mock_wallets[0] - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before staking for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) # Check that all stakes are 0 self.assertEqual(stake.rao, 0) @@ -1318,7 +1337,7 @@ def mock_get_wallet(*args, **kwargs): for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) # Check that all stakes are maximum of 15.0 @@ -1330,65 +1349,64 @@ def mock_get_wallet(*args, **kwargs): ) self.assertLessEqual(balance.tao, mock_balance.tao) - def test_stake_with_single_hotkey_max_stake_not_enough_balance( self ): + def test_stake_with_single_hotkey_max_stake_not_enough_balance(self): config = self.config config.command = "stake" config.no_prompt = True # Notie amount is not specified - config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after + config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after config.wallet.name = "fake_wallet" - config.hotkeys = [ - 'hk0' - ] - config.all_hotkeys =False + config.hotkeys = ["hk0"] + config.all_hotkeys = False - mock_balance = bittensor.Balance.from_float(1.0) # Not enough balance to do max + mock_balance = bittensor.Balance.from_float(1.0) # Not enough balance to do max mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( - name = config.wallet.name, - coldkey = mock_coldkey_kp, - coldkeypub = mock_coldkey_kp, - hotkey_str = hk, - hotkey = get_mock_keypair(idx + 100, self.id()), - ) for idx, hk in enumerate(config.hotkeys) + name=config.wallet.name, + coldkey=mock_coldkey_kp, + coldkeypub=mock_coldkey_kp, + hotkey_str=hk, + hotkey=get_mock_keypair(idx + 100, self.id()), + ) + for idx, hk in enumerate(config.hotkeys) ] # Register mock wallets and give them balances + print("Registering mock wallets...") + for wallet in mock_wallets: - success, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = wallet.hotkey.ss58_address, - coldkey = wallet.coldkeypub.ss58_address + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=wallet.hotkey.ss58_address, + coldkey=wallet.coldkeypub.ss58_address, ) - self.assertTrue(success, err) - - success, err = _subtensor_mock.sudo_force_set_balance( + + _subtensor_mock.force_set_balance( ss58_address=mock_coldkey_kp.ss58_address, - balance=mock_balance.rao + balance=mock_balance.rao, ) - self.assertTrue(success, err) - + cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - if kwargs.get('hotkey'): + if kwargs.get("hotkey"): for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get('hotkey'): + if wallet.hotkey_str == kwargs.get("hotkey"): return wallet else: return mock_wallets[0] - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before staking for wallet in mock_wallets: stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) # Check that all stakes are 0 self.assertEqual(stake.rao, 0) @@ -1407,7 +1425,7 @@ def mock_get_wallet(*args, **kwargs): # Check did not stake stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) # Check that stake is less than max_stake - 1.0 @@ -1419,76 +1437,78 @@ def mock_get_wallet(*args, **kwargs): ) self.assertGreaterEqual(balance.tao, mock_balance.tao - config.max_stake) - def test_stake_with_single_hotkey_max_stake_enough_stake( self ): + def test_stake_with_single_hotkey_max_stake_enough_stake(self): # tests max stake when stake >= max_stake already config = self.config config.command = "stake" config.no_prompt = True # Notie amount is not specified - config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after + config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after config.wallet.name = "fake_wallet" - config.hotkeys = [ - 'hk0' - ] - config.all_hotkeys =False + config.hotkeys = ["hk0"] + config.all_hotkeys = False mock_balance = bittensor.Balance.from_float(config.max_stake * 3) - mock_stakes: Dict[str, bittensor.Balance] = { # has enough stake, more than max_stake - 'hk0': bittensor.Balance.from_float(config.max_stake * 2) + mock_stakes: Dict[ + str, bittensor.Balance + ] = { # has enough stake, more than max_stake + "hk0": bittensor.Balance.from_float(config.max_stake * 2) } mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( - name = config.wallet.name, - coldkey = mock_coldkey_kp, - coldkeypub = mock_coldkey_kp, - hotkey_str = hk, - hotkey = get_mock_keypair(idx + 100, self.id()), - ) for idx, hk in enumerate(config.hotkeys) + name=config.wallet.name, + coldkey=mock_coldkey_kp, + coldkeypub=mock_coldkey_kp, + hotkey_str=hk, + hotkey=get_mock_keypair(idx + 100, self.id()), + ) + for idx, hk in enumerate(config.hotkeys) ] # Register mock wallets and give them balances + print("Registering mock wallets...") + for wallet in mock_wallets: - success, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = wallet.hotkey.ss58_address, - coldkey = wallet.coldkeypub.ss58_address, - stake = mock_stakes[wallet.hotkey_str].rao # More than max_stake + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=wallet.hotkey.ss58_address, + coldkey=wallet.coldkeypub.ss58_address, + stake=mock_stakes[wallet.hotkey_str].rao, # More than max_stake ) - self.assertTrue(success, err) - - success, err = _subtensor_mock.sudo_force_set_balance( + + success, err = _subtensor_mock.force_set_balance( ss58_address=mock_coldkey_kp.ss58_address, - balance=mock_balance.rao + balance=mock_balance.rao, ) - self.assertTrue(success, err) - + cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - if kwargs.get('hotkey'): + if kwargs.get("hotkey"): for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get('hotkey'): + if wallet.hotkey_str == kwargs.get("hotkey"): return wallet else: return mock_wallets[0] - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before staking wallet = mock_wallets[0] - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) # Check that stake is correct - self.assertAlmostEqual(stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4) + self.assertAlmostEqual( + stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4 + ) # Check that the stake is greater than or equal to max_stake self.assertGreaterEqual(stake.tao, config.max_stake) @@ -1506,19 +1526,22 @@ def mock_get_wallet(*args, **kwargs): # Check did not stake, since stake >= max_stake stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address + coldkey_ss58=wallet.coldkey.ss58_address, ) # Check that all stake is unchanged - self.assertAlmostEqual(stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4) + self.assertAlmostEqual( + stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4 + ) # Check that the balance is the same balance = _subtensor_mock.get_balance( address=wallet.coldkeypub.ss58_address ) self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - def test_nominate( self ): + + + def test_nominate(self): config = self.config config.command = "nominate" config.no_prompt = True @@ -1528,39 +1551,38 @@ def test_nominate( self ): mock_balance = bittensor.Balance.from_float(100.0) mock_wallet = SimpleNamespace( - name = 'w0', - coldkey = get_mock_keypair(0, self.id()), - coldkeypub = get_mock_keypair(0, self.id()), - hotkey_str = 'hk0', - hotkey = get_mock_keypair(0 + 100, self.id()), - ) + name="w0", + coldkey=get_mock_keypair(0, self.id()), + coldkeypub=get_mock_keypair(0, self.id()), + hotkey_str="hk0", + hotkey=get_mock_keypair(0 + 100, self.id()), + ) # Register mock wallet and give it a balance - success, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = mock_wallet.hotkey.ss58_address, - coldkey = mock_wallet.coldkey.ss58_address, - balance = mock_balance.rao, + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=mock_wallet.hotkey.ss58_address, + coldkey=mock_wallet.coldkey.ss58_address, + balance=mock_balance.rao, ) - self.assertTrue(success, err) - + cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - hk = kwargs.get('hotkey') - name_ = kwargs.get('name') + hk = kwargs.get("hotkey") + name_ = kwargs.get("name") - if not hk and kwargs.get('config'): - hk = kwargs.get('config').wallet.hotkey - if not name_ and kwargs.get('config'): - name_ = kwargs.get('config').wallet.name + if not hk and kwargs.get("config"): + hk = kwargs.get("config").wallet.hotkey + if not name_ and kwargs.get("config"): + name_ = kwargs.get("config").wallet.name if mock_wallet.name == name_: return mock_wallet else: raise ValueError("Mock wallet not found") - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet cli.run() @@ -1571,7 +1593,7 @@ def mock_get_wallet(*args, **kwargs): ) self.assertTrue(is_delegate) - def test_delegate_stake( self ): + def test_delegate_stake(self): config = self.config config.command = "delegate" config.no_prompt = True @@ -1580,12 +1602,10 @@ def test_delegate_stake( self ): mock_balances: Dict[str, bittensor.Balance] = { # All have more than 5.0 stake - 'w0': { - 'hk0': bittensor.Balance.from_float(10.0), - }, - 'w1': { - 'hk1': bittensor.Balance.from_float(11.1) + "w0": { + "hk0": bittensor.Balance.from_float(10.0), }, + "w1": {"hk1": bittensor.Balance.from_float(11.1)}, } mock_stake = bittensor.Balance.from_float(5.0) @@ -1594,50 +1614,49 @@ def test_delegate_stake( self ): for idx, wallet_name in enumerate(list(mock_balances.keys())): for idx_hk, hk in enumerate(list(mock_balances[wallet_name].keys())): wallet = SimpleNamespace( - name = wallet_name, - coldkey = get_mock_keypair(idx, self.id()), - coldkeypub = get_mock_keypair(idx, self.id()), - hotkey_str = hk, - hotkey = get_mock_keypair(idx * 100 + idx_hk, self.id()), - ) + name=wallet_name, + coldkey=get_mock_keypair(idx, self.id()), + coldkeypub=get_mock_keypair(idx, self.id()), + hotkey_str=hk, + hotkey=get_mock_keypair(idx * 100 + idx_hk, self.id()), + ) mock_wallets.append(wallet) # Set hotkey to be the hotkey from the other wallet config.delegate_ss58key: str = mock_wallets[0].hotkey.ss58_address + # Register mock wallets and give them balance - success, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = mock_wallets[0].hotkey.ss58_address, - coldkey = mock_wallets[0].coldkey.ss58_address, - balance = mock_balances['w0']['hk0'].rao, - stake = mock_stake.rao # Needs set stake to be a validator + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=mock_wallets[0].hotkey.ss58_address, + coldkey=mock_wallets[0].coldkey.ss58_address, + balance=mock_balances["w0"]["hk0"].rao, + stake=mock_stake.rao, # Needs set stake to be a validator ) - self.assertTrue(success, err) - + # Give w1 some balance - success, err = _subtensor_mock.sudo_force_set_balance( + success, err = _subtensor_mock.force_set_balance( ss58_address=mock_wallets[1].coldkey.ss58_address, - balance = mock_balances['w1']['hk1'].rao + balance=mock_balances["w1"]["hk1"].rao, ) - self.assertTrue(success, err) - + # Make the first wallet a delegate success = _subtensor_mock.nominate( - wallet = mock_wallets[0] + wallet=mock_wallets[0], ) self.assertTrue(success) cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - hk = kwargs.get('hotkey') - name_ = kwargs.get('name') + hk = kwargs.get("hotkey") + name_ = kwargs.get("name") - if not hk and kwargs.get('config'): - hk = kwargs.get('config').wallet.hotkey - if not name_ and kwargs.get('config'): - name_ = kwargs.get('config').wallet.name + if not hk and kwargs.get("config"): + hk = kwargs.get("config").wallet.hotkey + if not name_ and kwargs.get("config"): + name_ = kwargs.get("config").wallet.name for wallet in mock_wallets: if wallet.name == name_ and wallet.hotkey_str == hk: @@ -1649,7 +1668,7 @@ def mock_get_wallet(*args, **kwargs): else: return mock_wallets[0] - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet cli.run() @@ -1657,11 +1676,12 @@ def mock_get_wallet(*args, **kwargs): # Check the stake stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=mock_wallets[0].hotkey.ss58_address, - coldkey_ss58=mock_wallets[1].coldkey.ss58_address + coldkey_ss58=mock_wallets[1].coldkey.ss58_address, ) self.assertAlmostEqual(stake.tao, config.amount, places=4) - def test_undelegate_stake( self ): + + def test_undelegate_stake(self): config = self.config config.command = "undelegate" config.no_prompt = True @@ -1670,12 +1690,10 @@ def test_undelegate_stake( self ): mock_balances: Dict[str, bittensor.Balance] = { # All have more than 5.0 stake - 'w0': { - 'hk0': bittensor.Balance.from_float(10.0), - }, - 'w1': { - 'hk1': bittensor.Balance.from_float(11.1) + "w0": { + "hk0": bittensor.Balance.from_float(10.0), }, + "w1": {"hk1": bittensor.Balance.from_float(11.1)}, } mock_stake = bittensor.Balance.from_float(5.0) @@ -1685,67 +1703,64 @@ def test_undelegate_stake( self ): for idx, wallet_name in enumerate(list(mock_balances.keys())): for idx_hk, hk in enumerate(list(mock_balances[wallet_name].keys())): wallet = SimpleNamespace( - name = wallet_name, - coldkey = get_mock_keypair(idx, self.id()), - coldkeypub = get_mock_keypair(idx, self.id()), - hotkey_str = hk, - hotkey = get_mock_keypair(idx * 100 + idx_hk, self.id()), - ) + name=wallet_name, + coldkey=get_mock_keypair(idx, self.id()), + coldkeypub=get_mock_keypair(idx, self.id()), + hotkey_str=hk, + hotkey=get_mock_keypair(idx * 100 + idx_hk, self.id()), + ) mock_wallets.append(wallet) # Set hotkey to be the hotkey from the other wallet config.delegate_ss58key: str = mock_wallets[0].hotkey.ss58_address # Register mock wallets and give them balance - success, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = mock_wallets[0].hotkey.ss58_address, - coldkey = mock_wallets[0].coldkey.ss58_address, - balance = mock_balances['w0']['hk0'].rao, - stake = mock_stake.rao # Needs set stake to be a validator + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=mock_wallets[0].hotkey.ss58_address, + coldkey=mock_wallets[0].coldkey.ss58_address, + balance=mock_balances["w0"]["hk0"].rao, + stake=mock_stake.rao, # Needs set stake to be a validator ) - self.assertTrue(success, err) - + # Give w1 some balance - success, err = _subtensor_mock.sudo_force_set_balance( + success, err = _subtensor_mock.force_set_balance( ss58_address=mock_wallets[1].coldkey.ss58_address, - balance = mock_balances['w1']['hk1'].rao + balance=mock_balances["w1"]["hk1"].rao, ) - self.assertTrue(success, err) - + # Make the first wallet a delegate success = _subtensor_mock.nominate( - wallet = mock_wallets[0] + wallet=mock_wallets[0], ) self.assertTrue(success) # Stake to the delegate success = _subtensor_mock.delegate( - wallet = mock_wallets[1], + wallet=mock_wallets[1], delegate_ss58=mock_wallets[0].hotkey.ss58_address, - amount = mock_delegated, - wait_for_finalization=True, - prompt=False + amount=mock_delegated, + prompt=False, ) self.assertTrue(success) # Verify the stake stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=mock_wallets[0].hotkey.ss58_address, - coldkey_ss58=mock_wallets[1].coldkey.ss58_address + coldkey_ss58=mock_wallets[1].coldkey.ss58_address, ) self.assertAlmostEqual(stake.tao, mock_delegated.tao, places=4) cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - hk = kwargs.get('hotkey') - name_ = kwargs.get('name') + hk = kwargs.get("hotkey") + name_ = kwargs.get("name") - if not hk and kwargs.get('config'): - hk = kwargs.get('config').wallet.hotkey - if not name_ and kwargs.get('config'): - name_ = kwargs.get('config').wallet.name + if not hk and kwargs.get("config"): + hk = kwargs.get("config").wallet.hotkey + if not name_ and kwargs.get("config"): + name_ = kwargs.get("config").wallet.name for wallet in mock_wallets: if wallet.name == name_ and wallet.hotkey_str == hk: @@ -1757,7 +1772,7 @@ def mock_get_wallet(*args, **kwargs): else: return mock_wallets[0] - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet cli.run() @@ -1765,11 +1780,14 @@ def mock_get_wallet(*args, **kwargs): # Check the stake stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( hotkey_ss58=mock_wallets[0].hotkey.ss58_address, - coldkey_ss58=mock_wallets[1].coldkey.ss58_address + coldkey_ss58=mock_wallets[1].coldkey.ss58_address, + ) + self.assertAlmostEqual( + stake.tao, mock_delegated.tao - config.amount, places=4 ) - self.assertAlmostEqual(stake.tao, mock_delegated.tao - config.amount, places=4) - def test_transfer( self ): + + def test_transfer(self): config = self.config config.command = "transfer" config.no_prompt = True @@ -1777,45 +1795,45 @@ def test_transfer( self ): config.wallet.name = "w1" mock_balances: Dict[str, bittensor.Balance] = { - 'w0': bittensor.Balance.from_float(10.0), - 'w1': bittensor.Balance.from_float(config.amount + 0.001) + "w0": bittensor.Balance.from_float(10.0), + "w1": bittensor.Balance.from_float(config.amount + 0.001), } mock_wallets = [] for idx, wallet_name in enumerate(list(mock_balances.keys())): wallet = SimpleNamespace( - name = wallet_name, - coldkey = get_mock_keypair(idx, self.id()), - coldkeypub = get_mock_keypair(idx, self.id()) - ) + name=wallet_name, + coldkey=get_mock_keypair(idx, self.id()), + coldkeypub=get_mock_keypair(idx, self.id()), + ) mock_wallets.append(wallet) # Set dest to w0 config.dest = mock_wallets[0].coldkey.ss58_address # Give w0 and w1 balance + for wallet in mock_wallets: - success, err = _subtensor_mock.sudo_force_set_balance( + success, err = _subtensor_mock.force_set_balance( ss58_address=wallet.coldkey.ss58_address, - balance = mock_balances[wallet.name].rao + balance=mock_balances[wallet.name].rao, ) - self.assertTrue(success, err) - + cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - name_ = kwargs.get('name') + name_ = kwargs.get("name") - if not name_ and kwargs.get('config'): - name_ = kwargs.get('config').wallet.name + if not name_ and kwargs.get("config"): + name_ = kwargs.get("config").wallet.name for wallet in mock_wallets: if wallet.name == name_: return wallet else: - raise ValueError(f'No mock wallet found with name: {name_}') + raise ValueError(f"No mock wallet found with name: {name_}") - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet cli.run() @@ -1824,15 +1842,20 @@ def mock_get_wallet(*args, **kwargs): balance = _subtensor_mock.get_balance( address=mock_wallets[0].coldkey.ss58_address ) - self.assertAlmostEqual(balance.tao, mock_balances['w0'].tao + config.amount, places=4) + self.assertAlmostEqual( + balance.tao, mock_balances["w0"].tao + config.amount, places=4 + ) # Check the balance of w1 balance = _subtensor_mock.get_balance( address=mock_wallets[1].coldkey.ss58_address ) - self.assertAlmostEqual(balance.tao, mock_balances['w1'].tao - config.amount, places=4) # no fees + self.assertAlmostEqual( + balance.tao, mock_balances["w1"].tao - config.amount, places=4 + ) # no fees - def test_transfer_not_enough_balance( self ): + + def test_transfer_not_enough_balance(self): config = self.config config.command = "transfer" config.no_prompt = True @@ -1840,84 +1863,100 @@ def test_transfer_not_enough_balance( self ): config.wallet.name = "w1" mock_balances: Dict[str, bittensor.Balance] = { - 'w0': bittensor.Balance.from_float(10.0), - 'w1': bittensor.Balance.from_float(config.amount - 0.1) # not enough balance + "w0": bittensor.Balance.from_float(10.0), + "w1": bittensor.Balance.from_float( + config.amount - 0.1 + ), # not enough balance } mock_wallets = [] for idx, wallet_name in enumerate(list(mock_balances.keys())): wallet = SimpleNamespace( - name = wallet_name, - coldkey = get_mock_keypair(idx, self.id()), - coldkeypub = get_mock_keypair(idx, self.id()) - ) + name=wallet_name, + coldkey=get_mock_keypair(idx, self.id()), + coldkeypub=get_mock_keypair(idx, self.id()), + ) mock_wallets.append(wallet) # Set dest to w0 config.dest = mock_wallets[0].coldkey.ss58_address # Give w0 and w1 balance + for wallet in mock_wallets: - success, err = _subtensor_mock.sudo_force_set_balance( + success, err = _subtensor_mock.force_set_balance( ss58_address=wallet.coldkey.ss58_address, - balance = mock_balances[wallet.name].rao + balance=mock_balances[wallet.name].rao, ) - self.assertTrue(success, err) - + cli = bittensor.cli(config) def mock_get_wallet(*args, **kwargs): - name_ = kwargs.get('name') + name_ = kwargs.get("name") - if not name_ and kwargs.get('config'): - name_ = kwargs.get('config').wallet.name + if not name_ and kwargs.get("config"): + name_ = kwargs.get("config").wallet.name for wallet in mock_wallets: if wallet.name == name_: return wallet else: - raise ValueError(f'No mock wallet found with name: {name_}') + raise ValueError(f"No mock wallet found with name: {name_}") mock_console = MockConsole() - with patch('bittensor.wallet') as mock_create_wallet: + with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet - with patch('bittensor.__console__', mock_console): + with patch("bittensor.__console__", mock_console): cli.run() # Check that the overview was printed. self.assertIsNotNone(mock_console.captured_print) - output_no_syntax = mock_console.remove_rich_syntax(mock_console.captured_print) + output_no_syntax = mock_console.remove_rich_syntax( + mock_console.captured_print + ) - self.assertIn('Not enough balance', output_no_syntax) + self.assertIn("Not enough balance", output_no_syntax) # Check the balance of w0 balance = _subtensor_mock.get_balance( address=mock_wallets[0].coldkey.ss58_address ) - self.assertAlmostEqual(balance.tao, mock_balances['w0'].tao, places=4) # did not transfer + self.assertAlmostEqual( + balance.tao, mock_balances["w0"].tao, places=4 + ) # did not transfer # Check the balance of w1 balance = _subtensor_mock.get_balance( address=mock_wallets[1].coldkey.ss58_address ) - self.assertAlmostEqual(balance.tao, mock_balances['w1'].tao, places=4) # did not transfer + self.assertAlmostEqual( + balance.tao, mock_balances["w1"].tao, places=4 + ) # did not transfer - def test_register( self ): + + def test_register(self): config = self.config config.command = "register" config.subtensor.register.num_processes = 1 config.subtensor.register.update_interval = 50_000 config.no_prompt = True - mock_wallet = generate_wallet() + mock_wallet = generate_wallet( + hotkey = get_mock_keypair( + 100, self.id() + ) + ) class MockException(Exception): pass - with patch('bittensor.wallet', return_value=mock_wallet) as mock_create_wallet: - with patch('bittensor._subtensor.extrinsics.registration.POWSolution.is_stale', side_effect=MockException) as mock_is_stale: + with patch("bittensor.wallet", return_value=mock_wallet) as mock_create_wallet: + with patch( + "bittensor._subtensor.extrinsics.registration.POWSolution.is_stale", + side_effect=MockException, + ) as mock_is_stale: mock_is_stale.return_value = False with pytest.raises(MockException): @@ -1925,35 +1964,42 @@ class MockException(Exception): cli.run() mock_create_wallet.assert_called_once() - self.assertEqual( mock_is_stale.call_count, 1 ) + self.assertEqual(mock_is_stale.call_count, 1) - def test_recycle_register( self ): + + def test_recycle_register(self): config = self.config config.command = "recycle_register" config.no_prompt = True - mock_wallet = generate_wallet() + mock_wallet = generate_wallet( + hotkey = get_mock_keypair( + 100, self.id() + ) + ) # Give the wallet some balance for burning - success, err = _subtensor_mock.sudo_force_set_balance( + success, err = _subtensor_mock.force_set_balance( ss58_address=mock_wallet.coldkeypub.ss58_address, - balance = bittensor.Balance.from_float(200.0) + balance=bittensor.Balance.from_float(200.0), ) - self.assertTrue(success, err) - - with patch('bittensor.wallet', return_value=mock_wallet) as mock_create_wallet: + + with patch("bittensor.wallet", return_value=mock_wallet) as mock_create_wallet: cli = bittensor.cli(config) cli.run() mock_create_wallet.assert_called_once() # Verify that the wallet was registered subtensor = bittensor.subtensor(config) - registered = subtensor.is_hotkey_registered_on_subnet( hotkey_ss58 = mock_wallet.hotkey.ss58_address, netuid = 1 ) + registered = subtensor.is_hotkey_registered_on_subnet( + hotkey_ss58=mock_wallet.hotkey.ss58_address, netuid=1 + ) - self.assertTrue( registered ) + self.assertTrue(registered) - def test_stake( self ): - amount_to_stake: Balance = Balance.from_tao( 0.5 ) + + def test_stake(self): + amount_to_stake: Balance = Balance.from_tao(0.5) config = self.config config.no_prompt = True config.command = "stake" @@ -1966,21 +2012,26 @@ def test_stake( self ): subtensor = bittensor.subtensor(config) - mock_wallet = generate_wallet() + mock_wallet = generate_wallet( + hotkey = get_mock_keypair( + 100, self.id() + ) + ) # Register the hotkey and give it some balance - _subtensor_mock.sudo_register( - netuid = 1, - hotkey = mock_wallet.hotkey.ss58_address, - coldkey = mock_wallet.coldkey.ss58_address, - balance = (amount_to_stake + Balance.from_tao( 1.0 )).rao # 1.0 tao extra for fees, etc + _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=mock_wallet.hotkey.ss58_address, + coldkey=mock_wallet.coldkey.ss58_address, + balance=( + amount_to_stake + Balance.from_tao(1.0) + ).rao, # 1.0 tao extra for fees, etc ) - with patch('bittensor.wallet', return_value=mock_wallet) as mock_create_wallet: - + with patch("bittensor.wallet", return_value=mock_wallet) as mock_create_wallet: old_stake = subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58 = mock_wallet.hotkey.ss58_address, - coldkey_ss58 = mock_wallet.coldkey.ss58_address, + hotkey_ss58=mock_wallet.hotkey.ss58_address, + coldkey_ss58=mock_wallet.coldkey.ss58_address, ) cli = bittensor.cli(config) @@ -1989,13 +2040,14 @@ def test_stake( self ): self.assertEqual(mock_create_wallet.call_count, 2) new_stake = subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58 = mock_wallet.hotkey.ss58_address, - coldkey_ss58 = mock_wallet.coldkey.ss58_address, + hotkey_ss58=mock_wallet.hotkey.ss58_address, + coldkey_ss58=mock_wallet.coldkey.ss58_address, ) - self.assertGreater( new_stake, old_stake ) + self.assertGreater(new_stake, old_stake) - def test_metagraph( self ): + + def test_metagraph(self): config = self.config config.wallet.name = "metagraph_testwallet" config.command = "metagraph" @@ -2003,28 +2055,39 @@ def test_metagraph( self ): # Add some neurons to the metagraph mock_nn = [] - for i in range(5): + + def register_mock_neuron( + i: int + ) -> int: mock_nn.append( SimpleNamespace( - hotkey = get_mock_keypair(i + 100, self.id()).ss58_address, - coldkey = get_mock_keypair(i, self.id()).ss58_address, - balance = Balance.from_rao( random.randint(0, 2**45) ).rao, - stake = Balance.from_rao( random.randint(0, 2**45) ).rao, + hotkey=get_mock_keypair(i + 100, self.id()).ss58_address, + coldkey=get_mock_keypair(i, self.id()).ss58_address, + balance=Balance.from_rao(random.randint(0, 2**45)).rao, + stake=Balance.from_rao(random.randint(0, 2**45)).rao, ) ) - success, err = _subtensor_mock.sudo_register( - netuid = config.netuid, - hotkey = mock_nn[i].hotkey, - coldkey = mock_nn[i].coldkey, - balance = mock_nn[i].balance, - stake = mock_nn[i].stake + uid = _subtensor_mock.force_register_neuron( + netuid=config.netuid, + hotkey=mock_nn[i].hotkey, + coldkey=mock_nn[i].coldkey, + balance=mock_nn[i].balance, + stake=mock_nn[i].stake, ) - self.assertTrue(success, err) + return uid + + for i in range(5): + _ = register_mock_neuron( + i + ) + + _subtensor_mock.neurons_lite(netuid=config.netuid) + cli = bittensor.cli(config) mock_console = MockConsole() - with patch('bittensor.__console__', mock_console): + with patch("bittensor.__console__", mock_console): cli.run() # Check that the overview was printed. @@ -2032,15 +2095,17 @@ def test_metagraph( self ): output_no_syntax = mock_console.remove_rich_syntax(mock_console.captured_print) - self.assertIn('Metagraph', output_no_syntax) - nn = _subtensor_mock.neurons( netuid = config.netuid ) - self.assertIn(str(len(nn) - 1), output_no_syntax) # Check that the number of neurons is output + self.assertIn("Metagraph", output_no_syntax) + nn = _subtensor_mock.neurons_lite(netuid=config.netuid) + self.assertIn( + str(len(nn) - 1), output_no_syntax + ) # Check that the number of neurons is output # Check each uid is in the output for neuron in nn: self.assertIn(str(neuron.uid), output_no_syntax) - def test_set_weights( self ): - + + def test_set_weights(self): config = self.config config.wallet.name = "set_weights_testwallet" config.no_prompt = True @@ -2049,8 +2114,6 @@ def test_set_weights( self ): config.n_words = 12 config.use_password = False - - config.overwrite_hotkey = True # First create a new hotkey @@ -2063,7 +2126,8 @@ def test_set_weights( self ): cli.config = config cli.run() - def test_inspect( self ): + + def test_inspect(self): config = self.config config.wallet.name = "inspect_testwallet" config.no_prompt = True @@ -2072,7 +2136,6 @@ def test_inspect( self ): config.overwrite_coldkey = True config.overwrite_hotkey = True - # First create a new coldkey config.command = "new_coldkey" cli = bittensor.cli(config) @@ -2092,163 +2155,133 @@ def test_inspect( self ): cli.config = config cli.run() -@unittest.skip("") class TestCLIWithNetworkUsingArgs(unittest.TestCase): """ Test the CLI by passing args directly to the bittensor.cli factory """ - def test_run_reregister_false(self): - """ - Verify that the btcli run command does not reregister a not registered wallet - if --wallet.reregister is False - """ - mock_wallet = SimpleNamespace( - name = "mock_wallet", - coldkey = get_mock_keypair(0, self.id()), - coldkeypub = get_mock_keypair(0, self.id()), - hotkey_str = "mock_hotkey", - hotkey = get_mock_keypair(100, self.id()), - ) - - # SHOULD NOT BE REGISTERED - self.assertFalse(_subtensor_mock.is_hotkey_registered( - hotkey_ss58 = get_mock_keypair(0, self.id()).ss58_address, - netuid = 1 - ), "Wallet should not be registered before test") - - with patch('bittensor.wallet', return_value=mock_wallet) as mock_create_wallet: - with patch('bittensor.Subtensor.register', MagicMock(side_effect=Exception("shouldn't register during test"))): - with pytest.raises(SystemExit): - cli = bittensor.cli(args=[ - 'run', - '--netuid', '1', - '--wallet.name', 'mock', - '--wallet.hotkey', 'mock_hotkey', - '--wallet._mock', 'True', - '--subtensor.network', 'mock', # Mock network - '--no_prompt', - '--wallet.reregister', 'False' # Don't reregister - ]) - cli.run() - - def test_run_synapse_all(self): - """ - Verify that setting --synapse All works - """ - - class MockException(Exception): - """Raised by mocked function to exit early""" - pass - - with patch('bittensor.neurons.core_server.neuron', MagicMock(side_effect=MockException("should exit early"))) as mock_neuron: - with patch('bittensor.Wallet.is_registered', MagicMock(return_value=True)): # mock registered - with patch('bittensor.Config.to_defaults', MagicMock(return_value=True)): - with pytest.raises(MockException): - cli = bittensor.cli(args=[ - 'run', - '--subtensor.network', 'mock', # Mock network - '--netuid', '1', - '--wallet.name', 'mock', - '--wallet.hotkey', 'mock_hotkey', - '--wallet._mock', 'True', - '--cuda.no_cuda', - '--no_prompt', - '--model', 'core_server', - '--synapse', 'All', - ]) - cli.run() - - assert mock_neuron.call_count == 1 - args, kwargs = mock_neuron.call_args - - self.assertEqual(len(args), 0) # Should not have any args; indicates that "All" synapses are being used - self.assertEqual(len(kwargs), 1) # should have one kwarg; netuid - + def test_list_delegates(self): - cli = bittensor.cli(args=[ - 'list_delegates', - '--subtensor.network', 'mock', # Mock network - ]) + cli = bittensor.cli( + args=[ + "list_delegates", + "--subtensor.network", + "mock", # Mock network + ] + ) cli.run() + def test_list_subnets(self): - cli = bittensor.cli(args=[ - 'list_subnets', - '--subtensor.network', 'mock', # Mock network - ]) + cli = bittensor.cli( + args=[ + "list_subnets", + "--subtensor.network", + "mock", # Mock network + ] + ) cli.run() def test_delegate(self): """ Test delegate add command """ - mock_wallet = generate_wallet() - delegate_wallet = generate_wallet() + mock_wallet = generate_wallet( + hotkey = get_mock_keypair( + 100, self.id() + ) + ) + delegate_wallet = generate_wallet( + hotkey = get_mock_keypair( + 100 + 1, self.id() + ) + ) + # register the wallet - _, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = mock_wallet.hotkey.ss58_address, - coldkey = mock_wallet.coldkey.ss58_address, + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=mock_wallet.hotkey.ss58_address, + coldkey=mock_wallet.coldkey.ss58_address, ) - self.assertEqual(err, None) # register the delegate - _, err = _subtensor_mock.sudo_register( - netuid = 1, - hotkey = delegate_wallet.hotkey.ss58_address, - coldkey = delegate_wallet.coldkey.ss58_address, + _ = _subtensor_mock.force_register_neuron( + netuid=1, + hotkey=delegate_wallet.hotkey.ss58_address, + coldkey=delegate_wallet.coldkey.ss58_address, ) - self.assertEqual(err, None) # make the delegate a delegate _subtensor_mock.nominate(delegate_wallet, wait_for_finalization=True) - self.assertTrue(_subtensor_mock.is_hotkey_delegate( delegate_wallet.hotkey.ss58_address )) + self.assertTrue( + _subtensor_mock.is_hotkey_delegate(delegate_wallet.hotkey.ss58_address) + ) # Give the wallet some TAO - _, err = _subtensor_mock.sudo_force_set_balance( + _, err = _subtensor_mock.force_set_balance( ss58_address=mock_wallet.coldkey.ss58_address, - balance = bittensor.Balance.from_tao( 20.0 ) + balance=bittensor.Balance.from_tao(20.0), ) self.assertEqual(err, None) # Check balance - old_balance = _subtensor_mock.get_balance( mock_wallet.coldkey.ss58_address ) + old_balance = _subtensor_mock.get_balance(mock_wallet.coldkey.ss58_address) self.assertEqual(old_balance.tao, 20.0) # Check delegate stake - old_delegate_stake = _subtensor_mock.get_total_stake_for_hotkey(delegate_wallet.hotkey.ss58_address) + old_delegate_stake = _subtensor_mock.get_total_stake_for_hotkey( + delegate_wallet.hotkey.ss58_address + ) # Check wallet stake - old_wallet_stake = _subtensor_mock.get_total_stake_for_coldkey(mock_wallet.coldkey.ss58_address) - - with patch('bittensor._wallet.wallet_mock.Wallet_mock', return_value=mock_wallet): # Mock wallet creation. SHOULD NOT BE REGISTERED - cli = bittensor.cli(args=[ - 'delegate', - '--subtensor.network', 'mock', # Mock network - '--wallet.name', 'mock', - '--wallet._mock', 'True', - '--delegate_ss58key', delegate_wallet.hotkey.ss58_address, - '--amount', '10.0', # Delegate 10 TAO - '--no_prompt', - ]) + old_wallet_stake = _subtensor_mock.get_total_stake_for_coldkey( + mock_wallet.coldkey.ss58_address + ) + + with patch( + "bittensor.wallet", return_value=mock_wallet + ): # Mock wallet creation. SHOULD NOT BE REGISTERED + cli = bittensor.cli( + args=[ + "delegate", + "--subtensor.network", + "mock", # Mock network + "--wallet.name", + "mock", + "--wallet._mock", + "True", + "--delegate_ss58key", + delegate_wallet.hotkey.ss58_address, + "--amount", + "10.0", # Delegate 10 TAO + "--no_prompt", + ] + ) cli.run() # Check delegate stake - new_delegate_stake = _subtensor_mock.get_total_stake_for_hotkey(delegate_wallet.hotkey.ss58_address) + new_delegate_stake = _subtensor_mock.get_total_stake_for_hotkey( + delegate_wallet.hotkey.ss58_address + ) # Check wallet stake - new_wallet_stake = _subtensor_mock.get_total_stake_for_coldkey(mock_wallet.coldkey.ss58_address) + new_wallet_stake = _subtensor_mock.get_total_stake_for_coldkey( + mock_wallet.coldkey.ss58_address + ) # Check that the delegate stake increased by 10 TAO - self.assertAlmostEqual(new_delegate_stake.tao, old_delegate_stake.tao + 10.0, delta=1e-6) + self.assertAlmostEqual( + new_delegate_stake.tao, old_delegate_stake.tao + 10.0, delta=1e-6 + ) # Check that the wallet stake increased by 10 TAO - self.assertAlmostEqual(new_wallet_stake.tao, old_wallet_stake.tao + 10.0, delta=1e-6) + self.assertAlmostEqual( + new_wallet_stake.tao, old_wallet_stake.tao + 10.0, delta=1e-6 + ) - new_balance = _subtensor_mock.get_balance( mock_wallet.coldkey.ss58_address ) + new_balance = _subtensor_mock.get_balance(mock_wallet.coldkey.ss58_address) self.assertAlmostEqual(new_balance.tao, old_balance.tao - 10.0, delta=1e-6) -if __name__ == '__main__': - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration_tests/test_cli_no_network.py b/tests/integration_tests/test_cli_no_network.py index 12fbb5a1b5..8784bd616b 100644 --- a/tests/integration_tests/test_cli_no_network.py +++ b/tests/integration_tests/test_cli_no_network.py @@ -24,6 +24,8 @@ from copy import deepcopy import re +from tests.helpers import get_mock_coldkey + import bittensor @@ -43,7 +45,7 @@ def setUpClass(cls) -> None: "return_per_1000": bittensor.Balance.from_rao(0), "total_daily_return": bittensor.Balance.from_rao(0) } - cls._patched_subtensor = patch('bittensor._subtensor.subtensor_mock.mock_subtensor.mock', new=MagicMock( + cls._patched_subtensor = patch('bittensor._subtensor.subtensor_mock.MockSubtensor.__new__', new=MagicMock( return_value=MagicMock( get_subnets=MagicMock(return_value=[1]), # Mock subnet 1 ONLY. block=10_000, @@ -69,6 +71,7 @@ def config(self): @staticmethod def construct_config(): defaults = bittensor.Config() + defaults.netuid = 1 bittensor.subtensor.add_defaults( defaults ) defaults.subtensor.network = 'mock' @@ -79,7 +82,6 @@ def construct_config(): return defaults - @unittest.skip("") def test_check_configs(self): config = self.config config.no_prompt = True @@ -93,6 +95,7 @@ def test_check_configs(self): config.no_version_checking = True config.ss58_address = bittensor.Keypair.create_from_seed( b'0' * 32 ).ss58_address config.public_key_hex = None + config.proposal_hash = "" cli = bittensor.cli @@ -115,7 +118,6 @@ def ask_response(prompt: str) -> Any: config.command = cmd cli.check_config(config) - @unittest.skip("") def test_new_coldkey( self ): config = self.config config.wallet.name = "new_coldkey_testwallet" @@ -132,7 +134,6 @@ def test_new_coldkey( self ): cli = bittensor.cli(config) cli.run() - @unittest.skip("") def test_new_hotkey( self ): config = self.config config.wallet.name = "new_hotkey_testwallet" @@ -149,7 +150,6 @@ def test_new_hotkey( self ): cli = bittensor.cli(config) cli.run() - @unittest.skip("") def test_regen_coldkey( self ): config = self.config config.wallet.name = "regen_coldkey_testwallet" @@ -168,7 +168,6 @@ def test_regen_coldkey( self ): cli = bittensor.cli(config) cli.run() - @unittest.skip("") def test_regen_coldkeypub( self ): config = self.config config.wallet.name = "regen_coldkeypub_testwallet" @@ -183,7 +182,6 @@ def test_regen_coldkeypub( self ): cli = bittensor.cli(config) cli.run() - @unittest.skip("") def test_regen_hotkey( self ): config = self.config config.wallet.name = "regen_hotkey_testwallet" @@ -296,15 +294,14 @@ def test_btcli_help(self): commands = [ command for command in parser._actions[1].choices ] - # Verify that all commands are listed in the help message - for command in commands: - assert command in help_out + # Verify that all commands are listed in the help message, AND # Verify there are no duplicate commands - # Listed twice. Once in the positional arguments and once in the optional arguments + ## Listed twice. Once in the positional arguments and once in the optional arguments for command in commands: - pat = re.compile(rf'\n\s+({command})\s+\w') + pat = re.compile(rf'\n\s+({command})[^\S\r\n]+\w') matches = pat.findall(help_out) - self.assertEqual( len(matches), 1, f"Duplicate command {command} in help output") + self.assertGreaterEqual( len(matches), 1, f"Command {command} not found in help output") + self.assertLess( len(matches), 2, f"Duplicate command {command} in help output") def test_register_cuda_use_cuda_flag(self): class ExitEarlyException(Exception): @@ -346,6 +343,46 @@ class ExitEarlyException(Exception): assert cli.config.subtensor.register.cuda.use_cuda == False +class MockException(Exception): + pass + + +class TestEmptyArgs(unittest.TestCase): + """ + Test that the CLI doesn't crash when no args are passed + """ + _patched_subtensor = None + + @classmethod + def setUpClass(cls) -> None: + cls._patched_subtensor = patch('bittensor._subtensor.subtensor_mock.MockSubtensor.__new__', new=MagicMock( + )) + cls._patched_subtensor.start() + + @classmethod + def tearDownClass(cls) -> None: + cls._patched_subtensor.stop() + + @patch('rich.prompt.PromptBase.ask', side_effect=MockException) + def test_command_no_args(self, patched_prompt_ask): + # Get argparser + parser = bittensor.cli.__create_parser__() + # Get all commands from argparser + commands = [ + command for command in parser._actions[1].choices + ] + + # Test that each command can be run with no args + for command in commands: + try: + bittensor.cli(args=[ + command + ]).run() + except MockException: + pass # Expected exception + + # Should not raise any other exceptions + class TestCLIDefaultsNoNetwork(unittest.TestCase): _patched_subtensor = None @@ -363,7 +400,7 @@ def setUpClass(cls) -> None: "return_per_1000": bittensor.Balance.from_rao(0), "total_daily_return": bittensor.Balance.from_rao(0) } - cls._patched_subtensor = patch('bittensor._subtensor.subtensor_mock.mock_subtensor.mock', new=MagicMock( + cls._patched_subtensor = patch('bittensor._subtensor.subtensor_mock.MockSubtensor.__new__', new=MagicMock( return_value=MagicMock( get_subnets=MagicMock(return_value=[1]), # Mock subnet 1 ONLY. block=10_000, @@ -455,6 +492,434 @@ def test_overview_prompt_wallet_name(self): # NO prompt happened mock_ask_prompt.assert_not_called() + def test_stake_prompt_wallet_name_and_hotkey_name(self): + base_args = [ + 'stake', + '--all', + ] + # Patch command to exit early + with patch('bittensor._cli.commands.stake.StakeCommand.run', return_value=None): + + # Test prompt happens when + # - wallet name IS NOT passed, AND + # - hotkey name IS NOT passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + mock_ask_prompt.side_effect = ['mock', 'mock_hotkey'] + + cli = bittensor.cli(args=base_args + [ + # '--wallet.name', 'mock', + #'--wallet.hotkey', 'mock_hotkey', + ]) + cli.run() + + # Prompt happened + mock_ask_prompt.assert_called() + self.assertEqual(mock_ask_prompt.call_count, 2, msg="Prompt should have been called twice") + args0, kwargs0 = mock_ask_prompt.call_args_list[0] + combined_args_kwargs0 = [arg for arg in args0] + [val for val in [val for val in kwargs0.values()]] + # check that prompt was called for wallet name + self.assertTrue( + any(filter(lambda x: 'wallet name' in x.lower(), combined_args_kwargs0)), + msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}" + ) + + args1, kwargs1 = mock_ask_prompt.call_args_list[1] + combined_args_kwargs1 = [arg for arg in args1] + [val for val in kwargs1.values()] + # check that prompt was called for hotkey + + self.assertTrue( + any(filter(lambda x: 'hotkey' in x.lower(), combined_args_kwargs1)), + msg=f"Prompt should have been called for hotkey: {combined_args_kwargs1}" + ) + + # Test prompt happens when + # - wallet name IS NOT passed, AND + # - hotkey name IS passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + mock_ask_prompt.side_effect = ['mock', 'mock_hotkey'] + + cli = bittensor.cli(args=base_args + [ + #'--wallet.name', 'mock', + '--wallet.hotkey', 'mock_hotkey', + ]) + cli.run() + + # Prompt happened + mock_ask_prompt.assert_called() + self.assertEqual(mock_ask_prompt.call_count, 1, msg="Prompt should have been called ONCE") + args0, kwargs0 = mock_ask_prompt.call_args_list[0] + combined_args_kwargs0 = [arg for arg in args0] + [val for val in kwargs0.values()] + # check that prompt was called for wallet name + self.assertTrue( + any(filter(lambda x: 'wallet name' in x.lower(), combined_args_kwargs0)), + msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}" + ) + + # Test prompt happens when + # - wallet name IS passed, AND + # - hotkey name IS NOT passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + mock_ask_prompt.side_effect = ['mock', 'mock_hotkey'] + + cli = bittensor.cli(args=base_args + [ + '--wallet.name', 'mock', + #'--wallet.hotkey', 'mock_hotkey', + ]) + cli.run() + + # Prompt happened + mock_ask_prompt.assert_called() + self.assertEqual(mock_ask_prompt.call_count, 1, msg="Prompt should have been called ONCE") + args0, kwargs0 = mock_ask_prompt.call_args_list[0] + combined_args_kwargs0 = [arg for arg in args0] + [val for val in kwargs0.values()] + # check that prompt was called for hotkey + self.assertTrue( + any(filter(lambda x: 'hotkey' in x.lower(), combined_args_kwargs0)), + msg=f"Prompt should have been called for hotkey {combined_args_kwargs0}" + ) + + + # Test NO prompt happens when + # - wallet name IS passed, AND + # - hotkey name IS passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + cli = bittensor.cli(args=base_args + [ + '--wallet.name', 'coolwalletname', + '--wallet.hotkey', 'coolwalletname_hotkey', + ]) + cli.run() + + # NO prompt happened + mock_ask_prompt.assert_not_called() + + # Test NO prompt happens when + # - wallet name 'default' IS passed, AND + # - hotkey name 'default' IS passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + cli = bittensor.cli(args=base_args + [ + '--wallet.name', 'default', + '--wallet.hotkey', 'default', + ]) + cli.run() + + # NO prompt happened + mock_ask_prompt.assert_not_called() + + def test_unstake_prompt_wallet_name_and_hotkey_name(self): + base_args = [ + 'unstake', + '--all', + ] + # Patch command to exit early + with patch('bittensor._cli.commands.unstake.UnStakeCommand.run', return_value=None): + + # Test prompt happens when + # - wallet name IS NOT passed, AND + # - hotkey name IS NOT passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + mock_ask_prompt.side_effect = ['mock', 'mock_hotkey'] + + cli = bittensor.cli(args=base_args + [ + # '--wallet.name', 'mock', + #'--wallet.hotkey', 'mock_hotkey', + ]) + cli.run() + + # Prompt happened + mock_ask_prompt.assert_called() + self.assertEqual(mock_ask_prompt.call_count, 2, msg="Prompt should have been called twice") + args0, kwargs0 = mock_ask_prompt.call_args_list[0] + combined_args_kwargs0 = [arg for arg in args0] + [val for val in kwargs0.values()] + # check that prompt was called for wallet name + self.assertTrue( + any(filter(lambda x: 'wallet name' in x.lower(), combined_args_kwargs0)), + msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}" + ) + + args1, kwargs1 = mock_ask_prompt.call_args_list[1] + combined_args_kwargs1 = [arg for arg in args1] + [val for val in kwargs1.values()] + # check that prompt was called for hotkey + self.assertTrue( + any(filter(lambda x: 'hotkey' in x.lower(), combined_args_kwargs1)), + msg=f"Prompt should have been called for hotkey {combined_args_kwargs1}" + ) + + # Test prompt happens when + # - wallet name IS NOT passed, AND + # - hotkey name IS passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + mock_ask_prompt.side_effect = ['mock', 'mock_hotkey'] + + cli = bittensor.cli(args=base_args + [ + #'--wallet.name', 'mock', + '--wallet.hotkey', 'mock_hotkey', + ]) + cli.run() + + # Prompt happened + mock_ask_prompt.assert_called() + self.assertEqual(mock_ask_prompt.call_count, 1, msg="Prompt should have been called ONCE") + args0, kwargs0 = mock_ask_prompt.call_args_list[0] + combined_args_kwargs0 = [arg for arg in args0] + [val for val in kwargs0.values()] + # check that prompt was called for wallet name + self.assertTrue( + any(filter(lambda x: 'wallet name' in x.lower(), combined_args_kwargs0)), + msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}" + ) + + # Test prompt happens when + # - wallet name IS passed, AND + # - hotkey name IS NOT passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + mock_ask_prompt.side_effect = ['mock', 'mock_hotkey'] + + cli = bittensor.cli(args=base_args + [ + '--wallet.name', 'mock', + #'--wallet.hotkey', 'mock_hotkey', + ]) + cli.run() + + # Prompt happened + mock_ask_prompt.assert_called() + self.assertEqual(mock_ask_prompt.call_count, 1, msg="Prompt should have been called ONCE") + args0, kwargs0 = mock_ask_prompt.call_args_list[0] + combined_args_kwargs0 = [arg for arg in args0] + [val for val in kwargs0.values()] + # check that prompt was called for hotkey + self.assertTrue( + any(filter(lambda x: 'hotkey' in x.lower(), combined_args_kwargs0)), + msg=f"Prompt should have been called for hotkey {combined_args_kwargs0}" + ) + + + # Test NO prompt happens when + # - wallet name IS passed, AND + # - hotkey name IS passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + cli = bittensor.cli(args=base_args + [ + '--wallet.name', 'coolwalletname', + '--wallet.hotkey', 'coolwalletname_hotkey', + ]) + cli.run() + + # NO prompt happened + mock_ask_prompt.assert_not_called() + + # Test NO prompt happens when + # - wallet name 'default' IS passed, AND + # - hotkey name 'default' IS passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + cli = bittensor.cli(args=base_args + [ + '--wallet.name', 'default', + '--wallet.hotkey', 'default', + ]) + cli.run() + + # NO prompt happened + mock_ask_prompt.assert_not_called() + + def test_delegate_prompt_wallet_name(self): + base_args = [ + 'delegate', + '--all', + '--delegate_ss58key', get_mock_coldkey(0) + ] + # Patch command to exit early + with patch('bittensor._cli.commands.delegates.DelegateStakeCommand.run', return_value=None): + + # Test prompt happens when + # - wallet name IS NOT passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + mock_ask_prompt.side_effect = ['mock'] + + cli = bittensor.cli(args=base_args + [ + # '--wallet.name', 'mock', + ]) + cli.run() + + # Prompt happened + mock_ask_prompt.assert_called() + self.assertEqual(mock_ask_prompt.call_count, 1, msg="Prompt should have been called ONCE") + args0, kwargs0 = mock_ask_prompt.call_args_list[0] + combined_args_kwargs0 = [arg for arg in args0] + [val for val in kwargs0.values()] + # check that prompt was called for wallet name + self.assertTrue( + any(filter(lambda x: 'wallet name' in x.lower(), combined_args_kwargs0)), + msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}" + ) + + # Test NO prompt happens when + # - wallet name IS passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + cli = bittensor.cli(args=base_args + [ + '--wallet.name', 'coolwalletname', + ]) + cli.run() + + # NO prompt happened + mock_ask_prompt.assert_not_called() + + def test_undelegate_prompt_wallet_name(self): + base_args = [ + 'undelegate', + '--all', + '--delegate_ss58key', get_mock_coldkey(0) + ] + # Patch command to exit early + with patch('bittensor._cli.commands.delegates.DelegateUnstakeCommand.run', return_value=None): + + # Test prompt happens when + # - wallet name IS NOT passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + mock_ask_prompt.side_effect = ['mock'] + + cli = bittensor.cli(args=base_args + [ + # '--wallet.name', 'mock', + ]) + cli.run() + + # Prompt happened + mock_ask_prompt.assert_called() + self.assertEqual(mock_ask_prompt.call_count, 1, msg="Prompt should have been called ONCE") + args0, kwargs0 = mock_ask_prompt.call_args_list[0] + combined_args_kwargs0 = [arg for arg in args0] + [val for val in kwargs0.values()] + # check that prompt was called for wallet name + self.assertTrue( + any(filter(lambda x: 'wallet name' in x.lower(), combined_args_kwargs0)), + msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}" + ) + + # Test NO prompt happens when + # - wallet name IS passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + cli = bittensor.cli(args=base_args + [ + '--wallet.name', 'coolwalletname', + ]) + cli.run() + + # NO prompt happened + mock_ask_prompt.assert_not_called() + + def test_delegate_prompt_hotkey(self): + # Tests when + # - wallet name IS passed, AND + # - delegate hotkey IS NOT passed + base_args = [ + 'delegate', + '--all', + '--wallet.name', 'mock', + ] + + delegate_ss58 = get_mock_coldkey(0) + with patch('bittensor._cli.commands.delegates.show_delegates'): + with patch('bittensor.Subtensor.get_delegates', return_value=[ + bittensor.DelegateInfo( + hotkey_ss58=delegate_ss58, # return delegate with mock coldkey + total_stake=bittensor.Balance.from_float(0.1), + nominators=[], + owner_ss58='', + take=0.18, + validator_permits=[], + registrations=[], + return_per_1000=bittensor.Balance.from_float(0.1), + total_daily_return=bittensor.Balance.from_float(0.1) + ) + ]): + # Patch command to exit early + with patch('bittensor._cli.commands.delegates.DelegateStakeCommand.run', return_value=None): + + # Test prompt happens when + # - delegate hotkey IS NOT passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + mock_ask_prompt.side_effect = ['0'] # select delegate with mock coldkey + + cli = bittensor.cli(args=base_args + [ + # '--delegate_ss58key', delegate_ss58, + ]) + cli.run() + + # Prompt happened + mock_ask_prompt.assert_called() + self.assertEqual(mock_ask_prompt.call_count, 1, msg="Prompt should have been called ONCE") + args0, kwargs0 = mock_ask_prompt.call_args_list[0] + combined_args_kwargs0 = [arg for arg in args0] + [val for val in kwargs0.values()] + # check that prompt was called for delegate hotkey + self.assertTrue( + any(filter(lambda x: 'delegate' in x.lower(), combined_args_kwargs0)), + msg=f"Prompt should have been called for delegate: {combined_args_kwargs0}" + ) + + # Test NO prompt happens when + # - delegate hotkey IS passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + cli = bittensor.cli(args=base_args + [ + '--delegate_ss58key', delegate_ss58, + ]) + cli.run() + + # NO prompt happened + mock_ask_prompt.assert_not_called() + + def test_undelegate_prompt_hotkey(self): + # Tests when + # - wallet name IS passed, AND + # - delegate hotkey IS NOT passed + base_args = [ + 'undelegate', + '--all', + '--wallet.name', 'mock', + ] + + delegate_ss58 = get_mock_coldkey(0) + with patch('bittensor._cli.commands.delegates.show_delegates'): + with patch('bittensor.Subtensor.get_delegates', return_value=[ + bittensor.DelegateInfo( + hotkey_ss58=delegate_ss58, # return delegate with mock coldkey + total_stake=bittensor.Balance.from_float(0.1), + nominators=[], + owner_ss58='', + take=0.18, + validator_permits=[], + registrations=[], + return_per_1000=bittensor.Balance.from_float(0.1), + total_daily_return=bittensor.Balance.from_float(0.1) + ) + ]): + # Patch command to exit early + with patch('bittensor._cli.commands.delegates.DelegateUnstakeCommand.run', return_value=None): + + # Test prompt happens when + # - delegate hotkey IS NOT passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + mock_ask_prompt.side_effect = ['0'] # select delegate with mock coldkey + + cli = bittensor.cli(args=base_args + [ + # '--delegate_ss58key', delegate_ss58, + ]) + cli.run() + + # Prompt happened + mock_ask_prompt.assert_called() + self.assertEqual(mock_ask_prompt.call_count, 1, msg="Prompt should have been called ONCE") + args0, kwargs0 = mock_ask_prompt.call_args_list[0] + combined_args_kwargs0 = [arg for arg in args0] + [val for val in kwargs0.values()] + # check that prompt was called for delegate hotkey + self.assertTrue( + any(filter(lambda x: 'delegate' in x.lower(), combined_args_kwargs0)), + msg=f"Prompt should have been called for delegate: {combined_args_kwargs0}" + ) + + # Test NO prompt happens when + # - delegate hotkey IS passed + with patch('rich.prompt.Prompt.ask') as mock_ask_prompt: + cli = bittensor.cli(args=base_args + [ + '--delegate_ss58key', delegate_ss58, + ]) + cli.run() + + # NO prompt happened + mock_ask_prompt.assert_not_called() + + if __name__ == "__main__": unittest.main() \ No newline at end of file diff --git a/tests/integration_tests/test_keyfile.py b/tests/integration_tests/test_keyfile.py deleted file mode 100644 index 39ccfe0616..0000000000 --- a/tests/integration_tests/test_keyfile.py +++ /dev/null @@ -1,159 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -import os -import shutil -import time -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. -import unittest -import unittest.mock as mock - -import pytest - -import bittensor - - -class TestKeyFiles(unittest.TestCase): - - def setUp(self) -> None: - self.root_path = f"/tmp/pytest{time.time()}" - os.makedirs(self.root_path) - - self.create_keyfile() - - def tearDown(self) -> None: - shutil.rmtree(self.root_path) - - def create_keyfile(self): - keyfile = bittensor.keyfile(path=os.path.join(self.root_path, "keyfile")) - - mnemonic = bittensor.Keypair.generate_mnemonic(12) - alice = bittensor.Keypair.create_from_mnemonic(mnemonic) - keyfile.set_keypair(alice, encrypt=True, overwrite=True, password='thisisafakepassword') - - bob = bittensor.Keypair.create_from_uri('/Bob') - keyfile.set_keypair(bob, encrypt=True, overwrite=True, password='thisisafakepassword') - - return keyfile - - def test_create(self): - keyfile = bittensor.keyfile(path=os.path.join(self.root_path, "keyfile")) - - mnemonic = bittensor.Keypair.generate_mnemonic( 12 ) - alice = bittensor.Keypair.create_from_mnemonic(mnemonic) - keyfile.set_keypair(alice, encrypt=True, overwrite=True, password = 'thisisafakepassword') - assert keyfile.is_readable() - assert keyfile.is_writable() - assert keyfile.is_encrypted() - keyfile.decrypt( password = 'thisisafakepassword' ) - assert not keyfile.is_encrypted() - keyfile.encrypt( password = 'thisisafakepassword' ) - assert keyfile.is_encrypted() - str(keyfile) - keyfile.decrypt( password = 'thisisafakepassword' ) - assert not keyfile.is_encrypted() - str(keyfile) - - assert keyfile.get_keypair( password = 'thisisafakepassword' ).ss58_address == alice.ss58_address - assert keyfile.get_keypair( password = 'thisisafakepassword' ).private_key == alice.private_key - assert keyfile.get_keypair( password = 'thisisafakepassword' ).public_key == alice.public_key - - bob = bittensor.Keypair.create_from_uri ('/Bob') - keyfile.set_keypair(bob, encrypt=True, overwrite=True, password = 'thisisafakepassword') - assert keyfile.get_keypair( password = 'thisisafakepassword' ).ss58_address == bob.ss58_address - assert keyfile.get_keypair( password = 'thisisafakepassword' ).public_key == bob.public_key - - repr(keyfile) - - def test_legacy_coldkey(self): - legacy_filename = os.path.join(self.root_path, "coldlegacy_keyfile") - keyfile = bittensor.keyfile (path = legacy_filename) - keyfile.make_dirs() - keyfile_data = b'0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f' - with open(legacy_filename, "wb") as keyfile_obj: - keyfile_obj.write( keyfile_data ) - assert keyfile.keyfile_data == keyfile_data - keyfile.encrypt( password = 'this is the fake password' ) - keyfile.decrypt( password = 'this is the fake password' ) - keypair_bytes = b'{"accountId": "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f", "publicKey": "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f", "secretPhrase": null, "secretSeed": null, "ss58Address": "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm"}' - assert keyfile.keyfile_data == keypair_bytes - assert keyfile.get_keypair().ss58_address == "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" - assert "0x" + keyfile.get_keypair().public_key.hex() == "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f" - - def test_validate_password(self): - from bittensor._keyfile.keyfile_impl import validate_password - assert validate_password(None) == False - assert validate_password('passw0rd') == False - assert validate_password('123456789') == False - with mock.patch('getpass.getpass',return_value='biTTensor'): - assert validate_password('biTTensor') == True - with mock.patch('getpass.getpass',return_value='biTTenso'): - assert validate_password('biTTensor') == False - - def test_decrypt_keyfile_data_legacy(self): - import base64 - - from cryptography.fernet import Fernet - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import hashes - from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC - - from bittensor._keyfile.keyfile_impl import decrypt_keyfile_data - - __SALT = b"Iguesscyborgslikemyselfhaveatendencytobeparanoidaboutourorigins" - - def __generate_key(password): - kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), salt=__SALT, length=32, iterations=10000000, backend=default_backend()) - key = base64.urlsafe_b64encode(kdf.derive(password.encode())) - return key - - pw = 'fakepasssword238947239' - data = b'encrypt me!' - key = __generate_key(pw) - cipher_suite = Fernet(key) - encrypted_data = cipher_suite.encrypt(data) - - decrypted_data = decrypt_keyfile_data( encrypted_data, pw) - assert decrypted_data == data - - def test_user_interface(self): - from bittensor._keyfile.keyfile_impl import ask_password_to_encrypt - - with mock.patch('getpass.getpass', side_effect = ['pass', 'password', 'asdury3294y', 'asdury3294y']): - assert ask_password_to_encrypt() == 'asdury3294y' - - def test_overwriting(self): - from bittensor._keyfile.keyfile_impl import KeyFileError - - keyfile = bittensor.keyfile (path = os.path.join(self.root_path, "keyfile")) - alice = bittensor.Keypair.create_from_uri ('/Alice') - keyfile.set_keypair(alice, encrypt=True, overwrite=True, password = 'thisisafakepassword') - bob = bittensor.Keypair.create_from_uri ('/Bob') - - with pytest.raises(KeyFileError) as pytest_wrapped_e: - with mock.patch('builtins.input', return_value = 'n'): - keyfile.set_keypair(bob, encrypt=True, overwrite=False, password = 'thisisafakepassword') - - def test_keyfile_mock(self): - file = bittensor.keyfile( _mock = True ) - assert file.exists_on_device() - assert not file.is_encrypted() - assert file.is_readable() - assert file.data - assert file.keypair - file.set_keypair( keypair = bittensor.Keypair.create_from_mnemonic( mnemonic = bittensor.Keypair.generate_mnemonic() )) - - def test_keyfile_mock_func(self): - file = bittensor.keyfile.mock() diff --git a/tests/integration_tests/test_metagraph.py b/tests/integration_tests/test_metagraph_integration.py similarity index 89% rename from tests/integration_tests/test_metagraph.py rename to tests/integration_tests/test_metagraph_integration.py index 2d929aa9f5..9165fb04a8 100644 --- a/tests/integration_tests/test_metagraph.py +++ b/tests/integration_tests/test_metagraph_integration.py @@ -19,12 +19,22 @@ import bittensor import torch import pytest -from bittensor._subtensor.subtensor_mock import mock_subtensor +from bittensor._subtensor.subtensor_mock import MockSubtensor +_subtensor_mock: MockSubtensor = bittensor.subtensor( network = 'mock', _mock = True ) -@pytest.fixture(autouse=True) -def setup(): - mock_subtensor.kill_global_mock_process() +def setUpModule(): + _subtensor_mock.reset() + + _subtensor_mock.create_subnet( + netuid = 3 + ) + + # Set diff 0 + _subtensor_mock.set_difficulty( + netuid = 3, + difficulty = 0 + ) class TestMetagraph: diff --git a/tests/integration_tests/test_priority_thread_pool.py b/tests/integration_tests/test_priority_thread_pool.py index d6ff31f391..b8d62cf343 100644 --- a/tests/integration_tests/test_priority_thread_pool.py +++ b/tests/integration_tests/test_priority_thread_pool.py @@ -16,17 +16,21 @@ # DEALINGS IN THE SOFTWARE. import bittensor - -priority_pool = bittensor.prioritythreadpool(max_workers=1) - -def test_priority_thread_pool(): - save = [] - def save_number(number,save): - save += [number] - with priority_pool: - for x in range(10): - priority_pool.submit(save_number, x,save,priority=x) - - assert save[0] == 0 - assert save[1] == 9 +import unittest + +class TestPriorityThreadPool(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.priority_pool = bittensor.prioritythreadpool(max_workers=1) + + def test_priority_thread_pool(self): + save = [] + def save_number(number,save): + save += [number] + with self.priority_pool: + for x in range(10): + self.priority_pool.submit(save_number, x,save,priority=x) + + assert save[0] == 0 + assert save[1] == 9 diff --git a/tests/integration_tests/test_prometheus.py b/tests/integration_tests/test_prometheus.py index 3ce98b82d1..992ef5cfd2 100644 --- a/tests/integration_tests/test_prometheus.py +++ b/tests/integration_tests/test_prometheus.py @@ -2,34 +2,37 @@ import pytest import unittest -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch +from bittensor._subtensor.subtensor_mock import MockSubtensor +from tests.helpers import get_mock_wallet +_subtensor_mock: MockSubtensor = bittensor.subtensor( network = 'mock', _mock = True ) +def setUpModule(): + _subtensor_mock.reset() + + _subtensor_mock.create_subnet( + netuid = 3 + ) + + _subtensor_mock.set_difficulty( + netuid = 3, + difficulty = 0 + ) class TestPrometheus(unittest.TestCase): def setUp(self): - class success(): - def __init__(self): - self.is_success = True - def process_events(self): - return True - class fail(): - def __init__(self): - self.is_success = False - self.error_message = 'Mock failure' - def process_events(self): - return True self.subtensor = bittensor.subtensor(network = 'mock') - self.wallet = bittensor.wallet.mock() - self.success = success() - self.fail = fail() + self.wallet = get_mock_wallet() def test_init_prometheus_success(self): - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = self.success) - assert bittensor.prometheus(wallet = self.wallet, subtensor = self.subtensor, netuid=3) + with patch.object(self.subtensor, '_do_serve_prometheus', return_value = (True, None)): + with patch("prometheus_client.start_http_server"): + self.assertTrue( bittensor.prometheus(wallet = self.wallet, subtensor = self.subtensor, netuid=3) ) def test_init_prometheus_failed(self): - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = self.fail) - with pytest.raises(Exception): - bittensor.prometheus(wallet = self.wallet, subtensor = self.subtensor, netuid=3) + with patch.object(self.subtensor, '_do_serve_prometheus', return_value = (False, 'Mock failure')): + with patch("prometheus_client.start_http_server"): + with pytest.raises(Exception): + bittensor.prometheus(wallet = self.wallet, subtensor = self.subtensor, netuid=3) diff --git a/tests/integration_tests/test_subtensor.py b/tests/integration_tests/test_subtensor_integration.py similarity index 58% rename from tests/integration_tests/test_subtensor.py rename to tests/integration_tests/test_subtensor_integration.py index b9cdf2d20a..87bc8b98ef 100644 --- a/tests/integration_tests/test_subtensor.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -1,5 +1,6 @@ # The MIT License (MIT) # Copyright © 2021 Yuma Rao +# Copyright © 2023 Opentensor Technologies Inc # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated # documentation files (the “Software”), to deal in the Software without restriction, including without limitation @@ -20,19 +21,25 @@ import unittest from queue import Empty as QueueEmpty from unittest.mock import MagicMock, patch +from types import SimpleNamespace import bittensor import pytest from bittensor.utils.balance import Balance from substrateinterface import Keypair -from tests.helpers import get_mock_hotkey, get_mock_coldkey, MockConsole +from bittensor._subtensor.subtensor_mock import MockSubtensor +from tests.helpers import get_mock_hotkey, get_mock_coldkey, MockConsole, get_mock_keypair, get_mock_wallet class TestSubtensor(unittest.TestCase): _mock_console_patcher = None - _mock_subtensor: bittensor.Subtensor + _mock_subtensor: MockSubtensor + subtensor: MockSubtensor def setUp(self): - self.wallet = bittensor.wallet(_mock=True) + self.wallet = get_mock_wallet( + hotkey = get_mock_keypair(0, self.id()), + coldkey = get_mock_keypair(1, self.id()) + ) self.balance = Balance.from_tao(1000) self.mock_neuron = MagicMock() # NOTE: this might need more sophistication self.subtensor = bittensor.subtensor( network = 'mock' ) # own instance per test @@ -47,6 +54,17 @@ def setUpClass(cls) -> None: # Keeps the same mock network for all tests. This stops the network from being re-setup for each test. cls._mock_subtensor = bittensor.subtensor( network = 'mock' ) + cls._do_setup_subnet() + + @classmethod + def _do_setup_subnet(cls): + # reset the mock subtensor + cls._mock_subtensor.reset() + # Setup the mock subnet 3 + cls._mock_subtensor.create_subnet( + netuid = 3 + ) + @classmethod def tearDownClass(cls) -> None: cls._mock_console_patcher.stop() @@ -57,7 +75,8 @@ def test_network_overrides( self ): # Argument importance: chain_endpoint (arg) > network (arg) > config.subtensor.chain_endpoint > config.subtensor.network config0 = bittensor.subtensor.config() config0.subtensor.network = 'finney' - config0.subtensor.chain_endpoint = bittensor.__finney_entrypoint__ #'wss://finney.subtensor.io' + config0.subtensor.chain_endpoint = 'wss://finney.subtensor.io' # Should not match bittensor.__finney_entrypoint__ + assert config0.subtensor.chain_endpoint != bittensor.__finney_entrypoint__ config1 = bittensor.subtensor.config() config1.subtensor.network = 'local' @@ -68,15 +87,15 @@ def test_network_overrides( self ): with patch('substrateinterface.SubstrateInterface.reload_type_registry'): # Choose arg over config sub0 = bittensor.subtensor( config = config0, chain_endpoint = 'wss://fin.subtensor.io' ) - assert sub0.chain_endpoint == 'wss://fin.subtensor.io' + self.assertEqual(sub0.chain_endpoint, 'wss://fin.subtensor.io', msg='Explicit chain_endpoint arg should override config.chain_endpoint') # Choose network arg over config sub1 = bittensor.subtensor( config = config1, network = 'local' ) - assert sub1.chain_endpoint == bittensor.__local_entrypoint__ + self.assertEqual(sub1.chain_endpoint, bittensor.__local_entrypoint__, msg='Explicit network arg should override config.network') # Choose chain_endpoint config over network config sub2 = bittensor.subtensor( config = config0 ) - assert sub2.chain_endpoint == bittensor.__finney_entrypoint__ + self.assertEqual(sub2.chain_endpoint, config0.subtensor.chain_endpoint, msg='config.chain_endpoint should override choice derived from config.network') sub3 = bittensor.subtensor( config = config1 ) # Should pick local instead of finney (default) @@ -88,18 +107,12 @@ def test_get_current_block( self ): assert (type(block) == int) def test_unstake( self ): - class success(): - def __init__(self): - self.is_success = True - def process_events(self): - return True + self.subtensor._do_unstake = MagicMock(return_value = True) - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = success()) - self.subtensor.substrate.compose_call = MagicMock() self.subtensor.substrate.get_payment_info = MagicMock( return_value = { 'partialFee': 100 } ) - self.subtensor.substrate.create_signed_extrinsic = MagicMock() + self.subtensor.register = MagicMock(return_value = True) self.subtensor.get_balance = MagicMock(return_value = self.balance) @@ -108,21 +121,15 @@ def process_events(self): success= self.subtensor.unstake(self.wallet, amount = 200 ) - assert success == True + self.assertTrue(success, msg="Unstake should succeed") def test_unstake_inclusion( self ): - class success(): - def __init__(self): - self.is_success = True - def process_events(self): - return True - - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = success()) - self.subtensor.substrate.compose_call = MagicMock() + self.subtensor._do_unstake = MagicMock(return_value = True) + self.subtensor.substrate.get_payment_info = MagicMock( return_value = { 'partialFee': 100 } ) - self.subtensor.substrate.create_signed_extrinsic = MagicMock() + self.subtensor.register = MagicMock(return_value = True) self.subtensor.get_balance = MagicMock(return_value = self.balance) @@ -132,46 +139,29 @@ def process_events(self): amount = 200, wait_for_inclusion = True ) - assert success == True + self.assertTrue(success, msg="Unstake should succeed") def test_unstake_failed( self ): - class failed(): - def __init__(self): - self.is_success = False - self.error_message = 'Mock' - def process_events(self): - return True - - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = failed()) - self.subtensor.substrate.compose_call = MagicMock() - self.subtensor.substrate.get_payment_info = MagicMock( - return_value = { 'partialFee': 100 } - ) - self.subtensor.substrate.create_signed_extrinsic = MagicMock() + self.subtensor._do_unstake = MagicMock(return_value = False) + self.subtensor.register = MagicMock(return_value = True) self.subtensor.get_balance = MagicMock(return_value = self.balance) self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock(return_value = self.mock_neuron) with patch('bittensor.Subtensor.get_stake_for_coldkey_and_hotkey', return_value=Balance.from_tao(500)): - fail= self.subtensor.unstake(self.wallet, + fail = self.subtensor.unstake(self.wallet, amount = 200, wait_for_inclusion = True ) - assert fail == False + self.assertFalse(fail, msg="Unstake should fail") def test_stake(self): - class success(): - def __init__(self): - self.is_success = True - def process_events(self): - return True + self.subtensor._do_stake = MagicMock(return_value = True) - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = success()) - self.subtensor.substrate.compose_call = MagicMock() self.subtensor.substrate.get_payment_info = MagicMock( return_value = { 'partialFee': 100 } ) - self.subtensor.substrate.create_signed_extrinsic = MagicMock() + self.subtensor.register = MagicMock(return_value = True) self.subtensor.get_balance = MagicMock(return_value = self.balance) @@ -181,21 +171,15 @@ def process_events(self): success= self.subtensor.add_stake(self.wallet, amount = 200 ) - assert success == True + self.assertTrue(success, msg="Stake should succeed") def test_stake_inclusion(self): - class success(): - def __init__(self): - self.is_success = True - def process_events(self): - return True + self.subtensor._do_stake = MagicMock(return_value = True) - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = success()) - self.subtensor.substrate.compose_call = MagicMock() self.subtensor.substrate.get_payment_info = MagicMock( return_value = { 'partialFee': 100 } ) - self.subtensor.substrate.create_signed_extrinsic = MagicMock() + self.subtensor.register = MagicMock(return_value = True) self.subtensor.get_balance = MagicMock(return_value = self.balance) @@ -206,44 +190,31 @@ def process_events(self): amount = 200, wait_for_inclusion = True ) - assert success == True + self.assertTrue(success, msg="Stake should succeed") def test_stake_failed( self ): - class failed(): - def __init__(self): - self.is_success = False - self.error_message = 'Mock' - def process_events(self): - return True + self.subtensor._do_stake = MagicMock(return_value = False) - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = failed()) - - self.subtensor.substrate.compose_call = MagicMock() self.subtensor.substrate.get_payment_info = MagicMock( return_value = { 'partialFee': 100 } ) - self.subtensor.substrate.create_signed_extrinsic = MagicMock() + self.subtensor.register = MagicMock(return_value = True) self.subtensor.get_balance = MagicMock(return_value = Balance.from_rao(0)) self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock(return_value = self.mock_neuron) with patch('bittensor.Subtensor.get_stake_for_coldkey_and_hotkey', return_value=Balance.from_tao(500)): with patch('bittensor.Subtensor.get_hotkey_owner', return_value=self.wallet.coldkeypub.ss58_address): - fail= self.subtensor.add_stake(self.wallet, + fail = self.subtensor.add_stake(self.wallet, amount = 200, wait_for_inclusion = True ) - assert fail == False - def test_transfer( self ): - class success(): - def __init__(self): - self.is_success = True - def process_events(self): - return True - block_hash: str = '0x' + self.assertFalse(fail, msg="Stake should fail") + def test_transfer( self ): fake_coldkey = get_mock_coldkey(1) - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = success()) + + self.subtensor._do_transfer = MagicMock(return_value = (True, '0x', None)) self.subtensor.register = MagicMock(return_value = True) self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock(return_value = self.mock_neuron) self.subtensor.get_balance = MagicMock(return_value = self.balance) @@ -251,46 +222,32 @@ def process_events(self): fake_coldkey, amount = 200, ) - assert success == True + self.assertTrue(success, msg="Transfer should succeed") def test_transfer_inclusion( self ): - class success(): - def __init__(self): - self.is_success = True - def process_events(self): - return True - block_hash: str = '0x' - fake_coldkey = get_mock_coldkey(1) - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = success()) + self.subtensor._do_transfer = MagicMock(return_value = (True, '0x', None)) self.subtensor.register = MagicMock(return_value = True) self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock(return_value = self.mock_neuron) self.subtensor.get_balance = MagicMock(return_value = self.balance) - success= self.subtensor.transfer(self.wallet, + success = self.subtensor.transfer(self.wallet, fake_coldkey, amount = 200, wait_for_inclusion = True ) - assert success == True + self.assertTrue(success, msg="Transfer should succeed") def test_transfer_failed(self ): - class failed(): - def __init__(self): - self.is_success = False - self.error_message = 'Mock' - def process_events(self): - return True - fake_coldkey = get_mock_coldkey(1) - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = failed()) + self.subtensor._do_transfer = MagicMock(return_value = (False, None, 'Mock failure message')) fail= self.subtensor.transfer(self.wallet, fake_coldkey, amount = 200, wait_for_inclusion = True ) - assert fail == False + self.assertFalse(fail, msg="Transfer should fail") def test_transfer_invalid_dest(self ): fake_coldkey = get_mock_coldkey(1) @@ -300,18 +257,12 @@ def test_transfer_invalid_dest(self ): amount = 200, wait_for_inclusion = True ) - assert fail == False + self.assertFalse(fail, msg="Transfer should fail because of invalid dest") def test_transfer_dest_as_bytes(self ): - class success(): - def __init__(self): - self.is_success = True - def process_events(self): - return True - block_hash: str = '0x' - fake_coldkey = get_mock_coldkey(1) - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = success()) + self.subtensor._do_transfer = MagicMock(return_value = (True, '0x', None)) + self.subtensor.register = MagicMock(return_value = True) self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock(return_value = self.mock_neuron) self.subtensor.get_balance = MagicMock(return_value = self.balance) @@ -322,7 +273,7 @@ def process_events(self): amount = 200, wait_for_inclusion = True ) - assert success == True + self.assertTrue(success, msg="Transfer should succeed") def test_set_weights( self ): chain_weights = [0] @@ -333,9 +284,7 @@ def process_events(self): return True - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = success()) - self.subtensor.substrate.compose_call = MagicMock() - self.subtensor.substrate.create_signed_extrinsic = MagicMock() + self.subtensor._do_set_weights = MagicMock(return_value = (True, None)) success= self.subtensor.set_weights(wallet=self.wallet, netuid = 3, @@ -346,15 +295,7 @@ def process_events(self): def test_set_weights_inclusion( self ): chain_weights = [0] - class success(): - def __init__(self): - self.is_success = True - def process_events(self): - return True - - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = success()) - self.subtensor.substrate.compose_call = MagicMock() - self.subtensor.substrate.create_signed_extrinsic = MagicMock() + self.subtensor._do_set_weights = MagicMock(return_value = (True, None)) success = self.subtensor.set_weights(wallet=self.wallet, netuid = 1, @@ -365,24 +306,16 @@ def process_events(self): assert success == True def test_set_weights_failed( self ): - class failed(): - def __init__(self): - self.is_success = False - self.error_message = 'Mock' - def process_events(self): - return True - chain_weights = [0] - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = failed()) - self.subtensor.substrate.compose_call = MagicMock() - self.subtensor.substrate.create_signed_extrinsic = MagicMock() - - fail= self.subtensor.set_weights(wallet=self.wallet, - netuid = 3, - uids=[1], - weights=chain_weights, - wait_for_inclusion = True - ) + self.subtensor._do_set_weights = MagicMock(return_value = (False, 'Mock failure message')) + + fail = self.subtensor.set_weights( + wallet=self.wallet, + netuid = 3, + uids=[1], + weights=chain_weights, + wait_for_inclusion = True + ) assert fail == False def test_get_balance( self ): @@ -391,35 +324,49 @@ def test_get_balance( self ): assert type(balance) == bittensor.utils.balance.Balance def test_get_balances( self ): - balance= self.subtensor.get_balances() - assert type(balance) == dict - for i in balance: - assert type(balance[i]) == bittensor.utils.balance.Balance + balances = self.subtensor.get_balances() + assert type(balances) == dict + for i in balances: + assert type(balances[i]) == bittensor.utils.balance.Balance def test_get_uid_by_hotkey_on_subnet( self ): - fake_hotkey = get_mock_hotkey(0) - with patch('bittensor.Subtensor.query_subtensor', return_value=MagicMock( value=0 )): - uid = self.subtensor.get_uid_for_hotkey_on_subnet(fake_hotkey, netuid = 3) - assert isinstance(uid, int) - - def test_hotkey_register( self ): - fake_hotkey = get_mock_hotkey(0) - self.subtensor.get_uid_for_hotkey_on_subnet = MagicMock(return_value = 0) - register= self.subtensor.is_hotkey_registered(fake_hotkey, netuid = 3) - assert register == True - - def test_hotkey_register_failed( self ): - self.subtensor.get_uid_for_hotkey_on_subnet = MagicMock(return_value = None) - register= self.subtensor.is_hotkey_registered('mock', netuid = 3) - assert register == False + mock_coldkey_kp = get_mock_keypair(0, self.id()) + mock_hotkey_kp = get_mock_keypair(100, self.id()) + + # Register on subnet 3 + mock_uid = self.subtensor.force_register_neuron( + netuid = 3, + hotkey = mock_hotkey_kp.ss58_address, + coldkey = mock_coldkey_kp.ss58_address, + ) - def test_registration_multiprocessed_already_registered( self ): - class success(): - def __init__(self): - self.is_success = True - def process_events(self): - return True + uid = self.subtensor.get_uid_for_hotkey_on_subnet(mock_hotkey_kp.ss58_address, netuid = 3) + self.assertIsInstance(uid, int, msg="get_uid_for_hotkey_on_subnet should return an int") + self.assertEqual(uid, mock_uid, msg="get_uid_for_hotkey_on_subnet should return the correct uid") + + def test_is_hotkey_registered( self ): + mock_coldkey_kp = get_mock_keypair(0, self.id()) + mock_hotkey_kp = get_mock_keypair(100, self.id()) + + # Register on subnet 3 + _ = self.subtensor.force_register_neuron( + netuid = 3, + hotkey = mock_hotkey_kp.ss58_address, + coldkey = mock_coldkey_kp.ss58_address, + ) + + registered = self.subtensor.is_hotkey_registered(mock_hotkey_kp.ss58_address, netuid = 3) + self.assertTrue(registered, msg="Hotkey should be registered") + def test_is_hotkey_registered_not_registered( self ): + mock_hotkey_kp = get_mock_keypair(100, self.id()) + + # Do not register on subnet 3 + + registered = self.subtensor.is_hotkey_registered(mock_hotkey_kp.ss58_address, netuid = 3) + self.assertFalse(registered, msg="Hotkey should not be registered") + + def test_registration_multiprocessed_already_registered( self ): workblocks_before_is_registered = random.randint(5, 10) # return False each work block but return True after a random number of blocks is_registered_return_values = [False for _ in range(workblocks_before_is_registered)] + [True] + [True, False] @@ -434,12 +381,15 @@ def process_events(self): # patch time queue get to raise Empty exception with patch('multiprocessing.queues.Queue.get_nowait', side_effect=QueueEmpty) as mock_queue_get_nowait: - wallet = bittensor.wallet(_mock=True) - wallet.is_registered = MagicMock( side_effect=is_registered_return_values ) + wallet = get_mock_wallet( + hotkey = get_mock_keypair(0, self.id()), + coldkey = get_mock_keypair(1, self.id()) + ) + self.subtensor.is_hotkey_registered = MagicMock( side_effect=is_registered_return_values ) self.subtensor.difficulty= MagicMock(return_value=1) self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( side_effect=mock_neuron ) - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = success()) + self.subtensor._do_pow_register = MagicMock(return_value = (True, None)) with patch('bittensor.__console__.status') as mock_set_status: # Need to patch the console status to avoid opening a parallel live display @@ -451,65 +401,51 @@ def process_events(self): # calls until True and once again before exiting subtensor class # This assertion is currently broken when difficulty is too low - assert wallet.is_registered.call_count == workblocks_before_is_registered + 2 + assert self.subtensor.is_hotkey_registered.call_count == workblocks_before_is_registered + 2 def test_registration_partly_failed( self ): - - class failed(): - def __init__(self): - self.is_success = False - self.error_message ='Failed' - def process_events(self): - return False - - class success(): - def __init__(self): - self.is_success = True - def process_events(self): - return True - - submit_extrinsic_mock = MagicMock( side_effect = [failed(), failed(), success()]) + do_pow_register_mock = MagicMock( side_effect = [(False, 'Failed'), (False, 'Failed'), (True, None)]) def is_registered_side_effect(*args, **kwargs): - nonlocal submit_extrinsic_mock - return submit_extrinsic_mock.call_count < 3 + nonlocal do_pow_register_mock + return do_pow_register_mock.call_count < 3 current_block = [i for i in range(0,100)] with patch('bittensor.Subtensor.get_neuron_for_pubkey_and_subnet', return_value = bittensor.NeuronInfo._null_neuron()): with patch('bittensor.Subtensor.difficulty'): - wallet = bittensor.wallet(_mock=True) - wallet.is_registered = MagicMock(side_effect=is_registered_side_effect) + wallet = get_mock_wallet( + hotkey = get_mock_keypair(0, self.id()), + coldkey = get_mock_keypair(1, self.id()) + ) + + self.subtensor.is_hotkey_registered = MagicMock(side_effect=is_registered_side_effect) self.subtensor.difficulty = MagicMock(return_value=1) self.subtensor.get_current_block = MagicMock(side_effect=current_block) - self.subtensor.substrate.submit_extrinsic = submit_extrinsic_mock + self.subtensor._do_pow_register = do_pow_register_mock # should return True self.assertTrue( self.subtensor.register(wallet=wallet, netuid = 3, num_processes=3, update_interval=5), msg="Registration should succeed" ) def test_registration_failed( self ): - class failed(): - def __init__(self): - self.is_success = False - self.error_message ='Failed' - def process_events(self): - return False - - is_registered_return_values = [False for _ in range(100)] current_block = [i for i in range(0,100)] mock_neuron = MagicMock() mock_neuron.is_null = True with patch('bittensor._subtensor.extrinsics.registration.create_pow', return_value=None) as mock_create_pow: - wallet = bittensor.wallet(_mock=True) - wallet.is_registered = MagicMock( side_effect=is_registered_return_values ) + wallet = get_mock_wallet( + hotkey = get_mock_keypair(0, self.id()), + coldkey = get_mock_keypair(1, self.id()) + ) + + self.subtensor.is_hotkey_registered = MagicMock(side_effect=is_registered_return_values) self.subtensor.get_current_block = MagicMock(side_effect=current_block) self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( return_value=mock_neuron ) self.subtensor.substrate.get_block_hash = MagicMock( return_value = '0x' + '0' * 64 ) - self.subtensor.substrate.submit_extrinsic = MagicMock(return_value = failed()) + self.subtensor._do_pow_register = MagicMock(return_value = (False, 'Failed')) # should return True self.assertIsNot( self.subtensor.register(wallet=wallet, netuid = 3 ), True, msg="Registration should fail" ) @@ -525,14 +461,14 @@ class ExitEarly(Exception): side_effect = [True, False] ) - mock_substrate_enter = MagicMock( - side_effect=ExitEarly() + mock_do_pow_register = MagicMock( + side_effect = ExitEarly() ) mock_subtensor_self = MagicMock( neuron_for_pubkey = MagicMock( return_value = MagicMock(is_null = True) ), # not registered + _do_pow_register = mock_do_pow_register, substrate=MagicMock( - __enter__ = mock_substrate_enter, get_block_hash = MagicMock( return_value = '0x' + '0'*64 ), ) ) @@ -554,24 +490,7 @@ class ExitEarly(Exception): bittensor.Subtensor.register( mock_subtensor_self, mock_wallet, netuid = 3 ) self.assertEqual( mock_create_pow.call_count, 2, msg="must try another pow after stale" ) self.assertEqual( mock_is_stale.call_count, 2 ) - self.assertEqual( mock_substrate_enter.call_count, 1, msg="only tries to submit once, then exits" ) - -# def test_subtensor_mock_functions(self): -# with patch('substrateinterface.SubstrateInterface.query'): -# sub = bittensor.subtensor(_mock=True) -# sub.total_issuance -# sub.total_stake -# sub.immunity_period(netuid = 3) -# sub.rho(netuid = 3) -# sub.kappa(netuid = 3) -# sub.blocks_since_epoch(netuid = 3) -# sub.max_n(netuid = 3) -# sub.min_allowed_weights(netuid = 3) -# sub.validator_epoch_length(netuid = 3) -# sub.validator_epochs_per_reset(netuid = 3) -# sub.validator_sequence_length(netuid = 3) -# sub.validator_batch_size(netuid = 3) -# sub.difficulty(netuid = 3) + self.assertEqual( mock_do_pow_register.call_count, 1, msg="only tries to submit once, then exits" ) # # This test was flaking, please check to_defaults before reactiving the test # def _test_defaults_to_finney(): @@ -579,67 +498,5 @@ class ExitEarly(Exception): # assert sub.network == 'finney' # assert sub.chain_endpoint == bittensor.__finney_entrypoint__ -# def test_subtensor_mock(): -# mock_subtensor.kill_global_mock_process() -# sub = bittensor.subtensor(_mock=True) -# assert mock_subtensor.global_mock_process_is_running() -# assert sub._is_mocked == True -# assert sub._owned_mock_subtensor_process != None -# del(sub) -# assert not mock_subtensor.global_mock_process_is_running() - -# def test_create_mock_process(): -# mock_subtensor.kill_global_mock_process() -# mock_subtensor.create_global_mock_process() -# assert mock_subtensor.global_mock_process_is_running() -# mock_subtensor.kill_global_mock_process() -# assert not mock_subtensor.global_mock_process_is_running() - -# def test_mock_from_mock_arg(): -# sub = bittensor.subtensor(_mock=True) -# assert mock_subtensor.global_mock_process_is_running() -# assert sub._is_mocked == True -# assert sub._owned_mock_subtensor_process != None -# sub.optionally_kill_owned_mock_instance() -# assert not mock_subtensor.global_mock_process_is_running() -# del(sub) -# assert not mock_subtensor.global_mock_process_is_running() - -# def test_mock_from_network_arg(): -# mock_subtensor.kill_global_mock_process() -# sub = bittensor.subtensor(network='mock') -# assert sub.network == 'mock' -# assert mock_subtensor.global_mock_process_is_running() -# assert sub._is_mocked == True -# assert sub._owned_mock_subtensor_process != None -# sub.__del__() -# assert not mock_subtensor.global_mock_process_is_running() - -# def test_create_from_config(): -# mock_subtensor.kill_global_mock_process() -# config = bittensor.subtensor.config() -# config.subtensor.network = 'mock' -# sub = bittensor.subtensor(config=config) -# assert mock_subtensor.global_mock_process_is_running() -# assert sub._is_mocked == True -# assert sub._owned_mock_subtensor_process != None -# del(sub) -# assert not mock_subtensor.global_mock_process_is_running() - -# def test_two_subtensor_ownership(): -# mock_subtensor.kill_global_mock_process() -# sub1 = bittensor.subtensor(_mock=True) -# sub2 = bittensor.subtensor(_mock=True) -# assert sub1._is_mocked == True -# assert sub2._is_mocked == True -# assert sub1._owned_mock_subtensor_process != None -# assert sub2._owned_mock_subtensor_process == None -# assert mock_subtensor.global_mock_process_is_running() -# del( sub2 ) -# assert mock_subtensor.global_mock_process_is_running() -# del ( sub1 ) -# time.sleep(2) -# assert not mock_subtensor.global_mock_process_is_running() - if __name__ == "__main__": unittest.main() diff --git a/tests/mock_subtensor/bin/Linux/node-subtensor b/tests/mock_subtensor/bin/Linux/node-subtensor deleted file mode 100755 index dd6c668e74..0000000000 Binary files a/tests/mock_subtensor/bin/Linux/node-subtensor and /dev/null differ diff --git a/tests/mock_subtensor/bin/OSX/node-subtensor b/tests/mock_subtensor/bin/OSX/node-subtensor deleted file mode 100755 index df7e556c97..0000000000 Binary files a/tests/mock_subtensor/bin/OSX/node-subtensor and /dev/null differ diff --git a/tests/mock_subtensor/specs/local_raw.json b/tests/mock_subtensor/specs/local_raw.json deleted file mode 100644 index 258f5485af..0000000000 --- a/tests/mock_subtensor/specs/local_raw.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "name": "Bittensor", - "id": "bittensor", - "chainType": "Development", - "bootNodes": [], - "telemetryEndpoints": null, - "protocolId": "bittensor", - "properties": { - "ss58Format": 42, - "tokenDecimals": 9, - "tokenSymbol": "TAO" - }, - "codeSubstitutes": {}, - "genesis": { - "raw": { - "top": { - "0x26aa394eea5630e07c48ae0c9558cef74e7b9012096b41c4eb3aaf947f6ea429": "0x0000", - "0x26aa394eea5630e07c48ae0c9558cef75684a022a34dd8bfa2baaf44f172b710": "0x01", - "0x26aa394eea5630e07c48ae0c9558cef78a42f33323cb5ced3b44dd825fda9fcc": "0x4545454545454545454545454545454545454545454545454545454545454545", - "0x26aa394eea5630e07c48ae0c9558cef7a44704b568d21667356a5a050c118746b4def25cfda6ef3a00000000": "0x4545454545454545454545454545454545454545454545454545454545454545", - "0x26aa394eea5630e07c48ae0c9558cef7a7fd6c28836b9a28522dc924110cf439": "0x01", - "0x26aa394eea5630e07c48ae0c9558cef7b99d880ec681799c0cf30e8886371da94f9aea1afa791265fae359272badc1cf8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48": "0x000000000000000001000000000000000010a5d4e8000000000000000000000000000000000000000000000000000000", - "0x26aa394eea5630e07c48ae0c9558cef7b99d880ec681799c0cf30e8886371da997ef6cd30938d51a5b77afc536d09165f66a8cb34cbc4280ebcd6981c0cfc5f085b8beadea8340d8fcf6c807f3f94838": "0x0000000000000000010000000000000000407a10f35a0000000000000000000000000000000000000000000000000000", - "0x26aa394eea5630e07c48ae0c9558cef7b99d880ec681799c0cf30e8886371da9de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d": "0x000000000000000001000000000000000010a5d4e8000000000000000000000000000000000000000000000000000000", - "0x26aa394eea5630e07c48ae0c9558cef7f9cce9c888469bb1a0dceaa129672ef8": "0xd901386e6f64652d73756274656e736f72", - "0x3a636f6465": "0x52bc537646db8e0528b52ffd00585cd804ce2c861c1352107067947488f1689c1647b13c32104b52d0853ce2918cf32e099716c3e451fef600c567c8f6c5f42b49fcafc12a0e672e460b80baaa02fc6d3e8713706f68907542347bc9c1407e238d344208d97b4b290358131612ba13dc114d5afb0e67d3490f029ad506dee7ea8c1b5e9b4eb2fb5c4dba72418ab4761e5a7b367b10d0ab361ad43cd51df0dc0f79f8e57c64d9dc334ace8f1b621bc2d9314a1f942d56844d311828bf49f86d39fe6ac3d8fb829af70753553da6bdb3e7a717eb81addc7e87b797f5747e31cb5627dc81dc8375e9d2658dce8ed8d9033bf37aa0bc5ec7e687dd89ae95441614fdf17bb035a0e8d24e649f96241e3ebb7a5fd77a9e25be13588f1327b07e12a4df813aafb30f37e9d7495ecfaed1519b3edcf1de09c8b61a9e391ff0d7d46f7f1d4e27b8e3b76b5efc5df7be9e9f8e971fbc7e0b6a6cb1ccfb7e3a3bcf6566cebe4f827ae00f3dffea811d3b7a49e0f7ca058941f6d04ba267d53643c1b6df5eb46738755d7fcd295daf37ad29e973865337adbf806c73fb067778a7f67224255d7bd9de94f3a112a66314e9daf5cbf958ef0bead5e60ca3f4a4399dd8feb6b5ef74625bd3e206792134add703fbf403f67ebdab8d7e7b9b9a9724fbf6c8f5647d7d2b6d2f8ac1941eaf7db90b74f09aa75b36bfec1ac9fb829ab43e489bf7fdf4f69dda84bcba63ff2d20a808adba63af7d07f6fc34d42069f6c086576d1ee960d7f03bcc537bc3a64d5f53666e5bcae627bfc7e669bd9eedcb93b1c262f50f8ded6585126cd890e89813011937d221d1ed4b9ac3c18ea567d8608c537e17cb2e290f9fcd4e4a1e02e4e4b7b52594845646c9c7b9bdc152fce5c9304a7a713b3293a1cf34077e320dbf303bd40eb1c3ebf03abc763dbbe0e17508e1a50ca3aea92d7db01176987f7daf9fe2b7b94f1109d725e6c930ea8a733bce4c6d94b9b0a97cd49ceb7aa939d79c6194bc105a8651f2cac12f4342e65045ba3c0aaab6cbdf704a9b615476791ccdb9da6497df66c735d94933e39a0c872cbbfccc9496c74173b6e52f1946658745d754991d5e48a8f97b46653f6187cd7dc2bc9eedd2d8b5255484d696d0105a1995fdea89ad79328ccab029d546af5c541bfd0e5f731b888324432a9b720a8323c0b26989b91fd1fbb6e3e1b758c2e6b61c423e9ee5ecd92e3fe0e3b120f0f1d9a3f7651db12fd7f344940f40c1ea9e6f036b604397befe24a82fef09e67df12ca7922cca07a0e82d7e4f301724f3be2736e586908fbfb81fd7556d3e89cded6bf66c97de4b1fd13b01818f878f5e127ef47a7efa832d97af6cd8b7e57c6c2f5d3e9bdc55734cbfbe99e68fa0fe4adfaedc8fcc9424bbbe34bfecfb6d5b5b425db4549b8bb91fcce76676d2dcde36e58ce06bdab9a99d3475f53baccd6f5bbd4fbb36b7b7eff0a6b3f46d6a6fca0da1deb7ad2f7de3d23604a9556d3efdb66d692b7d87b5b4cd1981aef4eda4fe32205997bc24da799b9ff6fd0e542e085f5bae67bb34e93b708749535b9bda99f635e58c33324a4b0b55b02903baf84fa665d6a138252f84bc8cfc85a86879d372474b9996ef38c59752ca2b8df49268ab049beaaaee914d254c8951314608a7be5b8939f5a764c4cb63997c63d7657638f5654470eaeb98e014f61e9ccaa1b113e1943676235a1a8b896a54e2548efe62fac369ecca658c92dfe1998d2736c51a1b45630c02291afba6bc0a4d3ddba5157b0ca7e231ecd89506f392e816786553067c8a216640b7ff646466349b428de665f88c539fcc4c07631ab0e90682ee2b9db19a43c308d894e8ba4d892eee15077c9c70626083579a4f5b5b40087368cececb342546c9382028045030630a258c71d4832b2bdd6016d5c7e1141f75c3296d4d4146f385e01437bf47c73834bfb45ea9b53585958eda82546c2aa4874d89e8f8dba664e8421a5a845114032b8c60a5dcb6579aef66eeb61b9f318a8fd836e586b8bcef7d3d53091319e57d3e9074194e6d879fe114b6619ae67dda49d7b0c8a88e53d8e16338a51d7ec3e6f6a631aaf4e586b8a14bde573ae9d8d42e4d1fd8dab7655d68c2a6f636b549da54465dd5e69be9f5beebf2f01bdcbda63602f6d9f491bddea61b979ee8eb3abf0c722a61f61a39c5604af058c628cc833a3346f1e3fcb6f5d7f481addfd9318abf9c0ff5a457e2d45efb5015592c168bb58247690e6cd24a167051b4c10d4e0574fbd87d997cd665697e0c9cdb526cd1fc993815d0ed765c0f6ccadba6a5209863d3233a6d4931a5656b2b4a512bcc948dd167c3e920ea05ee4ea7dc02823a462ff37457013e484091295440a8cae7929eb922823a897a3d40adfff8abdd0a21ca94d656146bf49775fc0fadad28b074bc4c2cd0cfc828c91511d47188edeb5f505f5ec6d35485fc16391f127245043554eefae9f27e6c5fbfbc6da5895e92ae8a0d27c9fe83e7dd6962c1aefed874a7e3549490cf1e7c94d7107cf81d564e9d7ef0e1e1f5c927818fbfae679ce2be5a9bee74b1d7cb18151fbfbda47a803aea053665f892710a9e1fafba5c1141716e2b0729b029a7b8a473d39a2e76a9b717e514973825677c51ef74b2cde7c319af9c8f9e95535b5158696d9e706acbc9c5929cda8ab2452ba7387e9f716a7bcf9c8f2db5723fa2a78c5a9e4171d7089bd24441a5bfae4b379cfa7a34fc87eac1a978f81d9cc21abe83510cd17087a11ab2aa83351a399584bf8fe1141f5e59a5b006421865548951fb7865d4fecbf6a5bdb24a57dafa8094d62fc757ee073f1e083f7a91515be2146bcb0a579a6f59cd9aa4d39610975e56eb3756e934d22581d7fe96d55f6b2b0442bdbd5fee8885a2d5fb9615afd389d8fa3883bcb41e4ed59c2ffe0484d4d1eb7969bd96d137fd6997fee0595be7164b3a9be8f6dbca04d02fd7f39eaea127c3a8fdb2f8caa60ce8f8e7148a535f94e1143c4d0605977dc3fe66607fa5feb286878f9ca2815e929b9f1b0874da82c28bd6161455f4876afe005a5baca356ce878feeeb76b8eb4a9ce2943cfc26a7369cdb9bd674db172cc9797d391ff0ba724504f5b52d0744fef23e79f98d391f9151f1ca057969f92d96e0dcde60e99af2cb29a3be98de6f9b7297f705f57538bf4c1ec2f9c5bebec31b9c9f021a7e530ef3be271a3b9c9fb61a5ece4f19f54413268c1069f84f19f50969f84f5e19f511d1f03b504e6d6cf6c02e5dba70697938bf1e1dbf6d09ce7839b5953b02eb6bf6602dbd6f1b7a3ddba54b972efd05357c9c19a3b6d4fb9b561d41d74629369d0ce8b4c5c2d299cc50eb3b4e698b75a5f53331ad571ad51fbabd5eb56d534ee9f6f61239c5d76fbb6d491526015caa6094368c87e733e7833d5d98047099c228ed3d9c1b2fb7572e7a719325e594f697b57e83259edbcced97cb1805af1c11da7bce077f87570b9b36f397d90bda05b2ed77fae8bed8fc6c9f650a55ca355fd7fbafebc829feae1270e132fc0e2f8410c23d84cbfcacd93b05e1c7f302d9b3f7f121ef3261539aef1a806ed39a4e0faf9c0ffd723f9e60b15850b47acaa82f6bfec6a5c8294ead328abfc3704646f1515dbc0ca7b43b4e29a3bed87a7e0ca7e0f9329cdae62be703c6e9040ffd2428eb7dd6eb9d82ece3f7d1f3d9869e723ed6fb5669d84bf2561d417077099bb6d27c5df3b78c535f0e5315ddfe2b35f7d0ede1f5ca1db14f02bd6c276caea4394ef6a89d2899b62ded8c5ddad23d583b61832b995e228bd5fb1ddeb8a49ab3ffe023a3f419a7be6d78fd0667c6283d4d07af576e0865941e7a19a3f43bbc7169277749739c6cab9db87acfe734c7c9b2ba0701bde72f7744c7ea7d8953da7ce5802cabf73bbcc33bbcc33a95517aa5612f49d7aa238897afb0296fba85a0dbeb95c4a541158218907e4c5198581c1ab8357069e0b870667059e08eb832382d7049e0ae706770697050e09ec039813b028785530287048e099c185c13b82590b6705e2081814383e302c9091c1538296c3cd876b0ad6053c156c596820d05db09b6295b0c36a2ad059b159b94cd045b0936126c546c23d844b0c160bb627bc1c6c5b6c57683cd069b161b952d8bad061b0d362cb6196c32d85c400a01490d242ea435486b20a58124065218485c20a141da02290d9217a42a242e484220b580c402920a48566864d0caa045419b827605f3029e41aa02f605cd06241090a8d0bcd08690919161c9989035211b23730297814b808511873232c429b0326067906190668061c0c0c8c600ad6042c080b0318054322d510a1886d4a269c149882c6012f00ab0a22802d2195917e215da0db0236c0d641a9a11e90a3c01c928e3824403eb02db81dc02b78075417a61938071016a21e3a2110166c15d220ca20c3835983eb03e58202c11e4172418b82d300570044e44160c0106062c0c581ad8162884990153036e2159214541c222c5005120cba051d180008900bd209d219b8296038d0b0d0790055a156c08da16100cf8052c02090d70082436401d603a9064d821904e9099c17d0156a161c18200a9d800e605cc07a4303436686dc07a80fd407221cbc26d01b65809bc85c3c069b017580b490b5c174849d82ff016cc05a7e59202e381544376418e8164041212641b241b2e10c835e40d30316068c0c8b047d7142429d91a58134825d82c6c19320a5216b0312c13a40d368c0563bfd81637c6e6608bf607bb8315c2e26083403ac262b13258182c11d765694012ea00078a58a0021ea0c00490f8b1fa1230e4014604594023e0000680c2a3874e4e0154041062004108e00900e8f0c12b829fb06deb4a96c690e7c9910b1469728487892245b6854b2ce1295284a4c5e0490f1ad1131f9e251768e201341a0c9407ed0a7d72ed0534da14e5f141a2a4c893254b9838698287013d682ed025ac501d5a0bf4c912489ab40398606009101e9f2319e051c2a3001a6d880536da0a6e34152851724409254a8e24a00a267c32b08447490a948707034598f814f161220348783650a3a1409f284172c4876789223c3c18f0413b8132c1c012244538e084a7899e22489ab4cf121e0d70e088906b52944789254c96143952044993e65162091306f82c698289124ab40e9a0914034c98386952a40912269e38e159b201264538c093c407ad04ba04cf920c2079b2a489224a30710249133d3c457c9c3069c2e3a4880f120d5ca08927381a09740924457c789e386942891c342a32d0648925479c0cb1833682259828c1b3040f9a08f4090778921479f284033c41963041f2e409cf03a616027dc283c4078913264cf83471e48626a44b9820f139e2a489258af83051f2a40892267dc449138d4303816ea0089322489ab4139e244e9844600992234c2881010c1c31b52994c7e7c81248941c71a2084f133c4d3460c8123c19c8c091213f6852e893244af030a144147de2b384c7090f134b905c8b6209120c9cb40fa812489e2c41e2c4e7099e221c50d2a4c991224e78965082a489551225982859cd6850a8068a1cf139b2c4111f1e236034560790283982c407c9122438b22e9a4409264a2e5084c7a70925789ee0b191b5419d304940499747c9058ef82c690203457c94e07182648925488a14c9c1075218b04dc348032b0c2e6ab4066dda62b17a83600e174381a1b04d168b180e0c071c180e1c3870c829c0cb0bbf70aa515595a1c6a81aa5d4c81aa52a26a5665a8c58bc60bc6098e5a5455ea932e67a61ce5e3846853146a952aa54185923ebd05435de9092256b0aa594528b9119421c293528194a2963c43472849a4ae6922ac50c6122c7c8aa302a471839b22ac7c8aa0ac311008623b18c514d316a548e1ca3cac82aa5941153de368e9a26554a556593100360c89299559539c912728c91a594ac524a662d6a94aa1a131313b56d8b71dbe2b671db6ddb6de3c66ddb2deeee6edb6e8cbb5bdcb81bb7dd8d1b63dc08635435993446780a82871b2792a63046cdf46219f58a1a21c313332b10ac6a8af126c6a85dd7c51c3972a876114615460e46599521abaa4921b36ad4a81aa364a952a534718c1a356a91996f9058d3342d6a8c517521ab4c8c5083ac3187498e870038582acd4725a92a8425084b1042084ba51b7ca3542ac11294b9c11ae30d374069236f9456468983943ba2c6c8913dd9e9f04cf31c358d52a5aa461ca242a81aa34a195555658c9199638491798831b2aac618a3fc61074d63a9ed623b0400e3648d9498948c7110d3a44471e428a5949279c3a4547931843090218cac9263bc22c42447a9914b03b70608a3cc01274c34c0a3812b53c2e3048f92224f1cd1000f3e648089922219588224031960a224071f747892c396238912472ea0440913270100c0111e273972e04952c4e7099e253bf06420071b4f7c9e207982e7f251e2c89325479cf814f141a2010d3041f5a04120c1008f93265ee7e4088f12254e3a2cb184a749932345783270c409139f27ab40686007e5e1718281224e98f81c5182891225477250203480c46749134ebc0e033e4de8d029104ff03051c4670912274d8a28f1048906666834081f264c9c145982c709cf124e9814491120081e244e7c0220444a8727986cc06787234c3cd921c9112594e04992224010b7a140e0040144912078921c517281221958828489274798f8e5e3e488cf131f8270f2a4079206b104cf93273c4e9a68808912459cf038c144091e27458e3c5942099e22402449112088264af03c51654c28e173e402459878826409120ca066388001244e789cd0e1c4121e78830251640913244f9cf0385184c7e788139e253c19285200200840802096e029c2c4a709079c2851c4e7099e1e9c58c2033d120e182806012d101050d005141424818282302341404041170694014123411bf4622468838080a011a0c58c006d5010105050664481808282828236231a041484195120203612b4414041d00810d0aa11a005020a8246340808080808888d2810d01ad1a0cb8802ad91a005020a0a8a468256818036688d006d903412b46a2468558d042d0745231a1414048d046dd01ad1a035a20385b0aaba1a7872440358f84a1c6a412a1d3fd6128f5a7500c5a0655886ed06aa78a5df5ea8f4da508ec84ff6ee240c86fbf6cd8362e3dc75856d2b18b691f151c368e93db605c70d8f30988d73d937ee1bb7316bb1c09abd0da313e4d27b8d859888a5f0d0c6d3893cb765b7711b1e149867d715772227246cfcc66f7850b0dbb8aeb60fa10d637e03e631578e08f61b429b94de7373a8f731f3743d6815f34d4e27fc27db3d4e7a88bc5c3b11d24d87ef41e2677be92f47e2877b77d2b7e588c03cfb7244b0dbb89028d96d949edd86778abf61a93c016bf63093758a4bd47b1c5b2566aff4fec63c5d5bb4ad5ed2eca60cac71d2f364bba7c701bacaee005dd9780f911312a4bffcc583b29d745d65277242e2050af797ebcac687509863df60ae1c11eed8796edb980a6b60e6891fb4cabe45ce01ba8a42fa0b12db49678e08f79773444a275d39229df7e29de04d58b87958b32fcd6c2aac21cd13f484281340f38cb2b9a583560a6b3ed5d27ba02e4872c6c5a6bc3f1c126dd7e57dd957eaebba948a66d88c377dbdbb8c0eb8f4f599eb925ec6a8f8d5a283315e692263b1ddb4e2b4aa3e832a202884e18b276439a30c29acf41d6a8755da5224aa14e5a00859cbf4f6927d38dba5b6bf5c5c4a685a3ba6cd4c75d0c5d6560e84d050f56de661580e7ed018866117156d5d52ca1cf840a64f83ca4177b5b67250a5bff7c227dc40da208b2c6eb2c8e20647c86e0046d4d1daba010c6e8082f838b3b49c9b721026d1694b0b2cfd959aa3e6c8f3a5e6e82aa117020142952a54a80af5ac0d9fb029a7629452ca281f639497761d8390f9db8ed7258140a9fc4ffefa0eec819d84af0ffe4910f438285bb8854df946bb291d0da7b0ef519f47c3a83da3c058431aa210861a94400236d8dc608aec06258897a7e4a6310cc3142303106260822ed00083129c61871a320ddc69502cf8a12afb5ebb5f394a411756b480061ff8c1ea87c6de6969acb1db200c8d7d8655d70a3b0da76263474195ae30ef5372d39747c3824efbfbb27e1a54c94a7fbf91318891e9b41082c631325fd79d4a2daa6c98a7850d2eee05b18ceda6b5475c7d6aa5f51a372e6bd3213a6dd9c0a8615a5b5a1cb5722ee8e4772ff329a169ecd7d78b181b78af2cb2ccbbbc6fb3d557eaeb6a51e9d2d723abe0ea7a0c5fbfe6685fdea7842666059d767bf5ba982983830e87d6961653746c6d69f181569af592dc8c0d4a69fea60a93316a07a3fea1224cfca7ade69fae6bcb075eb4bc108e61d4e9f27e705fd7560f5a1d3f840854c1168bc58262c5df21a2d5fc2238059bb3306abe9227a4ac3e3da3a34456165afad32daa28262c5c688b8a507fba46f3af99002e69cc049c210ba3b4e34cc0198e18a57dc3295ea1e607a06bfaeb91002e5b18a5bdc3706aab479305e6753cc3288649001aa860d4d7356752600fb6065e6431d45fd632596840c637b3bdec0cbaefcdd859938f9b36e30e75a46b93bec73820d849de8761570e26b92e4999f64d723eb422827a1788f6f53e2d097cd4be1cbc36935cdadcceb01b6d631419b6e426bb86ddb476f94d932de832a85a9696794b6e1af392c45f59ef96c3c4023897d07412ecd9f97afca784a6b36bdfb29959d161d796b115aab4c5cf8e7002f396d0f4e52589875e972d4b817b1ab87021841042b80cad0017ae0a9ca0e4a661d6b081e0b041146e70f3c55e15745fd7cc1c5b094ddf871bc84cd118cee87d9c4a6862efe366516ea10727c667c8f9e0dd336c394c557caaaa51e37681f3be1b2331bc8fbb724a6e7ac94d27299df4dd7c25efbbe9d2b19d1fcd8d7f4ff4aecac6d5fb94c0fc33d2fb31d1fb6ef66014ffc614826b6c780a6bb66f87998735df9ba391317d5973dc3ba88279391f6632e7e3c553ef2b1dfb0e34cdaf6bcefb4a53fbebde1f697e421406bd70c401866118866118a6fa035ba2469d047b86cd4f8d70fae59fcf135fd6377cbd8f09987f4574ff84d8f83704c73f2231ff2860e4c6bf255a3df6b2c6d141d50d9b0d1c328cc2cec731631885fd8679aec1be4d14a3b0dbf8693875e37c14a76c9c1f33e38d596214761bb36314769879c328ecdd148251d85fa6021885dd34896014766e16c128eca529446db02b54e1700d869df4e57e70634228c1896169b4f1374af765508fe45cefd3a396a62a3ab5d29fcc8aa0cb6e449ebf52380b8a8c8b21a356b01e2a21b8461ecbb2ae9391f9519f1e35df304aa2b68abebc2cb44bafa479dac21876b129cd77d32c31ef4335a6f3eb54a67f3efd43ba41c74dd20d3aeda6898235fc0caa4c678e69eca52bab4a5c83a5b06f579e69eca6a9b006fb723e382f6314f61d589af393e9f5be12697e3eb1e3350ff34a2566216cbbcc10c6282fcec7769574629c0f990f74ea6d149d7ad79a34e3b804b74892da55ca324669cce0257c12e975ea6dd6aaaa98e97ba2636719a3545b3b168bc55ae926458c70c3f65ab591f107c6584ac57f1042a033f717378c02342c25341d7fed96958c1c0ad9f118e723be0421848a5d32425ecd4caa36fa261d097ca2fc300cc3e2f5f940990d05db6ac642ea7d44b4e280405587b0438a6e8f8aff2084aa361faa357a5f90171d0fe157a8db43256aa5e39600c618b35600ba2b9686a47f3dbe1d9a43446df6dcbf529bfec5c07f34ad5d2f04ab50b006be7455f94d9bdb9c3df33605ddd7795fd61f6abb69d2b317d48e9b4d094d900e44c1065190d24a6ec8789945306a9f4d2cbda60ed62c47dad2dce67070a0aa3bfc4e3715d694480a55315c030f1fa3dc1705dd87d3b074f81b9c1e0ae05476782238c5a46fe7bc6fa6e47d6fb811a15df294e034e9900bf29304146051a595a85193223671160bc7d70fb46db5fc0ea396ff50ad32ac2202d648150eac91d72ba9d5c565fd908ad09180d029b3e7b3ed8333a22d3895795954e15411a74ebc54b4bc7676f92650d5c11a79798552b77499f7bd3fdde245dd42ba41274f0411cd91d27d7ad4f24696e09486b19af8708a3f25a8de9f22be4cf33e25a37d609e4fdc2ed4463e4ece26e906dd0ef1bfccfbba8635eb7d32bd27e83eddd2f2df0eb57c139f0fe789a5b2449cdac7214e5d53160b4ec5cbe3f4e05496d2ceb4af12b512c40334788296a356a247333636635f31acd295fc0cab56bb962ba88297dbc258b0eda8cd4ccb5080660a25342d650f7d4c0774fec6b854acfc958662ff208450622aa5bcd94173a0eb8c49a936f110ca6cc6296deecb5a08b5f1a417a29432aa7e26861992209680beecbc17ff3ab3e7137bcfff4addad185b89517b9e5b04a3f8087a3bbf9bdedddd0fd53a95dcf42e19587cec308abfdea746bd572b8b93ad5143a9579a3fb58213e7a7448f1a7e891e7512f6e0106ef82b2d76bd9c32467e53ce87a6c952cba85cc6a8e308b17a9998eff211c3a8f77579726eb7458e71559aa2daf0699a5906473e61db363d4f213eb5d214689ad6fff874936fe63b95d0b4f668fe709ae37a1f4eefafef407d6f9df171b94a70faa6f9dbd157a2567acf5e8428888750f5f2df75e92f7d924bf550150275fbb85574fbcdb249a92aaff249e2397e3eb0e5e38c8cfc17637c94de67a4e5e3fcdef10b45b78f3eb2a5ec62fc276512193d281762b129a73ed81b333343860c828da332eaaa39b03f7dd4d8aa29f88fb99338e1865dbae8c47f10726f27814eb6398876e9d2aa9a03fff1e3dc786623e87c3808025a83e0c22a68f08321b477777721dc599a443a6d5db1450c9a48a7ad177069fed6daba6205ac67adb57505099a53bc6178e1e8f56c178e62bbacb8509b246cbb134208610e6ac370c6308aaf105eb9f564d486bf53029db684c4e82e07cdd1ded2caa817a8d1aa6a033de5186c0ae84c5b6beb0aa1d61c608d5e390522c669a44b12ef437fad2d29b650cdf96e8264cbcbec5ff43ea08eff3423540b2dbd2c32799dd9e5448247195f7459e9e3fc7e3a7ed795044d69ec1f50cbabeaa8f2f8b4150211b4cf36e62dd133faf2b2f8d103411d6c59651eebfb69e97df19fd6f03b304e1ffaca25a14d90b2d2fbd0401b24999fb0f56cf351e045972ecff3c9369f3922b4352f899c1f3b59b591f7c9ba47bbf476e67e944e3a6993ac6920be84822dba74eddb264b4cbac65c9008447b69fbceedda2c1d7241b2ef7aaa36fbb1b76ab3d7becd8f3527b2ee816d60f96cf3f741d7f217f6d78fe8e27fe84d9174f1d74fa0465fc750428810e500e1e9fa695046983841151d66d2907207a7beeb28dda4668744948818e194d42b45706a5bfe068753da9292a565bc89395c6979e5540efde9192dbf7149b3268d9df53e57330da7e49df072418a34f66cf6f0d083805e6e01413bd4dbb48b745dff2d769e9987cd6b7e417d25f9b0eb2f0e08e661c7bc0fcae57ddbcc3fa778c26fcbf0fc207b7941f62cc329ec320197bea6bc2ebf50f4975d5b526cd1c56c901b22fb0705c8fef23e86a2b36fcaacc67ecd0c9b3bf082a297e1162d04dbd7a355f572eefa184e91aefa99d3702abb1ec5a9eb414d9adcb319b46ddb77b8344fd9350d0c753cf74d995cdfa874fcf54d35d069cb04451dbfc56f5bd096ae69dbb7ddde037bfbf624a720d865c44c5810cc846dcfbe69a46b6b24ef4776ed9a968e27dda8631279cd2b3d9bdb1ae47e904824929744fe8b7f1274c493a02e3dc9f523e231efbb5ef2b4efc0d6325952aee47d3f5dfa35bfd8d75f26e4295481d85594974fa10ac4f55dedc0d2dc64e99af02f5357d90ca27949b8c77357ce8729051d77786dbfbc8ffbe69db6639741e239efb47941b25fcfb66b928e25c9322c48866540e235efbbbe7ddbe657fa49bbf6f66b9e34ef87fc765ddb04573a7efbc6a524a4ef7ae91b2c9166e9df76e9f2c7b66d87d7bc9eeda21d1e11bf791fe9d7b7ed6b7b5b5b0ad1232bad0e43d17a93e668eb6f70803688c4a5116cbac16fbac192724340efdbe688d056dd01499aa3fdd37a83d83786ad0c61111777443cf4ae0b6e5809ce6f5b720bd8b09236b949735b7245041501e38ee02de318b0d3db02828ad0db375289f46349f70075760d488987ce328e014144bd71430411758f91debe652512b7802013f4f66d2b691c901bddd2db37aec419c1df4b675f69fa80a490bedf4a2512c9fbb435993e20a2c6be4dd51ddff67d893b82bf79a4a95c8934b52f0e081ff34c1f5053c1a5b16f6b737bcb4acc01893db093c0cb2f77655e1332ed23d398a76af3c143f823a8a59764bf8ff314e4e340f3b5af136869d2c5122aaa50c271e3b9dfe43cc92bb3fc5ef324bf8da98489f7a1e1b1eb9467bdf4d20bdbebb587597a271f9bd777e035656f72eeb765a2dbf3e4928425194b529678eee51e723e32d0ed37c9fdd02c178632ad27e74db37a1da37c6818742d64c2a64f7c3ace416b4b05426886416b2b05531ad5da428118407ca2d8d25f9017577f4d9ce008ad7ff1e92018ad2d2ba6e8b5d2da1a02a377b4b658e085e5862031e1a87b8cf4962bba07c8d49f4f16ac200383fe7ca8c060cb427f3e23c882ecefc9147af04310b0f4f7a4084b6bc662b56a97d6b79e53b0f56cc76f71cae849315a96d16b043c7b5f0b06471d1fbfe79da720b28c8e9765f4f724a8e3e5d4eed12e5dba7c41cd9747bddfb4019db6aef0a267cfe6a1e59465f47243dc091cebf97090371fce8d991b62bb74cf7691de09c81e7e883df47c366d5d91a57db8a5f7f19dd8860fead970ce669fadf54172790da2de17afe1f7fafebbae4588d75c46d2696b04527a1b027b768d74edf2a5f337d33c99aefd05b5e9a5f95d97d7f3d24ef8709bcedc3c99befdf22d96787edaf91bcfd3f6d8b0b7f3e4560ec8b2981bc2746ebebc344dbf262c7133f6164ba5a966686ec2de64a934750dcd4dee2df2aac4e2f5727e692a1b9a53b91c4da7201a46972ecd87fd0579c16295a666e96bf2bcfe32b9ef406e4a4dc2d70e8b082a8276e586d86b9ac6d71f7bfe7240f8da37d3dcbe6dc934350cbd71c934950cbdc19269aa1abdc5d235b79be613ac9de0ce8093eded128adebe5d1c13dd76e532d045ee8766e9ed9b4ab81b79f7d261da6e9a377d4db5c2fa4093ce1b77559b8c7b89f412a944fa369d9438ee7a89b45d244f7507c9cbbec39a9ea7b65ec248af6354f4bce616eb624a5146eb727ca59551ac36d81ebb72cc3b33b5c1ce530212c0ae9ae6f8e063d833cda9c23684f6cb379d0a46eb0c43466d78af737b39b57929a9cdcb7912e94ce75efa76221de9dab36327d25d975fd62f7f794973f82fe73447fbe53c5fa2b0c16f70be00b98680bfbec3cc6967ce0eb5671066da766587d389ed2cfbb5cad004e1a1f7dee1cc4b921dbb7246649af705b5b6da4f5f87198430fb0eecf969de67dc039b33fe0e1fe9f8bbbde8e3097e0fcff3040f27113badbae3c3bee7d26287531b3b9639f12935e9ecca0131d2d9331d029b5ff624f0d8b76c7a4e7a740b8bd53e369a74087d4a4d7a760dd04af3e12a1b453912d22285b954812c4e7d5c0667e1941ea1f9bc85537b23f4c767a4c16a58e98f86c3e80f4631223697a13955b808aab8f3b90555ddf97c05aa62a603a0c80fd91eafabcbfb91dab264e9f2f17afc10985fdfb69bbc6d6dde4fcc3ec4f47829bf7dc88dcb97a4dcf0d2356f593f5c0cc1f1eb3047da616ec35b2cb0465730decf45f27e32ef67733224bef421d9e34d8ac5c64d7ff176600d174360fef21b44d76ffc066fa5e0d8221fe3fddcf07e5ebc1f93e9db7908e9a5677a94fde5363c21b0860fe3fd5489efbc9f97cdfbf9f6d24d5ef7c3c510d239efa7e4fd6cde610d9fe435f18c6c1558c3bfbc25f2760bace16bde1ec11afe0e63f306d6b4b01cc96c59d64a59a2ae8a6c5d58f42883352baf0e8bd755c652a02aca0c4115d61141d59551812ae9458912450867151f056c036b2584c2d660b1564232abf8285eb8b058ac9550b68a8f12250a1eb664abf828107024b38a8f1285769902b58a9f0267153dd51a21a682536f3e47e194422cad3a806e04319bacd8182eecb23cf45bd41d4013a4ea611bbabd62b3af14cd79e94c76f6b56275b467df29abb397f1418783ceaefb6d0d32e601e2883192d5d0dd60357436580d5d0daba183d9e9c4f2c8ae46c3fe94cba76710e2d456c12905e309d615323ae3d2a5fe564a4c93382358b6800558741b12ce4042962b74a55fcfbe7125564367a5cbae54d1505b3fc0e20a159d2d169ded94cece9c116b85aee444153ad3f2c88060d9e13846a7713e364b771dfb9ad1713e960a55302a93c2a8ec3b2ca5e84aff560ac69952d0bdbf9bce760add75713ed6887ba62b2a9d6d14ba6fa574f64ba893e766d4d2490e095e41812f3d7243e04b1e17a7c696d0fd0b1f44216b05430d311a721c85ee3a734728811dc7a0c672474ce18c8e6f5a1ed935c2ecbf95d2d94fac299665de2f15a89ad9927da5704a87b0e84c76e6c55ee1947676f46dabb36f154e69eb0862dcb058ac2b56df76d1d926e5085ee86fc5e8ecacad104e7d9d694876239cba9e81d19f6639a3b32c0877fbac93bdcf360d22691a89e47da5d69e314abb72a4a83624ed9a675a1dde23ee8a4d49439d86c183a10e830755b476c72955a80af5716d10d301c1fe2d532cad44b174d462a1d0edfcb897071fce0fa8e1e55d87d3675b0662aa65a17607a3968651bbdcd063b5591e7b3e949161d4eaf0bf1899a8830efeeb60b63cb8a147521b3074df4f83ba11028753fa45ef7bfcf069f0e2dde9968b049bd26c349cd2e61b70b85ebf6375f67a01ac0ebcfe6675e2f541ac8ebc5e88d5b9ae4fad0e763dceea64d7e7ac8e767d8fd5215d7f80d5d9aedf599dd2f5abd5e1ae57c0ea98aeffb13a2fd713b13addf50b581d98eb8b581d1baa4c013d18a59bba1113e3832e26c6eb18c5492c8ac8f2e0e334cbc01a7e96314e3373a1bbcc9450747b1c666443b7b731e3966e0f332315bafd0d537ea0db773362e9f62f338ed1ed4d3362a1db733392d1ed4b338ad1edb719c1d0ed49337ea1db6b3336a11343b7c766cc4217add0a1a1dbe398514b57854e0add5e6fb06461c9bace073ff0417f32458dbdd409a1cbba319ae00499acc342820d8bd5e11aacce52599dcc06aba36db13a241cacce5665754a3b581dae8bd531f94073b01f68ce8bd11011952a454658aeb48ca408b18cb6a8a8a8c8a8c8c8c8c8288b51963d32ca6294658f8c8c8c8c8c8cf6c8c868b91819156529322a2a2a2ada2323232396915191d11e6529322a2a2a2af2c114160bdb851d8391111fc544d98e783396c7ae634de84f49c466c441870d817919a3f60a6ac397de5a618c0e7e7760cf181d8442b73f62104c41c5175274018bb668f9c582555b046b547b05d6c04b28bf5eac0e1156672fbfadd5016375b4cbaf95d5215d7e8fb03adbe5f7caea942ebf4a581deef22bc6ea982ebf4dd01cecf23b86e6bc5c7e85a0ea0654c55c7aa62aba152a82aa27600d3c9716972b5c8ca08a8bcc162d190f41d50e690dbc4c958e0a51ff7abcfc2545d708d6c05f46f20c06a31b58036fc3fb3218efeb3a4f7a9f4c5fb142c7bf50d0c97f3edc451db7fcb547f324bdcdd26b4485aef3f80a1dffe5c58c9898a9ab18055319386dc92a95cad8363290a212b4e8851b3ef0c6d455dc822e8fcf673b63c210dabb06a3a4c7ac98862e66e986b870e1c2e5896e59325c86e010a3b2744b34858c1d7a02aa74c5ea156ab8fad604f18a1719aad273b2fd650d372150c5ace522c35c0374b55c600d14c228297444a437440854e9ca48b5687fba65b94866ed1a8c82672974df1a7d6bb4466b740355371cc7bf2ce6e6c64dc386d75475832704d6c0e3c86062ba1c6e78df6cd804cde1564387b593289f314a08546d1ab4061e0aa31034e4133484cb456de03bafc42808cf53e832c8eaf8062b189dc1c8e80c2ad506f59bce2fa87dc85f53285b5d977308b35640ad4ec4963f0159a296117a1fd0906e75e57cf22720a4965ecf4b472f8b4bbfac66f53515d6448666fc2262c135a01a70cbf6a825878e6bfda2075e4498049c218d083df63a8c34fa87d65618ad1fdce14d137a1faa4dcf61fee4f06d68e8743ac12a8c2a8255665a575a33309ab01861317de633de4f8e19188fb4407804a1e99f1c8a5ab8f706d1d119514bdcc2a92f12751a518d1d3ac99ce651376c06ce4ec4818f72e8b4c34fde298b1cd7d529ca9863122124aed17d514b0f316ab560510bf64f460b54e92aeb64a491daac113a493485510b46073d233218b51783517b8551eb4348d4d67b2b9534c2c09fe4e3a5961694de0f6e7909415514d8e2c2296d8131d47bb905aa6c5461b15850acf6d2481e714a5b56b0e8bd3c8353b1f7320d4ec1decb35583565b5bfa248e194f6feaa825551485bc0e8a2f732ca16a73e397485535f9c8285539f2ceafd5e5ae19414835308e8bd24835307e87d893302f65b39724c22464e6afda2684a73ab652b0b0f4e99451e310a7a9389d984cde8e26ddc7023668be96e20e1e86e6836e6132ce562e34676c306c6f990de0defebbc4fa62384bfda80058449001aba6094b611549d86ccdcbbe7fde4f88c77c2715dcd3c681573830d79e3366cfc869878c3c60d38e60a316a3f33578ab6847a2ff3dd34bcb61a87dfe07dd90dc7e17d1d8ecb789f4c4bce878c390ede97ddf80d1eacb1711c4310a6f58b2e6a4c2982aa1cf32424ca0ee5d8211ecec34ff033335e14ef33d795143a7c5e57df7a3f39aec33b39393580e60078123dfc1fa2e300f07e4ed73143731d4f82c6d3e141d92258b3a7b9901ebca5026b76e639fe2d95de19cfdb2b45394ef38d66fedefc095a79cf317f4ecf71e5b67738c784b23d78de6a5923285c0cd1f11c87e27de63d7850743cc769a6ae683ee39de07f5dfde4f8097a4e4e0df09ee349e838cd4f0d98798e0f99790eef870b08bd21394ee3fdfc397ef23ef324bce50285bd27a1c35b2eb0669f43573a3ef36fb9a8408737c4434233f7fef150efb799f9c3c5101df7bc9f5fc77f4e9f3997bca9e3cb69cf4c283cc345544e9ff14e2fd7155f29eabd69051dccbdcffce32bbdcf31d908d6ec754c85356c24e4f41174dd3def043f0fe5870b07e82ac7a7a7ab19ef04bd0728941cde49488ee7f0a41458b3469d43870f5dbf1ee37dd9f51b43d76d78439fccc384b2c3731c66f668974ef2726de584442db066bfc35918bad3348da0ebbc2c4e2f5e16a7ebeae49d1ca0abd397b5e2c0e927eff4005de9701d0e4587eb6a1faf4055ccb2ae8b577acf64e84cff2295de4722a8dae1fb6d8779121285b9fce4e9caa4820e040858e5788e47e1021e0ac19afdc75c7a77a0f70376e9d2f0a7ff688bc53a7953dcacb88059aa68785d7d50a8f7120e4d6885911ceadd61ea4a877982d7d5e932ca2c59ccc099098960cdfe34211558b3c7cce8e491cc02897a1fbb6c46501e79324b189d294ee194e9fb1cf3b0c6b43d87a92b6e7e4a50db55ca017af28851d2c8c7e6c92cd8189dd7f0f11226016888021fff7892a7795eefb0aa4ef618d9d4fac51a5acd1105cb639d6c7fd2e89a5fd65fd4d22b65796469741fdcd2fb2f4ae93ddc02d5e0147f0fb910adf691c529f87d8cc229214e19a0f7910acdd15ed9c5c33367d2fb32fed7a98c4a1ac1f70ecb5630585d344b23980470118351da2fad6098614a7fd2e83205c9704e0a7268fde2045898483f5447d95ebac273aa37ada0cb0e858b212f9787f2b271d1fb8967ee175f7a3fdd599a2e9fc4cbbb9f1a705d7ec87579c977d2f4eb49982e0fcabb27f1e275b2bbb9f9d579504cbffeddf40fbcaeebbaaeeb6a09d9b9622b42cf8862311dfec583623ac4be95e64fd0ea878b212f27793fa4bf78a5c7f9c3c5cb23e40e9f84e92f3f35201e7e483c8cf02ff0d400cec31e3d28cb8235d85f9e84c96b0249ff748b9495c282c2c510eef1504a27fdc583c23dfedb6dfe6c3f69075a05ad48f3870bd349de69f5137fd23c27a706941e9f0477d34f0d203d7e08e9d1fbe142fb0f9f74cd1b126ff27ef6d480d2491f42f2a06c1558839df4211cb6df12a9a0e46ddeb6600df66fab347613159daef8db49ffb62584349935590aacc12eb16fcc52b044d92d47b0067b9c51b07c7b140fb19b56d0695e8f76e924d9d534822e3b4b69ec5fa0004b77ad5fa0e0a875b55ce665717280aef83fda5a7160cf013e4948143e13b5ea00c282209384b6fd8c68741a650a01acf642365643f6d23bc10b0960c8f513f484c8d57242a2ecafcb796ac05e9ec85e7a420258c913b93e858d95bc7242648e8846a7570e0241ab7d3ceaf4d7bc990a6b96d045f1d265f5c3c590fd4f076be2f73ff15100790a6ba2aedad0b00d2b2ee0bfcb9ff8420258ad49059dae4eec09d94f6163b5dea90776e932858d5514b04b971517f01c864da14abb4d693e3deaecd90f3a2e82535fcc6688702afbb377761eb2cf64e7ec317b96bdcbae349997448ff6099bd26425e8484338f5953a8f66094e7d3d30c1a90f004a38f5f9d0dbb7a36096759d8ccca27abb0f9a43fa7600688ef6ed3d684ef6ed349a837dbbb73ad7b7935687bf6defa02a7ebb0c54c96f579a8da5386af82d9321411e74990c9619a536ebd5288fcd93f9a1e13f1294f1bed89c318a15461995314a33a28ee70687ac883d308ab205f28a726148c0b22064da09349212485b11b6212b4a2b302a715638d3114c2f61bc743ae8605c0063a306366e14dd8831420c0e1fe0b8010737e060021c64c090c9810639e420418e530c4e3343333aec40871d66b0030f40e0e143f87cc11cb2c2a3c2c81bb242470a8c74d048a1e921090a883d185101005eec484101444800f86045114cc6e603aab5832a2954317f23cded3728a8da5922a87a7400af364f06d6f02abbd25d1163b170eacb88a4c74cb444abb33a574d00bee8848c7056a66fb04841e606f52d5d1600232afc4084a1381353e2e5f183f28e2c3a1ca8dab982c272f3e1f427238528432b745e2a58159908cecd56915de93e19994f864bc3af1486a08a74991d3657ba7879c5e5b136465410c022e1a46fd7bebf96c75e9ba56c79ecb1794d569bbd9c506dd466bf4dd25458b30980870a78dc5c6104d5bb19dc6e8ca800040a5671729081e9d59a4feb9541dd2816997f03d0d97f40b4fe50b55d515b456de0776875d65b1d280f78f80caa366f89600d3c5c1e58d1d05ba2204a67ae97486de07738eba2fb96a8e129c0a96f8d76844055c735ab8a2c0355508dfa5331b2aceb3e99fe64faadab130fbf4650051f272f8fdd238c570a356c305b320a421650a00255981611c8804b4b6194179669a4adc4995e3a182c2450c3c68d181c37e02093438ed3d11958333aec0e60cc600ccbc317688081f5e901410c2c1d429ca10c2d9a92425500cca002560f008869b8c2c8878d0c425358286dca0aa068d590ba60a4820d80145b58f1c336450d66c0da31061631d8281625706909800b10583ca608a18b9b52d6da6d409414aa20165a78011a5a2b7dd6698e0271c34300363b7e08400dca0700f440a3c39be761071d664e397290c1e1061c31376cc0742f26aeb491b42c08d317d4252c7e50456b145a5e234c0ca34e0c7f62ef073776c94055c7357af9cb0bb173d3a47f382db986ffa99596fc6ccad4f0b10b62592ff0a6ad7153debe2ca007c4859e8b244af87b257af696f41e8883967f5c18865d4c5a6bdc400d2d2003952d383bb042dbc10c72903132d8bfaec33a8c0834da108523cc60065ac650e9600da51d80e08616cbbaa1b5d5832bfaeb0ebb707149292f29a6181929a5ec4194968fac2aade46358fe524ab9553a536bab8b3368cb0b5d6c81b5659ce26c1f57773788779b904aa59e40a10b683822431757c820a55b5c59762cbbe619458686794a6e3acbb2ecd9954dd32e0dcbb22a55e82cab0285ce76956519e66971d3c5193830a1bb21642cb9b045500d6670023654c1f2c20b5606a08326683a28636af18311b4105c3a1002a90b1d5c21b3850e7860d2a2b381765dd7016c2086359270c510984045103a3001a78329262daaacc1a4831068711ad44b6bab0b19f4f79b4b082fad2d1ea8a15b60d7654cc7a85d030ec64882189c10a5058415107dbdd3425f55d0c0213d25372dc3a8d562635a5b5d94a0b7d65617510461cb56d894e68b589abf3ce83ee57f4ce508aa2e2dbbe55a2cb0e6f2a05c2ca38bcb91d04a79828ad00d54618fff70e42acec39a0b8b9c2724e2e5a507257b5ce96adb12e643c8956ec04d738c117b163179be0c54c5c8046071692a44cd2724e4af4b2c2724e2afc75fbb5258d3c11a7e9cec4519c48cb1800767b4b676e045dbb4b678e083e62a9a93a94d7c8c471ddf005ee1c09af81dde1e741f57e9f82feb78ae0255520d5c88582c160b8a553ce4025550f82b04558c5df2318f05749df74750b58f8f2ca88222a54095f47eb84062c3d00325b056d9b1ff005d97cf300fe8f2b8808702243d16ccf2852a582bbe16a832620dc12a46700b54ed2a3e0a41150f261cb156f1b14abc520455db8332b2305ac5732b4a2982447c0455ba0a546452a820c4a5099a0e6b0a6b0871eacb8cfad3841ab24826d2af953d2bbd1ecdb8acc12912c9f3ae341805332ed996eb049d1678cd0b4e917e8a9f422382aa53f484f085bee2eb0a7e93591805b5c74752578aa46f51d348d2bb2ec965468c8a30c64b4669329988381f42bc3f6323d275311729990b6b612d5a9be4755d9aa66997469a59966c87b32393f73dd1a6ac49dafc9e68ed47610de90a6b2eed3b9034bb4bd3f4812de58db2903c1f7802f272f9974b53159d76134923cdeeca69ddcb33ede5d9cbb3acaf995121672625138351bd050e6b4b3621841042d39599ae0c0b1add97095dd1bcd0f5702851a678149aeb4ae8ab1e3c58d47dd95011152b28f04a910a683c6804af5001757853ec58e9f8078d1afaa0206645f32842312b1dde295ec7a1e8b8aea6d8b1a2f14ed17b808eebf04e42745c8717afd0412d0da97427dd34bf6d531833b0d230375d836bc8402e0da9b884b4a97db5295c03afdda4822e8335dabd3874bdcc1ed36d5cb923606eba483013b3619ada1dc9cbb66014ccb8741f4625a382856c281bcaa66443d9948c8851309b921189211b1a80143132a1ccf4f84c28a322bbfe2d5e574e5e4468e748e58514afc42bb1281699482f3b6c326dc1d260541058bce07285eec38ab47069a8a5e1819032852a9891138e3aa386e742e745c3dc54c1a2e15e59ad0b1d0b76e922d1581e571a6a5be8241ad85051c3c35c7b472319a3865aa48c5a2ed34957121792ae325843fa17b9348c11d055f7050975fcb2a4100555d91162a4156ec70508d62fa4cefbb65fbc1e3579b1d411265e3ba3d3d614c8e8ef126a784d0bdd8751c1a840955604358cca165418051fbd588ad7b5242f0fdb6256615416ba0fa3c2e58a152f8a4cd7bdae0ee9dea3f745465ddfe28c3de8e2af8b347579c87bf324c49b6207a0ab52cbdbb8697e3e0c3923a47751591ef0305717ddbcb660147cf1ae2a8c8262e83ead08db8251f03013abc2286da8a888c7d0ed15854ac36b4550b591f44b53a3d3306460a52f2a5638c50d7f90820d41218ff44f0a35a4727d924ac321681aea683c102060e5dd7b14a1af747c8aaf408080958e47a849215e26e3fcbcf9e35d614dd6c9bc267e075e5cfaf28ec03abe07ebcbfb9634b117cd44d25e805abe98a48c1e46c428288fb13653dcb6687a04ea0d9bc228b87db921e2b76b435438b5958838157f6c88535338257ff9c00642040c12a3771d310afecac228f89aa32944c184b42a74df85093536c5f280af21323a039ba2fbc6d8145fc058714a79d316e78b7c34bdcc9e9feeae1c90ee3bdcfde2c2a9eef0d71a9c3299ba2b0d46c18e0b54759d776d6114fc75a1aa1859b9a49631ccd022af6f5cf2a18a1195cbc8090d8daa68edd7b7585a2e88e93b7c1d71ea3afcf6f9c0befe72eeab93511df71e658aaf7478425f799274395f7ead24cd9e6de9f5e8659485532f2f9e0fecf8eb32baae4ccd0add775d696801af4f42bc7b9efac4be7e89c11971799a776161147c51aba8e16397cebba6300a8b34b5a0bbbcaf495f07c27d855362704a1e5e9e4fec78054a3496073c00a41851710d61d1a834c4e222ba884cff7290fed9e878d5fe915aaa746b2eafe3af298cba8818051fb7e8e2bf6b68886be822d282b03c20697e5d5f7d794e622f8f2d9d3604551795acbba87017bafd775169f86b08aa7e11d16ce17209ad762905315a7b3c65a1e3baf22eafdc11dae3a5634a6f4a2cb0061e33a3d35a9a17f24ac3731548458cd25a8c8227799a17f11b6f6a41f47ce0257451a169351d371796b64922fdbaaec778ee9a5f7c163bde0421736b4fb6556d34ef8bcfce7d0746d3a7fd495067377949b26b67ce07173ff94c4ed18634ed8aa6d1b0ca302acd6dedda26d1b505a7740cf9ec5146c90191dcdcceb6b71a2e1a11e50e6fdcf2dae3fcba8e9a33446a8135f03fba05d6c04cebd286d65abbbc9ec52ee51a978e32ac5c5438f5492e179526c42874fb4f72691dac2db66c81aa2bddde2b438a960368812a2e4ed2486681da95865a5143ed0aa73e4dc30255b3a515714a5b4756c4e0d4199cda023f4d4bc36b4652d0d250cbd2300a46fd696a34fc15b5214e7d120d6d0aa7148c28d1581d128b075e194c61357418af8e6e0731a36ebd1846c17319ddbe24024695a2ea152ecdfc838e198d6dc12cdbbddaec3e665d8ccc7e86992564788508ad59dea3f80557695e2edd0861142b80516c446df81b0ece1350b5dd5445a75c7cb08e1702a7a88bff6e6e70168d0ed3b2287d878918c51b53c1825166f0570842b8425927f5a88da80d1add8e028810328409cda780c615d21ad65abf68c3193a7a5f8773036564bc1f6faea231e6056e8c5bcd6722225edefd6507aa4e42a2c0fcc67fcd50387a46ab9e029ce2fea9d1aa98e5b33a1be97a265815b1f971635a9c5b6c785363d715e3c77dfde3ce96c7e57dd8e333efc3ae9966cf4f6712f37a30eeccf9d030cf679b9b8b05774473df1672cc82aa92f76537264b99366c783faab0a6ab89c2c2b4ac6cfcb0868a1b58c33791a0d35507482b1b9f22b5b2f1efa65fde00336061065f00030958582b5d75426c1c66ea6adb124c1630dfe1a31d1b93083362ec72745da12684341da0c58b56ba8f599b795f07bdc85518c52da86228f87c6e4195ae328e596ac35cc45d30aa4a9cf0fb323befa7fb8b4704d6f07f5efec304ace11ba91698eb8aa5c09a5d42874344854e571f0c3c471182292acab06f178c2aaa5294651e0b45333a5d1e319b9b85e5c1dc0ff9e87dfc83eb3de22cac4e76e6b6f544363bb057d60aa360154e5d1e8497f365eaaaab5244054761146b3fa0362c662d152a3af09fcf62414507444454845604995096f117ba6d2d116b9675dd6e69de3418c57b06a3788f18c543101eaa5ea2f729a982514336e8e2a5306a88517c5dbd784c05d630278459b086f7880555db8235fc2cebba1683a1fbb6c5d0fbb2ae5396d2dce21887246c3ef44440d57a50e21925c369aca2d37e81bc5ac0f283eb4c6df88c868efbcb885c3f2121bf7ad4ce937a42be8a50f67b28e4babcae763258c3c7a0d01181844eff690bcbae61531a0bac10cd79e97822ab233b3e89d5d9c7536075a4cc19360c8b46c76fdc2029f2206d5c35184cc672a69647fc13841a0a491131c448cc58206bd06d58b2c32d3a218d8646e289c4ecf0f2a9d5911c102ea2dc2c6b4607a119dd0909289907cbe87475713e760b5d26391f7b469779128aced4c5285de47c2c15608c314689c304549584107142958ea7c0123f2bd42452217138154d3de289b0d2518c8e31c6885d41525e9797314a62419288d4864d891180a0f7db8b129d6c6d8d6004ad033b8661d8d78a9d0231885d776b00a30596076b79605f42150d3fb5c210ab09a7b60aec4b6484090c8b9c118c29210b86611886c9c363dfb00c5302170cc3300c9387cb5a9dccdb2a747c8cefc5313abe9c5c9c1a5b02865d9777daebeaba7cd02abb668729186ab441ce780279b93b6518859d340f6b302cf3be256a0c75836a9c1d9cc61ebb9c86907ee449de298bedba223d282302d660274d21b0063b6646b743d8e31a5d8c2c461981aa3d81d6606f81961834762d1ac3b8ac14b5c1568851d8336fa3300ac3b28d5916b36cabd0e60e65fbec469ecdad6287cce8e2bfc31bf61dce86bcad8251d8b1554ecb324df3be5267cfb4eff05a6075f89af705659ed2609061b470e5bcbb90b43c70ce3d76ee54a1fb703a58a31345c35be8b24ce6939150f5f18b29bceea4414d01075e57e8951ee7ce1d86a658f1826e062e20ba02063490543aaf778f795f9641f5a06a0d54fdeed6fa45972e409c495a1ef071eeb03ce035c2e84912a152aa2482908c280d21841042082f57490dbf030ddcd15088863bac4ef4e2181d54f1e149ab53227181dafe70703467a74e55f8f89d3d74ee5461f97b9cd5d1a9329f8c7e9daa0a67e78e1b3d63dd7a9155aea0fb7070a00a85faf63b18b5bf61548c69e8d44335ce4e0f462d8f2c74c72eb7d0e1c467de976597f775d263ef9369ef93e95800ad89ffa24b42c7e8848e31c688a336f1c6dbc1a8c853e8f67179c478622284401557c189ad281475bc0238153bbe88259ac4a34fb5746c9181c50a569f66e928858c20f4a76a743c6fd6c5c7588a3108e86fa6238c8fe7d559b9a52faeb0a509d0daba42516f2f7c2983ee9be9fd97a3f732cb63732c8f7dc6ba42951c4670f3b75c8168871d56078ed1c92c0f3dc48e502d2ba8d17b217a0c31c25b8830838a5e16ac3e15a3974464c60cf6989e539f0ccdcaac8e7e9f6375f8fb99d5815dba74fcb6dc1059a974d24b2779da29c876d2e709c846d2be7149b3493a4ff8e586bc1c0a77d39933a2e46d5ecf77180d9dde34370bd375c5edc5dd60de0eae4207bf8351bb3b18b51ef61b46adfe43f5aa96611f4e6f972e5e8949493832a5a07beb85645055a3357c20ba02347fe7a3d48689ac3724a2e48db78351118c6ef9047b5d5dfd6e1d9c3153c68c4ecf7532316074cb5fc48d1e496df8d8cc9607e310c1282878d1da32c21114a47413a822920232a44c81a5c825c1825dbaac606ea2a23b7157eebaf23698191fbf9c8f7b3013015db6e8a625c8fdb8ae5dceeba5f97197cecdaf2708e9a60f21bf9dfbf6f2cd547ad74d238ce2bf4c228c62e5a0470146f1133aaf23848fcf2e1f113b182c323ad3f6611493b6fb306a7b82b4c599c4f2e02f3704d744d4869ba096073fce002c0f3e66d4492ea3fb348de663ff74cb46a1db6b586cc2287e82517ce8f91875d8f5d9bc591e1cbd20d4860f069c1e3b7c228a10c21fa2397ac108afafebd8446bf8117a5ff7c225156c4af331968ed72baadaaaa2842959c654381569ba47eb154e7d311db1702ac62d9ce214023a9e599cd2937180fe36cb8ad11f4d7f7b458d25f4b76bf4502d91d6c46759678551daa8fed4882501ed219425a13d84ba09680f21190a680fa157407b08a12ca03d846e8a680f219c15f7e0c73fa12a214d54450413aada31a2aa78448489b068c189afd23c340f44bd43ef20a575681dc660f54ccf6ce9539f8e3a47e7c0d239740ead9669992a8d43e340d437f40d521a47e36802ab633a664bdfe81b476da36d606998866975d75d957ee917a236b5494a73cd3181d5a52e6de9adb7a32635094b6badb53aebac4a638d11f5d59794962dc56075ecb8a561c3a3e6662cbdbdbd69033a6d2da1d5dada4ba8d2dadb5060f54c6beb0958f44653d2c105d934c2401e82d7057bb4c61063770c32a2b8245c9695289c52251801bdb0c2a96faf2c1734473e7eb5680ecde3790ad5818f672934a71fcf513467e7f11c85e620e0f1fc01cd39c0e3190acd31c0e399a5393d1ebf5d3447e7f1db06cdc979fcb241730af0f85d4373701ebf6bd01cd5e3570d9a937afca6417308f0f8e5a239423c7ed1a0390378fc9e41738278fc9a41738078fcaaa139378fdf32680e8fc72f193447008fdf31688ecde3778be6ec78fc8a41737e78fc86417302f0f80583e6d43c7ed3d01cd4e3f70b9ae3c3e3d70b9a0380c76f1734a7071e9407fc5ed11c1d8fdf24688ef7f85582e6ccc7ef1234e78f5f31348787c72f13346787471d1e671ebf5834e7f4f87582e6e478fc3e41737278fc424173641ebf64680e0e8fdf2868ce0d8f5f29680e8ec76f199a13f3f835d29c1b8fdf29688e8dc72f153407e6f16b86e6748fdf2c9af3f2f8ad82e6981ebf56d01ceef17b05cd293d7e8f34677bfc624173488fdf2c688ef6f8d582e6648fdf3334077bfc6e4173aec72f1a9a93eab9c3d4416de0113067d406fe00f3a436f0069839d406bec7cc416de07566cec4416de00b306f501b789c89436de05533466de053f386dac01360da501b7821268cdac00f60766a031fc47c511b7820a6496de06f26a736f03c66496de0053037b581b79924b581df3135b581ff61666a031f8089a90d7ccdbcd4061e35a5dac0fb30a3dac0036042b581ef61d2cc551be8e998323307b5899f3387dac47f9ed4269e8739a336f13b4c1dd4265e87b983dac4cf4c1ed426fe34af36f139e6549bf81ca6a736f1325387dac4e33069d426fe86d983dac4e39800509bf898e983dac4df9828b589b7316bd4261e6606406de2bbf983dac4bfcc1d6a136f9a366a13cf4d01a84d7c69f2509bf86ddea84d3c6902a136f1da0c426de2b33900b589c7a6106a137f4d02a84dbc9c2ab589f7b81f3c34fce48cd8a1e1cffdd0a1e179e08c9869f81db81fa786d781332247c3cf703f7268f81367844cc3e7e07ee0d0f0397046dcd0f032dc0f1c0d8f0367444cc3dfc0fdb8d1f03838236c347c0cf703a6e16f7046740d6f83fbf1d2f0309c11a686efb81f5cc3bf7046941adec4fdd81a9ee38c20357c89fbc11991353c89fb8135bcc61971357cc6fd900d8f7146c486bf38c919c10d1fb91fdbf070a98811bba5228178229aa31daf58e8a85de8a863e88805a3e212451879622389613275c4425238a5ad2768a9620aa760c773171c798b8e91ad743c7649229cfa96d5f1718998e0543c0538a549e8782522a13f4d0388842a2aef28af807f32099059c97454808c4c02ba6ee55d51adb757604dbc94441815af401587406be24f80a5051d1f838ef10a54e90a8b51c72316529b68c4a300a3627c344227e397c529e965b14974319ccaa08acf580ec03aa8b1f980589acb501bbe4618fd09eee110a73e366a08f59008aa842082429c522956aa98c2a9abf9900aa7b485458de6c32e5a583e1a8645cd01a2eba8a409552ba4a91ee5204308cdd00c240100d31440404020168dc703324d166d0f14800e96b8505a1d49844910c4288c32c618438c018610000052023033b3038b9296f19c7d928155765040bf62a123e5a338e50f53c0ec9a719cc39bf0cf3a7ee71760b26a17460ddfc4307b44d4054d0c4b3c69fc32772d39a790ac78acc84079950ce02553be1cba1c8b97c0c2054361e21d3c661af5313d56b178021e93fb50bc4d95b85beadede6ab4cf4216d76c13dc673d125a14faa963d96933dfabb6591d754ef8999a050158b6eeaa03370381d977d703c203ca886aaf3c105febcb87da374ef618487aff93fd7a3d9de8a53ab40037c6e1aa0c5ada407b34fba822c76db9dd5eb3ae66be358fc92f570c06b08a5c39a984b19b52da47041c780a2ce5248ad2ff7b905be437eb6c599e09949fa96804c3e829ab270d6cee8bfb97d304aeed1bb608076930df9ba6a8138bcd5d5672aec29ce162c99bfa91ba5b4b3875256b8083ad657074317f476368ea2d01dc72e9d56bf653133ccbc3ff1ceba35d2ea943f2c2b91f3847854f8c34498188960c112ed43b6a8f8846bccab86a09575157ede23ff94c8e4ee0c69c3b5df3f9d37feb073deafda24faf7bf9367a106a87a442dceaac143ce3632ba63a3995fec65b75bd61a9d355631ab6c036d5af3923b9911e1d729fae278de5d1695858378c6ea595772ea48c2409af9c1b6d440746aecd1bb4aaecc2d73e082b72ab9a0a15578c0eaa77acd4a108d0a9d77ed78ffdb618d9112305d24d3c9c196e60f7a560880c1e232d20de0f488d112a7bc323bd5a30d96467dc8c7e3008dc14453d9ec3c9b8fe96d98211aa40a4b785ff5733989816692e1a101474433437f3027397e821e07789f387ad5e4a1ebe7104ab46e9499bba04710272b2a63467a27a870b90a9a824ee76536ff08f251c81e6eb83e28895fef5a58025a2f98c4dc219e5c4ea2b88561ab617ec8ab57102d0d2de3bedefa716d2d04f46ac844a58e8d9fd08c4ae259abd54b5b9b9eda98261354a05eb453c250e2e42c55cdc12d646fcf909657182631b806463b678e99053d95ff84d18251b34ce212d98b40593afe41850d493eb13cf86ee2904ffb05e502f31fd091d8fa2a12ce3998655545ecc681510e16694aa65d6b3a4082166dac0552268718c32fe91dd2a2219b65258f185f6291ee767d2fc6a759f20d0e045ca11c0558b639038667329126c0abd747920240dc72f1392acdf6f22d80366bbf36193d1adc702c6a15d97cb21905811f8bfcf3b048a368d7386b7c94071d0c1894919341c864b5786c26a3657249febbb3b1f7b20044f503e18dd110efb795acc964affedb450688012334bd952eaa18f1d007c08b88900457a1f023ce03a75e89972b04522afaedeb466f5af4f0d4e0713dc18ce1d4bfc9a2b5fde1eb3135a61ef0a3ccd24a81acacdcadc74e39c7d72bec587d1f6bc64f1f891ca78a6632727e0ca6a27d04e2410d9ceb8b91929a6f8220ba9c717a1c86b642bfe23f8ef15dbc580068882afd314d12d08b43779c2518b05020c5c922541648522f8d1f6e7dc3e0b1882c0c8b0f4d09fb227c3cc26dd538f988987f02bc7acb6d783cc05d022000bea7f9eff3b6dfafc95b43cd6ba007bacf6d23273f727ef9aed8df4f8f0c94113ece981869bbeab73d345608428d3d503eb9652ccc0fcff2f1a11e0c3439b2a2b1c7d56956640aac7e8bb5f3006835d3ca0775beda2b6f257ec37490a9286a8aea8c52fbabd2f23d45b4a967d5dfb3bd25a606420fc82e1609f24dbbb0d4af80389ff1963e757e4ad0df6ec6243c9a88dc41c994d8ab645f990829e665ca5941dc1bea35c2a49dd8143318ea12fbda5fc032411256323a9792ba80d40d85261032f3814ac5547f47990bba9410f990330b9ed71b07074dd1c16c71ac43ca19baa130721f7ba16ba093c9fdf340af566122c6db766c1f49cb62b3bd90ff60dc261f8f6ec782ca4c3028cee5d55279da7227d398fc7174bcd8c61881078ad9fad06d4c2667fff405d0ab5e156985c20292acb91eca25ccf4a667f425f9a80d81896bfc84ce7bdf879a1b159b8895f267131d062d8b516a6b8811d5c1519aa13f13aa957ef3562dec07a2d206a9ba08af265bdbe194faabd04d85c79f6d5dfb244ad70f99e3713d8c32863a0a989ea3398e62e7638a2d069be45fe35bda111c8b99d99dcd8a15af26995c1ca164bf4304fea2d92ee2c25264af0c042e332098b85006e84e58c7d756d309ef551bc44bb78a5d8b731106a933dd16a440b476eefc80a5ad563cd6d385a17ac3c8d7d8d2bfe96c466b9f16c1c10d111547836da591fab33bdb40367dfd0ef4a4e1a2324a11a2569c532d5f2bafe0422f6c15e883ed1661246e62cc23362b58941cd59c3aa494aff02c95fb985727ff087290332bb8465262ac4e8aa433745f1cef5e341c431ecea83c72d35b20ae5395109828a06de1b7f22cd33096a058d3aacbe6c06b7b141e0be8e24e88514936061ed718cc73d329b103031d638aaa75dfd4090f10842077fbf00de2f6e96d66ff390ad22b62433571d187ba082785fa410831a3d903d2fb887c2681e3c647ee71a2e39a0abe629a7d56c666413118ee7113e8d724b159e963ad2cb53a022be4b7677138d7a426ece4646e6e199d15fdf958f897c29bede42dc5c629ad62ca9b7b1b193557c921326da83ce9fdea9c1d14521ee7d53d5cff92ea1142e3d27784b3fe23bf6c4615f504d7b6c260c585bfeb147f272d881682413ba8bac77ff56ae9e3fba71912fc61cfb4c7902b50f872a3b0f4296ef32827e1456b06adfebf45873f09a5d7e6ae193119fe0820fcca73ecab4b9a961479d24525d84fe7129f6f1628c3aaad4d8e7bb158302f567dcb41790f807c7de2d5fb3079645f6a2dfd11ae448f80f19bf2233d1f0437115998bd7904b808edd0beca11d5bc5776fa6160ae51201809d3fc44548bcc598c99d4a7b3b21a373f760e55d860b6f138d6e65ee933b2e013df1cdba90b4b5e99959fe5f06e18068c2d59a587b8f29dfc40ae5d801fa3e5b25ec35d3b6b32afd131a5144a87b1e0bed5bda6a20159c5ecab42845de3888ff1e728366e613a58083ea005c15476539a9074b177bb6f27aa20e99a3e899084ad4f4de541c211e5d0be15d12e5e95623c4f63ba94cbb50626576411cdac038efc75f7e4aba2034cae82df0e3971ace200e48470d1aa5d557b4d2ca50f8f584bd2cb37b68726e32fb0e40b87d545f43b8b52eef2fa049b0b2f36a9e395a1f8cddc089cfb75caaeb73158a6ad793956c2c371be53a1ff03244d858c1a1218e2db8add01d85b7db006abd5adf388f8edbb811f759db8c312ea577098d21eef39478cf07a9e381f29ac61482d23e97ebd6a4a48aeeb3cc0a9c46747ec505db70b3f2aa7c912dc7df3df4a164a3f9229e36914b293dacda6219a3a95723a3cd804db9d8bdeb9a0662ccdd3515f9d6d3f43900fb11c26f31cd9cf629cf940da8681a293453eaecfbc4b7233add284cf3d4808652f6db741dfde6e02c108c01e90640b0a1240a101052e645d998f36c053d413eba045c60d1e26e8aaf6503d88f03944ac6db76b13dc196b5a78e74604e22d7d33988e423b84751bec8c4be18353b69bf4cb8a3386470c264e4f0a85623f9ea8f1a460d7c4ae008e5592363d7762c8dd1f82ce262269f1517480d4f3dd58b955e08ece83e9ee64643eeb6c3b904b7f04ad86a9324b4dad30241de2055b590dd3ffdd117bf3ca6675ad464c220646e7321062ff185e2582984630fb676ae97a6f8e14df7ab416a878f61718c26c71fa163a39d555b198c4e5469aa7470b5a0fcd3900c68a1d756a6993907d9aa6aa94aacc4b00d054e20e8731b59eee683d22f8333945ca1acea56c1b158e42f7d108be4d620b30afbae163a13cb49e7c91e3ac0cd519297c7622a948b00a10be1e675b48731df6a8ff507f13007cad0f0c3b79249d2a8e08b2669689443b4128c72d872bbc886c394a93b6993ea263680187c58dd503451a3403dcaec1ad51d6bbe83749b31e078c5393deeec0f0526aea84a53808fdd98e75978c8adcb172d8f2f938537ebae9a51b95762fb4d212a61e78c628650b0fdb60042fb965823b783d5aa1a989f306861fc09d2403e2f18ea810d3d35a5e16245e267e1f0d47c1d3e14062b610642e680aaef0443933d9ef5089503cd56efade57610d0a20017cbd0b4d3caee9aeac880641a702ea29e7fa87f989a8c9a2998b8e4ac8ed3281a7169daab1906e3a2ea342bd0c2c18f8fbb1d7bef0223a22bfa2cf211d095a8f67723ff8fcb9ea46dd77ea2165067aed5e5afaaf99f3c897544012f1f9cd9d11c4f444b6ff6714dc62fc0a1a4e1e6c4c416a77e947a21f830029004a350a5839054c36968ce836ff788552444ed93a29cade37f62e9fcd342e49230665f4293595e97bea7ee61bf5df963b14a8aabfa49a398e344ce7e1ae61e8a84cca0590d29de4ea88ff2289cf8438010c1607f7a8b0bc6369d5f2b9d7947ac0c8e0bb55cd04ee298b3334a3ae8dc5d6aa021ea86b3b1e3724465053ee10fb75a943880dd077ff3176bada3768641484a0561bf51fba77f8fcba5191e6af21a7f0f56f77038feaca04f15da9887df8d709ea613834bdd38dcb4a004571ed0c08e11be0af3bd9e390898b4da4b964017c73eff8236502a34a8734cf557a12a838d8430855e442a929fed4265e797518d9a951b4ea9560c6ed4ad7ec0d8ba2f766c3285424240d492cf9bf9794911f42d437d86bb1e8ca17e7779d8b8efaa934cafa6d0e58032e29b6ac6d6982addf77aed6e51a87881d85ac348578690700b4a2dd16fc0bc3ee07c131b05efca813c8cdb0e209cca156e228648c6de2f18293f3b97c321a7e45be03ecd4cdcd0d4a063bdc0088cbc6c85b4b4bce62878a876fabed216d5d0ce9f57bb8fe614af415f2a9a1257fcf2da3b0edaed6c262390f823e2811081bd27709e30cb9357012deaa12332484c50ab5ec31bbaa8a53339197ba20d1b415efd66a28f8610539f4b851d8e8940dc4d91835b695814e0707d00de5ce598d50103e7a0ff0c440396d1f6518452b23e8f57768fae3ed81954c6580e3697f181186a2c8809e71978ac68979ee151c6a79e7c385264d8fe9217e7dd4513810a9a861a3aca685a2c4aec3fc0af2379faec28bfae11e0523b8c0e30fe0fc5033521b03518c2a4e09604c559d7399231eb0109b24cfbe14dd66f4c6440b5801ff8a2f95c1e2d2775cf15830d901bfbe9e2092b37af2aae35e2924501c8034c11f5c2fc3efa95ce4eeeaad7d6f585137b2950f5181c745c70aafa4e8178fbec3c51d41cf682e3150f221a1538eaa561f09fe08a4b08c38f0bca2880299a09ada16a5d116e4fe9409f78567c2e4609c16ca9fc6f04109eecfa33c582e16c167801e349e9d31ea8414ff274b931692d61809a4b9f2ecf4cde984adb9888d9f8ad61b5b7c0d28e096a66d88fc1469ebb3824bab39b3962171c578ce54b8d96ab130c64fb56bf09114b6dfb3d4a25a69b6648cff2d5d3c823d8661203f5be3c28175c090685d0625cb9a846fd8ab79cf9ea9ccb66bd020334b675f086b0ccd54e480c6e6bd159ba56247a0b786cbdb0f9abb2ff1afe8199c57205e59133226d357ff5292802ddb91bc0af3dec8978de0dfc79c91c68b270e09290b16d2d0906de84dc9c786385e8cc248bd92ae2f609f3a1c164be20633fe4992d4bc61170219c0d07b9768f768e7bba13475d6906af03591a5e17d404b3992c4b4e08ab9a8167d10c347fe38666a1beb831903567afc3e19091f820702aea6e0b8561b51674db6fc7232ad60620e85a416d88ba45fdba0f9bef127492113342ea1d3b06c0fc887a4a4eafa0a45391d16cc930e407a7059980aa4ecab31db0d854da728199767e09a11a66ddd73e01faed5002de855d03d0cbb2ba39fcd2715a5f5708eca4159c6d8187b47b11aef59c23264a2d18ab548f47d9a7d897d8e65c47c9607e673351c76d85403e00cc0093a26c4fa8b7123c438bb694706cb5c3f8546200a7631b4960160c9c5fa902df703c0ec7cc1bd01b153429d7d3af64f5b98a777878bd8f467592740a2b2d59d831a5aeb6b8213bbe804e8d1c9a87e823fc8c93453f2cb5e6612a05a91091eebe881c4e7387d8a848944f390f9f36f233c17ed37c20fdd13c39b5968f10bd14a35940122940b9e69631ce96deab049b847c27d06fe31bdf9981fd6234429e7c7b4cc5fe96ee243f0a1e8ce802c0fbf2a917032511a05532680f326cc6d74a40572d090c6916259b13cc7007af32d868b1e2927c2eab64bd8c5cdf465f05748920175acdee8883865c05c0c495c646f44e99b15c0180aefedfa7a8e5eda7c6694abe4f60e8697055b42aabeeed79c070b63c38ca214477f20ee250bc8be755a7b2bb4540782e060d277ea725827251688d121cb285d29c93f72c29ae265d559e8496a5f17280310f6888ba13c305a11a9bb70b1155cd795564db433f336ea3b57b981c33e18d0b1d06a6ad32178c4298c10822f68a18ae3c291fadcbdb47d2d74fca02df60529fc46ddf4e190935528e495056c1b95c11e95148e04a1c2a7579eac0a9859ec318684d921fcd8a73152f31bb4d8c759994a235005d0c1421ede6906af66c3c3db336dedd91890287bb72a41194f04e24ac016555a473b540096b5d00ccc7bdc663139e9d1c4e505038bc415b0777763b54bb2743b0100e4e4fdf3ded0a85877a0bd2c813affa5db6706c1d62060250de48e5958f70fcd547f4acdb3740f72e6ed52e525e29d27a9f6403c464ccff747ab11567d9d05bf0caa54a7752e6724cf66470bbda8785e1aa1fbe513cdaf220f721cc17ee48b15081bfb3010c43e9293e4023b1667b9979dc109c72f36c71d69dc1ee8ce8e7637d1af337d17e6aa4ff2e2dedce43bd56dfcff796ab51cdc49c9a33b5bdb9c090d587a965436fb0ee97115b5672e65a2eca21f3baa96b84aa9b54f5b222ecb75476564d5259353be620ef7ae360b6e1871b97f0ef138e34e446cea4477d285646486f138e0fed570611f57626feb3e3697dc60ef448fae8a35e83244a4f19f404e716cc3df438b0013f2f32c9cc1508fc9e0062ef23ec4b9f71174a5c378983ed9cdbee435e3a763b74afa06886ab38895209115d974abc389aa5eb5cc5733b07ce02216d6bc1b1226d5cc79d2b272ce31bdf0333a77861248d1f051224290fedb87b1a1e3d0c0b4d0176536510b8c468287607034b38aa6cafdf0a544dc2ff760131d6456c256a60e03cfe6191e08412bcc9d4f0a29ad44447d7cb6af22dd964302000165b86249791a29704947f2075294d89ee3b36120e0953b3b26af91bfdd5a1f724c55f3ed04cab73b8410473a1053950aa8ca63cb7193c2158e4ad4850a8b3ce33c936b58b575cf5789317552db3a3b3251ec41cfcdb9d19c42b53ca66da457993d1907ed797f58f05bd48eac9b7a0a43ef4732ab688d83eb040e8747c985536d2e7631c8eb821c59c00c615566a5874a958dbd6d04bbbacb98ed93037e0a49f578ab14c5afddc160665c77b30e92a5155d768dbb4a09b5afbf93711412cf75d95715a9dac8a40c96654a10ebed9188cdb355a5ddf24c3a657e42bc1ca4247376349b30abe9ab8750ce61364b21d1a9a857a180b9e097b884d65742d6a77b427a24337a78a4ec03bf53d90989827b3218c2f05adc1f7d0cefb570a8bf84d6170f1d77de5ccd04c3028580dabe93df32083a2bec505f4f4aeb23d9a89cb89b224ce90836663bb130e2ddeb50473373c6d8a324d4659f6436a3a99b5ca0a0382ebb0e8e6206b7df8054273eef2da5e101fd21076da5176b3ae2ddcc80dbf56188c5977057520f748d09d8a64962631dc93273c261cf25484f409079f6609b9a97f4c3ef2ece77cf5adc5836028b754bf5a7f51d6224bfc97a8a232c4b9a398efa9939eecc90896199e3346960cfeef1b16aed8f20253416278bce4a28c745e761a3a118e18fda00afcfd3c2daeed5085a8e712cf43333bffdfec62cd6595c0960d8f191c0589b4fba703b2025fae30d8d00202608fdd1c0d1b06b870c78a5af96b0a9ae9049c676f3aef6197a14d82d6b66603f7c1c6fbee2803bfcd84be6d0c9c0b773f812b72e0ca72d920451d323610840b853419e4032a1b05dbd0e3c85d1bd9f978577e5d010508e6171ddea2332fafa0b06cf272ae13439c6733f4660da7ead2707c0e7be7c4cdb22e91abab49a29894feffb574dc8e5acc96cab21c6bbb5ae0bcadd94724db3e1d608e8ab15e4c7baa1bfa43ae29a6ce20c3dfb21dbcdec16d5b0ce993f9fd0e3e9904c633750a548ab483f6ab27f1a9772eb46f224c66ee6703afa24267b474746603865588acf238df85092033444d14a36f4c1e4c0a9baaa01ae9797227e7aa5e791f289480154b8c565549454cddf4ac3727d2411de7912643135894605d2199fe26e66a9bd7b65ff85dc6c233e4b9d20983746f2fa62863ce82068c01126857830fda463e55d30bb98ff35faa4abf64d19179fdd81626fda344fb6956cb1ee7e752cfb7d9bb633e6f25d8ab92e80dfb3e8e1f848874ddaaa47e0e63ac7b6f6afa6ca6f19e99e12df4f368672bef09afdb1e4c0543d246a07309daa692e997ca2a9c545a5147b34f0f3ea111c8cb4dd0cd438622c31b431377a4337a6c0f5c9ca51f37022cd72551d9724d1f193e252bd20b27b1e3944927e95602e811ee70d0fead8fcdf57399dcd32b3a960bc43a9db6916c0db03f842e642d6cb2845cabcbbd83490bff3fcee1358fe5dca871b259a0305a73dc635ec28e04f69f8b94b2b2d670d57885b250ce0e99f5d6be939d75a7f57a5d0fbe0904a1c34b0613fcfe4b4d2232c5dd8f3c84e8a79dc895cca622c09b45760dab2d71973ea30ada850a2613d23ad0af2a2513474d4e31f95d27c08df5d205ec0ab42a12a57411bfde832eb3ca72f994f77c31edc96dbcf819adc4bff92ca5f3b90193370b329829f61dcb155c14f91953b57ae06937c71a0299b8dccfa90e055db153c97c9218f58b321ad04730825fc7068cc12f63b44873870b4948897d2d8ae4ec1a46578a40f0c9bcf8eb91cab66426c1710c703d01872ae14810a0d3e12a5ca929b038df349a0b3dc0f7c04a35d1ad55223d49f356c7ff4252000afaccc228dd4e3f8f8ed60745b736a8e319e45162a2b6e2604192ab9349e21e785f0514b3558c79c1d1c4dc828c99801520ea8f829eacee2107326a651aa493305cb09dfc6835320259587df6e46be4df75b5642b60cd55e41afbab579bc61edfdee0672769713740fc8e2d84736ae2cdf5392b212fc5bc05119a31cbcf7465d2a50de3e64648321336c20c2f5a81c630ad50e53856d0233ca1fea8e65ee592994f12fc7bdf0b8e48ad298df9df8591b00463181c7927c41388d4d093028339b72a82f3b1bbe855069059c7807c4505c2461e08042708e9e67d11417ee261642342e420299c62f67b8389ce10fe03718a2dcc4f111c198a4f25e2577ff511a3e4f270dfa7f5f97e2d62f61afe00498387c95d3797bfb55c759028d419ff69f3ac6d782f8a912dfb4798c2348d7855eae508cf803f4017eac12e0ba8b1dc50824b0572db3ccf5e051513706861986ae132ed46a22363fb8a1f3ae18a071c917b91705f7fd4ec17d94064ec23608bc569bd3950adf69e1053c95e7b0efbd738793a02743a8d58b50aec0b0d41a31f00fbe938e39007191b20b6df22dd684928a4c8ebc2b9a370b231435377bfd345659194735c9ccb27b4600bec73ef20d976b300b0a41131b00a647f8d4c50bc39a20ffe8fb2c718be09e34a2f919a6c563814ef7ca6e40e2bd02e2b9e067c3d993c2b16b77f3dc68cc54f118c9c2ce67770cf28bb2ed6c6b854b98ea3e754c041b91dc1247b481a029a13550b3f83e7cd86a2b3039294172758dc6c2ab0601f2a2111e50e3a2813e43da359d00d7a0496e42822243561ace5f38891ac35be25aa07482e1a000924bddc157c0d5614ad125fa07f56dbde1527bf65a7b025524164cd4293194528428f781b8372c0255157f8b057d14d35c6ed949bf78b99b90e42a997f912868401c923f23d010cb40b14c60e168e8258f1171588ef0b17d213282d0821ee33ba82acc3f11c8cd07aede8c76854e78fa32d3437222c9be1b44fd0e101eeaf1c687cb74b2f1815204425f5ce96a9149866f82700bb0154a0f78f3c755ecb16b9d1f318da4b7007bf0bb204a7d5c6e96d72fab69cb4944e37c6e8d944c1695d531e0ca52fca902b02529dd33b26c3b29e73fc1035a7af0af81bc9ead9ea1973fa5c261647f8e588c821c8e51bd3ccb6f221ac0d69a6ddcae6b36ca04e0b04c5d34dd75ea8c1e137073e31b03837d1911de5b41bc7a9403fa8e2579897dfc9650ad144916029e020375045247ddd02536ee73a3572d042525f2183e52c35062b58cc4768d226c9de740a834c19b0ad29878cb824221d7bcf7f834ceb2ff9c95636a602ee784f1d3ec4a1109c224fb7a631e699cc4d85cd433123a5f60e052ae64320194d0167a80224bad3bf0c7a89e1c4319358479aeccb0d00ac4dad873952c18f3cc356f769461c2a40680822234a35c56f08e53f26299222c7ecc986430f6583fae4291c7f2e0daa051aff490cb09c762c90a68dda5ecf498f1aae7be7443476fcd2896aede0ed9368dc111b2768bfc37605b2bb36ef3250f3e6c33c1c2d999e5312d2c8c5a6a30c55647c81e7e2346410e62775718abc7398630d6c7b105c07a0b10af079eaa741dceb20465dd193779568073d46a92845169983587e862ca2d4aa1e7c0d12878d54a516a9ee501f6ed6a9b6fddff23f0150be7ef2689c30da31175b87f9962f7e4aa8827017757eafcaa53b046ecb0e130227024e8318865e595e29e674f952f77c035b2f07c6412ccca129dcb1481f5c7dfc4b34c9d7f9c850153ec01af412f738b4c0b203007b06c20537b990d9bfd5d8f52af2418c661bff0836d2dbdbf59d61d333b34032262b5dea7c09d45f6c92e7bf468e115ce61686cf87b3891931be403ad9792c66c8003429ea38aab051c702ac3e7557261ca5991aabbc2c44b128d595799958f2a9ede3ca9b9df3c7ef7a8156f55e6414083cdffd56d3956b13313e0318a0bf51aef1f2a1555daa723b407b220569863d4e030afbae7a80fb85079c3708be43d3074ba969a8a9a1b39739cac8a753870b2437bf8050536354379f9e67450f275434d2fa980804c2d6efc3d5cee6d3656764d38cdac5fa3cd2d3b20ba8243ee400e9b12969ef591fa3a6369225624993c00b8662c704ed00e5e01d586a6215bc1bed25b441c0b2e661b47a0a8339efd7322270fc84b242623c61075480f4a12087e89cf3bcb7be981073d0791f7e7a07ca7ad81f988199dbb960becbb569e831b0724d7ce11907b6458c47691bcf4b23dcbde223f60735c81499a854b8ad4d74a979d94c3a35d70b2ef222b2bf6ecee7f84e461710aa20afe229b4a00aaf0d82a510b806f98206779f17677cf4bfbd642e88ac05998cb2ca1c7283e7fbd444b115537c4427d0757bc65209a5ea2aab691055e0d835ef747363c0337d00cc5a9c3648208cc18d05bb1038796a91810a3f513d0b74ce6365908e76d7ef5f7fe6c458fde9a5d8ea22d9b97aa18821b39ab0e8f62003408e423205a69c664347bf731598bf176770e3462fe87c2b7263c62aa871f765a99f2536a076ffb1a1d79a6d530e54a115e2fe2ba08a5e3863fcb64eb3212c4f85e856f32318e25756631729a53eddc3692e1097513fc11f3965fbe11ff6a449e23f58c92ef94f6e8d8ab6c8006d2aac3f4c398d08bd68b46de671c21cb5e3ad05fc165e8d644dc67e0cc6b3dd992b3dec1cd5a21df0e5f252e437445dc947561023384815f6f8c29afede27e11e79f9a64251b9a33d364e97ceaaee879f199a39f4de258b3e3e186eb45e89951c909a611af953e51165d2844d84f87f7b0f9009d426ffcd7683e4b896e062c6ff64462dc1e3b20efa769749d2155a3e0b5fc36e771212968c910ef29c5d6a3748c748b62ee4f8fbdea9452a8be3dc7d461dbe70be53c341ad0477bf46d7100cf16909a30c3d97e0b162e40fab19685cfa4bda8e27ab0490ff4430595b6312172487d3ce61518ab3b54e53bdc2ead30770010bc2d6d3fcef7918648c91ee340faa926ffcf7feb030085346df7f0633b148096a5f2b527a47fd188006027f77102a66117b49d024693f0cf1e3258c2a4bc5eb9540ba8b1cc18f409c23aff1a500b02c06dfe010dffd0ad098c385db9b633d8e441c72e2ac9689a58b24ccc7e4ec14fe227bac0d036892ac4842e1f838eadb413c0c8dec1b6bdf2139e7c31befa3bd09a1f75ab22e4c5f9b9671fc8a18eb0df75ba7842f8c2220aafcc06074c87fd71835a8ff0ccff6911b3161032d74f9f589d12d9dda636305dcac2fe0cab660fd008cdcd6b5d8b6915f106614834bfada64bc283001bcf1e077ba3025445a841f1b7252764f66944e1b292391b91cd873b7648855326bc13391834e5b8f3082b0297b0a790b7e56deff05223631664fbd42d31abe5c18015518122e8222b10716ec6f42b65c09ed5f0dd117167a8fa2988b1a42693a941ccf4694173e2ef75921ef7632847f4bc57710c3f8bac7239cbfc8228afe18cd00802b8d793cb06075827bd144b51ab7aa909af72dd5fedc264980bcf91b284d269d10c3d2b06763ca8895901c0bd8f2a3cd9298ddb7019198fa0d8e96e408047d111131e120baf3a359661439c414891fd61a3976547efd2f402260f52bac955fc4aee375b9656f9f3ad0d9a8db3eee57b1bc562bdd98ceb3d5048fd31e40a69a2dafce552d370b88697cc519bb052f877947fa95899463c7fdbf816a80c85c3d28288edd8e3e901a6b728e4d2e2f216cc7a3c7b13d50695b9a7c47078702e191fddb775e71baa0fe56024a11cf7f3943638c8b09e0d458cf97fb4f8925ac8de9b9767c9b61301e262fcea4a75caca2433be935379788e79f9604b83983b534228fccbc66e93eeabb91451ed4ddfddae943013b6f2082c967eb20f09f996821e65388565abbdf1dfbf3f3c2c6fcf9fe5ae3a0c09d7bda8e7dfb2651205a582209e4f2085b99fce3208983b17d611017a511864fa8a3896d1f702d73c26f2801b583b3892e894e0832f635b1877b89cf332eebf9a591cd78da993397d39126040ae0a04453c0af02c5b3ccb74f99837b273c6fa8b8989066aa24b3b25837ba4296b6d9901f28110ee6c4828b50c97d53fab60b78e323b1b5dba627eaad9d42baec2535461582b546be5c879b5025aeb675f61195334352581591a011be8c6b81506a40a60096ac253a82daf7fd1c414e4de73788fa99b67a49862284b24e4fa28dc8936b4d0ca3a1fc8f6c121ec39ca8f6c976f27700c185927fb9b21b62a9be9018a96de6e883b866cae575e3ad29641a4dea3236a8bfccebad55fd9dd7b00440011b575dfe1371e72432c61b576459639c1737bb7211fbba3beeb405bdee020a116fdc2e674937bf793dc81236e78480025f07c90447f1eb26ef1b5962e8b942fb6f746496826b40207c13ef5802c78a604c0eafa8e4505b8df058bb488b6dc7347ca37219a19e38617c316a22dde72487749d71144b4820c6c6ca68b263e42568943700e564ae7a515fe65440b916fbf2ca620e21b94b10d8dfdca441e2d06c045f3c35fd8e398b2f2fa562c30aa00a24a058f9bb1fe74780809f140e04520fc61d5155c61dc8b6dd844e78b20dd1ef20cddedfee03681e1e7518e0353ff3b3b30347d08951f86e88f6148cd046907021c917d9703b1cc57db4b5dee8aeb9512f8562890e3588eb4890609597654da5784a6ff5a1d6843a49cf500ed8e915b84df2a5dbf7011d0903de46edd0768f0ef82f77a36264cdaa8641f454f6e1201778882f1b9e7715d5fa7cd9e5a3f653eb9ba05c1d5402c1e7bfbdf531b542799b9e5430bdc81fb11f6c3afc8ba5097e81effbcd0508d1c607f4a99b61c0fcef9b5dc5ba35a47cffec1ff60b32877dbe83b617befe1f7f5f6120039ba30855c6b06625cc8664459732f6da7a07f1797792fdfc08667118bad0ccc556fb7c0ad9b87635f17e0ae83528e85ba8d01eb116002ce803e57c00d186de464a137f384be13f9291b62c05a151fe4037fe003fa62890d31ad9ccb88d94ae02776fe2935785467dc046d475079925c3a78a6879e9299788e146470b7a18f5a97cc11d835e8816c73072cfe3853e63fd565ab867d813c4053119dcdb831fc2afb7837ec88d5ccfc176d55c1fd85a08197a4db83896c12d8f16f419e841e9e21ba39e242cc462ecce87851e867a50bee096a1d7848b623374e768719fb15e942ebc33e80989fb4aafd863ec71c4fe48aeb963d08b0916f43086911b73c06fb915fb8c7b48bc108bb1bb8f167a0cf540c9823b432f132d8a61e886e3e23ec67ab1f242c95b865e48b8186e2c86ee3858e895fe408d5ac7a1c9b2af43548cd20fdde8f5396857cdc5d0789d0f448c7b19b136a6ab19de887bd56de0a613ffcd714b06c1e6a870f153231b2af9211abd4e4711bae12126504d59ad1bca2071984dcf88820e063a95a8ab547368159a3dc3edffe0781fb5ac19602210ece21dadbd745c3119a1b88f34424924c33971a134465a7d02a7eb30a584659aff2fdf030ee26d81a3e35e92258ed2e02a1c5cb7a19a9204527b7bdaab077e0aa01b76f37338bef3307950c769e4e09a3baec54ae11e32136656d03617c9b95217f43f4eac541cea1afdd5b113c37e2407da98a7c8c06b5f8aeaf7aba2dad4eda75891f43e5cd1f4025b496b21633368fa8b93c53dfed902cae2d672493d9aac15812addfa894d73aeafcebdedb250bf411fbe2af0da496eb1b1f538981c876bd5d192e35416b391c3c8948d8fcd7a00eb86227e3e6f4b744d9e5da215da3fb0698c13f173bc7b40ae2f9fa71791ca096bafb55661591de0063bd1cf68128396ddabd42f7af313dc4db021271b2251d4e0c6d958550a426286dc12ac533b84d783a975ab771c7ced4029fd808d729d9c2bc732a8692fd41a6224c4491b340f9097ce4231574c2da2429e80fa0a00057c05251863f7ec50a3b39f913e15567a465115a8a4699e0265571bcf8e4021c3d3e0a3663c108c931fb231eb82eb353f61c9504fda47ed1acc064ec9567a7445bbb181298baf00c31c95378311c7761f48f86534c08ca666c18541a363d1659e652af82b85e4007d7e87f8bf2e6fc312bd22b815e60cccc36274e2c4291e910ac05086add9a0f71ac922809684904e06efe34ec9c8b0f9f90236799e66c107b8a0c0b7c872280a288eed98383a64f69110a1c0fa168f4a386a05050998aabd077081358143db02ecf2fd909bcd013d3f7e028fc452bfd953ebb4082bb0f5acb090b377f6338b8f7f0e8179c7de0bdce0f8cebec6e00388459d66d7f5ee756cd6619e9a93882331cf0c0bc2db6777618fee373e98269a9e4e7382aebdc4f9c7be69eb1c4c39ddba931680bf6ca82e27ce796efca7681120f5d6eb2e3cbed25a69536ddae7b23e0815d583ba435b92d7074d0799b8dc24191591af45b3eee71fec2e6ff9f5a7a99068cc9869931d0756b3a7f6d65e9d5e2c4b48c1f5f29f6ee0b01da11c0d780f393564cb6c14fa5204b54b2ef698f39dcaa2de2487f1926402fdeb93a658b3e647e0deb6a8c3a3bce63c0e8ad42a241f2c3f0ff0096a54e1e3cb9865ef9419ae4bb884087f4713ffeaf5105f79f828156adef16767bc28bfa702b2498398f2e1293896d089cfa02c86e4244e553b180612c9849a9578335e94d329e5b705e86c85f1f8122657bb7ae85c02b7ce84a7f6c5afd3fc3bc8ed9e3253de26b4c6fd33db05b251a05bb76184397b88f7cfc0bbea8082f86256a13062a324de14b490479a70105360054c9a22b010796e5fb96f3e64ff86c681dd10d80d316d5f4a17e7a4ecb694a8a1e79a708151f3717166e54fb33f1abb6b716d32bf010045dbb0c6c8b134a35ea2f1940ec84d4187c835bc181457690087fd3a44a633e0f4bf77ac5642f8aa7713521da76645f69cca3bdb4d6c8a95c53baf2928eb7acbb28bfb71da775b8aa3da1899b22828bc704f56a9fb0784d60730d40434abbb83f8ab04ea79adfea1fdd67716bed911b25aabd775e9f54cf826b2599da6557680af3f2c14404bd6bf72016118a9b133a6dbc929b009ec3aa82703dc5bf2d91946d3da3722d8319cc10afe5dd4a81178d7f03fb013d187e1bfab52d23900029914989ce10361da2fe4387cb024434d97a799bf2c87d482e682443634d0af7ce1dfd0f60266eb326e6b0e53c9eeb442b908841f52781e4d1b73dd3c900519f44eead8cc63589590cc89c0e8cf96277d4e290c3cade2dcbe2495161ee54e7a60d4afa62ed0d580bd8567f8dd3ca714fc7a73e9261bcd8f817411b652e4543ed3deb5e4789cfdb7125a614b82f1fe05c679e778249013c442d8724ba61767066fddc53bc69ba06c1fc0707c9508086bb9025caf8a962257e6e260b9595aa6a7888e20911d5e319d24bd3bc778c4b1942b4891c3147884d2679bff34a981d8c8360a5185a71c0015d91a0f8f040facd32e35d1804b11cb068f4da3fc85f8db134c42a6b9ec332f2e15e7a083baa879ca685eee18607811384cef40b9f5d5a69844046f0c14df10a722cf803e43a574628da606135be87de8c9818a6ad5068ebcbc9336175703a30801db36371c5852678388436ca167809f15006112ac96428664c915ae35d4c704b3878f1549c88828ea83388ed5ae697fc5b70461d83aea5559af51e386b4824d79ce84ace309b9863cba64792656db97cec342eb00adf5c792df763308e8000e58d4e54f024a03d2d84da599c63a1bb6b05645e5b6ac74bce0cf9ebbe4e529a210813b2810b155ad28d963a5d11326e29946cb8471e855801bff9d8d3500020d60ea5c00dc46ee0a946806aef1e348e2eb00115b28d494cd2597d1341f502806ad38a91dd30d7b080310852e1b1a3b6e652cc160ee41174f166a3a659596c5a39307eceac5e6693c68d9c1c85820066e9d921078072106c20d1c365e828a05f31c07c7e1637872484c59cc22c24e113ce0e681fd96f85fa38ee98fa6e3fc3ce7ec582c5fae0c03406f81c66429642c1d264b760525f978ab37533648c584e047d65d884dcf07779ec90afb6d24f90aeb4d5ce0a4291639d9d27bc996c4f262e3b0e29c4e7baa0f397cb95f1f7a078d40031adc5098d1370cd4bc2b70ba3b0d0cf448fa921bfc0a176f2a837b668f26a615fe97167b0dccbef1386d9c01c47e14d476aef8a823aed62942fff121dd708f7eef2f8f95d4f415e9a108cd0738986893d7a80bfaf242b4bab82b3495bc6393bef932b69b2f6841bab4bc55169bfa0da1ac48b95ef8105bde04df688e919622d6ca04c224968ffa50762dc9a91127bde76a1e45880245a64f47913a5d05df5dc879412650f5e6aaff8cf5c559101c0f36109040be0bc0ee7d06f2998c2f6d7da89d0383916eb705aaeb60c34d9b46c59b8087b50917b293c32a3163f6b189ba44d518529a70d5a818988b1eabfff73e5d59d52bebba1b299e4b6380b14eafef978ce6d9d254b0df9a2c8bb9206000d4934df97f78b30191f8b95afc782a4ffd04c44c3e78945ae6c77350c53cbd1b15ae99e2c33b7336a894df98c669d92e08355ef18156350b15d7215c32f60e01e653ad66414af0fa1c6ab5f06453aaa4810e2a41b8cbdc9f109c4b51f89ab887dd7e31dfab104dd743055389b6462905c0d63123e77206478a2c9eec786a8d164b10b7d421402bac5e2c70a5619f1844359ca1b7228dba0bc945a9f414d3f151273c577c6c0aea19a2958b5fc3ba2178a017686f2d5072492c86e087644e1bedd14c0e63aebb494fe90d868fa7e023f4df165e8abf252862f68d980c2b017482ece0b7747e1c0d7d99b1a6f724f57f5d182cc20629b036797f6d0e444d8d3c2f9993896ac917ca02aff5d133d74e830a003a14c3794f1e86e4c4e8223a6efc9bf9e2dc687a5de62da467eed98e71442f8e71cdcd429dc8a3028801eb88a89a76314aa45b67053c178a326dfbe1b570a146fd567197097333ad72655464a8bb5d00a046859c70a742e7020db08847f0be1cd88f318002c116101740c2d011b0f837f0feff5d475f77165682e701ef129eeac20c0f682036b67ae08408b6b9bfbedc85624efe46aff530118ae75007155fbe31f40c2d0b52090f1da0b0804c6a96ff5517a177b5c97e7957ec68ea50493d058162d6bc1bc07f41579c1fd5d0915a455561ec1e4e8608d4e461b391530dad9785f00f2930a99ae99e44744c3c5202f74dac552838d124402497b17942d680ed825c218dcb91f1be48a7efcded5effb556e666245be29464bd109127ab821fa16ec9ee0b9d4b94bbd665ba7ee3809aac39fd1b79300211dd3ed222b4fedcc88217e3365f8e0efe0448e006cbb7c3961a68d922af1ff0a7c2ca017fe94e6246d7a9b439ce1538c92dd8c59004fe39c42e5567be2c59615482ed4bf444222241021d134f05ff566a79d1b630e87c25d8418fd35f0761258643ac5b1d04e41ad4486d4285d5cad182d96fa9709f6effc87216f02702772bf20ff0d11e846fab79ae3bde1f167b3369c082fac8ccfe12f91f74f5971b83db09824d977f6bd0f9d3a9c5cd4a1a4ff9aff829e26de1649fc5a0d0c8e33d8d8d093cba5c8bfef930fe3546f52f7da6fd1347f0efe6f9fd5fd641af8e76c2e7aecaf8175cbb2ade271c459d710b958b535fe42436dcfaedc504c2762a16fb2027d58cef588f8cfa3e67b4b1858464571d9b8c4a1913591313a9cb7941b61a825c81fcbb909db86b1129344ce0908a70a9f3e0501e3cfe93792bf9a22c4eb61051d4414fb10a289470d5d8b433cdac2a0016d478ce515da8af7a1fe5b3d95e99cb8c3363095b81a5cf754127fa4c4bfb8818352f09c0c2e04c3e3abb2d48ef899de5245c5a74ee22de223a4f0b0ba8a44ce6ba3f2466f578443af1c083d4d8002e72513d7d37a674767fe85cd81a7e793f4bb0107d2bc77928f00e222d1d396390e9d429bcaf3d06f07d50f86c4a72a47de2ae2236920d2b73bd63e5d8e4870c48ab84434e046c91d49939d164fabe67388c16eaa3f1260b92c2306f14e6c44775ac3de95623a0f2f1b3768bea62aa0086c8523d52678d4e1580973556439eb699bdcd09760044c94201980d21d194e0efde47cb558388bf6d54807a59383b929b5544d59e1aa780fe137558295d1e8c9e6cd15f6c2c6a90600bf44c49be440fc4edc3654368b815071a28037520041ce1ebeff4a2f27f204d492b901a6c15dbbf391d8a2513d07cdb61ab13b2738f8d50f691bec31f71ea153716b7d07e791b716de315844446f360ea70ef054ce9154d2fe16f1e0db6bec7d3e8722a2691fe84a34c28d07edff36ccf0e6196fe51bbeff7f29fc80c98b1e5671fb5ec2ba334cec780ca550b9c63da99102c580487104dd3233e9d260d42ee1beeb9439c36cc7ee555522a418bc02534072e43c74012d02104397950a0a63ef152bdfb695b3bf286a311391f82c8a55cb66ef2d0aa485e14ceebf0bb6a52a695189e5fd0b1451d5da7c71d89df64cbcfb6553c253396b1f36f7d45429720205dd586630f47e512ee26a430474bc4657c4b2310f11f99d07b7ff25740b252b649a4fd690a6204d10f289e7f65a694167220240cf37740132c8e8b5400f1614caf1895da6ec761e0f7b9926214604575ffeadaa4c9d110d50a8be156ad2291fc45a67a83448d51faafcac86067a2bb286a3f7a7e5538eca88b608bf5ca85ab70e7c5416743546400d54e6a82a9c4ada7aabd2a722ac41990e1a526c6220b1008a6deaadc4344bcfc2b654400d821814040ed3a404be8ac64ddfd1025f13c46c499f6d90d2bc7545588110a3d093b89103749641d6b95cc6e0c4037c4f012e6e5a083d8676502154ebc33f07e01261e8379f462610c1d0f1026eef2557717aaf1beb5eb2a79a0c68518c1820e36e8e59981e0d2aafec6c40639d25a6cb3252df0a7651006220c2e6c6e7018da0cfbc562430ae6e883b03c3d52c234f1342a43d9b2cf09c898c396209df2aa9e4148e63f945310a30880a1572e901f8cc085f40469726feda727508f6fccd0ad5f55e12e9865a9bc742cc3a88d042dc20e4c25ae70e5f03f68c28151e0199f6bcd9b52bb353c08bbb5a7f38ea2e9df810353ecf10d89ad7abf1ef3fb37b9510ab4014b29045c0c7df7f86a4b1119cf51bca79c05e53ffcef4983efdfa2a1721aab6e11d22e9c0e9fad49a983487c767ea6be081f73b5306a4810355f879270f6bb549b1a37a758937a64f54a9ca574da8231ddb03dd2072908afea479680efeed21ddc2f7d87062b6c02d99793d65277335fef430527c1d11e77b3b8e0f72125a200bb215391bc82db48da347250c1811090cfc1982799bf844511800266cbc18ea37952b72b41d3f38fd4bdb7799cb3bac1d065a4acafb887c0c6fd6e42ddb710fe2ddc1bc115bfcf825ddcfec2ddef78980d03816639f652888b5537d642fe8cc1ef7a38fc9319c2ffe0b3f5534bcbc12df55203dbdfd3cf00a1a18964ab27398aea321ae997377ecde44ec355dc90547d3d0c10ecee8763b0720092eb35f2bf5f35f7f723055ecc9f49ba92d36fb8445ab651d9cddf5f9420b51296767fc64b7abf493e9e8c17b8f2c637c4ef2ff5b1c78fc1fa5d7ce5782cf5413cda758e193b309f5de88ce1523653896474f041e5866688e33bd88a4bebb985bd2140298f5f6d2211568fe515e7794381db14c7c7b749803a3a35e99d291a56d1360dca530fd2dcb0472a73ac980d63104d04b1e69daf3c563399dadaec8455ed1cc5dfd818dcaf278b963cc9ca7724e272defef7f52e6a319c526240168b77b444073543caf3a0fc4767771110d94eb6040a747a488bf22846dabdb038b24e63f988712a1414b0733edf60c3e7bb5e997e619d4d27f7484beee711a51ca0c14d597a000c4e94dd7f93f24bb32cbd1ca2d688426b1e3f1f527f042d6065b48006809541297ce1de42d81ab23e568a47621d679c3606efb48751f2eeeda485a142d0c9623d9a37f2ffe3c7646e640d198d5d3f0de8be1794f75b14adc709adf4e4aab52dc74694ca9992b05b17d2b7fb109494f5d02b0eda6ac8e1b0b8c007eb7422126cf69ab77cf37b46218b9bf1b49bfde55c2e7f22c8313d5a050543af61ef4ed0cf18d3f9e18c199c5496b24983b4b092e48988a47ea462905612593e394ce6030c5922a2dfd714c938b3f038e8a7547e87010fc5dbf6acccc398ee9ee055c6578b786c48bde20b2f9413a4c02902f49b3da9a5acbcafe24506f276d24bd5984bc6cb7bca2a6e6acb210fbd40a70f3f5f4d2d43ca901eaf140e45c561171f6d82698b41dd297c473047ae0cde58e0d4135a5f7ff291e42af01559c720d45d7a3dd2f499625d315802e9cbfa1ddb86624e20baf581db9258e2d3567baad55d80f7b875054a41afb0530459b4319308b3f5e1d28321c3bbc4baccbf5f3700f869e2acfeb18b0d428467d54be929d71c6ea207819e9d01e8d6eb7db52024d46deaf0c3580609720c4474920b39fd4f5ded691c7cd088cdbb01b80fde6942070e3b062a09e1a42c70848edf8c301c28929899101333cfb2376653c5596772129896b9b1fa49b8dd6f4ee06dd68019a2aaa60a5f44b020700a638447e8ff44a958236987d3baa056b0722c11a45311bfa00a9333da989e1a8991501083704948165a02915f64f902129338cb7b32ae56322b7c8f25c4f71ad5c4420ac7f9f483a9b519c9a19e450a8d256572ca5bca183c83f94569b1872d277bfb71a02cc8672e0f233e8a10a80e20ed8f3592a382ca8d59109488325eeb03104f2f10108f4acf7bd12a0b59f90b8620164ffd112f546759bb95c20193dd52f3666c733827416c9c001f8f4bda7acd564b6de4ec39f472d23c50708ef257433cded57d37c1c91d7cf4d53e8fe84ef26dbfe466ae1d65138a8f85cd8d9ef9de30c89ab4ae35a8fbba874a0854ac7d33ea00a4f562ccebd5d7b61c67f68281e2a07e24b7c5235ebd532e82f5b77441f66bdce70c2337990c6d675ffb1bc77ff937702b37741e8a63a58bfeffea8ec5595a5fe04038906e446b141c2356e7afe880f3c2318eafd12b5138c4c4071da08d0f65a9889d22366bd2ca65fa5ed142e1375e34ac0387ff2bb24dbde5aac9bdaf32a8df2d93a150af7ad041a18525820cc00673178c8bbb7f6e174dde3dec570936b5da70f1fedb077b7046de02eb5f239a41ccb70217b6ec904e400457bd06f995438778eec95e8cec1edfab9ba743dde72959bb90bbd040dd5a7ac721568deba2f1ef490a589cc82d48eba4eb206a66b4bb0ace02c3539da7d8962f4be2455e13060c75c65752c158ba97e842895f4b2018b0bc75c6ec3a5e8e0bfc71f23c19d93c1945811e90d2e41891c75595920d356b22935bac36145455893c647b4f23ada766622f7c56f16f7a7972ca4810462c87055cf5e2fae26da8d11d50d64de1e2b5fcec382149883b099d17cc3af24d0c5559d3be0edd543922a0f67f90110c0020e241751a4aafa7e98242bec3261b43e73443e96dd3a11dc992439014936b724418b22d00d1355379cc36da87396ee1d91bc73c9a2b7b1e784d69fa39268d816a58930aae86c96c6b57ee3a7f21d5a5b9fb65ca0c872c514588bcc86f087a98cd4d3243433058e8e56dab8208ff4cecdc01254d0a17ea1081f4c0027b2a073a722965fac1851dc0197148279dcd3c6cd750a4b9593e1c9a6ff289e756ab7e54efc96daa020e24fcbdf390c1304727c2f5f285a753d983a6320fc73528973835a09b81d9730d54868675b2fb27a0dd83d0ec03517e26a1fbf559f3dd1bd00c940a30dc5cfd6f17b5ed15dd68caa952467fe06ffc5499855440adf591576492144bc201c1f0bcbaab21bd1ea23071a466159f06b25458210d5c7fc7f82369548b0bf2c284fba419db4310b9aef0167b5ea02fedcef77eb50400b1288f804d489fd097911a2c738e91e0c8f707396fe08b6d28c03b3581e7952f032a62cd453f942b78693fc2104c6baeea8f999afcc5436f07f8679572231128e3922ec8df14604a72ec95a947426672a6bf23bbbaa676e41e54c3003411a4358a67e230c84edbda1584afc79727b76ec29f7485cfd96f421b95dbf50ecdfd9927e6161ed5f3b8541b7badd1b333867df85f09ef6e062b789bd75954e3c05721efe84e289a9582b42ca1ec920e970b3e5c5ac5e5e1a74b6a11e6143918454fc900519e9ecf6810813813ec59442d2500f24fe239d13a5fc555502f080b3f55371c8f963d0f3fe25e9ef43d757885527dfefe3be4dccc7be7a124405b5f9a0f8854e724404706ab9442394339811eb141b71c97d51f7178b5e614ad326b76c3e93f504d66da2ea62d9762c1f43ad41991a9104e03cf9423d88136e40efa18f63000cd0daa8ba2ff1814c9bd2d021dc488b4489aeee79246b3cb41ab17fb83387d65d0d31f9625a16f81aec678967c7c26139a073fe86c8e7111d229ee3261a16d2d5b4bfaec5f7126f50fd998b19cbe2b9cf623e14e0c527b0495298759c7dea4ee36457d2c4e370a27a24509a30b631817ef2875cf8b8e3ddb30c2169248189fcd125e1c2f85ac6f0f9fcc0861074a58cc6b0831470143ef4d92cb6ba019f5fb98d979881eb0a49aed20df8ad7a0fe39b943917896ea4d58084061a202be7fed44b7d2465ec89c94587d17570045f3551686fce203be3cfb109ba5d2b86af9e5ae012d797521418566624931c681644b9eba3d6dcdc82cd908a0f975ef7272aceb49cbc0aa17efdcd347de0962f6372b9e0dbfe4f8e97b6f66ff98b912e0ae07ee37fd1f6a93d924ddec582fe09ae853d1550b7275fbe2ef0d1c22b10871c017f611ba580580f0342f0cbe8be0ef9639b12a5c7b435fb2a4b60f117d5e61895527156ea4f8b206c3a6ff2acb35bce1721a98e9eea3cb09cae83fda1c8bf56c48d97b42aac5e9ba84eabfbd886469a85484f859b91c0b4d52cf52cb7500c06746e3381922fee42f508d0ca495e649e6dbff8a317c1125d45ca3fc4a6d228ffc86c1c86d58c4283ef2c36b4a4285ca25b0cb8bd40538e20a151ad242bf535ea3541e7bb85a65c89aeca683773d2a8222338dc9c100a38446a69eceea6eb68889fb4a9bdb460073df1fa838e0793c069f9b98263dcce43661472066962c658d5082d2b8b2957a0556df6d0e7a34a5139d8de90b2c78c42b03d62440fc96cb1f3409a36fa880c3eeaab761c5cec6a6ae82c8a35700fcf4148e2c80b622d616f27b7c487183df1b2825113ba56751720f1a2ca3725e1116fbcbd6009c04a28ddd0a7b72b8eaa3c59301a4043d8b182885a1b84520961f288bd8005e38e0ab5ef7e1def7ec9ee06338c1e6f358e50173a9c00ee6d47c2d715e0c271b966ce031bcd3cb3f110bb9d47048bd4f7807d13302853d877ce953333c2fd5982d1e58a447d1339bdd461983ffdbe242cfaee8104d01044f7d24e3440c401187e74b05d8a5918d6888e5b2c5d4851175e3738592d111006792743986f433948484dbc4a126366be159128a5cf912466437b111600eaaf96824f061d715c5c7ba25c15d8b0ddc03cddb4c0b618d71ef023442d6a2ccba30bac079776db18f11e30262dbb30e82ba150a21a735652b5efe809a586cd36da2eeab94b4e120a04041a925a26774e8491a2062a12a7251f771aedd38f5eeae432fc6b0ec1131648c992a21c2710f3da69c39f17128a147192b78545fd04d86d3e3951e068baa7b6efba1331b6c4ee3c5473b672957deecc790fd51ec4e718d56b5b8360d9091363147494a8f2402229821a62e5a3ea78a8c2282fe876bc00611aaa9178059c13c301c3f0282491fa0e974215442393b60df219b131db2fbeb3184d985084121b0523aa644e6cf8a1b7cb5828ee0f062e06c5f1520d0292a3a4891b7625b73b0b46c52c4879cabb620434beef6f6f409d155d8f93f2a98272d32924febbdc3156e0cc64a8b24736916678153c2120647a48030cbe84283440a1941021db78082716a35715a33a3dec141024e94ef4cb8af48046c7882a2f64c63e8c074643ade8b3c7688946b3e382c52763b58ef4b326a44532277cd67f778c9018d1869ef62c78fe8ba28f092bdba696106daed3044d30fa1b3223c3810aa7b6fe91ba02cb1e1d2fa2cffea2f16e80609104858acdd93bbd36093a5aa319ab94a98e781b4a84d1e0b9b2f67b93330c03c2f13033541a08bea1bbad58edd3c9f012638ea12300298f42c8a6c77d748fb8633a4ded123e76fec70442cdf84cfcdc797afd61d922bb0222f712b342e8268427985f0c98f291e62c61d5b9cce0d1cb02bbdc15268cffb14fbee4179a0b8a4bd05c60be1c24eb5e17e3a5b0601077b119d696e9dd1894e9b0f883e8fb486bf058cb5ab7b490c7f1064047bcb9d693df854634c021a3c33ebd5e40ae2622ea598e764ba2b8ad8001f092373f9782c961097cda94571cf04b0a67188092da5092abacf4db6034e1711a9c0bf24e93ad36dc45aa4651c10064606775da04e64070490db4b99592a6b8ab276a68054dc0ed74b8b52bba89fca816af32623fddd205856a57cd03bdc078029d5162ed26686711b6268557514c19017d636ab882d2f18a37c348921298ef3b6c9d09bfb2a33c0ffaec10f08d2aeada4bf17c55e1c9368374e0a15a59eac0768f7b8da1ee799f0e00d088615d4849e3d38f7ebff8e7400f807f481554a4ee64a213995c8d01cd610a7ea3af0f9e5e4536e21f65f75a1aae0dc28e7a84d00e3510e2780e8927fc7cfa2613966382af40b99733384eea508a58d0fb359603b02c65e63ce2a51aed40833cc5a03cf19975ea632c4dba5c6d0cfe05e79133348f8d5deb5da948b0e440ed2b4603328310c889cfc008c3f185df5651434ebb620e29f83b81e6c5fd0789094f684aed537c0683215fe9d9360cc3efa8cda216df7aed4d65ea738f7e0884e6292fd96f747f9a7463c6f4f42a1c3600f081d36ea6493fbac0d2d806fe591de725250550379ad60932951dec1f952853fd290514589a87d3c84555a484c4f08eb16109a3e439716d52bd663e013202f03f92c6cc16e07deb1052258c16357d8b0ad0afccbc5c8c6a788ac4e7ad59fd22e84d306becd78a3051f881d289b03d217d2f8862aeba08a18576ee844020650d032a219ab512b98adb4e6281280a54143ce83d5dd029f140bc2365720a58086cd6eacdf967a6234451621f6e49e21b4971f80105b655b61d5eed8fbf63c3a98d553f4e9653f7d71ed0f74e2e0a0e2be94761f4c2b8ea01a3ef15b9b4e3d0d1b330071aa669f701c48d7be2268981e4e1e637a08395b6fca8f2dd124c341d3e6f1ce1977573b94b1c5f13a9d417cdc0311f528c0e67dc5c5d7f68a6c1a991e695b96f7a1d9e4a77c2c34ab7f9b1116d804a18c3ce4d9a2f100d763e31b17e57f4f4fd01073950e0d49d8bef59aefa318c097c14d7746000b025ef2040ec5e65a7d982f082902af521109429a54d8c97342389e149d24e21d90c4f285d5ca2582fcd787ca6c2759842e76817d9419f7dbcbba29c99ae110ffefc80e90511f9bbcd3d4a30a6d2f63b39a2b064c0c3866ca79406edafec13a4b9920110d74303a87765429a647d0d45776e4504459b22f2a2c251d13e8dc670f5cad39238eaa8e01828f6aff28732d9c461135238b0c6100dca36c4e0b5f53272142f8cceb9685cddcf42176781a47da4feffdb0b121325a81140e022ce0c4d7149f00c8b7733c96464d89ec867551936b1561705c217da24b461c250403ba76013df920a77add2eddb3be62047c9035d359c3ce81bb979f53b729eadeb9ff7731851353a3db51cf09a3474be1b5e924c497f2fd7454cfcb1e0b6370a2224b43762f7583a6490c99f3c7b17418561a8cd1e9c30e2526bd2301149121a0d05e5a900a56e5a8e8cbb89b52838f3b436ae118d40f668de57adad9f3f2fb3a57e6ab7e9905366f05cab7055baaf557508e8831a565a9075beb0bcd089314c21a87df12184fbd54f8e399096b5b1ebd18e0e0001bfe4a56558b58c26315ad0564bce77a8734f89bbc54091b071dbaede42f9ea1e588620a6c33ba49ba217a91dde0143269d74a06f349dcac4141bb6644452fc07b3b07885565b41835b0786ba6b00ccd40e593daccb03d2415637bac42cbd04c3d5946884c9b99b78296424201da42a8c95b8e03acaf528a680d45342671e93d6fb9a90145ba3dde6c959e6f04d2f6927983ca1ad11b0fbc867a8d9111013148583aebb46c16a1f8cedd1254b835b54741680e323ed3b0f268172d34477ecc60737eb3c73ec676210d7d768739048283c025693ec042bb5bc647f851daa0c69869489dafb0d16eab0d1b5f080a151c8eb523079b9110a04a8fb0e0e676f4ff87cefe1bb2236eccbbc553e12730aef1bb2ab1a203b764be814217da07564ebe61aea1498cd621fc4e0a37b91bf1a766c7cee2fcff224e998e80fbdac568ca512a6ae8624db9cc918183f23d9e41d96e9a3c3162bbca10f7618cca8da7ba1ee86a3fe03d1369bc65a94f544311d07935a01c2c42d3cd88c23a577fe13b6474c44a11c36634ed824067994a5332611c7d26803931323923f25460c9273c4141240bf5f8d8b4b055289665398e2705a089a0dbbe85012f9bf61c36dc48bb0743a4038d560b93ffeac979bb7831251e2b002b1f5bcda181b47683537a541659e82b5f8cf1497dd376cbdd2e3033a38f8a0460882ddb2377525beff59a7b18395181a2e439955848f9b82cf5e82076520fafdd1df179f022cbd106dd2ac3887b3cd0f7b245f9241ddcc2cf331f95b8f586946b4266ff90d13f255d315c283436462d4252966c79cfd48a7613d87dfee69c0148c9517bfd89488241371105d9c052fc2186b7df8a7edb84397bcb30e897dff11aa1a17056909970068e4bda7d1f0049a4994d5e7fbfe492f1c4a28f6c268c7e395f6c4e7e1d644d2a0a18a205be2908e4545cc6cc0b654f140d8df989d0ef1c62a93b6b53198eeeb10401a828509d4ac57257828b018a9e687187d58aee2b47d578531a72f1829738767e23990d4c13ed82a5d5735bf29b2739f87b5a09181e405810619298cf0ab9f51a113bd3af6e2e09791b55f2ca4bbd59ac2041db75ad430121339a70df77c1aaa2d25f940765e236d53958013cf68e0ab0efd8b56d0af5364e2397fdc9edd59c38804f241cbd50109af406348e027a32fa715c30ceba4a070bfa6a4becea42fac8a04e1e03d2dcb4c903b990dc6a216871ae4ccd5af62bb80e2d8c43651004510b303abe3f4e8f8b3f02510751df9651e31caaa5a0ffe453853dfb9bdfebe2fbcba26d6b9210b937d9724b2953923296070b07e0062d0f70c8365f735f363c72cdd3dc574dd3fccc7dd1c0f2cccbdcd74c96f998fb9279588e7998fb8a91535319e6f17dc1a8327ed67de1ceac5fdd174b28288c24d7eaef7dadb2e4fbdd7dddee9ebbafaeb92c414513ccc5bdeabe38d56ff7a5dadeded7368029fbda7d59a1890514984bfb7a5f9a8d59e4faf4beaaf694e9cffba273c8c2b28cf725af51e6919f09c0500282d44b817e7d28d27b0a103f143c84f950e0186331f2a7bfaf016ec6d1a76df4543cc030c67ab45a2e8ffdb309444fd9b62d2bbacb1c680e4ef7df1c114493151db068a28414477ca00a262d3c80c08a52144d92d06e86ba16727fabaf7ce182655373e5d2cc40313565251363a5ca972760f0d40eac550edbeda4c0aa504ea50409bc706a8154120aa280422a0473b3b2008db115a20d8c600a228c7881034e535c7ec58b93a0ea5b1158c7737fbdafce140987b90abe02530c082b96d0828aa62b9cc8a2044e4264a182022853a82009295182688262e12cb8121030c1c4240621edc9d54fe90a52f00123662e780b4f94c032441332e4fe498f583a4111574640842c0a2a85278162882ae8137888286e8b78420621db256440089816585c8165b1e29fa3851e0683c5e0fe11cb51e691dde5c2a2830a923200454b0e2e4f4176185cfe403e2b0476b5e4800bcd5a6bad1758b27d97b560b6d65aeb85d44c0d23190d78e032f8400dcf14168d2facb4be2f5a80e3b140f338b0763c90c10c282768644065f1e9b0d0e53059e1ec507333044c8b0430365ba468d56cf9a245e302376301191e704c1030305364e0175c61c37a41946a95e45e1a683a2730dc10352a13d86c30c0a26561d065469ba1e680e90f7732b17cb3960b13d95a6badb5d65a9171596badb5d686d65a2e5b646badfd42223c339f147ddae0072949a05488ca564a15a8441a03abe9c822b8b4389836e2253b40aa4fd3987ec89a8e4bd37eb2a669d308ac69f5092626ea17262bb41df04d927a036ee55063f80ba208e3e41833020a1b8cf84283619e18387004103598e08623bcd060c05f8e3124985c008920aa17249a9ae09b1c634868618ac2820f516a2822ca0e574429428628577489e2a52ee11da30d601940682a609d1c634954f100dec93196c41314b671a11e816d74a849f80ea17900c3487901c35891826b728c8920cb0c3827c79808ba28611a1b340b601b3e68816be41853e2ca174a8cd1022c53433d01c61da85cf02542d301b396d06ac03605750ae3298eb02ac758ac0b1c7097632cc6e5096f300d08ccca31267ba11a81b91c6332a4dc638c99f7c45ce31836644a5bad1c63b228c660b23527d57190e4c5941939caba48d9c88865b9c9510a8c853bb4e482a231946870665a965a39caa4b0ccf034b5e08a141594c86800eb42ca2605505b703ed685939a1c65524d726ca05850452647293023a06a6ac4a2a4c060304387d19393991c6553316c8901a1b8688a79c1161901e002260576850635b12d96bcf8d0d231d222cb971aa0606c6881d48509ee26294b95981c655e629352dc92433d793ece7b4f4ea5b5d219b33467962679ce39e79c9562ec421863dc25861ce2ce714687b8d9813989b1058e081ec3e641656124fba2c46e30a3a254e3a35a4087461216b02922b6ac7ea0ae6012a3892d356466505fa600698c664fee173ed9ff5d287a0fa514c6c889ce0f2c131387818b3c37fb178ce30e56f66fc9a10cb257f9e36ebb75401762cdac50cafe3c3c427020cf91dde50c6bf7b688a7ce94ce49e967a9d32f36389b33781176ffb08883fea1e036ca24aaa0199414113772c0aa10a2cb13d406005892d272944949611f232c53645e90d86203c885aaf59e0e9ab01855fb819d39cabe4c5539d42ee43fa1b807e5102bcfb771a3933ff3dd5bc99f9798521cb107b9e4f93710307b4c49bbd50cfd32fdb716d3f994768e52604fe41028cfeee450fffc951cf29f50740b1744ab9259bf7a152cfb53291a65efc7db2d296a8284c549cb5395aa514b8a9a204569d099aa34e854b038d1684f9a9416b5a4a809529324a5a5069d2966ab50c1e2a4e5492a6a5bb2157d797eaded439c898099521f30a5d17bac144edc54797efdbe22ac3ed6b7c136a610678fc897ebcfafd6299ee498577d933c5f35b93945943c5ff51c773b790d8055ab597ff56de3b782b1ad9a2906c1fa953e4c8dacee5c59aae7fe8aa0ea643381b7d816a5735b81552fab960aa52d515d11546f639e8d725ff8a35bbc309f4e6ebb2f3a3523ad8597818fa8057666b08c746e564b7270542eb46636186c6a2a0d3132ef31ef4f2f80c38d698b7510f7dcdf9f9cbdafc9612f9419eb8831fc327478183f4c08914a10272ce361603c987fedbe64fc632c0d3afe4f9b4d81c3ad88e58532b34a60fd8cf804c3623d365254248b58331e6318bcf3f5be66ec3ccb8b79ee7496e175e6562ba4a4d5df20c1ac568f95949498989856378f310cd6797a5f373abfb24b743e6bb4ca9c4a454536a91e878a4ae5e4249fe4d3930a27e7e77de1e4bcaa4acea73535e89d6fbccedcb6c928d8f633326afb5645458e8434f32b0f055fb156b4f57e5f3332b77ed3a41af4d66761312bf5521199ebd72865574da666ca01a2e4fbeec15736609f9a72451795ec345f33695edef92a9b8ff75523b38dcda7d5789d693c6dc6eb2ce36d73c6bc9c312f6366ccb74935e8b26b9ae2e8e8c8682bda021cce1cf71cc771abbf5d77bbb70d761fef9dd1e70b89722797b82e1e953b76289d4a9bca4627c21003ab5c50adc0aa2c80c01a961fb04c1325c0d808262b786087156cd09ce0558eb2155cf001df1c6549a0e0568eb224584091a40ab652d3644990c89aa6699a865b43609ca32cc90fda05b02a4759120d6420090c29c03460682c60982c1ac0354e8c00df0cf1038ec951a6020f51e0991c652ad0c004d6818109b00d96ba05ce914dc13739ca604a39609c1c65b019388069c4d05cc0325b541bb0b5620bae4e5a07a69870c1d413d99405340c601a2f21c017aa62c1302b80018671425b01b394d001d3f88003c638f0f0c528878fa37c519243fc59bc74c9e113e1454bae3d7899e5f7e224f544a492dc0a5c7c6f46f4993ad37fce39e747ef8941f4c339bdd5e0ecd9448da4bb9fb8f691377a03b7317b28586b68ad81edf6eeeeeeeefe39e78c2a779fdedddedddd75ca9feef6eeeeeefaba4fffd5749fbec39a8165c0e8c4e4c8e0ccdcd0b46a6c6c6a5a343733383239313a3032f00cd6ceeaafd77dddddedd9dcdddd2a9e2d03ab8156a3bbbdbbbbdbfb6c2dc6527e3547dad3edddf443a736cfd9ddddb8bbbbfbc3ee6eeffed9dd11c7aa47239144de3e92389040ed38a60fbd5137d06cd89e0d54d5e034e832b83c2b1aac0f7b301fb323336346068d4e4d8e0d4eebe6a685639353a34323636686cc4eccc378d85934564374ca9fee7c33e83cc7e894abe13e5de55ee7e93e7deb99d3babffbe7ee9452aa8352dab236e88773ce49a726a5fc42fa31c6f88534c6288db3b6bcc7ba63efa937a66dead34d6f2b7fe2631b85b4f72f74c743dcc3de135ff3ba2a7fe2e3f039e7b4de13ffddb3196377eceeee7d8e51ca393fcf9d3c64ce2fbaa7592d0ff1f71e4a5f7e439cf650cb9fe8410382e064fba455b39b8aebee8a15777410d3c5b88383b803e8275b239606474d8d8f8dcd8d566b839b1b1b38383d39396023d1913a35ac11199a0c0d66ccc8606787e79f86e77ddfe7d1a0f13c3c3b19643043030d6434921ab2864e23012598d3d383d3486c481b371b6cd0ba71c3c6c7a706070e9a9f9f192020190e3888e9a003986c8d580fb0b54672683900c002c0aa917041327b23e9a00b04042424745ffe3aba18777439fe8c411e6310fd080a3d4f0c4e81b846d201e8248d8e24c7f5d1d991c81c57049963c7d66ad2d18a42f6c17c67903522420c92de2fa483ebe3331d9add8c8e10e78047d772a11bea00dabed21e5108ffeac34885f5616cc2ac1527804e00f7557f87536dda73705f740996b4f380e8db9febc3e64a715001d4f97248be7cc78e1d3b3ea03bb68fdf7187fcf85ee55095439abddfbdcfa7d4a9dcbc187d2491f692d78bb1f109d68b11f481e5914c795c113c53c9430e60f6e8d1751f1f47a10fc16ce773b9fba1bd6c7da6320aadbefb70bb1fc2dce4f82b2fc6ce8b9f65eef27d6def2a40175b0ed58ee299f89d17638f119507fab0d9bff80df6c7a3796083fd1edcd7fc7864c0f2c38ff1a79174707d74264017e38e0e3ce86015a30ffde2eca090e89b0dfa1755dd430c6bbb6d77777777dbb6d65a6bbbfb1a651a3472fc68e5e3a32c7f8606977c1dd774c907d201060f398c3159253609894f602091c30815b96419a560d248228181821cca24e9244bf96489495201438b1cca2679254b8945ce24948c02634b0e1bc6c415b692dc4b8c90929492e4b09784c8b2a3c49adae90239ecd9912bec2c59b69686ea2858910f3974a3d08f541f7a9324a525a62172e8b129597a156ff2993f2d9143879ac1153a97ee439782cda22c7f1a4da4183c91c399349d64399fcca5c994e5cfd8a412832e72389be6952c2796399b50336a851c5218075c21559225a594d230947248432a4496344a8c0a750a410ee96c892ba459b2a45a28148dcaf22bac168521821c56a37a94656d5293aa52965f972a53185672586337b8c23a25cb5aa536d5597d0a434b0e2b54e592659582151921b990432d497392a5f66489294665871c6a4dda959a0f352cda4c83d2a2a292c8a1855925361fda25d648be4db24a5158726897ac10ad0f6d141bb354b27cdb649da2a472686736cbcd87568b85b251b0a2a31c6e46db11ce875b9324a525261f72b8c532e00ab729391f6e55b6a6cd29cbdf66db131339dca0362e3a1f6e52b02223242c395425d9e00a554e647ca87ab2c414a322c6961caa9a767085aa2b333e546151cd544f59be0a4a156594430e56832be494ec7cc82d31424a52e22187dc520eae9013e23fe4a2c49a42ce490439e466cf65f13ee4b464c9417151b0a2a61c764648aeb03bfa3eec9a24292d313de5b08b7553687cd855e99a3aa72cbf9bc98f61f7946597a91c765059be055c61c785e7c34e2acb0bcbd215dea22c9fe769dc174f072e1affdd170dfbbd775f9ff77f5f1e2cffefdcd7fbd454def919f7b59367bc8cfb9a51a328cb789dfb92a1f339f7a593f338f795d341c6f99bfbc291ae9b6fddd78d4a87dc7a9bfb0afeb37bb083c006e7d0eac3fa1ce7811e0f0830f7320481fea53c0dce5f755ef7d72b2233b7bdf7d30039ad8c237fa601b4af4eae391b248f4ecece4846c3141ce46c1e39f389724894a5bb0fddd7dc1180fb9a3a661000ee6be6e8e035817eee6be2f0b9af796383fb9a367aee6b8235ee6b6a90010f1134ee6b7ede7dcddfb9af3943c67d4d9d1c9c29372d161736f7356bbe3041735f73e6bea44ccc972760f096036b35b5c3ed645ea4e0545e9c6cf7356d15ad4a85c04a530a669ec0fe3113e5fed9744e9f3806e00a2ef2704fe983366e4cfad35bc99ffe1c09d452d2d7e1421afd079d644a1f014168528f19a2648a036d9a218a2c6bd3a7f429a5d3b3c02d830b2e74729449c5b8c8c670028b2fac70811355bc8c81031522c2f06eb2f3e9f3ae02f7cfaaf3ad66cd1cabbbdd9c02f7b3ae9495533d964339f5e5d3af9e953ff43527fcb489b9592919b42ae30e49a756bf5c63a6f52badb4fa7dd5d505c1a7a614e079caa1cef5a30c9a3058aef9bb60cfe1e7586bb5575bc9a1f9540b21c6c9a12f2cd2441c60c382e3c76f56c15166222921c01920e6f642247936ed6f36e8f2a78b780538ff19a9f6a1f526fd963f1e76966f7f46efab5744663ae5c7e8422fbb7093c69f9409377f46e1f82968df2f20daf76857b4334a59fd5a67ae4be450c5ac55e7e5f8b2254e4e302ad24151054b9ac8fa40186b9fd5360d54dd5df0951e09f2678996551452965ce16e77efeb842ef8e6be00d32b70285da846a7f656f2a7da7682a3fcd1362dc787fd7efb19f9c0fa41ea0be101b746059eafbaafcec0767b03d8ded600beaf4ec2bd03a61f947ee94d25980439243f07c89ffaf5697dc982fa1cbd0c50bd2a88f7d4a7de09de535f4ec15586a7ec40112ad7af1fe45a6568cab5d60f6a1017527d130c2cbffe092e44bfbef4bcef0f14c895fca9f5aba6d9a65a04cbf5e5920ea2b9be3caa3506a51a8352ae3506597dad25d651bf7e8efa3d2a8f5c9daa53ed5a6bd5344dd3ea479efa0d914d2b1a643214b144988f824f4d5d10e44fdf97bb00c7279232014f5a08c87b260c72c85ffb90278712c613bd674a4769217093231cf2e75d73190c51244c084c491110190c4d3c364991b01a1a9501e3903ff33b5acd6115865b1e948a819ee7dd5fba9c2ac49ef92f3831c9f3e7cf3961461a9c53baa44ea9cfd90b58a874201cb12c40ee400ee440d5dd29f54a71c821efce6916357b92430ed5465db459ed8526d9732867b3a717947099c925ea3f9b18e1c8f6e55022579c52ce2613ac7a4b554d7083f337aff5a0f73c09db73cf7d29d8e7a26bbef46416f9d420908ea92517cae9e10a41a302d34fc6a436bb7d394082d8cfa9689a0929c1b599349a9d506406a5ea3b827f4e0fdb47979d1979cad2e09cdf5460f7b86e6a702eb990bc22f6cc6701d396981423cfa51893b3e833a593bcd2e0ecd95396d912b3a72c33a80e0ae552940bd540cd9e8aaa68e1d24b1aa98d5a4937c9f32b9572c928264407c5d8943c8da691912fcd96f9624a588e3b5872a8f3f46f2de5a9a49215c1b20b316a179ef29447797ec0c9fb8309b6ef2ac039236c47b6cfa3c059f41498d6f005f7fb13b9fa690c386784ed55affa8e6cdf1c03873a80bc673e91cb6a8183b8907f8d1c551f2f90ede32541f5e5d827570adb97d383eaa36b7bfb3d5aa3426a2aafe53df337efa1e4548d0aaccd2670a803bcd1e0945f288df284e9c826f025a82c924aa4e0e666740aa2fa1c12b6b7f4841cf932c97bbc1cf901913024cd0a3652042a0507e223f8db2f07884b4a98f7ccb79e2cf29ef99a152ce4010d4e2670a883730fc87ba634aa15c9856e5211953c5f2e914c6e14caa5286be1294f20e9441593b9a0420e65943cbf52a9034848d4a243870e1d4d4e34fb5a6189b4fd6cadb576b36fa1ac7dfb42ac656196ed66adcd82c3386beeadfd234888e20ed5db1752420f176a7da1b596bb9f42b775b76bd0bef46c8336b4d9dacebeca73215bfb3d93bfd9afafe0b095adedba6d536d9b07826a53596badb5d6c6441f6bed5b6bad0d5bb95b80c396b5965359995cca1c91161277b032f725c8a19dcc7da8247ca98e5c32f7b2935330f73147190b48d973a8a3837408e1d1413d389912a43c9b49ae391a39fee82d70186732890c026a907bee4f702128f7b937720409918449158494e04234942d64ee7b70df7db1e85fd8cad79b4a3027ef08f853603d941cf931b33e5e20f89b5ee09e0d722fbdd820c7b1bca8cc3d50074509861286cc3d2767c8dc873916105d42b04457274b6890fbeeeb28fa698e3feb7d01517204ca087385145cacef8874cde76a2759d0a2e4cd01429f01d1459f04d6d4d4d417d77c4e935af45578245e204a68fb7d75cf751e08f7e3ed3e1066be5fa72473112a136990fb0834c87dbc2f210d72dfa3e5a78a3edcf770212ed4c37bb8e72c8eb31c60bf7b3309cf43e89c3c8a5a70d70a9a0bca046696dfb0180c063b2a5ac115cecfbbdd89ec02557294d970254713e428b3a185fc5c8e38c728450ba8ccc97ef762047b667f9fdffe0df1efdca37bce2430a5df107fcf43e6cc52f6175c8c3b3a2e46558c9bed96dd32fa68dd35463a630c0afbb9eec7b9bdbb638c7147bc360b67edcc4c8e3279489c734ee9ee1d14c6f7f9a1bb7b94ee918b31b65ad67ef2397797ee4e575d5fd841e70c911949dbdd2d4c9c894240de139b08dcd98d072c5b373039fe56a7b7a42b29a4c09b971c7e14deb824b1a56681698eb2232d39745185155dadc2010d6cc900ac871b5600c5f63ca0ec2889dc81200b20a056576bad5645803d47d9d1073e803b47d9d1132b7ec82198290a505d70c0e1c8c888305cb48a4c65473ee40cc40530485141822ba0f0e202921d39c9e1e7a41ca21c73bc3364e8e4e0dcb46c6a6866646260306b753b4eb559add2e9f1cae8137b70ff221249d4dd596a9d7148fbe6ac2f69ad9a56e79c512a658c73fa6c61b6f4fe21d3dd3fec5afea0d5fa5ad5b4fa5afd219a46b5c833a9a494ca1f42e7944fac3927ceea28943fa79c737ed8e539a746bf223eab70ffe9fed3ddddbf22d29d98c1b5cb527e3314df577f215150fd9956f516adafd5d9607d9c97e6f5fa985f573f7bedc6839d78a6d5dd034129ad9ca6691aad9552dad5082cc1ec934aa1986a41f4fd21f3fc62f4992010d1aa555a6911d2bf89015c3f39a47dad44e05a3bfe681fb5f8a3458d7eda9c453a6b2fef8ffafdf5a34067facdc8e35dd49753ce9f73cef94d2b1a906f16b43e604da39bad60a654c8d3aa554dfb888099bea42680ee24dd6dad9ae6cd1e54dff6bd902f4277a6a6a6240528903fe162cf9c4ffb25fde2cf1b707fccf3bd5b1a517f469d5694528633f9155a19639cab187d6672fdee0f6794ed7315c399dcfd4d14dc466d8b427826cf19023c3fda6abf18142318b25839c420d04c3f30faf83b9304649c56f6cc0f0f6ea8b2c50c1d83fe8cf4e95b28987e04ebc77ac47a5d7dd83c83b4d728fd88743942e103143ee4fa33c75be9f7d3cf842ecf86f249a777bb743afb9d621de2d52fca9fdf5cdd80e5c74c25bd21c42c3f3abf229efb519053534453ca2a668c7548a4d59848eb08db4dbf1b6260f921d8b201850f50ccd6d48043657f476294fbe9149e5fbd5042e5a708c4e813bde7ab4f444275b383627deb3d4ee7477717b23fbfb06b700281a76bf4f3a61c366c01eeb29f3771c1d2bfc3551291503fe6069814e5f8e8f99920a1f26c104a7211dddddddd27a5f3059ccf0643e9eede43d6b5b9bdf6360ad9af9fd3cdbabb87b6418fd90b01175b5643963c81cf32fa6011ba044acce04451901110104209f2a77f1ec1ef52603a38b501aba4a8f094b9e7fcbbaeebee8bfa73df44c090089839ce03ba2102eccf19c9a2fb9a9f7c71ab4edeaada3a04c1fbe1e7985f7d6833cb63bcebbdf7d06779aafbd25e5e10e6771cd841dc7dd9e7ace7b957c4b38f9b2a5b0891b9d75ebb3f6eaa6c1144e69e83e98e48d7ea5b7b4d192172a549b07b36835c5feee7cd01c27a064417eb4988f9d57cf0de67addebde91101c314a8647f232c2f0851116b96fd592ca6ecbfbabf426294fdef1f8132c2eaef97c213eb7bf8f45088f6dd0b9b0929d3ef4883bdf2a24bf350d0b209609e5f1824771f1130731226bbc031fdb2a8d6fb729614b97a8c375980fde365c0ea931c91aed577c41fe6e39b207f5c0335ad09075b422977bfe6a01740503772b4f7804d903f5eb013913ffe85f2a72b8c7744ba7009f2470b0e010140823ca0835e00b93f02717ed89355f00accd16083ddd1a75d0c3026ed98bdef8b7e32cabcacc06a95dba67b9760f5f5e3f77168fed4ead3aff7457de5492730fdf9ac1ff607f5e6962c346f72814d985c58de7c5a7954056cc21da104e9417f0a27784fffca88f7f433e12c8d20763473bf7c22a7b4d07d2f807450c7a7dcdf316a8407f447c0880bad3e2839b6b325c8a1d9059b207f7a3e81e7f79b2087e6b77b3806bc01a48347077d5001e28e981b4fab7da16db9172149a3a2dc2f9b740ca95f2af5c7503ec9fdb1bfd219427f90a1e4b4b20e526e249588eb0ea4d090499231798283a4dd6a664ea792823c0dcea64871c7fd70c8a1d9f2873e7d5a69add8c8e86966fa396acfda794f497fbcfcfa560e793c1ee884fdfbfd5b5e107aca21a74178793e0d57e8ad5330358265cf4060548e1b793e6767c8fb83f593da55f5ec7d51faf2b595fcd99ee56d1784fb8532dfd788f2ca0b3103581f02b1a8bc5f6824dfa71e8b4529a594521df5dee7248b3e0a5abe94a571d60a96d60b91bcecf7089877a8c44a5bea929cb222010080001317002020100c89c44251124569b80f14000f678a48544e261747628150244b611445310cc36008020c018800e30c434c391f361997bcbd7a2eba7493b0d90e48a193be6719ee62d18d3716cf93c3677544857d350d9ca7833f5d280cc5d908166d0be33d824e8f2aacbe74e9e7c9e0652d505aea601ee5628f85b459b489316d0f22216e38f96993efa75c48fbbb0795144bc3933631d9667bf1cdb310cd0e11c8955f9a208a25b1700b0646e1ca28bcab0878dd65b872d44346899c5613f898c02d267a155a10afcbbb535f91b5697a91cbb243fafbf5077d3e51356101a65d1ed1e09f6fdb983575f8b6ad9fa2c41e55a99ee578af971d97cc9d8fee9eb3d6886115780eee493bdf518401ff94d0d9d14c76c329b760917c475479be40467e60195c3b7506a3412433037558e159b52d616c30f4dfe152e8b1455e6977d5d56464b23c21d9f09d00d97c9d3845a51f40433a810db33de3113f07821bb85a3863bc62a7cdbd1648785fcbb0e0866c86e5ca00daf1ed52266645705c67ec2343a174175b5663a3698f12d7bcbcce60349f39280383912ad5f3383170decc99c641f7587e9f99dead8174b6fe625b9a9683f509aacde6472d351e7861babe137aea4570bf0a6489e85ff48cd648da3bd3b041ab70d16ce67e6dc3d93f048315de89669b074a862553856041e1947faec1479e6c1ecc89821d2d177092a59b8d22bb83c646a0b92dc36d0600e44c60d288dce74a51fa620603b209038cd5c8bc94475258050bcafcf34660ac72f524652f322c04ad3044a6bb281c293daf995a9c41f38414475422d29dded732d898458d6ee6678c03f076b6ea8b392886ca5300f9a8bfa3bbf077fd1e5ebe0c5822362dedfb6a91cca59671842f37ebb0b27f8ffbe713d8a597f1e68500601e10e7516e745e1a4d854a9ab722329716cc35a674f0d8fe8f8c427c8815839a9b60924401adc9f555cabafd7ff79579846b94b269d003e3e12fccca71f66d62e04f598aa4490cbc8e1dc2d20f60f333f72a8d48ad7a75fb3018a4ac30dfdc0b000aa254e02a802547466df862b5885c766e0afe08cd2d4e9e9e5f5df1c1c52853f428ba658c4c2e86a2fd4bfa9b833d46ca7c9d6dcd813cde83349af90f137d342f9781f74eaf9ff36021d85e558ec435611030995aa17c80bf127a00815bdcb0379ea39cdd3d66dc00701c4afe00e29300891b69b0e3f64c76fce9615a38868d893caf3d61384e8380b0744a745f6dc4bfe4d6d96eae01e2901c32dc8851e619d5a19ba0b9600331981df274c6dbcf46a131661a5aa559d6c08de516ceaeadba4e556da12eb8f4474c84b57ad2798cba4a3246019589f7bf0bb17bfea0d8debb32d70b1ccec6f71b0e6cd325d029595a9484e32e0998c62e9e284545e4e9dd656672df457b1d581b080e58d2b7dd24c15d2ba949fa1f6b5bfe6c717f0aab7f87ea2d7753db6ddae13b8b32ce80b99182918d8502b1eb885b5d1144fc697e0d33e940c00c5d1e4a3ebc661a46b0270a1ff8150de8460ac883acbe685c9072ba720c306a465f177183a850f7f2d274c853e614b8cfe0f0b662ee643aeb2a6d772704db73e81b35ad3fa6e38ca4bb76fb69c42b5b65e67aa24e2452efcfe05b5eb63e50db159266d46b640e0b7ceda28645dfe81c2efb9323401daf5e6a5e582b3a2fb7430b180fb53662380b5a97169dddfc44091b9169227bab16bd9d21ca981698424fda52a3af2499265a5f58b7134fbdadaac295a6a3b00a77816a88fc0c0f522ece06ec7f5aa7daddcd5ae5e50e3da67dec345fd98732232071ae02163a6fe63a98a102352d77d5772d07f8a53f8acdbb7628c2b58fc2ccf0f091be044c923fd661f62e232bde1c561e55f0969f598b415320fd5430e1651250cc8716af5dcd96aad69251b65b87b61b1d39d3eda3266c21621a93eb52a57a40e5388232cdd366de234eb86caa8d7340667cfad9b684cff60f6a15e663c91f87f8d9e66a809c2a6319d1a4e3037d1ad5e2cdbdfa40c08d09fcf0e0ea591ffed6c1000236efc60de560c0dc1a7402bade08d955be757f1147e11ef2ec79da189a9f35ca0c62935c295a9e9c40263ae4abc816a19d137e65060ae4ac37f8c02765c6d3e50e382cfc70419ff0b28475a32c9c7569adadfd6a35188a8cdcbca9cb05f63fefe0223d129c68df4b1a8c5df0bf72a8922e0037e21cfca667298ed5c54a0e341d037568da25eb3c63681c2ca15b99b617e9ec43a0416fe5415c0c7d3bd16a83d7120c2b2c5c365cc7813fe340368cf65ab8bf6046e80ac56f54b3630ef91f3187b4cadaea587ceb55032c66bce91093b33f3815543e9816d98984a8e3d2c05c88adf63a5c5fc91b6c2383e41a20290c76e4072ddc83587e5beef8a94b26a4ca79fbe5453ec57103e1a767b08f95a675e3e7d434e63d7d16192a0c07182a59aba1272b2140f8cd653119adba8c2e54022021ff2d70159843d77b91152e8a32e6c89b8cc00dc7d8daf2655cd996f4dd6a72df9e84e5f14eab670bf0b711ab92aac7fa47ae28f044c29c0fbad320d1ac585e16a90fb5576129c8a564fe59581e31cc19edd28647dfb3607080c615f4dadd9bacfacd62295b949f075fb8704e61cc32594ed24eb7d850854b0e26a18d734d5f8e0f7911735c83fb9a2a1d4ae40598767fdbcd7881ac1d93e142b1a1474e431bd6ab20627926e60ebdc89974a2cbfa536c71833ae532cee6add65579dccfc776806bd8931558731f95167a1ef27abc8f8a222e5b1f81048588ff5aba31796e447376b702af5a84ca23ac36996abbe500f410c953bb9341cd58458571cc0347522b4ca55e421b667f5e88447572dc6cd7220d52d98b15c507f063838405328a55f44afc7d7bf97878d6aa67ecf8b855e970f2be48327410773bb9de6224df74cb1e01d3b41c56592f9708b1682411aaa4452e2a76211b336e04ed8d997884b2b5204fcf1926c529da1b5ec8999dd45eaf82d7110cda977f96b65cc06b1fc0574dd3e6ed6200d3fefb9cd4883d50e3e315bae9eafb5ef8dcb293e32541b725ef8db6247793820da00cc005fcb74c3c40caed3db6f819df46185d038690ef51184302a83ffb60729fca0d849d87a19958e751d25d850a19b1440b63cf62c20f2decd534747b78186a1a14c282efd044305e324d931aa88d6aebd76ebef6c7cf66656dfe474310618fed56a774054dd402b138b19f9c4189bff57c9bf2172e7c6ee0a509c00b690526c003bd2b935db6664be04075551c05fcd9124883ab29a8ff44ae6b4882d84bf1124d6b9ad965bd89cca07bb15ad4f7e44cce4e8447962347c117e0bdcd51af49f814d14a13fe0766ee92605236e5aec0e42be6c22c008e1c763bdcdd538f52aa4da94b6cd6c0ce6eccc3fc0e67b26ec41d3ca0f7db26b53529c330e574e3b16ce2977c300ac1ef168229866c24477f3230ce2c500146f27fed8ff3f24b9d83a081073c792fefbb1e945ac2821875ad82402006cfd4760759d5388a9a63d011a1bfa51ceedf3d29f7f3d6256528c6b945fadc0a7c2b4483410c121fee183954c561de891dc7756daf2ce4832aa4969c3ab8928912eac42d1d82b0fb15bab232ab49ac14e6ce53c9a9c72a28a59d1225a56c5732ff0c2fe9b98f1c3255dd06721dc60dacfa99d2c8f9ace74bf8e05e6327e7a03c28cad4d53aadc7bbb4c97686996437a65a8a0dc3e9420bba613261475904052d7d221c9a4274de790fe2d1ab6ed254463f418131c85fc513745c1e70ff1255b6ca9ad3dc06ae87f1acc9ba3140b7b1b4ea114ca10df47517e0acff868c673fe40c151372f26e4af33fbd167f497d0de0eb3bb4de4b52418958128b16a55dacb6c75241219f0a8f2f2b4d52b0dcdf18b76f207b123370feb80753c8baacb42c6ac8b357ca760eb7994080ca9459540b20f92c9e08bf8e25a2268e1b1bd858a391f8d5e3c61a82651d0fa01ccf12afec14a9e7ab432da367dbc99f7303e9b0342e4b396a514b4622268ab37c947717d639855a8a133c0132ffa7c29b203470fd80a03c8559e5b327c04c717fe944063a1c427649b9776c04d94a47f365f7d42500478c40af29f2a8cbf44a42f70b66f435f13f87f8bb08bd8e7c36d71260cf2684d3778d10344e1b0290c62482fc732099705175b3b9ccad000f318a7b3a753508e29ad999e1d35b60e5a32eb74fd8e7f8a0d38258ad6d0198d9251cdf01bbabe0ce9fb283631276a204cdc40c7fd9175eef808e2af2cf2e3376197745535c8d22b6e5d3bb2823316924ff573d27154e223a53553c4bac692565b08b9433105bed4f831fe590244d8da49a611504b5b33fab0686033fa5626ce5f262c3cfca20a2bff70bfd175e7878e5605b5053ad8ec232946527053859887b8f3ce22a4138d2298e6f306e4306928bba64d564af8ea8f6d5fb929257c7147d55c667f69f5f819136f6c7702cb887180ca6c7ea7eee3ac52826bf34e8f300d5eba9fa190495295a006c454e8349fef369ea8624da71310c4586bceb74defc9767f90f0c2e43e5e9d108a42f293c2a0cd453bc02293934ae39ab549716196942034588198a794bea8f99bc62f597e9cff675a548b49f9586eaff84a091b412991d043ba27485e3e33f68b4b84ff30fdbddb199f0f89a497d05e2fa87d75e986ea94cb35c8aaebaf5059252dfec6b8272df539f58435b8b6d02037ee0cd18449d42ccf6b8259c8d5d06708404adca41b165a5074a0411335344d172b2d70c16e35ebb3dfed84fd84eb2a9e0b15c2454e47240082e31296e6ef4a564120b388815e80e9d8a7b08c59b11f4b2173fd60a877aed2f76754d3134c10fb2b6a17dcff2784f42c46efc38fea3abb185ef8009085488b3534ccc54f8f32654fcbe69ffaa44252de248b8e015c2ecb0b33b13c5e5725d4eb1d9e8c41a32c60e96c01b6301bdded900196b96d8eb8b2b0f7b3ff1cada57e756f0a8f8ace95e013250ef7152743a628e2660dc2ddaf8e130a32020aec35743d7793e91eaae93191f02c80e21d710646231005f1026bd28b2cf1eca53cd49da9f7c7c04313632f00c5ef20a8fdfd842d3b8f8aa92f18220c2308ce242ae843a770be77508ab54eb5d26aacbe46ec4bd6caff45aaab4c1fd068d70e71672638cd4255167abbbcc53de4478ed439d2b5361a8822aa3caba578e1bca5dceb0f7e51c2bd8f48ddb5feed7d24e1338e1f4bb87aa73042a05fba32f80926cdd72c170f1ed080b0a61ced24c5b2fc6c5f9ede4476826fbb93ac0b206f7c52e7ce0b0fd1f052ea078f935610948d948936ebad7a83010cc30b02d8b346a4b696584d3958664fb5260a4666b0bac516ad87d96c7198ce807f797334fe414e3d0c1b94902ecab6e4dc695188d8d4c39438e43e51f240a311574ba168e84582be85923b436b947cc348b0104afc2cc2b781554e5ae0a5fc9ff81313d494bc6819b0a876d94c385ca9dca2534fc8c958f21408d4e6dba3ae5a270627800ce2bd6415e4a0148ebdbb3e0304d96717b8716377bcf52e80e8557e33e5730792400af5642f5e701b1b16a2e243dbdc8712f7a0d3d7df490ae2c24fb06bf20fcb8b8180a58e0121c723c48242d876ca3ff8b5f264a3052b8ef3d6dff4a9ec47a0ad86ccf8094648afa713bd353d43fa7457dbd3c8c2ae3891eff75e9aa0d37d5ea8ada02610e151a4092c19310896ec668c5d8286c820d0ba4935ef11e0aa247dbaf18f80676fcff5ba36b2222c3c91da9614202711b0251f0ab6a1975a71339c561694c4ed8cd1d4b192d162c3abf1ac852166482eac8c042d7b47339e1fafd966b6284892e300395850561cc708b967853cb9a950502263e7da0f20ea372b449d8e69636fc9f10998b6b9c7074daf0f0c2cbc3366ee3842aa9eaa33059d60d13fa58620d1e2ea2a81ae9b495e47a60b39802074485a0ae7378b0ebcdc3e9d060bc31b0df7d63bd791f0a0658a7b7080f380d2651439736b2b6451d6082a842448f8a8b9457d75aef893d3dbd7988670b4d3ee2e7f4565d16e787b64f944474871d53f0b3fcf64069fa404be7ecc68cfa0c836c0fe0005230f5d97843606f21eae376f7930b670230af735b1d4e40da2dd6ac46e069e07e7490c05182a199f3c419341a1f089da7d2ee1dbf17fcc859fa313a79e4bbad0db1dcb9680be8a5305a7b82a284fb5dd65b5b7823f8bb1bd888c78caaa717974d12c762efd832b8da3a2bfefc5b28dd04fe6a255b2e9427c90ee41b760925fd5395b60f2e9ec94e8b481372a77f87ad4249b705a6d5075f78b245a4ca1e3aaeb467700c00b75b284fa21d4a02da71a134217694117f9385d2a4d8f133fe4e0ba509620713f91b2d224f8a1db2ca7d61e2d7e11f0b6516e777ee08cf2e9458d50f4094bdb8506255bf8d297b6e81a585f48bb17816582ffe50d808b338f18bf392ba50b612bfd849387616adac6feab2e3b36e80ca827b510bb3f84954ca140e910e341b25a7683f872ed241f3094be6c83aac127af07fc36b657206bbe8f8e4db416af85bb00ab18e1fa038ae8a2a36f61401f5f62ba24e1895840d1d90c2ef70379365f8816fc8eda3b4829444e8a46809e58dec5e7973877702861ec900e983e8290225df0bb1d7e687818b361e6820d0743d75fe2c1a69ed2a4b8e4dc185ede1ad1f3e40acab1d731ec5d70693c49d4c9eec2fcdf95eb287c8e396ce258081f0a6cc52db694e559eb821a95a806f42cc2db547417d9e82edc2cdc88156053099e585dd3b9b087f2be55cba7f7456e4366133ba39983f74e1d60c9275020f57985c1179202b22220c1ec63f32d567a0618747796c73d33c3d4e2edcd86db8872250513c257c8de7789c213067bf80f518df373efd1c23707163231eee418eb2f8a324d48cdf15a673564f50123389a3654cef61731228c40a5e64314e5778ccd9d70495d879272d63720b3b3f896a8c5b4cebb1bfc2e648501963bd8365cc2ee1f393288985cb6135a65758cf9d5a0a6fdcf8ddc3e727288921ecc32aa7b7ebcf2e71970749596dfc43530a64e1ded61f096ac4266a663ff7d03fa32f1825e16140447ea3a85ec96b34b3b966e8b144d740993e814b3f69789b21695769acff881011287cf2365108f7db04b3dc859f48d3497d14a0e74fc83b26d9e2f1ceae71adc356fc2dfdc5b29f2528aec3ebc97a1b9af0fd69367b6b87ccecfb451bd29b300285e0671cf6fde5d61c0e523425d169647504f66321f1ab86bc16ac802c1c96411aee09f74f01fc2443f26a1f749cc3e9008b6b45583e400cdb21beee2ab42dd2db6dc7daf3cead0907377ffcc76875a863b82c51c0481a51b5025bd38c3c68296b3b9ecb0fcc325aa5c3ac685bd0aa046d791fcf0a34e4f9a8667113e172eb6ff0f4d2667a2a593a8290fd1868a7f1fa6c4ab40a04a4a0ac2a52e9bba7f61541c775260d9709e026bd799b9725ba9e197492bdd859fc63c138ae15d13906064fe7847c61fad1ea90de09364db7d1c492ab000214fa15bf68ec061cc60a9e12b5a86e0aba16f14f59ea9c2ed0340636b2c8e32f9063a5a8efe67c22d5ccad4aeeadcd172cfe07f10034b286050a8ab62bbff86a36c38a3c0f4da886f126be3e99a7826255741223e53c4dd3b8e866fba3ce075d29f126b8b618b8f50c598d3c2bcf21893cf31d6185a078fc44d448a42f1eb085a834417344811112fb14a206c8a06945d8efb8963662155ec14000cb4ba7699d93e6fe0084332805c0c0274124ba8bee0fec068fa335ca7be3b33a62846ae494e689150ceaa0af18707e857de02ae020fd91c4dd42e9c2e8443986ba274750db27e4a6091e7bb0cb37148b3cd71bfebdcff29e1b5920dd073198c1f78cf705ec3c5e3b2f0ada31329a449d5c5cd629a87efa6b777dba6e0d03c176b90bbafd17bcae31795e29ee8430443a39c80a264912d0e526f4b831452058d422cc3e47ab026090c26457c0d1298a61ad624cce158c84fe13c3ca7d0b1d269b02b2117f173a3dc9808a140e9055bc17d3476bd0640d7ef6b6e620e53194a762a93ec3bfbd2ba008e961fa392af7bfd43740e7573d32692640f13bc95c2f0835dec602ca35c2870a2f55530a0a603ced2143365337cd6ff00fba9723bca2c833b76d2b60ce6a0edf87952b8fb6ea88c1b42b07bc5632c420f4bd32ffd22ed28541a50a3d1bfc019b66cb581eea2f4a73959f51702bb86e353d54db58b802db59a4eeecd5d7ac20048f503de619264e8e966713e84de881880c9bdf203768abd264a23f0dbb5127f2056493fad8dbb8eae3397d1eabff4ff1fb6f3e1b907b07791ab9f1c564ec130037e4fa0ae405c8dbc2afdb4b3ddc3f14576bd1b5b0651ef4b77594224f3021d13585dc0dc4b5f29c3f875b3f3b0fc490c5c3a8d250b9a8dfcff8bc4e4b5cb0ee3058ebf5e32b428e13ba7698a18172408eb4555ceb56f11f147963596f58e2c196e3603f30576727f8f876c21d8c17a4fa558ce104475d54de8f21f085335f6801d6f6dc6ea8d23c25b2231718285057f4cd1051e8c9e2ab99b415bc038006ec9ff54a06051695feff83f6c1ead7fb6f6a0934c7a98f793fbfdf2bc5d3d6a7a7e4ca1f36febd5ae2699653c50f2ece0f952c029bdcf21ca768b582596022973456a8102540e56a22343697cec3ffe00972a83b93c4079f08ed5455e0cf17f2ae43c80fbaaaa670fe06669d0f46909cbe3bca82aa851158856a5fc3d2e0e000e51858f29a9746b273790ecd583bfd0b21ef123b4d9dc6d5bfdc2f728cea205869bd6e0e2cbb1ac07d574d899ec8bc623a475135fc7f261a747628ec31be51844bf6dcfbe00e7a685e5352bb233a2e18fe5e9b5c7362ca6cac24c86aa68847008cafcbc0ceb9166ab0797db47ce0a2766f552b83a30f016079d9df6c75536be77c5270603c07a814cbad26d244e747cca1b857ddbff718665769e4c7de4be2381051f303c94ccbb6a981f828557587ac776a922d5cb9ffe18d56ae9da9e36eaf5dc596bdb095a8e644ae323f19fe09fb54d0d20eb4bcfb9f96033b0b426a48b1a7c3b53ba85f195accbb1ce84cdad30612037335301613679105982e9c85a03fbae2f514273bfe5cc52e0584ea412ee59bd5fe3eb19c1eff2a54ddcdc8914d91c42228c1e4c6dd00f439d6762e6a1ee1c0f7a4f9f802003514c2eb14b1cbac67805efebdc6f022a00d6dc6686dce31a6951518abb58b5b928ece65eb3a5900f367d2ac80419a2ea176e4914aed075e3deab5baf4adbab23cb2fa3f3604a3d52efbadec2f3de61765338bcdc313299023519f68876125095ee835baf12b8ac78f8e44beb4af0f2d5f3a0470ed47ea443500ca171f9a1581e398b6b8f3e699c48f2012fa4c7be9e65b08c356dd79e7aedcedac0c0b88d52c7777f94ccc1dac343cd0a01402d05e2f66047445194444797a33b2c2fb6d792379682a5e40fda816e6fa5e8690ab64843cdea341ae68ebd0b9d2331db649f288b471e91d284e55ab97c3cef116d7169915cd497152b2c63138f7b7d123e02d27531ca1d6e57276e024f9d2da4e5eca3eb505106a209328babc627889c276564f3b70499449e4f428bb7697c9a12cbaf5d412a035cd745e0ec18e4160cce3d4b9c98d08196281e321ac55ed3736edd9de65ae2aa77a70a9b35f5b97346a2c3bcaba248713f63e34080a45dc485e460261cbcf68680010a7c622b5034884ffba666d0a6a144cc1dc41057c1e302068dcfe75aa4e4387ab1968bcc182d85ebd4f0442463c635fb87e4efe750fdb9e4ed188b5a22840b86e81e0a28bfb6b9fb69179b1423ff71e5d811ea002bd61ba57564a1a2d301b84e73e1b27b63050c59bcfa2902a1a43c08d4b5507237908142b0dbe09d55c495932dd7d65dfc9a5e94ccebce0fdfc33e52a2f8f7b5760c431f27fe5f3db93cc1ffad6397b9552208a5d2fa6fb5f0da8154344e1b2646da33b1e1e1e96bfad59a649d681281722018a82cc29bc16e35807173e79f71d4f0c128f43fffe2dbdac1b75d708a33aa1b8740b54ca1b5752c9e7fb4ad5f01f6f5ce99e755f009d597e60dd89acf88b1b276e2d5dd570ac952337dc22e36e1d2580756c01d8c0fa4a8cd50d79374ecc9f05033f3af2dc06ecb46c43efcfb90d4d0bff16e2a562b52920815fb6d5a2934f3400db8052d4920eafb503fb642e5fa93a9799bab554d87fcfb2ef98c8376059aaea98bde6d812036216e06f08ffd3e3669462b044bfaf5980acfd263674711abd0579928221d5a07ebbe00d10fe43de63426b1210e920fc0f69ab01755d801041cba217cd181990d887940e6fcf57aa09828635ba1ebd8063567de03f5f59c988b4f9b967b6457bf605f6c83a0ce4988f48d967ae88066fc01dd7d5bae48a402b7182ceea15205bfa847ebf45ec2c3ef7779331bfc4b22940a0dcb98f706a2bfa48dc4ac00e6b5d94ae0d51c57e5010ae8f600b7c560b0e9e16060469e144a08545390b4de1b49025dee8f5772b2aafead8babd66382e92ceb6f49087a592a01c7cc0e65081bf2feebf83b8dc98964bb81f617c6a6bf6e34d4386a8502e0a3dac0ae20a5e8f4d509b6b34790e54b8cb2932c298d7c3c0ba82487894ab0e50753f9232ffa4b9e86344fe86bac8ce87d843ad9c069ac8ff575a88f44d471857742a86bb1224ba2849c15a0f933cdc42996cff3d18e828c71a1555df1612a28edf37aa011b87697f63a3df8f5d516d8171fa6fff1d5bb1deaa942b625a7a0a13b718b5633d89d6d33c3a065ab7eb365076831b698d222204392b236880400b66ae847836cf5be41978822e3c8dee76c1294d7f25fcce2017be1a8f847d701441fecb2ef81054732537318e92c165cf2ea06d31088d727cdab7348bbcd58b9d3b148ef18c97265c653b37f0ed092e1e6db9babbabf69f53c8e6501e0a4aa93c3b54aadfe6b2662b80893b8ac5b50602279264c19a3e51ca4bba32f04e91eae2be6336d066d6a78aff2bab1156f5e199a6f99ee7182dbec82fc74d13e736e8ff2b8ebc2b02d7b4a0c8a95bca4258393133d7215886a519aff8989415cf91081cfda3f412eb2c1e1312573aa29ad39d8873bcffa01b5add86e4e6ccb0062959d4912d2f96c8467e7e0a6b6d7427396f727f8533ad3a5c5326920a87cf51036349a2fd5127d0a764e5f74b14adb4fcad4cec17c6f54946a3d960f2aafbd3c0c451f13e441e03c4b02484cdb220f1fb29f10a2c07686bbd6100a11f77e511ddf60d4a5f781a2aab36a410cf2854872a2b4c5622cc5745bbfba790e4885dd2925c22baf012b4da24ad33396c94759ad60514bd96c2b71aa74ca9d99a8725f5fa43b029d34ca2ce80a26b7cd81b268a4d3ef5de375bc525184a7c3cce51db86f925a0c3a8977bbbbc93d309bb7f3e9be152666deb390331c812defb5f589a4abe914e070bf0bc85f1422e85157647ae964e584296cc88721fbe6b0bdb04d9294838346d1ff24a3ca9838a5d7e098aba0c1a98dce888dcecc6623fe0fe0f2210eb080a036a713a280ee3e5a0b7a52427639713cb8e2cf94bde73029949d661aa201913ae447a668f5b95cc60e72771ab5822a678095aec242dcd25d08b97bd0dd99625b9b14bb26439794358873982448cf212b4b24b58057941fa47711634fd7f9b8d445bd70d1e84d893f68566265b7e92ee014167bc0720593ebe8f3e011601901ed3543efe1a4a5703cef1e3a5063cb72baf279d0806551fc626917f9288079fcc84546d80bf310eb0c9357ba0c3a9e34daf8009e00c91f5bffb5237803478b94d8ada2e150cc8d232d038f7696d196669fcf7df8fc249c10f5e11a0fa8f9be8c72a224c8ca5be6f00028e4bc533086105dee8f10e5fc96f36b5fd8b008d9d5fac08f4fcd4e592a94171696ee5e2e42252486eae6ab636422f0d302de802e983cea6a473215b254e2f888797084bdad2e5321808ab7539d54b51cb1340ff6680848d04865857872b48cb34cd78e2102563c349d0d526c19a5c122d5c495a6d122cc94ba287f97c92efa66c0b496eec245a934ba09793a48d5d9235b904ba7292b4d824592593402b0f7f1b32e5224ef4fd110474ec3804647e70d4b1c3a1203b59fd2771ab2011535e9236ec1296661268cb4bd202f6bcb1868408f28ff119fb36b01bfc64c440774fca1e3a9e40792aee425b34ea08a0837c766e2df104c6718781eba15fa06257d2af105a8e10bf40b8045b1024e0142c7d3a0a0822a02e20d909eb296bfeb6914e141a5f85892d6dd7b514d4ac24d581617b19749807e7c17a74df42fe28e6aba0b94181166339faab2a5618f3812d03f7c53935301fb1907c8fcaf1a53852b5ce97bbce4137a4f7244810edc3158d66620587f91635d71f6cc8e741fa9ae7677c5748c7ec58a3ce06ea833b6a500cd80f8f4ec29f98a66aeabd3b96f3954198d0357bff0ce7d325dae79d993bc1f7dff79bceb53deec7d50ba0fb4b6c518f60ee794dd3f5cab0e211674cd32a8db34ac985494fe32c53b46e06725e7c7ebd9e345618de8be11d361e28cde45d8728d9b86c3f92f6f749273ea6f5164cab6eb6058a8a0f6f1ce60b2cf55747afdd4101b2e7cc3baab2698cf61178c7ccca6486963d9cc392429ec1521c48bf7772a1b1125c2acd1d6814f61284cf7b96c97172294f5f189d38b60f0b05eb0fc45e28f6871d814aaf78857ad62a10646aaf91a8507e6a06d8a365d0c2da2031d21aed04cbde0c3e67c66a5124d34766c8d0304d1e48b138a021d17f54026a0537f44c50bc25c751052f82b164631306d9ca47530d280a029b15fed6e5e27c4e2cec1221eea57f853c72c3c8b8fa3fbc4f4174b5f64e471bc59a69fa3e25f2352dd6c17066708ede0040cc50124f37666fcac0ea123314931c0868933137eb05ede6e16915a44ce03f2562afd241fe8cecc3966281e40f900cdaa208a7fb5d431e835e2475c2bcd512f9643fec613a910a8fccd9b380eb098e2c6c5855e437ff7d3cff6c971d6f55d12f3ee907cdebac7758de9e9885be188d883cabc34a8454c3b0dbd56964c254c3b06dea642442aa617868d50d405a7d0a3766ae46ba6d8f80fd58ccc6509d364148342dd7a64e36212c1a96d5a64e9b11229a966b53a7cd10160dcbb553a74d40101119820e5f17400636b802017eb28c9e81cc61a6e54b482ed6f5dc4c31b98701064854936917a74d0a8ba6c5b48b93cd8544d3e2b40b380c21112ea86157986de2b09f4b8768284841638ada490c487504823674b5d19e486d8dd88b41a9804c84e705c4e7dd3dfd2e91be7d513b4113333de3f82e93670b7d9e9a7801fbee2e4130fe1e0740d31a16da89b5ca050d7562c1602c44481b728e0e2ee811aee4107ab2d20a8f778361e0851d77a3cc56a8654eedf9367a51cee7a14f5b15d5c7e7bce7160316c54155cb0db3ef28c6b5fee7b36ec0697a401d18062aaf2ced4cb42a0dea52318e95e27a1715c6c56af75f82315c194205f37a4e3251dd663068a2f71688ee07c01b0d864ced9528bbb565b04baf941836c7c52a94ae23db55165d1934c4e94217e1985a8e90aa9e1ed62be3f6bb048129f45a37a8ae3c6a1f30529f024b82f3b4e20f40961550e9cf4ba4b1af12a3744050a1363041de5045006721e0faa261e261a5e34bf7f6d2b246549c9d0fdb30aa2bf85f2ae140501bd63d0630df25463f8281e42bfb17149835f49146fd9d5de91669fd3d226494e0c5ae7a4397778c1faa231ed08a7259d27753745a8e779166d9fef41245c25935b278bdd972336b4a9e145a516bc824c93c991a35110d621d9ecdc24a3737434834a73f0fdde9b3f7a497f8d6a0e6adf1ffd800da83fd7e9a86e7dbf2fe69e3754a8c37ea9827fdbdefa920047fd200d55268426203583a237d88aa34c0b535666b2ead292acddb3e778b2dce066152c0843e1ef82b8d0d631e9405ce28a1de05640c05c862420098c726af1c354e72021b3f269493dedf6344da1aec79a1bd7c94372240873929294dd2c7b1925d4e834d74312ca1118dba5e9c3c0f8a2309c091822c00336e161d4c01806f09105e55e6c2820b5642f3c0d0732cbc531095cf7f6b20d24229f9b8a0bb8feb3ac2d24c4dea7375b43575848ac1d0e44e1e1e46a9e8807eb6521a473e640a317e6d03d00fe9b4844db7a2f14999220b9c7d91e73410230b3d61bd38a150452e641a94ab4ca33f35d85d6f56d9a76481eeb2391cb727017424345380410f61b073c238cc71e6e8bef94e418e711de182b67a67e6d0b9eed419f6070b538d5698503813719dfb4372029373e6fcefba0b2b066d411fb299f1fe642d56a50164bd99656eb57f33067228e7634c7019fa347759e596200681df6519dc56c00914c892b4106b0b39a2bb5c9af1864a5f2fe825c522adda3e1176ff14a3778b1ff523eff9e60abd32f1fd0ffa6391db9cf06eae5e32e52c7bc11d82ded8b0057ccbb3fecebb551c373a44ae3d31b1f7f05f9f4736a3cc301d3adb4e0caa60d39a85464c597a31d683409f3cc4c7f98d3797549f7eb1560aa7425c9c04a50f04c8299028741264cace0235415b2317e384f20747153fdd740687a9c7e2bbeed6997d905de6612c06721ad9bc2c80467d88f1bb2e3a896327c0b1aeb7766ef7200a9d7216849c4ffcc19d7a980353790a609ed66d96751e08aedf4129872b6d2b987b1ece7ce021b887f18ff5f8bbedbe0489463bfcf5adff29f9a8d119e0711153e783279f9a74204d7ad49653c333e4c73b9e521c07730330b20b961f9826d506312ec97057d9fb8243dd90260fa3aa76eddf8227b53c3cae62b7d2c5d2d485064930762fff1706a19eb56d31f1bfd766c2e0c0d937fee3e077db2735dd2d837b7a0eb9a8d21634813b56b5078544cd9ec0996e55626e0224ed4d43a00bc996bfea9a60d6ad373c6fe5cf3ae9fe2c9180d5c7ab46cc8a8b09197743db3b8f920d379d6f87a04b26ed8afcca413bec11ad88ac6c0bf78430f9a70a158dd7a5b5a80321dd12be7553e8d18f534f430b96d2d8a03f5aa0775e70396ff5b6c5eb6d3e57121b8eadd7cfff082162c3b9f0c085b6698b75593d8596ffa10f3edbadb0521a39948c0d009d2d9b9b75e1dcff7cf243785a996e8dc89b2f34e9bdd9501ee8e90ff0a6efe26d9da05a8c0e96191d1bea337d246d2da78163367e5bd4ebd4beb5e386be7319961978e7438075fe9c5811e3cf5c79ca5e68acc6e6c7cf439075227816aae003b251d6e97aa0a47b2aa41a470103d7abfcc12ad75ee16e2fce15edc100deb8306d4527b862000a6a06fe0bae05b91da3198eeccd1a938c09ae671fd4ce942701317c74d3b79330f58c83b94201373a34a0e937cd095dcce0457e13b69d3c985a9caceeece36e1cfca4d6ff10dabd79ba173336e13746bb65282d6854fc8c744ce49fdc472530de4b89df9ff53df0d237720d3d9d4172e5cfa78fae531d9f2a57f5083a2ab148271ae446ebec4d4ad1d550d11686ac3d5aed435430ac8a6b4a99fd98839c91baefd021973f2b79a65b333e6382ed43968ac6702b0999d5f58b2a38cb42b16d8aaf42016273bc518b6f164b31de754002a579f4b8ece12f37b4adf98487235cb1c189c97b33d46496e1ee10e9137b4d1e153fd336f59c48a95e909c35cabdd97d3416ac23534c9102f1ac33a0628ee442c1701d1a00a60b2bdbfdd019c03826e816bac14bfcbec98cf9beef1b04837220666309a533612ca134a6c61390c2dc08279494c260bc44d2198c4d280d531bd4c13690d1d8087418b466d368ac7186cf303696a834263890891124340cac7486f6be0d313443f197dd6c696bb7144623894b616824c10145e0d29859078412c68d2fc1227fd7dcab09c755bb291cf54c16202216c6e4f9042890aac504f19aeae96c2186d5803f5b6ba83de68cda5c936a07044b1902353501bac5064734ebf7ff68d8bc094f0457a3a870b180ab54a0a3a3fd1664d4e5dbf891038fc32cd7ba59c9a17913363ac35e684a928f28c04e0a5b183176a3e74c3b9aeb33b91eb1795eabcffab98f28751a61b24cf1df61abde483dbb0e3656885ec9af6b3cdc2e6421f15448ff08c889c52e82afd28135b790e8d14edeeb34794f9769a5d42412970850afed006935a8121c0e4a200cb0d4d73b89742d6a233c084a705860a9573b25558b2ac183e004c3003eeab51d20ad065582c34109b4e252a9ef4c5b13b50486c1460126f0a256ed489b36ea04068352840d4c6af58eb4b5a20e218320a0001b58aa833ee55d2399e3714eff612e6cc0d2887859976213a74a29009a7f0035aa6ad1de65cbfd53bde05bda2c56fb1439420ed2dbdd72fab54ff4cefa9291a321abc239326ae5169449ab4b2133e96720c0dbebfbae27f75d8dec954ca1e25d433d611099c6bbef5d38eabdaff49a4c90c67aa76c396e70df54a0accf9b2b85f4df71e0be7d08eef1146d31c0a0ea360925eddd111e60ec3224b03b2afab80aa0f1e2caf799b5127aa99af1bd1485287914db49b76a48b60588d6c5ea18726993f3385c413363b265d4df8f5384efd3d0512ce59f961663cc77ce875a0f3bd30296d45520b4234f1a9d9b74ad6162311a815ee48fe811132f510cc55cbfe9625d3db21cf5d244f720f13bad347eb3920db1dbca6746d5ebfde1c738f7c83222f9cf0402ba5347839e281fbd3be4f6186d117b39a3c8830e72bff933f2c2c76a946cbf82a939a5c817dbba4c085e6d917da3e9c239a1282716f40d8e2439ab32d70a64e9fbbe2cc41270b29dbed55707230768fafe15ae0791f3c9eafa56bd1ce2c8ed99bd7ddfcad743944ba6e9a2127d439a49c85a95a77622cb7aba13f8896a2b346317c6805e291bbf92aa6a852ae03451589ad7d7e76a518eda97acea95f67c4c46cd15bad893d140c2d66ba1e55bf4632cf9aa4cdba615b334af751f3a4be3cc4b9ed5abeffb6225f2b389505fec4d9b5200e13628c9aa743b51b927e0124c68ededae7a0bf4b36b6455afd5c53f84029266388380a6bd2bfb72dac284fb4f2af5120764a250f7b2137ab04e56be88e49292d86e7d881e57b7a41b38c4b4ccfabb5ccdc1b6e857ecabd374ed48e49d8b040a2b4d9beec7fd0c166082fb644c5694956bbb61b1bdec3b9eb59e6ed56804800651cf96078c91c09dd9ea86ea0b74b0a7059d3f2118a756411faab342e7606917492f79046023911d3d270a830457a7c126190296732051692c552d7fa799257cc2d2e82f80867aa53dbf25a96a8176f6b5805e3c76b82aa1ec7a1593b528f7ce9cd5bbef5fdd7a0872c7f657d21e6a6c8c16b64fe606d4a9eede07fc69f6a621b201dfe7b660b324525281d94998006f29142a0d2ae650d727fc65e586519040adcdf5990925178279b517b6fbb0203f885b2086d1cf4c2ccb961bc96c01708b766a9104e69793b66ae546b5df0b8ee73b3d2ec9e92d7cc5fa19e65ba91338fd5ae38184418eb309391eccd87f5700a20343b4af60a59f9aee6c00b8c866c4a99dca9f5e61623ecb540d5c09f5b4f44657eb9198ec1c84e64390f5b20d9a11f5996b6a8e6b74120af2c2b88f39b49053852a33a5a13e0cf02a53c0167548d292c179f5f0a0ad5d709730b56f91204692cdecb47afaaa314159d6ef6a5ad4afb435b067efb37a474fa98ddf49526d811a6c8ee9fa57b01ca65c58d6334ffc3d69e85131ee661990c04611bcfc8cd3cb7e355b8fc7ee76ba68c84dc6bf943d72eff76207f14a4b7ba78260e15b880274c95c2051303cb566e28733e8cf8853f9d28d4e9a9f6b102eaf6d964760ed5d7761cd4fbd1a411a6b05d7b6e573751ce01ab451991f9d902a02d841305bc0da36d5a738c9ed59d2bb1009cc9c0442192a337a5b6032f5396da91459b51be0e795367084d571fe0f231449aea601b11234cef7cf9709462f428d1d56bd9ee8b0c84aa312ce12409701d5a017145cac545105a1fb50c2ea6a067d641267e365350549f27403ead5c646cb522930b6a01983a1c1270bf6a042074904c8d342e60d0ac498790b7f85a14d6ba0cba1177c863cc4d761c6323343bacd2bcc2e50798e0ee4b8e3005e9c7028178893848e8d00372121ab47915001db132181428e7f361fff1f3e5305178cc49d9dfc3730e836ac29c2632eb4c466212b8d78beca6821cc468f4c706be5fea707a3435421f9f4d4822a295d1dab367878251aaad6f6a23b9418c7d9ff2d6e52177f12e711aa630eef7e84895e51df4ae20600253961be9c3f1efdc0f93cd905965d12c65a6a8572f506dfda458133b8b74a628058ae6b504bea907e51d7dd7612ae8258a6f2070ea4d8cab93fd356c4800081e43b949489a73ae291908e7d414059b9902eed9963dcbc2ede7e4c68ead618450d889ccd575280a9695a88af1091f42687c279d22729af21912522463f83991e6d5fd7a901afc93e164c194ff6daf854c83442aaa41a108db0d51e0ce339a97104b60b2d87c54f639513d8f3033babc69f373c2f6b2b1aae94d527917f16b7cb9f2d8170af8b0e14b306e86a367f1de3de0a83775ac1604b181a9cb9f14e41664dc050d0241dfe36cfa0f9f3a30bebbeb6e7f4ffbb0abdda05412b1c610024a68c875b5f5b5f9e57e610d77f14553db0d506483d5e7c04be112dfe3137eec56ab6decc70f513efe4ef2f01271269139261e50ae266b68000583c8b47b2cc1ab52e5a582ab676c02b8de5012555b868a30bb47d0ddb045a4554eb6fe484abf4c6f884d78a1e0c35063dceabaa9de29448fc1278aecbccd46c210adbe0b105c711a5b2a06c0b732bf1e4c32ee9728c884bfe892d9ce86d1281e6a9b84fe58820361896e66c1c3317a67fe0692bea233646027a09e679895a986a15a0ae29ed56e9673681a6c35590b1c098109dab5fc510ac9d797fc1f555ec08abea5d61a618b4f96818b17244aeac0b0b1d75114edc9e9becd8c3d07429b0eb9d3fdfc686c5df11c8043156448dfa6bf056192eed31218e612cbfb0e0944ca78dadd20b858cc6b163db0e7018fbfe3e3e3fd4abd1287f7066eada08ca1d9ebc276697036d1d71e47f6848450ea3162b42c689f6f3cd8ab5674ef557cf03cc1b6abc6c8716f1a1558a4f67470f4e334a4083aef0e8c16712ea75b4f2ea78deae46920c2bcd8eae9ae2b3bdc3ef6a0ad8576c19955087032ae91a3fb44b992d48e0bd7f84cce1991ff53120ecdd5a24384745a5c08a9f694d345624056afd34f39e48047c4b04ba7d3fb05830d0b24a07ea5bae3f557f2d284ae78adffadc038e83393d8463f2db1069cc2f457eb4fd3a24ae9af0409132a57507ff92aa065215d634140b86157c1e6364dfae22b32bcc88a65cb99443bfe2f52d219b6cb9b79429a514e50ad90af10aee5a205df96764389e2cb3d14832c2557fbc0dc302295ab85cdeb7af14d7414cff9506c5f47732e6d74d2ed7a9fcfcfd457571500ff1fc1263d4a6ded4394e8dbcd3603ed7c531b7202057773fb86bb7bc412453f95d883d299382b9e50da2ba2a12642acb3f7e87e96ec5d47efad24e2334653098cf82356ad36f932a7b0d6692daef2b5391fed8816246a63b11725e53fb1d67a773d6e54fd65b976d6d3e17aacca4891366a6eeb8697bb7b77747ff08638cb1a6b948b9a93bfe82c5094bd30a865841951e97bb404c144a54eaaa9c7effcd1530adc0cb50fdd82524b4ae82e9229a4b98ccda34906341e0df4c8eb947579cead74f619cfa898ece618ff27384a5a87efde454b060a9ec511c68b799b942f83d2709dd279382e99092e015eedec48754d5c6dc6eea0ed339dd4e9d8351d8c294341210f3c587a93d6461c6f85086d9efdd4dfd799a9d46f010b05345e29ffde64e4790a9fdb437fcdec3fcfa09f5dcac6401f750d93b37b3d7382549d8c94949b6188d049ea274b7350826aee120bc5466fe7eea1ca125f049f831c28f861650f5f8567f7aea8e273f33bbce7aabef2ab3bb9a8eeef8a114f3fba96da4bbe561ae0bf1a003a73b4c173edc60a5064d5049620e284950bcc0329beeffc578a9124a19ac659992348278c116627218438527e530438d271bf68584530c6520c19443932706897b30238b1b384031430a39807802e3210a31a6540fea462186abf20641b9ea6ec5a1911e70b55fdb567f6752cc9d1fdd4929a67357e2af8ac84ca429b0101f7b27d9639445044b21a3182582fba9906375a4abf2b9422ad185acd5dbbbf4ee3eaf6a06b3a6b957bb35d89a99999979dd31c71c73eeaba91efd9743c18f1cc1f895b4cef4d6094792744e7ca641d375d8c95bcd1ca688bbbd7497ee9aa97eecd44addc9603ec1bcd304061b484e568aa0c40165869ab997ad0d75d5aa7b64e58b95319c866d6be65e6646b5e6123e86f5def437dd2d6258cca074ad510c379948552be8a4b68433c9b0ff3849aaba6bb90295c92d5edb11599665596624b7e8021e91ad90c8a0e074c301c50a84b2824d4a29650d9bcc926519942734283c48285344285744388040c2071044c41f2596a8018d306cd00207191ece11102a00e206c985891432489ae38b315d88c2a891b191d974ff9f6959f6858c4db685114ac8e0e58a59615b08811d5165e64929e79c72a8b708a103cddc420823b2243258820c3ac6c4e043156084e88166a6ee911033080154bff9908e8d488cef9183565c4eddcdc3c622e284d728f2e2c367c255032cfe0438794e12e0e455acff488918b68459a3f5ad97ca7f5465a976758faa18d52a58ea1e5519baa97bf4c31ab59f6675708c868aa06207166c11c6ebdfeeb6bb1966eed10f667cb3bb55b0adcd179faa54d29d1f89cfd39cc44aaef314973ef9c90ec231b7a730edb437be542bec2076aa1f7459c0bd48c3ccaf5d30a85dd1a9db8992ead73d42b2a18a57c5a97b5485504ba93e085504d43deac185ca4c759b815b163f63d24c7599e8b7f2529d6e3401559db2190cc27e5491807ddceabc4c9de32fa54a6e7a4a429b9a3766d73bdd32e96cb7b888191c44d50399aa733558432c3628808d9feb99df6ae577ce46f14650f5da9b6ae9068e0bab305bac4470ae0d2d5b68a0a5e80a2b9e3b294185cc4cd5ae2ea8fdb05f4ea97dc44398da3dc7910f44591a93ff6b0afe2e38454104bcfefe2b4111f09aaeb79a0bc03c23e68a2a5bbcc0b1c54a1b390031471d37cc008a163cc1461133b0c003f3c2800b221952539418c38626aed8e107273866308506187e7680028324de1043a94b0d5fa64471a107296120470a9ef02025af1654ba38bae2ca1c5eac90c11661d8059962a107e6fdd98189062c63288146510ad6e0c26932e4e1610815d6cf47848610c27f4806fc1fad03e441082194fefcdd4d44089f19fb0f49ffc238a677fbf2d225b0429a048ed5aecc6d56a1fa3bef1663e4a85d59ada45cdff55fcd8ab9bf5a395d29337ec3dcffb872673117c25d125491e5740499da742bd736f68bd8c1cb778ec3884354545d055daa4709a39bd0d5e3435ab382ef14893fac48fce7ecae8598dfcccc02e1ef31ba9dbea8a059c115322d558eda959484104298a4dbed52f7352b26bbbbc71cb6b5f92451f5df3c8983441a85d8b758204c630dd2c5f2397b766dedeaa526fe730a555fd52be6c74b43bde4dde43ad97b122caafe797b59f682451f96bd3025c9612fd7c196bc95bd3e8851a83a525d02eb97bd1a8e1985384876146b582059147be3ef2f2ce68725252549a5a5a426cca9fa47a7eacf669c5165b03a49b6b5c4413ab020f0fbf3199ba3c6e6c097c3370c98ea75bdbaab22c1c8de9cc05bd89344191d3ad0e14424147d58aa2290158599cadf033bd56d867f91706b4bcc55f5c47e6795005d5562311b697aef518df1a8c6df577384314666d88dedece0e0745d8c90eeab62df11a848b0c67a0950bddb666f1c8bd9ef946581f6b2ffb63290670b9d6acb1f36b8dded70cc14bb73703fe2d45d88c52cb5716ff61daa7a9899d9fd99e322e450704f75b79c5af537b12c9352e350aa9e8d3399efa7c288a59acb543d735b4a29594a29e5a664bc318c23cb840554fe583f0a54a62ba16c4616883fff09473a277b7e065a20daf33392eba49e7f7ba63252e62e55b96b007661d2b14fcea8fb495ae748073608033108fdae22a12b0a3ca6bdedb786633ecb0488047657f9b540968cba84eb126e222e26bf0c3240ed3fe23a4ce40cc441d80906a17f4ea26d7202a8f66fa936f2563f52edf7af899c407d4bfa7994dadf15e5aefe0d26b98e7bfbd8f3bb17517154517f54515054d41e5594e426dad9c1c1e9baffd56a4e9e2a4bd857952897d1e8ee8df6526ad2813ca8bb589dbaeb8ac9ffb9ab767b4e02ac28ec98ea74f706db540cbbf4122e8c62242645f0031a3f6859a109235c5e16839afdcc60d1aa49871560fd7ca851bf1d176a4ca37e9ba4830cf55ba51a5d80469717de78391915a191c31c590754a4b820860c9060438b171fc7752820c41731ac6883892a5a78f185b80e8d1626c8c852c60b78f0c25beaea1ee5b0450e2f98257b22eeb6afba8bb3bb58d3c484dd452a13a67a60721d6fe7cad5e9f75460467246b3e26f658db037fe5db19fe6111a89c78cbadccece09f1cfbeb3cc3f7b24d973465bca3923c62518a9ce18c6ccdefeededee577ad4f54742e1fc6aea9601895c2e17156f57cbb06e7f905efa6aea62fe3d16c8ca7ec8abc9d41dc7a3283102c7a70993264e4a9a3461e2d404070707a70913254e4a983451c2a4c810264b4a969a3859ea75972e5dba649710dd6ea488e7d9eff7657a427347f6bbdf4877de0e8fb8b3b3fb91133823ddf911cfb0137ca910114e30d2394947927477f7ba131665e53a5c6a76e71d68f8d9ef07e1f6438610c2efee865ac39719ecd6a0cc208cbd5afd4c11cc4dc6e45f255eba644ff090a3f7db8b9ec77659e38fd97f80fd17638cda6f94598c32db17d691c209408710c2180693c1c6ba027f3d87bea235cf6dc4a69fdad4f5b589b1658886bab99bbfbbbbbbbbbbbbbbb97f02eddd3d448cb2bfdff0bf7da6227427a4bbde7d9eee7a5b48e730ed0de38488c0f174d7fb39d9f6648ff188d03aec7db446da75d736dd35f330bf09ecae7bb7e0ff03f8ccaf75fdf825e5edcabffc1ad57e57d334fac127e6d757f5688ce9b0b71fad4dbbeedca63b671ee6b7af0fc727304477333f4fa1a17e33295a99f2c4971d9c288da959432bc5302ca5d148208e309556c6b0e7c89123c767c6b0ef234eb3fb80830b3aaeb699b58145758f7c401ac1aa7e4d5c2e576522bf14abdce343870f3b8bf4831409ccbef81ebbedebd359431c529605d85b4ffaca5bfb3f9ee34084a6b00f29cb7f25c475f667d36fa7f27e376d103e4dc46c3aa76b0b61a9fd3e8e9830aa5b1b05de17426d98a4c58a192126a0a8df76a9fd19168768ae42faabed61b46fbc2af61b7783e12f98ddc4419a422106618d05c214d61053fbaa87bfbdc6d3dec4f46feca59c73b56a25f6a61d347346469582de14376f4269de24b17766017bb0ea8003ecb3c6f2c1cf97f9d4a35e7e7f901dcdf8d003fac915fd6607f45b7180bd2f2d90ec598fbd97b13950cb76d8632c0a85bc853deb37a0dff7f81afaad00f03ce8377f070dfd7c4c6a148019fa4900a8e83753f49b3d36faad5614cb2834da1bec3524821c7b29e75c050555ec2191147a3cebd7637129b0c07a00bc13005016911e0f8007004d01004e580f00eac3a2eb3d3822585ba1101472a3231b4a93d8673b729ba895f632db9173f55d854215fbcf8d2a0685e0102482586011340a52e3aa8b343f0faa18f66ef4729d1f0f7b16f7e339922761ef4ad8fb127c6dd01cb8d0d56d4662188661524ac902124148d98211101a5be5c724ff14630e54e58f4f8eac8c0125ffe4a73182925a72927f324ee51ad6a82dbf959414a32d985c23a4df743a549b7e6fd3393b2a4b9e5ab6eff10b218490f99db6136d6bf341a10a7f5b77c9a852586c2c529b72953d168b1a4bf587ef45fec5e6c017b6830f67a82f790bfe639fbdd47cf8a89f17ed909266ceb95ad1f5d6127cc1ccb056e22019f5a506638160d4cdd81b0a838ea67f3f790bbe94a19f9c2afacd558a7eabffa7af834f484848484848483c34c4433cc44347a4369be28ed4a65c658fc51d29579817c9da555597f9f952d15293d31374c1a00552f4e3c1f7175292eb400fbe2b2d1016da5aa605e623fcc23294bde9ff768dda0b448cf7ad0b42f851d7874d0f49bfee33fae160947efb12429dd28d428d4a1d26071d59ad6253e723aec32f6cabbffbfbfb3b686fda8872160eda57d611886d091359f7b7f6e26fbc75915c477b4959be9ec79731c6e923bfab3331312d8b9473ae56ff5df7e2cad31a87e3ad7d6c9fa775d8b0dfb61fa7c8021648530dec0df6d86fbc4148206bf60beb7ee73aed196069f70b2dea6e1575bfae8e7cd5c6d84389e32decaf043d718f08ff5ee195240414c5e56d474960a94cbc6695c3aa8ae3206fc9095516682f45d9958467507446b5233a9f0d6ba4f9ad52c58a748e506c6c28c618e90b60534c223b1358afb85844d88bcf5ea441292ffe7a8be5cf9555c24cf59fa94ebf9f7ac504914a6fed8c43ba4e6bd9ef15e92d2b3dba6bd7696fa501ed33aa028b087bd9b397d1a094a77d10f480b27f223d5e05602f39f65835047a1a7dd2c28c977d7b98bf473f765b71869d0d1656204de1822d100801c84e4edd1f0ba8fbda159c04f8a8fb6bd3cec3105c65cab294d426ed70149cfdf6d55fd65b5d5cfb72e138fd66f78e134e53b7f44a49bb2c54391e62bbf89ba72ac0452a24499a511e42852edbb38b77767070baee3f8a0a61509c90ede26ba5d16f25e96bb1a8310bf6fcf2d5ca5f4846d59f330ccb2952fd9955253cc6a84ec910830f4178d182a4274a40a1450cec42727702c3e44f115c7a68c3290b219cd0c251b8ec10b9106159966599e40284e45244675c8a52355cf13a92a0c26392f8c1055b8719394c112486520d9c180a518d5fb38a8f8394841430375099a2e94b124d2f262fc2b05a054165f592975d48b80142dd2324b62041030bd56d26d539248015094376798b9f83943c1969f251ea973c2fb8695459630a2c55b4b0450e1ea438de624641c579a17eaba0fa7528a8dfce93fa2d920bf55b25a0fa6d530b2cd423a0213adc78c10b6ee0153c10e68a19ae8062840e3a781978610c1410c1825291193bc86082062416c20441c55b35c48079fd8084143954f1f87d5ce787884a1c58ac50c50c9088c2e337e23a3f31d431441318356041104378fc4a5ce7a3c11b4370ac20f4861cde4f0e636871414c1b55a830e109d5d84512508078a20939c640e2920616ec925c6450e9f084b19ad59c53726161e31203d7e02401c353122fe0202107126f8ca95d797a0928628cc9c2862ad220aae301347431c4d04b4c0f6678fc5f57f5c3d6a36d3a761df8db42b31904f8e3ed4f87bbdecb595356090c584075cadd3e6c6bb389e1b44cc43d46121242ed2e872996d311ba3619454d75f583e7dba95ce454f9e5a3b2eb0857f55e550930eef2b6439924d7584d8ed1092abc5d8c353e9fa0fab3b3b3135498c980dcbb3d56ab6dae999331fefd095dd5c32957497767efbaebefcea98d123f4c995ab9d0b53843647e57f5ec7857f5ccbc4cc589bd9633f3fb369db383e6f77d74cefc7d9c1f3b9d23f3fb21748e04c2e3f7371edceecdf6bb6d3d5b09524a0a789534b526d4fcb6aa0bfcec50b402981ba620a20b32bcdd16ea37ae86a2b80fd6ed25f731a9f237989295c737ee73525933bf83e62597245559334f4327edd15dcfd0d95dd721b7db527563a2e2606f4daa7cfe14b7db6defaa1260456d4eaa2c0156492106352a937f55e3c4a4c283a31bdbdaeceeee6a9ba669da1e39e148e7c057c2a409652fdc85977c9d58c9e9c9294ce7f0b7ab866d6d3e29541d1ad91b887a9e1eaaf0bb6b14ebf4501bf50b15f60a15b21c51a398cb5b5890b7fc9ba651d1901c1a8ab068484a14da1b7fcc05b1604499d30731a2ea0c219442314a3fcd66a0eabbbbaba473a00e0b79cba593ff384af5a2ced24e547f8d327f1c33fb35edb1672e093b69cddddcdcadb186da502814aa5feb22315d34a53b294dd4551a4b0f75e743f3e3a2ea5ca55fda11dbee367163dbde58f26f2d338cc2eed6dd07dcae3befdec628cc81df5d960d42c81ccfde8430c7e7c49d1fddf142b8117efc8ecf8b718fad748634d8e40ac39e5bacdf3918b779a9945005ee9632a5b6699a26335ea6128cb9cfcdbbbbebd31d3f148a492aff729ec384cc25a9a9496abc586c1565365d66d17d743027428cae8d6394fbeef6e11a5f873fc7f0d5e474f5eb51bd8bea0ffd2596ea448c29c28bea3a0970a201ca15260bbc78c1f9064b0df2e0caa9440cb0736c86008238ab4cd364963db12254bfffae1aa114ad4c610414280e840361c44122ed1783c0c40271da5bb85b205d77b8ea02316798fe4fd31d3a7c87ef7015239d43aea33d34721d4f7a69d491bc053f2a55e84e71ccfe2fdd357507638c9894524affd21d7ccc9d9c0613be37795385fe54615cca98be614e7cff48a1fb6f77abc34b474171245b4386ea8bf9c1babe60e0eeeeee2e8430c20823840b21851e65982fd798147b61ca10869f06957ff7c88825c6c0b69df3b0e885c51e19d1b44745526a5326ddd9983a5c75c9b6365fab60ee6f3ec67c2d90d99c249a2c229d82c7224253802f3f99bd53d6107712df9b1681d837650de987ffe3fd7892762b871c8eb7f823b783713c9c106f65eb456e3dc8ade7dc7adde3b3c2627efdfaa54451fd3aa95fbdc4dd736c6bf361ec4ec1a8cc8e94c4b4b7d89b9e2e88027f6ada1b8ec1075150bb9f19c95baaea63e6e74fb1c95bfd52c6a6f8b4378d21711207f1efc75858205cac4e87b1988b83386d250681a27070108080010c80c5e2c1a37227c530fdbfd884a57e2fc5fcde05872b40062eafddd8e43a7bc59310425e676c78fdf1898374ed2ab1a9f662bf37fed45d1413ba9c8eee22d0ccaed00286fac98a7dff074843dfd74efaa0916a63d495bcd54ff3c3826af753ed383021b9c6b60798443fbacbbcddb7f676cf3698c10c6630cbbe9a9ad11fb1bdca1251541f63eaa0e803a6b0e2e4a1fec74bfd8fa7fd8f27f33f1e0fd99c6c943544b2866caf7a1575a2faedc79bf91f8f3f18162770f1a5ee519294faa18c2abf8fba475cbc50afca6fe4047f58fbbfac5673f2134e279156070ac990342a299d40772b9f21b2249a61d11d087ca8d897b0c38c8a7df69b21b98ee6a4396548556498acb8f5d32295a9eeb226c8b4244646bf4c8995fd3eb94e56c5b6f8b5707d51f9b3322a3fb58b8532a4bd91227b940bc35041aec340684eda53e7ecd11674748eaaf2a3803a27350585855f3621643d844f51912c46b41c614249134d3ae763272a3f8ba9fc0cab4032863ae7cba2547ecea8a8544ca073d8072f4c36f55ba40db3742c5214292aa81f4fa94c543faea269199293b7f857298bd239995036940965429950369409655832a1cc880ccc8505c1a5256ff1c32a9d03b1c022880562c1e23aeb15cd57e50f628149be34592f960ff35386e44d9de89c8f5fd964a45586f4d913232529356548de62b804979a5820dd5946591853c5b815d7a3bb55fc10ec19350458123a5056f6fb41c6bdcd6b5920d887c03324fe0c0ba64056817655a721c0de4de03115d2106094650257ecb962f423612b9224cb84d1102cd3aa4c7086624697cb55799aa9466e992ae47c74dd75593a8785f3630930b58b3a87b513028ba78a902135fd66865419f502c01bdbda7895e9cbea7fa31636ba1cc6e62f174803f1323dd63dd2e28cda4aed65d5394cdbeb98bf401608aa76bfbe566a4d0538e04889ab5dbb8dfd31554ced336ae3c8c0a374d7bc25c9a8766b813403bfb48001f457cf9aeed887ca3d7477c3289db34f8654a376b7513bfe022161eb92d5def073b6fee331267f3ca1da736ff8776ab386c8cf5ed2a518c71a923df6199590e5d402edb99004635b9bdddd1bfe1e5b5ded0d3f3f1151911331bf55e56fb246f05af99328a9d3590bc4830dc20bc2fe6a81c8ff16e95577767070baee5febc1be55d9b78a84ada2c0633a935997f9dc74c52fac6997a7aa47fe54ae3f87184ac451799b2a530c8b29b84c67e6a8ea413d4f2f818f3597c96fc28aa955a6b2fa235d021ffb99ef9f7999ef19194e95e26cb80ee7c78e08906322eb1226b376b037fe1b2c084ea72f31f9c5747115fb165baf9aab5975e71535860f01e142383ba7ca7c87b3b60c3c45b6c88d6190ccbd0f7d3848530f16047fcadaa27acbbdf181b9309705c27bc32db894ae5bcc2f9fa2242c4ee4432ee4c3c37551899558898bb0c4e122ae3331f9647eabe472a02a3da6d448f9388a921263491ca460ab02538c83eceeee6e98a221bce0deddddcdfc4d43e84ea220058322162116a111dd622cc20d8bb289f92dd20e5ced6429292bd26f27c475fc5d5209657e8b84d4d00580a0ed99ebe76dae48770cb170aa08a87b740553fde6b05f8e59fdccd41b2cd22affc5543d8b61dd28642ad466ae00e4264cd8f2380a59c3e5f82117b9c2284433f3ac3fe9cc3953b321fce80ec7c1cc2f04ec3b494d083fdc9f06f3db26486dbaeb0e9b61a6f68b206406b683914e46afa3d7ebc88bd71255e0983223b3c4f4dac28434c6162637e018b2294ae2298b0b66b941268bd10e0f5c496084b2a8436a4262a66c1153a597b6886142e5da12c7143159e634835314333c15c9901485900afa793ad53910cc8ccb0a162b45eeb232858a88cad31ada4684c3171cc260444330ccd790135bb0a07fd710ae965c2ed70a2f78aafc36ccc555f945f009e17543fd1629e5c425880abd1972ddf1c3b67038d538882a53265e7f72100534c7c4fdd826294bc856f9246c955426055356f9fe3f35cbbeb3eca7468e895748d90cff89cc5d3ae7e3c8b1aa04af3b7e7e98e625cdc70ad0d0efa7d2bce4304c3ec6e334dc0e5ac48c6d3b8717635a1855256cd5332cc6577129d5c7bf024b3540dda32b5c5774b9c2a86e9e8a4f308c3bf517accaa68a7d1189a1e407eb7f28caa42bea25f6715b87c7d3a87e6e3079bc3b0d0dfd96a9e66968e8b75e687e036eb7454325dd6ed1bc531a9adff13c683e7b154d92aaaa99df91fd7c8c9391329b32f1aafa2f7b24fcb0aa289219fa71a5796cfe8ca88f35d25024a8c73efb569990a5e8f783a248f8637555056265da1a167bd4d8d5d823e563f2797a04ec532fb910d454243cb89a194edb26273fabb9008faf7957f56834d51d863d0f0ec5f1f877977a6c864b75a7fa8c93dda9be86db6ea91ee3be9aaa7ac97db3aa9e5523a45ea57dc6a1be86e3f118a7fa557df59ed91d97e2647735dda33bb6891ce5f12c62907ed067cf2e586aaaeef8232b8caa1541552cd56d4130f7c88aa1ba55feedfb8da9d93a35cc65323f997e106b7e89497ec9a49d43827a152a635b11ecbde54691a41ee548986c5fd7aea967a6da38dc1ff7b3a41b547dfd9a7e90e38f131518fb52a8fcc567d248fc55a8cd7d5e39f5dbc6453a272e7ddc543f8a82d78f99863f32e99afa8f09463f66aaf199746dec318c2186bd63b2e2b8cecedf2880a218e59ab2502fff675e8602fd5c84975a46ca64d2b5e5679c8c8a841ddf54b583fbb8b2dc7ea677482c85d18f376cc7461ba5ead1b654ac282cd535c5f537ca972a967de4fa312e3e0da73d0faee6799c315aa4bb0827fd64e54167285471f6df7a69921945d29f7af99ffcd8c60bfd92a43e90c1617e4952fc810c0e9389d71445d292b1ff5e87478abf9d953dea71a4fc982495aaa9df505446ca64e295531f5f7b4c45023f9214fd38aa2a90aafc38a98729f91bad61e2557ea41fea356dabf650d5a375c46af69580bb65be1370fdab2a61ab58a43cb8dd56bf669ac7149b1315f8fb9d10c1948855c1b967dbde64e774deda6eff6ecadddd5a5e76c79b5d89e255bf39575118d1ba47596ed882340563612d485029fae2090c98213cb6614b51e50d2a7fcdd0f680fad97091e4b57edba57ecb547dbf2860e9e8523f8ec252aaf31499fa719526a4133254ffb97a27daa8fedd1343547f9c27c654ff1d1e2ed55fc818d58f5ac046f52f628486ea7f0488eaafa473f6088a35aa7f93d70e2e27a604f950fd05a6fad19115d59f89b650c15219d6008e273b369e01c31b714cb1610c132c65891ee60ca30d2eb698638622a8a8c12960011d4d43f52518b81a73e5c1f4d67a72aee45cb9108dd4d4d42454b37f94f61b856ac64635fbbe12c1afbdf63f1ecb47fe8f87d2b86ee25c35437199eb69bff256f6440c692286c8d7a813e9adec65d36efaf5d65bd40911943524d2ce89082f986842282b633e0be8a8df84e349955c3f792bfb6eaa5994625b1b6f9afc1b57237bc3537ec47eb91d6fe1b086c4f7d6aa7c56de5263424ae36554aeb756ebf96a81ccbdf10b4831f97dfa4bffe90e21dd6e4f62e170c30a1847588981684a0d3f472ce186952cbc6cc9610a0d9d2325974911ba81a9062d4c6952010b61526a717a32d2628a16a22c2fb0318396366ac459280950f7488b155a9c701b769f961803d552ca5dd94e9bcb252c9cf8818b17506a80c273199428daaabbbbc38865ffc9daedcdee22fd7020fd769c7efbf21ca418c23f0457e2a3e063e8a84996a95ed9a7424adda329a2d4a5ba475454a9484034b5df05442e28aaac2ac21a6897b7d4057f89f0182f5217a42c22ecc1670f5227445c60bd14545826cfb54c1e11f659a63a198c96212a3ffca53566ccaf47edff0f676e52ae564c628515e08754c96a51bf9fb9a2f0ca84df267cc7adf9627ee46288939f214e7eae6c6ba355313fb9e372ced54bb991366fbbcc8eb7fae7e3d4ed6e1c6f752ffd666de7981b06b1adcd12583fc93cbd79a18cdc9b7d09c4c1d4dd56d50ce6b7493e9d33a4b93d556448e72c615e2a9770d56f4a964b3fac6cfe7955597240f6d77e9befae73baeee41243d42d61c4f01b2c7e9b0f875940c6e27860e3ad47a332bf9d9dcee10fa1735825c4da1f6bd3d4d2d04840cc941aeb8733eb8763a56e0903666d985fd0db01106e5285344fc4c1d455007c8c399f119801f03d6e0054791ae55905c097f46fff96943524cb80b2c764b8ed539c2ac5ade79c101f6fc187bf7146bc0579bc0521ccb815ea965d364e01f5a88f357b9d1aceb4ef2f62e48bd4e2afca876781acf65a5279082f9ea64dfb194e7e8a53fdc6c964ddcde3adfe1eaf2e73a971932e895f795eacea1141f6ac4201bec38cb286684ee46b3ceda3693f5eb772da3283a5da3f8121469a6a7f922673998ad868a37eeb24e3dad2060cf55b3a760b1b4bb5df478f4a03d96bd409eab314509435845f7b76ee02ebcd7cc6a15e864bbd8adb2850ea81b607925a27c427c519f116cf0b487ec623c4c75bfd2f1642fdc4de9278d5d83a5a0d4a4aebd434c9fec97ba98fd23fc677ed797adb8a922847c1df52ad7110b5b51027254d5c87c76f6421d5efe0649e866361939bc1711d1ec25102a98fd30920714463628fe280e4a37e9368ccc8d390d3b82619c7286e093720928019101fe350d89ba8044b397745f550f5282e687aaa77558f08667e5528f82ff1c7342af3aaf7998ff4ca0c045234e8bd19ca1aa27a4df5b1db2651c9029162a6d725708ba91f0bc43f7e0356a7846dc5f89f6a5d3082aa3a15b2484554cf22227fe6e5cffc0c6b04ae28300a5c7d2b0bfe8c91198e857c941c3961a9073a912ff3ab5127329485a9680afd7248bfa42cac89c6318ae38d632533d55d8c915fdea92274e6a7205147a3c8ab8f1ee316b30329adf3de8aedea20ada77ccca8944e4e7362d4a7b7bd6968b68cbb327f434215157d2024e4c1cc8e0fb8f91e70353fc3b1b01d32ebed101a121a42514ec2c283aa912de674aa18cf776c9b6ae4d19e911510cdd7fcc68333e2adf833c61ddfc1d77034dcd6fddd3fe7e37c12db30ec978e1adf670705aaf9b97dfc22dbc7df4043604583dedbe8f6dbd4a1f9f8468eb8ce061f5f89eb70f0f19b748ef6b1838fefa475806afc8d07c7de061c6835fc54238fd413e133f39bd7319f3cdb213fc6ad71dbde96b9b23d4faf88b0b751273f1e4d07dc06bf157517b5cf729c5c87bdc8faf8483df4c17fedf220fed72c6efb0f380ede831dbfe278fcaa789ac5adf701b79e07dc7a2b6ebd21a21a3f468db2511634e6c75896f8ac5e72298e9fdad55dfc1d1cd093f7380e66407c7e831b107f6b56b11b7b1333ca4efc7c657bb9fd8eed5b7e7790bdcc9ec8755274458138f8edb30e3e3e63e91c48af74d010d88006bdd701650d89cf433494089fd406ed65dc159a9734bf830b9a1e0de5a2c96df05c14d9c85bf121c7afa2bd89f1b7146f1cfc166b5499c0b587ba8b52ba8b0fbf4cfe0f22ed4d8c9f15d97e835f950fcdf374c64e1b3431c5670d994ff334d449cdcffff1320ed68d4bc17f7b560dd9e02775e2bfc106bf2a0df86f94257fbdf95e39e058925e60bd1405e2f1403b1e88e6e7af47f31b6ceba5b2b84d9a1d3cb81652cd7040320fa47aa0d46f9f71317988b294f81b0fc74f5cbb3ac85bf165b816f2960cb79e93b7627c8c8ad05d7c48c5eca7f9c17a5583cdb7feaa9e55ef6fe408fb436e6eb72d5645e813f0f122eeee332ee3ffede452321ceaa5ca93eeee8e512122f0fac8705c61436d87a0bebfa9937e1465659e71724a8d03d21ee5be7dea533405d56fdbafe7eeee1c67c45ff391f956f558606f1c529fbd7120ed530fa798fc2aca1a92d23ee5deeffb74a5fb352e687a4d7d74cbbbd562665697401f1ca48334203f459df4cbdf58e5b3fdaa2e00dddddd1d52e1d3efda4f6c14954dfeda7c4cc4a57ef32da67c26ad53c392021101ffc97b33df6fc475e47b0f3ae66e6b81fc819e4c8fff89f45ae0311e11d1b92bd29198f1ddc775d8e3f1dbdf36f1e0e4efe0b4a7e1503fb9ed57b5835b8f66eb5ca789cb75d6c38954880833ed397745b6be1fd447b7fa5bcc8475096c6f889354f6e34d09f49d4a4559d97a2b15f718552df1994fc4c178325c11cec805b801fd927bf21e116640ff065d6581bd69a73e7b33c5dcdf4f7d105530f79dbb223f725782a4279fb9a0e9c987aa1e11301107e3f1b30a85f81e9f08142eb0dea440dacb8f748887b9c23fc305bdc7fc4c9d6c9293f914c7ef754c233e0b24ab2d93d6c9e5cb5027fc723d9587f1f857e5f402eb016d0f847a20d57ade714534ce088a3bc229f156ffc6286e3d1f1c1fddf1a77eb0de6ab0659f712c22198b8887f1b4d7a8130fe3a580bdf63facea598282095cfd97f4c4f7912c0fca1f3b44b4cf680a2c22d9cbcfa06f1a37b9d57be7adceb8f530e9adf646dad6063e866118c4b45d6d354dd37e97170853d7389b1f19e703a73b482783b040e2c79dbd89cfd39ec584ef49bcb0055006288333bfdc5db998fa154b5f1bf592ebc83cfc6f08b88a70863a979cab05a2aaf0bb093e4c12faa095bbe6536d2329e75cad8cb84e07790bfedca587c1fc7649ae607e5cb474c42e10fe5515f328494b859ee43a49cfd3cc756d1c9caefb5fadbe0f6c0b3e44c118316d0845d928250ed2b48dc060626f2009648d1f1fca30f93f121a20c3b5473328a67f7f6075b8a88d0ac554b8633e464192128a7e3245bf87dd34542414f4aed5d374aaf0db25e7aa8d2cd09e0c6db7627e9d11149a432b23dbe277f224a8e2a0ea4e3ba6707777581d3ac6e44bc518e32ee492a4aac7b8f43b32c7e4f724abfaeb107a848d31337477df76d87637849ded89bf1df428847fe0c08841317d745dc4e211d3e6372c833d56d26ba66c52b97344f86143ad966151764683e69994128565693a526acac24353102fa56cf2a1cacd935a5ed2eec95b9bcdd024144793ab764f497186dd5dfa7175bf5b768e77bbbb7bbb3be4eeee2c569ad1e4dfddc5b23a3ad844ed54b962395f6a3f3bf5531a51d4af5db5bba3b0e07d2dd44335f66f67a9cd83890b195c6881460cae04513f46f2523f4ee22e49deea5532d2db7446dd30b5bb7f6ab0c13506c9883198606305f5e321a624b84aed17db740248926389a72d4b30d5e03563e9335ae8ade5115498d0f2f252040cde1533b2a8fdd5f4e8513f1ba6330e80460c688491c6952007a8a8cf543da8155313395cd51e53f568af10767c54e69511f35b7db05bc028d56fb55a25b951e5c3a8ea91df3920c0608bcbfbe0f7f8d5770efc0d0ea797f7f5e88e5767c850b3cfd9b154fbd5af2f6a7cd83907a8ecc4fc3a69a9870b2e974bba4e2f999143fda13581d3c95e4aea9c6fd54dbd4454db49e609f326c91dc85860724421438f32c4d47e96aaa7e5f7124f863ed851fb251a65c850fb3d98cb86124236f08a8ba5266a7f07dfbe103083275d80d0a18830c078100816ac5103075d909146eddf0085ea128af0228e3660d0a10662bcb63287185b0d0f0240d8e3b37140fd7c8450b908234b1561d4c00b152f71884145858fb3e31a838b0ac900a2c2dfa1ea814971c0409380309ec8acb6cf77f7cf0e34683104041b494858f1ba4aedffd1dd4d259c51f5748a8a1ff5eb61e3e347854a5f7415be9c5fac50e1af902acce2e90b3ac0a0a2c297d9815f6610861349494facc0833f2ce1c2545d4008957808a1f6a7cc505aa14785bf01fd78c23928b8050650d47e4dd5d3533855c8a507c616786145012afc6c093042d826cd51fba39731f96d3aa54a2b7ca8ea81d0a8051936015d0451fb5b760ee449c6ae31071c433f2cf1650c25a632c7abc2ffd13959855e349f1f67a7eecb9d2a904e0a40187235a5d6830b3a521107a12c6806564dd3e6ca06238c71e0c10ee5ca1b5cdeeac9f83d88aa695a574dd3344da95bbbd4adad5e0a9abab515aa7ab249c5075c354dd334295da478f1fd4f66ad1d7529a36a9aa6492cbab5de0d5dbab5b5553d19921a58362b53b210c242056a6431c4d0dcea1e6561847d313ba87b944514494c59f7280b303020b230434c164e6a10a5a1c511971bd2c802e250539cbc22a6c4a8148db585494d2a44340300000000a314003030140e088542c158345436690f14000d89a84a6e4c1888b3248861985208194308000304006464466683060be2883a9ef6be43685ec17123f1ab3d95c774a2774110a64ac80681ec9030e26e9f15c27536bfd43522df21803967d11c430acda521677c3044200121b23749fec784ba2be973d0770ee19bb970688f2ae82983d19e61fb95068dafc7614599ca51427dd86187e38926d340a495eab78c4362e84a1f82ddcb69a34f88a1387438f1d6048410e565e44450a14ba730b87d6cc771615213cd2d8322a736d39e5ce842a6d3554fb47d19c2beef4517a26c07c0b65331e8c21251112debf1d5a081bdf668fc5c3b454411b248b01d38d05befcc30559cacda28d9fe9080b33ac91e9f9acc6243fd586017dcd26470bac413db5e3307c1774dc2b208de3c9e26af9cab274d0590c66e1ae449cf6a902e2b6584e0b4943be88b84ad4b7572d33425049e845605358ba41d820514b854db4a72758e9121c27abdddaeed6c9b278fbe8887005156f89ae59a90a6380ce1c710a1bea0a509e797924fb82296ae24189874ab7525e09167446655d4b4cc57f0c91962c8a026eecb56d2f351f6c6795bd22b911dd1377ceb8993950cd2b1721ffebed2b3f63931bd670f8ce328a7a59e7f7d67ea25c511201dc01171ff07e3e775144489b60ff9305f55bf10cd7b0b037ec61561e60b314daa7396c9b6f6555b4e4c2fde1b133e87cd28d9eade972c069695564828177b11aa28800c84197443574751baf4c01ca85dfac8c929e915017234e7a7beee2fc0ac2b256469e17e064ac374b2767d9ac13c9593b4a5d5588bad1bacdb4d9d374c1ca517ba7f8f0ac4ed3d79100792e56ca4f0c4cc5a018c14dfc85a83e046d18b457a801ded211a30ab8b30eb3139f89b7f0e2ce04b0e511398f5cfb95d262d7e3ee9b20418fa43ec02b6053519f18985416fb1b35857f1a2ecd4eb0bfffae103b340e0b8513103e99caeebc8628d0202b17a747cc70875d3f1c0ced0e2d348be2deebc2c544bdcc7dc4b23fc578036e8dc7c64acc5f9520f4b37d0cc9754439f57717416e0fd6ec783e3c5e6ec092c6d8c4ac9069ec6294d18085b9ca361e0b6f025e2bbf463e68275f5e0211f62ab4c941d4292d886c01ca2c02be4d6e302552335a08f6b50258ac712af6d59dc8f6b1321d415cd95682b90b11a8268de4f79971877cb6a49be8898cb9fbc11f87697a91bac91afaa659dc3ce4702cd7a011e878ee225e416869087fd6d0abe643888c84a74bdd3e5a8baffc0db3fbe248b1ea9d2f0d31ec5c809a68feafcbaf7f5051ff5b1b92adcc70abcf4914f0b2817776eb16a6062bf5b90a9adf4c820bfcc7c3157bcf1bf6be726f4b008e3f1783845fc384d750f053b0a1f84f26ed6114086eff8a4f0957ab37f294aee05cd076490f2a0f6a5df2decf40249cf38e941da1369c30730ed4618427c1db135b362a6f48c1dc5dea8d97377da226135faa32ed745b8cf273b2a6638b10efe29b995df7cb6458e0194cfe154919b12a55b1cae50ca3297f82bf4fbc70cc59a70314a4136f88534ec8636dc69fb4f9fb422311327bef25bbe393bcc8efce39f0e8ec61b5c00d521099f1eb05ab8edc638b319c8f112839b5a04ad1e961f2a3da3667cb5ff989cb1c84704c51090f7bdd500ac9741b4dd34fa0bd4b18332a18f2a37c593dac47904b15e06b3d3acfc1683383f56db79fcdef249e49726f7d5b50117299311a85a7b90df0afd14ef29788e02e00099b0e77d40e3d0af764053c88744ff8d0ee5816720b62b685d812d5a82d30c058a4794946763b9986684f929ea80174cd03858eaff91a3c1a21bfd2106f421cd12423f7bfb33d560ad5014d80080f784eb2cdd8e6849cf8f1a6fde2aef95c8b372879af32218d4c4fd531bebb35baaff1876b26c12f79dc57323ee657831145b057a5ee0457e3e7ea718a6e28ebe9b769d5a9c6b908a233b46d3b27073e11463280ee2f3a9349b803adbd7efe5bb03db79b5d73ecdd473af3889eb0f89e81c8ff3708d0dfc1147b5c0567e4cee2af3e44371b4229e9dbde281a5dd861eb60432e6232e6fae997c14112837123c51b84c9f17c5276b306214ec4170f1d48956dc1e5849ce557f1a1857bef9d0f70472760a4d7c342afff0e6ad17f390e36b5c6b05a028db6221862e819263ffb3117f8df9c3dbff3d04925f6de04fd51063174acbc7c06fac6215e35502d3fbbf9efcb21aa3e160edec5d50d68c42d985a86dc6de20fe1e29e7663c06e5fb5361f48d937617e07dcf5d99e0b80bb61ad0f10874a9d13e46ef2bfc19e50c76fa2f1525c636ca84c8ae4ea1103121846b1f05fafdb36e64aed83de0d7b636a0e101aa26ebb7bf611654fdbae299a8793903898e9e74c2ede2bcb101a0266009d8a01249f4d8a37cc25a9d2794c009a382520c99ee0c68e03f2501e6d0f1e14e4afaf83432d86b94ab41ced886e7d66eea24673bd348a029db02936ffcf34bd67bdf386ff85a7a28afe91f34a69d15662610cb4ac9caa4be85a3398acbdb3659970bd2ddcb94e0424fde00b0cb90eb247e483a4d4632c9fe3e1c6f744e74b5592d89f3b1470dcd80f05649bdee903a8690817e84b07b79e47eac9931bd8c0479830bdbc332b43322ee68a146de3f19b7fecfa7e2463152badfa399f88bd12615ae203ee9b938146be9125e38940921b9951b30ddc6bcd78ba03c2f9adcdf609d5eaf4bfbbf56981a55e58d5fa9ec9b644cbb063d848424539e835a8e0df5f5226ac323764d9e8fb35f4d740520a7f17cbece208814d40890956ffa0144becd4ea11446146d66704e663212ff01271639a5487bf3cb1adc23aeee2e944905742f895f801be7f96774912e7c7b9c2da07dfea00222fcf11d2e5a0f6b309833b4dea786e1990529bf309de27feead3c1c010dd16028be11ad46908a88f5244d09d723e61c2a75a1aa9c9167c4d61051e9169c5551ad285e4b8daa6d67fb424a7e6148b413176596443a4737f37b52f9a389a87ad31423627a5dcb881f63ec5751b0bb00a425613d724be1e5ba3d0c90489d7aa61372c101d22b50f07ea4d7ad8950ca8c5256f81c665946cc8dadc2c47069d8d072fdc645e69a313eb6c31b9d836316e73c349e92753e82af4ddcfabf54a2c81371f2e9f717f1c928e891cb321d5e01d15f26b8d3b26a10a191459552155e41b04335d95215c5369c63ec003076c87282d1a4883ec704fc3410e22d1842106fe48f0d47107b2a7b6fd71b571058a92a157cb9275197375fd1fdf833bff7b14b595117d48e1143a070c53f73f7b1a46d2527ee3317101535fa0dad661b47c1981b476c71f5eaeaf44304036e715281c1ca668a57c0046f7919542ee71b6aa16b754f5cb2d7f0824474b50a2f9e85289953a74925b7d2e047a1689f35ac76c566ff5e11dceb19c5c34d9939016b8d7314a1fcd5182059afbb31092a386423ae06c39cb77b2df1b9af9d436839685685bb8c557273991f0948cf7a2842be353c2993c10540122934b2e339382792828ef0729ab7b8c8c3f9f78fc50502ede18a07f7027882fb7484443271482789e5e10ae5c075ac947ff66a39db6b3639c54f54464d155388e916437b3e4b200163976c91fd5206160239456932d84e747c48bd285623dc34a77899886702a35e57ce1c701c20ee434edf202870937d2e7a7708a21a43c50672473ef134348ade61f9aef5a7b831051366e59a6095745a065d42b8099d6f1259bd66a50936da0f5d806d94e9b8a0fa1a5d2713e660f0ec168131be2e95d522f632caaa043fdc21a4207254ac6b685bd48bee33dd8c86054a1f1562c5b1bb68467b3950867e653a362e862e2ef7867a72403e2d92a12cd7618202eaf67e80219c4a234f2116b62722f2b3feec306cdff736cd4c4b50cc031ecaa3e83a6cd058f3518f5407852a05803e459a99a91b2708160b27cc0ba6a0dea4a5cc6572db91c02b4d9420b893ae84f22052ca0e3482934ead1396d544aa38a49cd7ecb0e43e0c2a9e7a89356868062b187204579882360e2d70df98bba38d339ba75716c912968e53759f445593e3e90c8132bbc37da00ba2d6ea4de4a5fa53f2e71e23348773c48ad306971b89feec926d6aa06694588495223104c4418256cf4cc0680f4e3e062e38a08db362601689b7f240151f24050151b31a044f0c5b353e5c4264a39c960e02e27055a7f256172f7288a45347feb0124baa505336395e34d7c2d2562084d827d2c323842038c022d1be9140fc4ba7e4c06a6bcabac0159d454d09e7b2a3606b7e86f07e8d07120c74d8928360d701fc4d2957606555703280a3111ce36f040c0c9bf8a85c2058ff3d205bc0bf975c09f6f7a2b576c3603d183444a586a3e2c7bdb73e94fcf7a4d1c1fdbb93adeac3e4fc3244efc318e830e19dbcd3a426e6f8ca53beca78beb3be10991b5d5dc82895cf10c4145ca7446fb35d2b400afad0740c731db0e551920fb34d4e25d45d8db39667c8933ad4eb7b3eeaaec305ce3421a25c8f6921d14310ffa8bfb56daa0eba8202ab1c5d643cb8861b1b087523aa0aca55ca4f02406cc169ab75e5dbc01b7d3927a493b049838ab9b76d241288e6f4b11fc6bd87e468f97a86c7fd83588c5cfc056314844ddf9ffe0084b86e6990db428a59a0a0dada58967387d6ac49f0cea77193b3a62cd4aa2d12d6a9a0741a7b8b6dc30f7272baea2174cf43df228c0ecd393c34caf5184cd8065cf74397f57652426d802e25fa1fbf161363541b8df2c007b3cbcc92db6503ed2d5fe326dfd5a1389db2e5ffb57b2f630632e888e54f06367379db51da8e6c1a6ffbaabb04a13fbcd176a73a9ac208c9e10d2723633e263d2d4f32baab18d498f11f5acc0149c470e67fa6b730a802cc80e7e4ce8186fc0dce83cc1ebece79d3577b062275464039e3b1e34624cb25a8b9f15268804bf8ab1ff9f2a7784d82f2cbbb1a68a8fd7bdb49b1e58c99ea53a140bc27096930a463f2f930a7ef02fe889bc3e2461cf5ee80497a26e9faf08b393605fa6147dccf390a7d70e472d8115577409f5e57a78498167ef76f4347c97d74c20b0b26baf9afad9b0d72fc35cc1bb6803c8e89c92c9326aa2276d41aff18a309cb34722900d31b9907904bfa88449cdcafb6fc398585191755324c9fe8f63a2943f289bfa4ad72ed4af87d9edd32240d41b10d221dec65d1abb617df8a87442c0a6c49234763e861eed06b84fadf394c556c0c934ba5a6a902f5a0d4962ccf3fbb9aa642f8b5c9b826939ec46f6a6d81e7f57130d0dfc5c948b2b0a80b484cec4105cd9fde330ad2213b8a6badb60197b001917ca5f336468ed2686fce001cd3eae231137b2c2c7e1774fcfdf540d50d391a4932fb01d2d5c36ee204659170dc42b5b42acdd78142d2b8b4c24beee00832d711fd71a113b9eee89bd3903de08c5ee4042901b5207bc83ff68cc5cd6e8e214ecdd5e2f7e18ad098c5580b881b00771bf7c34309c0b41b17fbf4bb205e26182e0fc721ed357ffd9a0fec4d0944b51ba72d7869e69da6f3b3718edd20af9e0d29c61af78e328639f94316aaa5634567093bebd8437b6fb3e3cc43abd74fda7da16d861235b8f07c367a35af96ac42b732c0a19b18ef793c20798389a0da64c8d76c4fe9d7aad4ab52e4cd54c835e1a6661743f625a7e8fa322d44ea00b85149e4b11c32730d07c7cc2d2ed92a491f92da75d6ff9f09faf8f5d8b71c1fedc00cd6e15ac29a3b37b2babb34a204c556d472c7a448bdad540228620c25730d582253ec5473045783a9735ae1cbec31916b1d08a4a74726286d6db0ccd9c3372605ef3f26296de77821c8e1712d4a0410130dc3126b87108174563b0822e2381fa05cf14f6c1e0e49f35d08155aee3e7e5fac82af8ef5f95de2f51a3824436fc53b4b54ec212a8ac3f9e85ed591cb50bc2e1483a598b08f7d802d4030c56e22bc610f2fc58fed70fb9d9cc3ad7e4a958715e1266f2f4850d3d7c3b7645c5c1822e50f9f23786726c197bc7c3691cd1a487a48326e9068f08e8658d9d9bec67c4a1c80f9d37cacc1cc7a68461c10071a8dc5b58a02a5af02dc95bb314527fab00b38c884186beebaf7c14f9c2f5601e56e48d54661fd3e0808b668a777504838e2a84d1dfb398227ce9404271d51cd0d3810580cac3665f984d5723b5d79dcd5bab30fbf1522dfa81e75faf000a4df5f770839e9af54b0cb992e4b0bede4dba0ce812c9fbfcb00853352313dceedaff046eda814d718c5005e0214d2064dc816005fe87c545a4259e752f5f9fe87d8269677f787fb43c7a61cdf7c5d3642cf6b4169f7d2d06d3a4690e16bd5126186b6285028181f7a14d1006396cb698df93a8a29dfa181db73e89fc218c3e9c4e0a50d6509406ca28631811688aa978d883ca0c81a54a248650fd40acec708081ba00434955801139815c0a283fb24dc69c242b5d7c42f1ee7c5a5e8e2c47af36166925b632657b88d4ac47f259db12d902f1ac645bdb5e236ed152948c38ba1961769d34d3184d25143fb18a43a50fa812b6e04d4eab3a796659d8580b88309e1a426a4d8fdbdc6284af5c68a91fc88e894a5eb412f6f0deaac0e84cd75451feb0738df8ba6c4e9ec06e1a865e869c23b3be5fdbe56dfbbe410cae10c0442a304dcc85fdf2f9ec4707d248f0b7f370b8285b09ebc1e8d1dec27323ed000b210446604612ad183e03bc2cf54488d2854eabdc28afe447853bfddf5da5e6fc9e1c0131252157820fb3aa0b6b73e1e4835d1c3e65e7a2d982199d4ab227d8522610a01cdfcf360d8da8a11b164fec650a308f40e6ba938fb31e4ed648c20965370743243ad210b6338134efc56466802905a1cdd66e47871b5083ed272a3bf9e53de6722e351115133adc8336c80a84d227c65b88cbc6bca67906130b0777ec5c767c9b3c93f3a2bc60d77c0516cbe63cb29ddd75067ba27e11dd000c5868b747b395f71b21efac58ee143f10e94b75aaa379db28e468833618a101e75c3e5ea269ceb83ae1c5c4455f150bce2f01190324b4a81cb604796f4490d527cf7224c18a2612009d23986a4446481e2c4127b8d7c9935559983fcf771bd3369d75dfdeec5dfa7dac030b93a2be435a0e40244d5e6d2e8876b985d73ae52aab9c5abf0aa6e3495ec4b3f726fc4abe92547ef70b9b9a27fda4083cc723e7a3eba4ecdc1d61800a19f69e14e36f128dff591c48334a36bf2ea687ab741d83bfcb176ea07e3967abfc9c08b2c55d99b224b7559394e549a2d0aa9d89dbbcd2961dcaf913b1f57c7c6edbea06b5858ede718d4dbcd5afdea9c74ccf589428c0e848b0e68c7eac105727625c438b888233a7d7cdc175ed59e21458526b8174229b12e3724e7190e29913c1e9758030b51a0e533b58d948c5553c5970e8c0ec51e508c3bd82dea8007137020d132003bd4d3225adcc6f0751a5380dfc90259b71f6c28f3415d18b106248fc7e83e689925bf307394c74687388991b748469f7582aae61c4bfa7c5ae470615ac49e9c330c0e695d1315b2a9d1b32a12c5a802c38acc651ea083305e88d561b69d54ac11916f997d85e14e9367abc07a226f62f81f46a26d4d0111cfa5a0e9077430bc368aca3dd862e56a977bc06074687125124de3813931ca9c7733c7ced619e25332eb851ac8da98d3a960a6112e07de35b020b72213d9a2cdc78bd579446fef9ab3c7ce39a1d01d2cacdcddbdd054a0710343c456358e3d558364169aebfaa19fb253ec6f3653d60e74e7c2aa18f74dcb2b1b1a47aa5970367c596de13e3cc1b92991fd1f5536bdd781d2fe7159bd258dc9eaa8b3fd59b783597bb7c55f5536e2ff848e14756ed16b55af882e7f460de7f78bb6baa662f407db87fe5fa3089f67cd342d2b119965f72086570548435747c3ea4503ae44edd69008817341832a61d1e7e4084a7e2f69f89d6833677b1a168bc1bb4f0d406b2fa2c65bdc233594f9b1050621b9c1e260af77623fa5bbc541204b96dafcd5a63bb85158dabcdcfb5c8a1db1710a16acb263f45d0f8734ead5e630e622c622b6f9bbdea0e6b2554fcb545d95f581d6755e4da2316028eaaa70433c7b950998ef38d9eae87ff226a066f87b435751708f9e40dce2baea31a90ec81c3c898d281c30e6bd06d95821520f8746407e659fffca1e9db9fa952aa2afc45598f295a6a5364e57ce57caf18b5f7251f934fbfdf2b515f20f80527f19eebe2e18f5c1829a79ecba84d8b0435119af6c8056f0a516085f3b72159ba92f5f5aafc10ed72b8105389492666b470dff1babb2337a49eb89aeb1547c165574a33f579e6afea88c04419661bd5c3fdede27fdcf6d4ad0dc90f5b8a0f410885cd1af5f4f40347a84a8505119546910b3bdce7a814d1f6bd28191e490d33f6a853edc8781f3fc8e48a8196806073e7c624b7a1215148617d2fa0e9635e78ad53b0a50ef28e99e77d1d732d4f1e584bc3a72a159f25f08e839ef006dd4e3ece678a7d24ebb64fe4d1ad11e0b707e54316a1fa288635e75cba6aa758477bc1c325f72b4e2b1cbb70545762f59a0cba750f1056ff47c3d79f924b112f63db41e4ae7971a225097d2e1af0b1b25d99c479a1dd0cd6639dbe79ec7c3b672e570e4427aeb0fe2196461b4fe074138ed13d21e8110708184443bb1243c0f18d79658ffeda8c1b880492e2204f7f5f4b18f90df2d600b4d876084407c66d249830c2e54ecfde984800f5829040e6794b496ed36a3b92a6dce775e19084fa1b00684d478cdd027bd48b5a2fb29db9e00cd5700ce21d20a953bee84be4367843a9a716ac20b67d0ec6dc4540acdb6095082bba14e71b220564cba98d05f4f387f283a15116d09e313fbb63a2996594e1a4680a0f342a13d1251bbd1af1644331b3ce0a1291c185bcd2e4f3770de1848474ae3e0cfe15454f261f40c260fb8517ef40b15fc23f71debe57cdde4a5a3c1474a237e00dc9e87b31d1f29686f8394767dba9ee0f90b752a118528271087a9221234dc990ef8ae968d0254488b7b781adf6198684561ac9258b821cd0c6f7471b946aea3c22b3cdf659ce721c32bd353eeb01dc11c6226373837d14da543ba4137e62a0055628cd565079edcccde6beeb9521ae9a392775af9a9b6a909903f28e3c4d7160cf507b7f8ab9e9ccb243b3e24087196e648f90b7e31d3109cb68428ed440dd16df9e7fc040ee488408f9afab79d1e2d536e2b1aa5abeb7a3e692519679206288088f19c2b6da42ec65a18fc6ae96d004f80bd34420c7e2db9ff44ac1bb0d6dd28987dc5bb96a275d864d0c3ff9f3180d7c5f9c8643f26480baac84d359b0ce0a7e756b894434d2c418e5fc168b9334e2096a95d804d72a01c3f238440cf724ef17e7f8e1477bf2ca2f3c20f3be07ad48dd7403e2d3ff275bc207d6f2962669ce62720d5b9478937de9d8c6b38001de69a47e7ebd1ed7c74f91eca03587c5a476042bc2f06a6481bd837024113d49373caf9cf77baaaabcaa06bcf79b4388028517cbae2d106f393006f2ddb0c7b73c3ecd24588db837a3390a1ad0d12c740dd5c0b01f9c66b4a0fe4b963e235e75209b24df2e0fd81745e938bccbd444d75be1add8803f5f4228cc2d507a4e3a111a1e4e748f99ddc7b0921144d43ff0098b1dbb0fbf288551b224ae0b3535abe483d96a63ab93c3b040bb9eb33117314cb08f37a6f13321c37a2f38c0b49bf07ba652385f421d76c66c2cbdafb10649bca4046f7c2ddc611ea2eefbb67e349067c09135a2c00b1486826778c61744136d1b04e04aff33013ea5334687c100dc12dc3f3476a356f7c64c7cf7d9370dd3269b01b79d912e734d0d1ee376c118147be135327c32fb91c0810c68fd1e58ba8c77e2b5dbe491d50fc9e08d6fb60e2afc29f395ddd6a4e844dc4b1d5a4915ea2824a8e4ac4ef98113ccd404226402096bd69f12bb4d0017c9ff70b2fd7d63ae26814bf8afbf679b0f70a571378b76f389033108695f80b16a04c06d5d7bbeee2e4e7664f86150255e08ab8884040d4d121a0fb785c823b37ad3a015d522723929d0c24d9b0a5325f2a0c126151366582c201dc6fa68f4e262d907cbedece2072bbc8e6b3b03469e2282db180d6b8450bdbc97a129623db9a23c9446a1a7d6832ba92d580dfe6a5251c512639d63f1abc4889857d2505a18c1db16fe68a5491bc117eeae633a9665502ce941cd32ba41b48700b3f0235cc5883608431a11bd3093739da1113b5e99e649c5287d62700b086017a2596db283c39503d5f6cc2ab71020a06e07904b9bd61e942f8e4213f3a911cd522db8b22fd6558743b741614897990d3b3b05edceb69479d71ac5308be54334e74ea68af9c4bb23a383549b674173d4e6eafff100fdf00e11337fed9f262899449cea336b585a71945ad01fb88644507c0760853d5e2d32bd380af86219a35ada432475ce9ddd1a107ddaa3ea32378ba73d70048c7e92c9dde88075c897528e654bb5876db4afd8e25d2751bc31eeae0ed726260b0b0a7770cfbff27e75429bf794ec6457c5a3528236f369004287770a94c0d240841e47cf018b8327003e97451efa0498e9d7f1ad77bddc3957cb60afd4b57e2548e2e37f918f4dc9b8320e2480c1bc7ab2cd260239afe6ab190a9056183a510ce40ef4016be3b2e73ee761205892be8d5953c879d61ad42ec6ef5d4cad4145363b5233d618ee68306c85dca2ff379c61e0849000906713e1b02be235b55d12b338f9eb0f0a2d23b1a56011d6d86a354fa82a2cc37c4417d3ea912a1a1239f7e5c320ffbf68c4c11a98ca49258bb3d3fba9fe8f2b30ffa86704f0d369af94d98e2d0388ead2c71ac25e8540389ef5b15c50f7b430f469f1d1e9632960169d2f5664de72c761e8165d5aada23ff3dbe2b48be81d82f2d26ffdbf276629ecc02bf72454431a41232d3df626b5befcb756cecb38e29aa626eefe16c60254eedeeaeeef9b9025283db33e36599c1cd5058fb18c9edf3fea8c0aebfbee134b9c92c806fcc7d2a1a6c079b43e85ec7374a507902de3d7c3e8bb193dcfa51e4410c9d61467565439dca4f19158dae2e55e30172780c4e85eb8a78e41a52425bfd919ef5470b751ea9e4032b80f501f3d36debdba076cb7c99caaa2da491a01618f207c06da32e9f4465afddb024828afff1a1a681c852ea07809e7c092d3256afb3e8e7c32079d45f7566864e8170f7a419f91f96960f6cd211bd70e835b4dad7c5d2389817c75a9147b18562f31dd5c0ba4490e815c034438504492c0d5a7edbdcd53df2f299595f3a8acaeaabf9dd5b3b4bf6887d2915392b54ecf78fed2ec502f37549ac70da19bf7705cb85a9be4bc6563dc10f57c844af6da8778da504d627c14f29f7865eef2170209f85cf4803735b4897f82e472352a2d1b7856c1a7e28582ed27319fe0f91022d49d7cd2c9daeb462f28d6fae79f0fe32d97820fd578d79b5a1d9d97b28761f05103791cf69c61497b5beb03299a3493ebfaadb4d5a6d1c4fba50f871939ef07a9479a61651f47885e879520e8ba1a7d3b3b4d5c05e3fdaed127c70b64293d4a12b3564bd2a1beccdf2d96c88ae6966b384d82de456dbcc8670d67679d60f5128aeb48546b551716a368c6f2489b0fcd1ef3df4fa7c17738856198208343893f0aeda6a9fbbae7fb9464f44db06428f83accc103b9930d13003c0c785b04d7ea048918fadff8a2a585af9feddce9591a0e9c1cccb81ebf43edc99eacfe718f6136d0e4d65bcb6c5f00b3d7bf9a1c8487e6e3c4588f2345f981ef2d784e7c19c2edbd4b4007a578e6f5a0159121c00fdad4ea6059d33acde74b0a17557c733b528bf765a1a7048ba427613050b115fd1102c8c48d410875f0be541a32bea0be7e573756d5b9751fdf255677e8f809a2171004fa52fc3a063239b976d39f43f3f038f1dd0dd724c5a4650a4bc127f6a742ff77387cbb9ca30731618785d986583cdd5d80f98791d65d920ff1a3aa938a4fa0b86b03540adbfc9451d834d88ca37363e2544eeb156e05d1381bc8775818349d7012605f15cb050e0e410178d91f2366dadd0e03f8dae35209807e0dfcddb710e398a8fe2dcce06a981e64a7d5e645f6a00f17fd0e0f59d420b73c08cd6aa9eac41aa77b86d57c2218cb1b6a653045b50dbe9f5440a3ab304b83ad9a686f3254b015f1fe2c7250daf0b50564c0cf63d3820589cdc73b867217d13f729c6123a14f3b32d9bc0ffc7cbe75f3fb64bf1d89642aabe9b8cf04a829e12d86af8dedcf8a030909aea7339dd69e04a84fde848abf08a4ee8f62e5ba91c2c14ea428b873e4c1d02d296348414012a9c30406e0f34e8ca55534b639514e54d024232f8746e6accee08376086df28d366d682f7b8ddda11c1a2af64c86283c5b39077b20314b9eb294bdff6258a4515023a6cc50c196a32f6dcc25e1a506241928e99620341746a581098b88a18f504c4f36ef5844932a38fa0db35c5ea6a057bf7b0e6a97b2b3887c74097ce8ab1f63fa0b1791a04f2a2c95cf4392cb6e4d933c716b69de2ed3a8ca321ed3464361e100ae30d8fec0e92fb80831dbc42dc65bf2b9aebc0567e23cb4310678bc76716b361002f24d7cb437b02b3d4e120aa51c308383256c0012067bc16f72c93655da6bf7de1278cc021c6713a8ac5f1a1c8c381da9f3d13a47f7ad630c8e12fc2815493d1477df509d1bd13cec89ca56537784cc34ecf1f746315bcf4b086768f4dffdbd163e9f208b4dd3a849c9a98ec7a628131c5bb7421c74c2c3ab770ba5090b0deaf67f9901e3846968fe87f38904fd45176822c6be65cd4b3e5f44e365cdd796f81f95367d4d18c8c881704b7c123c1b5f7ccb07867b1c224b64eae935b3e1bfc2631e3f665b9aa316df06f6b7c5c5c1499908d382653ce9bf109d6a1940386eb36bd79e24c9f48d6742af47b47d60a1d5b95567c5736ef38b0d2057f5701347e557ead167a7064314cebad0751de9e0bea3d08a1141346313867382f8763ee705e338cbce572f8d863d4bf6d15187f35285ee2a176d1778d175afebde700253432e243b933c0d19f486c5c7b166b58de9155c706c21f491a3caf29c0942b31a3787a31527c6294d853f197ee2f5ad1140f36eeedf4d5784ae7ba6f074fd17d80d9738ab66774b3294c7b41771ec18963d9d6d1a927624aa08e69671d709cd1886f9d2c6777fe170fd377843d351b9903cfcd689740b94eb3c83bfa17472978a775cb48dcba3a90e493f10976130fe1a2d3d57eac5cafae62a63b836187c48dba242daed1b8bc4e922065da69c4c92ee3d7dc6b94cbed50159501e0a0bb5e39b1e8b833ffe201e2f385c3281700cc2cc27dc25c847dc915065121d7a59eddb568dfd2c73455dd53505de105f115d9a4aba78648a06cc8b1a5a77528ab3343128dd1248c0b1476cb33d6e6d193d931cb66af32caf23c4139b16ee4340e89ca69d0a808041094558c8b6a1caf9839401b52210e37fa3d7a51828c79024d93551da3f3e0610545a6d6a638a2724673c62272e2798500bf3329b0593c2be0dddfe97168b2cba75ce3deb139c4411ccf20343a11dc0ee0e9523053144b6b048067fb19b6073ffda542ef317c1c6e31813015122df234018b4d3de3d8b9c037802ad33a814d6c2fe4f6adde1eb90d022de01ec6f0b1c4c230458507c8ee4b378736786df485bed0f18513a5fbedf244288af53bfc81139a714f08af712e950aa7a7c74d4cc65ed1af88137893e6cb63bd032bda4079ed08b452cb99f474b2431dca95fb2f53248a45dd151eec380f1f570e12ad9b384593d02a41985555579cfe1da3ed4563aeaa365a1c493e1a0e449a23fc0102d5621a8efa8565fd965b793261e5bbe030986c2b26b2a4b0bab2d12a5a29d543480125940476a6c16788cac28978ebeafe7aac4c08f90744f8cb8123298f95a95a5cd6dc415263650633d9d556e620dc810621d7a982a044431e5060b68f05e503ea8d4a8538d965c611b09afd8cea999e9858d34e8b0bbc4f4833d107a6fa8ae0962b579c7e59e9f6d28dd759f8eb27ab289c323ed154402c6b27e9e4bcac6a5c10fae2af485f7a21c4007b113a12f7583e3dcbd0f63de095b0f384e67c81f44902125ac5b0ef211a9d8a06c77a7459bc057d94707953525989a925403180286b5443b32797bd44d671d9aab23c67a0276b04fb2e7275038b7d08ce006c688f193dc09560c52c14864497cdb6bbd110b845e62c68d31d65c2d1f5fe964dec848a7177e234b91b33a2c5be2ce0694061c7615c06bb9e4cc9701a4064babf3b89c4cc2d5f43dbd4b0708113e0be775e0d024664b10ba044d76107a081793ad5b4eb38f4dda8a27ac9946851eb40daca79e6f848fcd2beafcc381c1a2165674cccf6c0a964549f64a409f7b35ad79daf7495df9351e746729af198d5a07e02e69d4cec3d0b4658e3726361f9c43be1689fb808b250736d3f31f35bc6a3d6250dad75b217aeb071b8d2cc22ae1c791b73991128a49efb6cdd43f8027755924373df962dfd67fbac29795f2d2b35fee99b99203b9b710aa9d85d514eec54c77e3e808262549408010e854c6962246c7d7e953e57b4ac1597c1add08aa19e08c3669c1755b5d29ddbffe920e8657c2412439192c10a8be6beaae229542bfff85ac0e73777d7256d11b6abddc638422a99b68c092770225211689ab24f47074db0ffe1822846c9b7c93c74733e5eba89818cf26efbc915c1de4ade601d26e7af323bbc52f3d0341a5bc9b1499573b043ca31f51b8890da59ffe9a002dd70d035ba0dd2b7a59d4533b28c5b828c3970287a9c909c1deb728627d20aedf266693aca8c2276c138c7dae9eab5de04cdee8f84307a40661f879290d0680991ba5a9b18dbddce14ae1fc9f9de227125b4024063e29339c38434be6a49bf8b8ffe5119793631587bed2a7ec1ad1286f358e9916765c51effc585287e899445eaa676444c85c0c2d7e133ffa05e5e71f7c49ad8cbe07859301e3742d5bcf8014483020c27aa5c4f33b7e88481bf897200b42b36d8dc3529fbb01fb9e3a7fd8ee71dab8600ccc5274097630af8d32ebe9483b99da1d7840909a1d405061a4b0118aed85bb6b63952ae3c46d5f588c7a9184807ae9e1bf926e0ada994d4538c6063ad39a19b0177fe4df94614c6ce06cc0090875bc521fa275ae185cf73e06e421cb13e229ab6ee79ce6d97caa5e33d0fce9df5c0b8865befb5ce04f365493a8c6b3301f3c8fa3bf6ee1ceda92af8f11936b070c4750bd712a1ba5f683b82769f568a46db5aec7d23492e6f14e969f24cf3667cfe2307295ccfa0a86aa130a8d668545e40e4d2178b9e9317bb0ced837911890e0407bd1a6b456e8ae510de2c1e1643e4439bc5955778c10f2eed3e4da93ee28f0a291ab9d26a7942720c75b62523d51bf68729589b58cd3e59e73298e0999882018c7dee1749af721874bd7b2d1682c14b1f6e8321003b48a8b0c04d3d521577269b76dc8186f6cda468057c77ff7d92a6660ddd8a34baf834c538ec66eb6497429059a274de7b9e608814c3c6fb00254392aba9c4ea16ea2b260079a3eca1d7e2338bfe0d4c57a086fb04d5b60848e8988c54c7b1018af47a13e4cbd71d2390aeb0d93be77c393cc80100b48fa773770ca54ea57baf328f9e46e80cb3da2148e391a30b04119b064423cf5ba0e91a24523d44b840a51c63d470a9573a0590029ea3e69fe82786162f35039c4a8b508f638a15901df66940fe3b1f57a1b253315e92d40f598baf62c476e7b049dc402ecd7eb8e071ec358ca7f20559b3011f174cd2064f84a341a4e3c804e6c36ab0d43a81825961f2359c97f02ec9c5969fe7f936960d53829cd4c8508db8912c3bee30489ea78e6b01d5ba4c231fb5e00812f609f960a10f58412e8a147bf5d4051be4b0b419c7ef03ca1e99645d037ed38701b17a5917c3963c8b311ffc5d09f42886f44f8fbf8b342e617392aecb05bd3000c8a73294db489d1e5f16a555774f2338b79d9c66d224a23cb9634347adbbcf92836d8e07c344f92eb64a2e657e4fa9dc324d053e97c2a112633f041b052c68bc8ff351fbb31f2a0e27d879ab03185a404c28fe09d774248b47e7fc1b3c50b053c51087a9ac179c7e6042abe761a0cf313e8ea81ba0c3dcf506e428e9dea42790f6a30a21086691b7e1850f01cd605033754e632e777548f6c998a5218a26a23965a0108f539fc4ff187a027b09d2a879a445f2681ee19e9629b35d49a0a0612990533e084d3d0e263871a30df1a16d93136158107ff56eb160cdd60eea977850cff54841fb6803ee0fe784b5e65491243a076e484f679549d8b42e3c0b9448f81dbda6a3638f93925dfd4eff56a34969c5d96210034a554d191ad7e3b9e16d800992dcb38e3782dfa33e6c07d3e59983639fb3554127f273db120202a4292d73f9d225c12f24a3acf34a4ffcc8c098bff8b1a17cdab248ce48d4e082918c0c8e63f1509bb6ce26862543e99dc956b354000711e682041ac01e214095271925f5760b979365bfddf644647a9ad509aba2f4ce37e778911cd164ba4e1150f4ffe615ccda896d983d96a55ab4f1a0d7602e6cc9025916e58742b27e82c82f39ea71cc01ff2c3c625fa5068a19a6b8e1fceba3af1a6c9a09dcf6e32785ea5b0bf9c43d5b06d21337c9768c03332c5ce0b90100b312dcd8646ecb99ee7e5092717c86d656c92d74f32c6f0fe770d07b8ba0f7f3827a3ede781063a1e58dd1aa1158124be9cedb99a6f2015c70a7867cca70fcb48c915ea70962a6c15669bd73d1077937df48f4c9d9722cfa59fc2ffa593622a419d27a4826fdba511548a2498682f01a1162f35c27ab0f9fe793eb52c29975383267b7bcc88b22009c51290b4c523e00ec80afc555355f238a66d2d47b7921231e7754e17d9134ff8656c82fdfdda33f70c955ed1ef08e88a42ec58737b352fbcea071f95a425e5de29be527cdab487c4a1abf0581f475a1200164066eeb418b50e5b05bd3f90a5f5b3016b4ab86fc1c56e3159275a247e7a938ba92ef8770a6f91b282ccd813cdcd7a8534f0c088e7fb6b852e3bc5e6d730a772c4381f9d51b47ec7d63161532ac10332c20bc375f61bf0fceb8f5d74b9ae1d01af41727c5117b0f5d454b972a88f113401e643bd681e272b8ab9f862fe271c9874776604dedeb9e39815db4a72f06148662f0de95672af4b9f224fe8cbe51b49aeaf2b89cf6a943aa61110962b4135ab3fa9f6970305405dbdeb88fc770ff8576a8d15abdaea22b9007916135c907b4f6671bcc3be533799aecfb81736a8404c74f35e6602238948815844e70da11e7f9fa87682dcad02c0d88bf2cbf8f180226b16d4c6e3a87e95e4e7caf68efe7b0643ff3190e4a7738d880b3bc3485145699f20622ff548f37108416af4420b294802b21108087120f938713133aef804b2241af98ff44fa475ea22cdc487a90a17fb095eaf12f361eb1f8a87be5d45176792dafe3aa50988a9a1e8c59ab8c4085a9efc5334243e23663093ad3ea6016e2417e7a2844e123cabcf29a5248532769f70b435ac91a48e92a2680d55220c950943d7ab29eed90c6c724b34bcf0efd244c14beab2dbebcd934980fd317ba8f5ac1c32569cfbba838776015e3d1e8b0b90a5e9c9cb2e25949f71a5adde0026908ff457ca9d445599f01b033e7a0d29a49d85303bec58f922322bd40834f10a9c9d95067325ebe537deea8989d87718daa8c093ebdec4a3a2f87d9d3b66597992dd107f7a9c63765f7f47f2da78b83f8d9a028c303f709c7173edff5221bfed73d454efbc22f8f4387cd4e5e35075559d8925164f425eb6173f38b56ac4d8705fc0d5602d652e95753876b5d7540d34770b3545e71147525bff06793fc6a92462828e9725bf451436a1535084cf8c25624ffe132f18a84614e2320a459c7a03caf1c588039eab6baa6ab0ac475ada1ca512124782ddbd1bcd34d555a60369cbcea564a91d7825791464401ac6f83034764cd28f775a2c3d2fdf45c7c8f82036470a5354c3a49413f9a14cdd54db17ba5d137f0f333c5be10cb8543de4cb904c9566e55175f40b7df978c02a07357f335f01c0d58e60ba202c0365582dce4bc742e4635f0a9f22680e6c372622051ada4f74bae893e6798b633fddab99483d1adaca2b28aa13bbec15f6ef9e076fff52cc96a24b98405ca584373e530d747dd6b916e90fbbca30795df2a756db8ea3918e8daa587fc8364e35a738e58a8b04a0c46eb806af4bd0b04fa8d444ab1a5db0b43b106318c20d0e768dcf047004caa1bf608e7c57f153f16fe2ff299ca420738af6bf6367d0977355381e3142ecc1e3a25144cc7c40adc77a0d81a4f8c7ca58ddc2101ff404a44fab47b34a06b66e8fa8b4fdd5395fca16b2b1b5d603d3ce3ec26821507234e9149d75116a36567d262bd216bdd07bb1c504c841e2ab87b1ab0385d67b3aef867504c7a34a01dba03d4bfa06b34f65d834094268baec1d1359f0965815a56ccf1c70e3362979ce94d41b6f45cb9cc944c894a086f9f69bb83b3644812799d250878d9cfff7874be4cab6503493183c33ee134fee3b81b2f75ab02b892367996b946175d33150eceb78ac95f3309aa9d7c2d0e724bdd75f1f78e8a255da48d59fae524d5a63664f135f1e33f0e22b15e99906ed0e907c4b40dc5e0c8230cfc290994ad0e5f5e9b65c3a100a07ac62ebcc8410c3648cbb8406116cff10884a49ac8e3e4f320c72d60e6ba077b99e3097ec395aff110337e585fe3ba8599a9861924a5db58642ffb79bef4e054aaa5e6f1702d704552c3f34c2cbf35ce4652249a9f2d5f02495ae52b2fa47f33ea3382907f76765bd3712f90aad46d024a6869669d5e99d6d3f864ece1584d51263f96c5c75923489b52b1fb3f8c5a6472e43102772a8cfbecda200835e2535e5e24f6bf80cc8e4db2341b91230bcd9089a84a14fdaa21d84c640012c98515040b117ec5bee21856c3d925ddbfd95882b6066aff2044d9a3850906080bafe568f5143d2793f6250c0b04d92cac4069f500a01e3578823d20843d7c823408a3400e3c199739c61a7adcffa323c0dc0eb5547d78a04c13b2b1a73404e2b87e31e2a760f3a0a26acb1866f27804d5995dbacf16554c6c708993e404e962791d810231a41013972200abf53ab148b91beadbf8fb0a4134c2b6fa8c684ad8702bfbf8d8ee0b37544bfb00c2ec7219bbe8d0e04d460f7eeedb844827661eebf8c764fa22ad9a453c41613248e60ec48ea514e3e35587c5db3b7231aebd0447f23b2be31230801728390db2866ba2d52012028944b045364adfd9cd5e2a35078fa069f84b567ae98c83be5a0ba60e3ef115ea1f22cce61ac0c0fa82c0422015608808e0552580eb51818a9ae52a0e241405b495f896091f5ac8a04f8a0f3266cdf35ae45f1296b7511c61c27a7e1533158a2081382cf5b66b3bc3ae561e7afa40e2f19a4974c0c8a1be3dd7111aa4b403b592dfb7da72553cd81094262da43992e818b0754a2b7d1b41b4ba094443cffde26f127efe235e5553848aeeb11c0ed5953d24fed537e1a0b75367b423c362e3dfb3ccb092142aa6289280048ab4beb25c5dc18ecfda9bdb035a22ba4de322cc2c8458fc1cad5cb4769e18ddcd7d0a5c64a2f4479b5a478cf5254c26ed59e53564102bdedb3f99888882c2625aad43479683f84fea4c1a3e851f2a4444cdf8217f4f51a70af0eb26ea9432809591bb2fb7dc2061872923d32abc1a83505c0ad7ec9cdcd9ac6ef09ae3219cf4db623545b00704abb7e5c45e9b1e90f1a8a62394239e0ac65f238415dbab8cc84f45f64e1bf55ac21f16705e2bfd1f38aa262e482e501711d6e3e01dee91c7b5eeae4a27d6c5471031ec9274eb90b2882c5b608c3ce530d2b9450e66c0e76865ff5c80a39aba9464784af03ffef192678f661d3999afd596b4bed2bfdac83b9f5a7b1d103d3561016b4242a97d2fceeb1d9eb5c393d4a0e727dcce7f6eabde3bb310ccf4b3cfd1a140d2c27c97dd83e181bddfef2c3da444088ddccb11f110d2546a020ce78c0c245108c1faed0d4a2b7466d0f4eac60335398299d9cf34c1e69a311e43cc5a728b315ecc5404e846c989463fdb18e48de62f70c38320b5e1f212174249da55d6555dd1648b3da5a969723943201cff64230476317d97a8483a6bf0df3d292a0d79c9eb46b3effed6406047fe2406e267e7d46094ebb40335fad9745186bc0a81f0670bbc96ab329eb75a287dd35460ba780312d80e27a7ea527365a3e9e02c781f2d2d7a8734f65d4f5e1d0abcd4b3a01f574ad7d524231e082c8a59c6293d38a2f5a14ad688b20f6e830ded07530df8e42a56889d20879fcd199f469bf88b86f500ea450d5003ea741814d21f88dfdd314689248357a3965e6f34ebf07fa5db3471ecae07fb1ffd4c9c74138cd6d3e1e1eea3cd9c63f4de06afdda7e5c61a6216583b2a56a8ab135af04f0e2c85b23506d2c58b2af1f4756d301a647cb64eda1884488f88ef010970181630fb991319935b685d9283e8584e2458023877b05b6d3cb8010a2b6b862c5f899421bc4cc69a18ebc325845aad0aedd05cce718571dd7fbbed96c2167a2841100703d19b87ba1385af32e60fd047c2fa2370e919ceb553fa1c26e4c18892b6aa80cec4d0e4b4162abdea5e72579dc9b0faaa22f31a7d7f5a55eb0a1b4580a4b01820e3d58c45dbaaa22a39983fb56328ae3a0db08690de79bc8353cfef9c419a84ccd82f500b50efa6014fc6ee342ca0cad1a57022fd754324e0c11924d808f9195e7747e5b3b7e3781cb3bea3ee8d56cf67177a5c0c341f1923332e3ee4c5675ed07441bb6dc6d307ccf496015574098b83e11dfb2d0384559baebe2258d77da455caa1ddf80c63385944d98b2beb4bd34990ec8f2019be197a37560b10f50c7afbcaa2948e771a59602120f78d84f69528ffb37e34ec1b1758f954a6a304a35b35e4f0b3d1bf2e52c20de375e8c503fb8aa37e1361077348f39fd7468af25fa07b8162b7867926c1645fd74c9d60024cb1b957be5947caee0a480bd6e197ed0220206c01bb50b66018975f047d747d180c5e52b9545a78ec6502194d68c36eea07ca0a5f5e8178c2bf3d3edfa30f1d3b8f284fdd1f77bf3708473146ed26b0467e197a403e4acff4c4116e70c940b8ef3d013c3111be57c8d7fb5e6966b53873bcd17d77674ca430f57a0190115df4bf1989b51ed1180779d36a8a848507773ee9c8b0378d3866ed20eda88473a3898508eb7ad645f39bb852dff8928a42424ed1d66ea389507ef89d3fba9e2b5b263d9a6420b074b6a70565ed22a13be4bf898643e80f92d5ad6e6c1a4f8779c8fbddaf2243dc5f03dc350a3073fe6dcbc4b9e5e55a3700e5de7f45106679698596c280c54be722ff624d4cc09ffb993130cfefba105c290fa469373dca62edcfdd4c4781677106e825949fade4957dfd67bbf909bb65aca34dc567d956c0a402ac0f6ab12c0cb9629afc709630fc8890275c6a00c3adc6b388d25f3ab76f1091a9a5d495c0e247a182897403f92a7a2216a3260a8d4f278e5ff7b96f7d69fa933acb98c7e5af988f1dccaebbfd28514bf45814dc0e848f8bfda2034b2efbffa062eb4550b71a431ffda4a4e6ebf12fa20b654b0ad0354c90a280f7756e4d6b52716b66c4109c5ee515ea4fc9c222055210e343991769e93bab7a22b9ad6f80d829fdfd061fd7c119e3a0d791d880145b994cee85f3161e8891f0e1638e11273a3dacbe24075c7262e6942b9ce142fe5099f19c22c2bfacba7a856577f25d2a94f32cafc97d096390f692f338e2b9ffbe9630aecbe2c2232ceb773632a0ae80e292d1db4ec050a71f9d822b597335ac660b3bb96a21276fe929c874a2313abb9295c7704604004900f15a22a747b4dac62f498763c1a2de650fb1d60013eee91d438b2de6b4bc171e5a82c2e1134b23a608d20064967100c1f2c1ca27e152cb67cd179603f9e3cff06442ad01b7a90e0f8201fa5d16597c93673984ec78faec4502e6c6681ab8dbae5f94e0fd01280dd6672d68f971f5ce44dcfe77f3b60364941c075cf75cf6b8e8ef1c5b7ef5c52a2938440303ec8177c88ffb454c33971068193c2194c994693c5f5a28e338f30d4d061c54255dcb99cdcfc42069ce83271fc44fb107697ece7e534a878905010b9c9911825e6f058e978561dd6356698cf47ea1b857830a656b8b25519d2f5e820ef6e957d2262464ba40e05014181e3c0096872712d486fa7b629c79cedd6d88dcb257c82e481c529680cbb8f41fbc651152bfd2005e7e754bfc1710829928716cda150f80b6264252261fded04683138bdd1e8d1277d2c06907ce8c5aa638b100618fcf6aea32910c8ef0bc3b9259b0d0638dcdde6e7c0ceb07caca2a17e2184cacf297a4812ce0dc167a1b9a308c5d128c670f7004d5da7dc3e99d1b696648a9faff8865be85e6bb72e66cae83d79ee55a101a99b9e4aeb6ca3812f702b284234bc3ac9a936cc6155446ed781214f4ac8f26c83374fecfa914d208bba709f2ecbf4a12a7ac28c8dda19666384fdc2324bc67c5ea9e6081ea2aa4e9492147057aaeef8110bf42c9efd61fe1835c0b72d4fe3609b0ec6164e3eba7e26365d5ef89581b2d98e1964006728c7ffb3a98eefde7d50e53119a4a102e580fd3ffb96025113b4846ee644760891408a1d1b26d14caae94331ed7964abac4c57de3b270de966663bfbaa1f420116e3f4dbbb0f6040ef750b199b6b9820ef44ff93e1c35ad6a8f0da76f78418dbd6ac5dae5fe0506dfd8067422a846ba61cf33d02be9fa33d8f368bf0fbe1ec4a116d344755b882586f380eace77b450e80d525e81907a46e071870289dbc243354f50e4970588f9e70577f8d660c814056d5982e03903dfc4e053697630dc783d48089c750140bdda84f8a54e0fca545a4a8f0360dd4a9394d3d70d34c3f7ddd273f9444dae867d4e8201f898f7ab216cfc5e030029164ab2b1e48282b0a0a842126b79d8b664cd2dd1ce4907eb94b704b08c30a95a3e9d512d3ac8f8efaecfa8ab2c0f8d64200d2e07616fe8c493507e68e1881e9dff51a47e71ab3a8168c228c5859d20be23653e204045aa2b302f446a8c1faf681e73dcb89b745d65622e105ca9cf1916cfb22cc978a5669177ce81eff96d52f2f0639de02a798ee65764dd3f6455646fc97fbcca2842f400853435f278a9c50c3d8a0ce27ba23d73bc4e79cc39e8f1828acfa173f031ce1d52351b96ff1c8b950e63888d21965e5ba808f263fcda61ce442d09d9aec7fa1e6a3df88afef5f478a032cf20de2f29ac6a4a01ad3e698652b2c0418a6d78fa235150c3a011cd8c1e15e441d28642e6ff44c4d9fb8f5ad5a16613ac22c7d022710a1a123f843ac7d0165feeded9bc830587855e451011c3158ef74bad9536a078bc92bcd93f59f8560972a1acdbf4b4747aa80cd4fd9a6dfa789541c5c5d4f4d4a055ecfe06f1e0f95c947436b353a18bb17d920868346726232e078df9a87133dcd3cd131db7f981e406246e7cc77b9ac27ae26b1515dd3ebfbe6e1effe329cc93dad6ce0fa23b4fc9591098fdee00456af4a8ab20ab04ca34610ddc4338d36ada743cd405a86c2cac5b5e0083133756b92db02aa605b09c51754ce34f52d60c91825e35e7922ccf1e0f69a83117d2e0b32b1c26ac003683146ee1b16013434323266f18093462a1fb18e9da8903ada117b6957d83a0c7b43d99fd75f0cc75d706e58279877d064181a9ee7f026b355956bde53a6c2a0175a199957c12fbbab384406015d4b1bdbb16bebc45c9fc7bc86a4c78f8bd8c08ab1a3122ef7785db64435c705e7be0a73e579e3f5823980e86213a4dafc1e11166ee02c49e617f2c846cbaa6190a5d0781860e39f7e96806c8b483d0c203c3ab03e7118335bca503c78fcd5c4d5fbcdbd1e9647ff800d33157a0f81e1f305cf25e40dddd4720da4f032811fad8cff80aaa00538009177a8e778b8be910e42230508ef2c78506327c77ed9dd81124532ad1de48bfc4b9bfdbc10246001054b1eba36a05a39d18fe3716afd408f0e10212218a62131f7b40fa8ac2be7829c711f4e3e0a59d1d985f1f528f642f2d8f39dd6c56f5582b7de88486a50b614983630e5bf0c1f67de78e3a29b758c622891e21c1eae0ca1c29d78b6a2c71914520344f3e8cd91d08f6ea596549d9212400e9b1ea51a414a5da24748413a6223237e0e2869c40bfdbd5e7704179280df4cb964bb8c96eb30fda37fa8cae66aa429ea829f091de87d621ae1ca34b06056d49ad8e9d42c3c89a886d8ad31e6d4d74898a3ad4600cbcd74b72c593a305ab2cf376c1e88a686c594f4952fcda87de014e2ad67befff894f4416b957385f0f446e58573316aeea9c2e4c7e06dc52bf391a69cc5220949002b4f31c8329a188fc8a842291c1f0fb2aab4dde89f7ff1ff2d070906e0bfc29bbede8181736a6baf990e3df932cd88a1f7a83c6e88ba400274d86b4a3f245b6866c63842532fc7585d5bdec92f1f92721e922a9149c1d8a714f0e56ed2936893a9cbc900620ef4e84724310487a62e978a357c5ec2eb7bfcad2b5faedc9f53d26a42f2884f3e904db54a30323671afd81083b391542beb6e9bfced66cc42efd6737c37f7baacd86a3823fadc21aa4f08bead82d7cc3908ccc3617579b6da35296bf77a1ad28c6db9a8f5f52dcde76c13e4ec0d37fd5093c91c93cd34fa004e5e6d79a5e459ac780183239746387aa980cbfcffd36d7d2a68acc0016ba5c3728c51794ab201c8389da9142d52dd8edba4e441235f74f65554a41b6d348994d4b58615ac19ff367c736e3a1e2b92efb0f04b765d1bc228c422ec34be32ff735be9e3d1ceae1fdd2b7dd2e58d72fe37d17e02b7ebb0d2fb0d6794b538b96ede4df48633eab8f091f3f164a5033be11cd7d19a79ba86fe14ff8063d22a1f875af7be944afa7521ef497af95cd146302b44739aee8650ad3411f15b088c2f84be54ddd44a51da1bb097e60294e2e9718897419db6ac2f7a3a54959953c88395cac9aaa73749f66449b18dcf2ed65e888009b6ec62addf40e5faef0f487cdd3fc8d50441ac34f7a2b9217aa250cd051a4db80b62f359d75905151cb5e55df1c14007f0d4e3cd128f3db0700ec25585767b5138ba69abbb1fc19bfe20a2ea7e3bf05f086e0692c42b44411c1cad11ffc4afddf95ddf17b4979d960086b4b7205533f94f79516f1211064c4e06568d650c500914d63f0d0c6f5fecb1b14edf7798458731d243841cb65fde661448ef63b744d411206d52dd37a17af1bbed4d3d0d6c8c4d440f51364d8b586f9e07a616c95d8923b2ee27c802dcad6bf425fcc22e390398b4607f66f895260f5c9966822d2b8eb329923466a2320a8adcce80fd1f0ba4bae3cf7d20d292cb1e8f8ce6a82760998496bd357233c31d87e90a6d7fd0ebb9affd48c550fd5552b324327ca0fdd7e92c0355a3a7b3a7610892333a3156991eeb968f696114c0e14b7a5b41a1db6756b907c2e05bbe40c084f561fa8c1a12e176731db5686785fdce3166c49c236c3c062a8f026fcff1c79b9a2897253364c5be01016f51472f13cebe4798cc75f26624c18c1f8a52603ef793e12b2cacb0371f8fa58712c4ca4e4c928a2d5cc09ed3188ec34deb7e66e70722c9684d90cf603863b194fa64888d34d2b4e3c8cc2178325abd33636ddb50d57858d627ed462a93f221024146fc8c8346c796c2940ad67dadd2ac9c8b6302e6bfa560fe81513839037067ae8a0466a40da4fcf496aa41516ae28a9c1f7f66c4309e88a39f5aad852ab12a5d42892b7823bb82ca7d4650dfadf625833e5849fd75d6820dc67d6c06dfe235f40dee50a0e65e6f05c1cd2f567da2626c75e558467602345e6b564ac06b18169c22c331961dde545681f42b60f054fe0874fa8a0416c5c706fafe7dc69a4fbdfbec0488fafed69932bbe92a8930eb3eff5c318417b6fbdcb658b04577c79221a4d5783467ef6b9ba8c9660e8e942df58d407954512c3844fceb4022b381944285078098523120ef836ed6abf97b08211ffc0f263342039e64ad47f0fca8c41fbe3db3440cb80e5aabd921891509d69749892fecf0b90694ba1d92b89444bfbeeeec90309c46bbcbc5ef6538f3e7ecef80baa42eac880cb7347708e3d05195559b1d1f17a53ede6d862b3c5d0110dcf5eed5402c004b81d9fbc9f2b361eedf777470171c88079a2620d04049cc80c3b92183f06487754e22854e39458489782671041a7a792fdb5cfa439d2f82729eb46913ebaf4fd93b977157d1d72abf6db4e85600980aea3e42105972cb8f9f6ef0911ada94433707012ebf30fb0a23711a2843c83116569087e03eeae5db3e7c25885ddfb6bfb39ade66b7157afec3f5e7445426c7153ceff8fabfa84797c0f28c6313072ccf118443320601a829c76d6e3f12b91b5bfc437fe2568a570076052458321155990ad118cbaa436059ff9ae83c660f1c2c573f9b316ec9473b2aa39af06211b6e608836a89662cf50854796929db48e3e322ec10c9dbe70b375965a903510467e79caaf67d8a955f10b28f0a0a0860182d1c4ee4596e8e4c0ccbe66fb9eb4a8b7ed085cbf148c3793bb9ff63dbe0dc3b767fcf38c4fcff1ed195fd5530afa50ace6b6a70616cc1394ed3d4c8053314078027408900b886e5fec0bc58673f03be9062a8b7c82c57a2fae7ed107af716c5ca3d1ffa3f9a9535a351063a5252d71b07e15c54cf87079a0653ae31ac404a6d2af8e118cc90a23381ca1b893b2b3b2fd958360bfef5baab1be53808cbf50deb13f95b20308891b40bb77c1437a4be6cceb1f20b003a0465700c62dbb4f2b5d218540222b06179428731bf07fa6148b02126464020daf2940792c5256c92bc42fcf1bdc0cef1d855e779be81b8042bf2163f8c2afd70feb9222994ae48bdc46d621d6343a48db1c15596165f60925fdbec01ebf88e6763c76b02954c71816b4fb701ff3379abfae8126b0dbe5a382cf5f3d742bc5748ea202d1496b8a6a0bf9f196ccd9233e9d45fa0d2a3e597e876482ffef8166b8c579ba7d75aed5fa99ba63a0b9e6e8638ae6a009048f4408b91c00e8366487098e3ed7d3b59f30e2a7b14d32e715c0a9f1f16e5c78302041d020547eb038e5fa9bb2faecd4b16f1710970a75a690357a3040d01d46b1635c2ccd54b39355fc394c3110e36527ed8b8686b792351cd6ac02dd4165e7c799b0adc1cd2412ee81ca4ebf9d91989d9555fd8e32f67bdd622137e01d737266b9cd9bf0e1827e6fd74cef24b22c9fabe629b186d6444816d00fe22f9e950a2054e668f06c9e9633a1a2b9946d1707a121845c9d10f25ffe0526f1fee3a507179b3d081ac09089dcad97563f5c82539ffe89a4eecaedb149ea969cd41aff1f32e511df3231c153f9f92b73a667142c718b4b93e3309a398544e35d02126d76f6e18e94b639b0fc7522f1e82d473d6dedd6cabbc61aff9a8c80af39b4931153e9f4e2f5475a6bea3bec0fdec24790881899714364f8265679f78b5052c3b42ba098a7812942271bf39569c82de89ceb88b75b91c849d80585f5a64907e982ad6e09c5c21a0e5217e21239d79494b1d08b689bcf9e83657690201bb31188c6874aec3f3d0b8602cb1c14c52fb09902c0ada101756615d1c4a183076478ba7a10ae6c9a350fd21d2c822a35d92841db6f8ed405586d46cf03b5d7228f07344669459e9abb1db14257b5ad8a7f8daaad1601a69887720840bad74678eae9504c4ceeef91ec8ad8db0b0bb16e3c228445d0b308f68af9c21c7ebd42e91290e0607ae46cd961b62de2410236218d9a05396a78ca4a092537ee4a8b67c300730e885755971b46e933863a85240e857f0049cb55e0adef88efd4bc103c194bf08834cf790ac919bb1f1191cb7e30ac2001ea0410e1c0c3ef92f3f0b769adc0b3b48f75962b9e6dfbe997e5b78d78df12ef5d51322f8a0298817ad32515ab1b6f235a7004ddc14fddf3dfb65de189a9680168be84b1616d23ff830da1039745eaecac38a06284974e1d5413f5d9d94affb674245a074f26378c7cda0065e841517c11961c770cd94a2ff41ad0e887d74e4e4bb54e1d71a27ad786850acabbe1b1326f95af9f0b2a40ba5e11b40df6006c8e3105f855e7f0932ac15fb6bdd4599392b70671f38c4604fc8db76e3e917631d6e067e13cf320c2559e318821c75ef780231e4c0f8b6aa1a195e9bc0dba63c54071c7486d8cb0c79c81087a884aacee99d9f916becbf3b113561980f0bd00a5c7d60137ea31c546faff01427855c969ab332741a2a9bf6ab60eb891c7e426d2fb0f3036ab7aee2aa4d642c434352e74abd4993d058255a43c3a510eca72e2a1ce6b75725637bd2289167c8647a196e8360bac57671e85cdd67f911103f75422a8e7b56a233c71f5a82c3d862c5203cb3582042ab61438a6da3120315b1f9e425f15aa93de1682ce094c8b652f8f631867017abb6a260b7cacc1a0346421375940912830a6162c7ca7c942661a99a1286009a2149bd4d876662e0f68d5d22167bb3b0abb4dcef35076fc48276ddf5557d73513182ef31c1dd7d99e75efec0bedd6c77d348ffe544d7cea97568e97545545f49de65c355abe80ee932a68b89efecd84874adc9b51f047cd45fd9838bf1df2f3bf3b61267b6438f339846fc0b72a5d1018efad22e776aacb408952e85686df9440c8ac99adb92875fa84ead65ed07c73b41e5075e67b36def94e3b2fab5c4101e02ed61dc8900a40f5a59a43269b4edf580ec5bd728a7412ba0b5ffa7b22117ad2f600b5c103fa216066d3fbbc55d18d01d8f2642dbcd3d86c729ec1eb963bbfcd320e2d274d0dff5892b0e30ee8f5de0c071f401311d1d50ba6a42903ddb88bcbce369909a2bf46f36391fcfb004ad2b408f3a5dec3015bb0e058f7fda9e85f97f22c055875bdae0dc4995c914e79151daf56bad41f8b0eb28ddb975348ba94b4f24835a138bf872a52b9bcf3a161b84362e61c46c52bb2ccd86f5a8bb640ddeb1ef25dc3142bbe179775bb0ea2cd9a702e385181789d920acef64f6062f27a12be18bd95701747e07c26fa664cef3d7e06138d5cab8e68db36798a2db79eb50af4318b12fc8680da0bb06ee2481fa35690c28e21b8d1bee012f77b5cdcacc9f7136d9bb9a835170e8ecb4b37d3f9c08997c1fd4868dbe0d71671d27650c75ebd6a282054c81bb431457d2518f05c44d1fa91aad2f6c463d2b014d7b49e81ec8d704dc4f17192dc200ad560dc503c942a82290c1023e4562c483046f04655121474dc2a9a1524dce1645abf15383a2a9676e9d4b9c6c79447a628511d43f935471afe88143c51f68cb2408c81a5afa9d3973f9edd158785be34389eb00500b3740328fa73f5ef8d60c34e9599c43f6a9d2ca26b20d91fcfdc3ee86d04d63e6199246beab205a87fd09275007a96d7bc9dc10ddb84327ca2d51e2fc2b98852d9851743fa7a18d6a4f762ecd1fb00d8d09b2d4595b089dae37ca2f8c033831e904376bd0d470ea4da51eac59f3a008d2061c7a44779d237b0b8d6bed7da10d62bc9ca459b15bbdf62f22ae62b89930b4e38a40cc546bef8371a518cf80e81c9666ce3d24bc2d5092dbbb02d644f0c562e6c611aef87dae381d5a25ca155b5126bde9bc0842558324a051e34431eb54c65582f8920962470affd55a74bd6182e1ea05208d5033c60715d58a76204879bd8cb0d75ead85934db926bd2870cb5eeb1b552be6020c579604719b10c7a7f0cff2d0ab5e2596c7134cac27bd0cad463be641001a0bf568059eb6014183b0ee6fc9658f35926c94bf7f62ec1a5f8f531997bed4c138ead900d14e7f53e8fe38d30b8e098297f808d24213983a3e605fefc6932837fe900d57e1921358741aa0c7005d49ef4111e51b29231c13e59cf0f2cc6f46464a99c4816769ee52c50d4bcb5dcfb5a8295c7badc0e74995460ac42761301c8035eda06cfb79c2a121c71635030f7eea51c40b2a6f0abaddb2f2a9670f07099e93685104740643d9cdb2efc922734131c509dad012749a5a6926bf96ccb92e95db06ae7dac8f671a415480eaa1780cd5c3cde61778aa189fbb2fa41cee5fb3bd427daa0e4188733e751fbd70febd4b231f690f32c014107e0186311858160fbc6a43aed935b7b480163b5f6fce73bdcf45c7518e435b5866e23fa1fc7aafdd7f071a4f50a7a751753c2681671a1eeb6dcfdc0bc0aa11731841e8b5e7638de9b3159a4587694a9e1d051c1beb4523dfe4c993038fe9bd72ffc435e9f15f1da689f04fb23edab1852470800daab466ba8c6adedbec1303392af4b12ba64501b813f931bbf76878fabfa442711c875d03360f91659d2234c19bc165aec7f18aab9c81766fdeec049df1d39f8bf820f5e3bafe68ae550ee2a4fab3208c28629d6ecaf48f9dfdd3c19cc8ac6fbf980c4a264fa110b4229b1b34453f999bd29199467c245f1d3dd3b5bcb8d265707dc8fea341340cf3a9a5c629a2f368a4b8e71517b0ef372d70a4a89aab46602525ddca024f5ccc8a106ba567a0b296133db72d319b6f166d9b1f8dcced928de1f12f1707fdbc587b2c01d406c314c82f9fec1d33a882fbaa0f8d46aaf149be60cf36885cad623587b5a1a902c7cebb579f377406fb61c75e437d97520decba6ee506323903973f599b12ba2a9ed3b6b50683a6d7c221121b7babb2f50b101b6a541a2f62f26682bc1f6f0ca7acc5fd32af5f33d2e44ffde33450a98a8325640e7f87d601a26c34512774f97eb31cdbf24695a7aec940f48151b4e542d803fe2723a49a065c97c248a7c9e40dd08f88a39db7bcfc9a4b80526138214dd4d633d82679f9ff07a5c4b91f147e19e196fa684578019af956f1ea5f4c3fd2bfe7a93cd11d0c7e2e00759944aa295fd53f249ed8fa84690f2188a55dfd5ed980c1633393961e838ce6362ccdcb43db08c9de98f19241b48f8a077069ada14e021e2130902670fcc53692b463fdbb9ef385c15edb5e686733320faa8a7655d7ad2443c71709abfc8a6637452f62dee284f4742340569f7e781b13f7cd01ef6baad1cc4bfcadda7e1f54c90851bd958ea9bb9b1158647f37bcc80a2d2292bc9328d7783b50a595cf15427d2dd8890b9344b09eeb3860cccf77683647a05884a29809f7304f7a32e418221a7a14e2de27b9e04c83078662cba6d7227a0e62fb90d103d7ea3ee45ec20c0bd381a709f7791e326868d5edfa1cdb94ddef3a3324d74c4a8d3cab8f0a7010ee5b31e653488d35bb130e144a1df6a0a7f3c72fa195882501858f7a76a9fe22d2c108fb3950779b1b4c9e620a8e98d43167c38b941c0b188163c8ccd5dce3230d956948a99413c4ea04a528ae19a804920a7bbbab45e87b7589a09a0ec8d76881e4444a6d9bf248ea8280451a411b543a5aef52e2a10ed10142c4d62d26eb6c0a31592fbb604f9de08f9c0a7dba93e46c2779def1130f421679a39737142f005b4faad434a60941d8e64d5c92033bc82b7a3aa6ca526ba6dd35f1f58211016858a36138facfb0f66ea9c942ebb7d5508c7ecef6c7f0039faaf1b84aa5c11746dbbcbdc9bca4a1c3e8633125b5303ee29194b8f044a941c12588e76f60a4841d945652e48fcf60ca654c8d7ef0610663b0e4b83e75f051d02153ed29407d50c9f4b9df90792dab146115536eddcdc58436551c9f9144e0d05be69c00a6e9d90d9bd61e59e2f8ce769faa101bd7e6e66e9b063c9c75630a0aa9723a62dfd2192ccae7e1daf20d30c27ee893b30266261648e9e0f299512a4b916db08d2bab29f960f43361353933a041364d45db5411da460de44a8c39f6136f517a1fd29d6a38df36db406e4d2f61723865a1168db3d78534edcd0d3014696ee37eafb1fa4aa729f46942a2688b5860d6feebac8f77495126f4f73c2c425745fa9a97cd96b2d3d61cd5062af4802dd81ca7faf33f31d451981fa00718bfff8fe5d168345105697fcddfaaac9b9bc3f760507965d81bccb168d3372c9ed482b5b447abe5180bc6b9dcc09721a78c906ba9b3e7bd4a91064123f115c5bee5d5f0de1a6cddc93218e713404960f82055fabc2d0ac869fa0ba94ec697ed57baa904886caa99c4d30a212a3d72440bf12e5ca3df40a3ca1c60b0df5c0cd5b1d1e3d281a28216d42c41a001cd302220956af096227177c8deea426b9deafb076144c1850cbe1190867478e62e609c8aa2349677b59ffc883bdc33eb5dce1548c16debd8c81353f6d257daadc22db1b4d86aa5966f9c13346c3a3f7349730ffde960a58d452e6de35d87a556b16d3a5214b7f272190fcc881697f363928c4e943737472ed5b9f76cb24832749752e1cd8b1655ad00c3e107ad928d4eb81419f5e8922544a4713abd74218c7b483be7c9e5cf32c9b59dce85dc0f7a79904d6bec56546c34f424fab104b186992129478bfbf88b6a7d098bd56f46340ffc85959025d146c93092ce77e37b5e542661c786f3cda494ac0e5070e0fa80814e19edd3ddb929103bcccc89b499eaed9ae51976f7fe8e4e87af4d5267858fdcbc16b8fe677e5cc0c9d6f631b2fef2a7004f3e7c686aa862fbba6a62838f0ee551a292529520a9a506e66d1e2188fa54c7229b5be6169c401eac2b86c04b8362a0d041b07124331bdd94d2a65e5fdad4e2aacd5b8899934606de572cdd40b8c6ba5749cbe90c10b4bbb89d4b66e5fc542533d5d562e34cbaecfe820457af560c5652bb1ce24dfa27c45109712d4f1ca7d51ccc057bd9df56f7815820870312cc0040af91ec3804840fd8046c45caeca1d5137b2947c98eb56b083b68e3818683d83f4d5c90415d9ad46a90e8faba21ad07b4a6d28a3fee3bc6023a932b19deff0cf6ab5f9ebb0914ab610498255c68707289b705d4f69d54ae05085f08ce3fac2450fb252ca9b740807e905d70440c6a44813e7b70e6cce3141f46b29791282eea17992da370e2d610043b30a4de18143529710e6b04090b2d49d3bd0dd6390a2326b803979bf4ea4e9e4514d5a8e95c8369343dc20278de1fc4b1cddcd7206d430c9ab82bd3105ef586952d5c4b64c9a3e0c22c82c4cc49f0838dcacdf620916673d1b55ddc2b494276ab6d89de542907812fb617cf6293b7d1aa789a95a01f40ea07f5baad209e8842fc3155981110bf8fb0fabf3e895521e07c1f8ddfcf9babbac5e58228bfa88c93471ba9a2b7773fd3f7126d179ec6d8b728bde7c12026a1980c2236415aa6765d16e8a7f939d956c1cfa1817444bcdd3864e6514909869153de417393b431649c795739334fc7fac875d053c11e75a4fba9c5e96a5a95f16aa979e6c2d77ab7dbc91a03736897730faa08b0f15e63fc45dc32427342e00158d2a2b2af58b618abebd6727ed8d2afe3c90b3ff4ef073545c621def2ae9a481b3627bf6642e5ebbc814edfa2e62b0a67fbc7cfe7f43438b740edb2a9f5fa27fa7b4eda5a92df002e86dd359c4f08807dc65aa44231ffec8997f91679be0cabbe9208603b3af9c82df6789f90bf4cf71d7231a005204918828ff4cde02e62371bf06d88ec915fe4905e6dd2b389583d40412383c7424198e403158ca9c43a529a39d752a64c867492f251b6c57e22797b81d3c6ee458b013bd1b56fb2a2c008b0a89f2951b66a1caa9868501ccc9014a1d093d5c408a499d0e8b298e17887c08bdcdf2dc020741b2aafe17671cd4058c598c6069a771d61d52af62d62f58fcc7c85779f385d2deb02bbf23d04d07d0d7d55f2880e8e75338cf43d557a412d3cdb460aa8a407f307a93bda59452ca24659c03a2039e03bfdfef05a554beca968868948551a0d5e1c60c9b02c3ba56e802b269e9747b8081d08caaf2ab56b0dd6db1fb9ac0d186a71d131866779d5216ecbeb8fb7af0bfdf6f06dbe3457d51c124c8a95e3928f560d884aa9a15a71e0cde0e13b6f4ee5b5dce0cd3833199f46e296486beceae3b467ef46084d0c1a8861c1a6307d88311426d4610beba9c2e67ae659df560f8087ad31be01bc379c392313e3dabab5e7a5547b7d39894dfdf3de7d4dade7bb1d6a1d661d6cd6df3dff7511a42ffd5d9d73a7ac5cef9d66a8ac5773a2264bd1febf29df52ed9a6f766cfcb302410b3edfbcd03c26553ea598b7c1f6fd3df31d851379362d4b5c64e19096605eaf8f5676fa602feb7bf9522b0bdfbacd79eb70ef4db7c2f5c02e1beafc5190fc27d3fdcf76daa8ddb7808e2df72c683fbe1df0f4b10f80e21ff0c04e61498b329305c5360b836d685fbea5c849b7e762743e12a8bd4555989cc4b8a901300c14c5008729402290a6d1ff938809921c66c8dc9ecccac0ccce621d59edd2bb6f75a4c5ef76aadb55eaba661ae865a94b160cca649e07f7c6badd5e79c73577d02adbd3cbf22e9db3967d5d9302eb24d1a6dddabc5a26e8634045a4b7f841da129dda214f04dcb7b2fce1685cdfd7bebc0777e1dd69b4a42d41b90555ce64f60def7e7f36aa6891634847ffe1b9c12b7261ebe2e5a9495e51b1cca044c026ac9eef6c35b07be9bc85dd3ebd77fdf97b78b3b4ddc883a7d6b9a3d483ae4a292a038bc869aa03466087f26fadb4e936c4c0538f5f12824b66fb54b70a52dadb588e49550b27db3ea8a873aa539f06c6a83e230b5f3dbfcf876228f9824b66fdcd701fbe240c217ab7ddfde4edc3922836d1bb0ebcf2f4324aa59cf064929aaa66f5559969afbee1bbd946dfbf6cb786cd27ace73dbc5b6fffbefdcdddd7d0879a7480342c406b3caca8a5c6edb37cd406cdf4c1380add65aa62adb478340b13f9fb67dabc9dcefb624d9f64bd8a0b8e162b5edab580166dbc719c1969fdf0d357c60d8f62997a96d9f1addd0b4ed9cba41ccb673aa8b0cdb4e7951926ae169a86ab22f4006208bb25d6a7cd25af348109f78c84dbc422f1b6ea2a4b5d00000264e5ad0422b27643b135427c04b968302b72ab3643f2aa04cca6acdd4946c890c75e243e6cc7e372d3c127c09c8946d009f7014306afbc8e7026e8d00381624f59e53c02d3a07a10d620a9803906dd290db030afcf657588bb304fac6c6524b2db56edfadbdbd9dbe39a6cb6fd3e117657edede5a6bed7b794198bf47f7442c02ed670176928dd9b9fdc8dcf59196a65178b11008e99263cc07d7af067ac44cdb1dd25df83262c08eed695249330d490d7d425a920e474a031272a46aedbffd9db5d65a6badbe210e7398c95bd69f5262b2982006eb68523a6795ae342f47ceaed8a0a4678d8891d2802689f96144c5ec08d2693c7c237901d24c685a6668aa44a9189a978c7717d09a10395ad311d2cc3d0f0de987de1d6836863200a3c391ac3591c17624ad7761a668f79db541260ee0e32e653f67ffbb848102c2143877b8a3f4a7cfc5f65b42c0f72d0372b6ebe32c10e0ae7f81081b27a920bf5d4910bb06b971a97ff52e37513aa574acccb13f337b3a57a194d6ea7ec2bd0a1364cfbf5fda7c4ee0fe2cb5fb574adddd29ad0ec5f601ba3549e64ed06d83626a50b74fad3967b0f2e47a60be70e13b145a7e9c9b746d97ea1319f64405848ab5e0b452cdafb4f45bab0321e7fbfc6a7fd637a564ad554aa6aaaac86f217126f5ddab4edb27b27e07f59d92375be2ea53b967340d41451a82fe51a6416a8dfc0c3ca9a2d253cafc67adf55dc702c77715c5bc3e7f859dbbb99bbbddbc2b58e04c9c79f381ef9c0597d7e44a31777ade2c5246869aa01f0389019f3e66a08a89872c54d6abc872638cf1751bf3b5f55db55f6d16dbbfd3f1759f65091156e4dacef67d9561ccce6fad89d931d3547ff672368d9c0f820c324c06076b685e6cf0dcb896b6969c67830c3490da504a71367e6b5e2035432c059ee9bf0c7c46eca02483676d6dacdea4e0a950910278a342450ae04d0a15372a4a8800a690ba91528101a9eb358615e621cb120d32a8bd22e4d7b3b5248acd9a420622d0efc037f5cf5a64b9e7f4915de80a3de79c382c6c7028d5e1b0f015bea3946a8ac3626c050b9c1538286c1d630124715a14e972cff79dffc9fc90c133b1642d734e4df64a88d03c25a5db9ebebf2ffd4b509f4a49ed5cce527a691d3354552867ea1fc0676b9d58fa4f3ae9244b77dfbb0ca9fa7a5213b73da98911680c1abb52a23096ea909504ed51995104dafafd76c203f8f5dbb2cdf9ca183a03e66be71da79560c65aeae3e46284a15d3fc747d0df88b9835112fdd1b47778aa6f9dde4eb84ca6e52d2c7dbf810df6f01e746490bc3e72f40c5677975aadb5d5d466ebb70f74c8df0ec2775ec227a2040ae7f2891091c653b1fc884b29d51a7fcef3ea99f1a5943e9a975b1570bc5a6bad957a9856adb5d64abd8ad124d749901868921c02ba569021f795f3d799ae5a2a3a0b7be1fe73305dc1a6974dcb05a3e9958563c0d15516dc97b516ffb5d8fe0244543d1c451516a302ef5102a9e0c88acb9124497e8de2d9a7f15dd0c6e83e23578ff61c5a63b470f8d40a5a9add3f05cc8e68c5d81214868ac2a035c5b566b034bbffe701d372d1129add3ff934ff3c123ee79cc9fc05c0f96eebcb878ab3d8e23bf3ebfce4d7ce595ad5c9da0f92bb5ead4b0b45524befdb5408d0bf1bbf5b5b3a55d696c7e77be2f77c2fe785a4b0bdf8b6cfa9c17903fc7bbe279639b3e4fdf8e1fb7eff58cee0c0f6b67206073dd15f2607397b62589236aaab6b6b6b61f67dfd4202150475df3284594f50da360fbb076c50e2c40e977adbc86dcbe9647437ddac352bb658faaa6fb7a33f6a4d58b3934d7537b484d65aef40f0aad21e75d4e7e7e595c3f9cefdf77377a74ea2e32ff16be963ceb656bbd8b6f4d529926e3a35c8a508df9d91f86d2acd7e1fdd6198d395f0dd2ea701a5a5883312977a37face46ebd8ebf78d37eb66fc696ec64b1c8985d4250fe9b36f7d3a047f8af5de33dfb7dd9cf1575b6724e021d0d7cd177ba2945eebc5669d0572b4b1d59f73ce39e7acd5adb5b596d691887b7cdbacb17a246dd386baba6d7384f81efe38eadb073a5ba49a7ae8da47dc6ce26dfc70fcf045b1bc7da0b3c75237e78e96fa76426b5d7ddb02b72bc53a5aff5e5bffd65bbd5eb5d2aae3940357b42a2d0a86577d5126b6ab27be3a7ba4393b8f242e77c157103a67e7aa435da8b3e96bdd8bba3dd89683b5b5b50ff67642c3d8f943f83238b64dcd3a1160b80874e7d2a743a7b5fedec2abffb542629c31fe5cea4cb94c5bf1d38a71cd19a1cea9b6dbf5b5cde6fc97e69c73e99b8fcb39e7df16cec3ce3927a54feeda5dbb6b555c4028935a94236caaa7ac7e74516ead35fbacef6a7d6cadb565bdba16c85af1dfdd62b767fd41edc45df2566b6df5ba1ae79c6db6f77308f72d05ecfd8befad48e838de0ff4525ace1a3d8276156c949502c18b841c57ce6a40cba24be6140b50a1b3a96212a988c0d558d08da2215460a040606225cb18952a4d5a2f6c00052c3766785919b2e4828e0f212148675dc6f4382951c1419118602c1c21cbfaa2498293cc027ae65041469762f094b001520e44aef5967d113cb4769094dc30228386ed3c7468aa9f8c233133a2a4e01a990da4ad121cc9da8fa0df9a0d1cbfed646b3b3e496a6b3c4a626a3ad0e8b9096a54804e506b224a513372434c8d694827a9ddd06a3c42b5ba7b120e8f247643072797128d35188ec850038f16a66829a44e0f58b085148898ac7e908cd9ba4e644c1196a4312f98306605229d243666c846520ae8ba31a3e3a24241a535304821498a1a91d2d955570da058830aee29d60fcb078ba2288e421fb68c8820b66a074ac802052cea85129470edfcf4148212aa12ba12e232c2d308a8f324830f159291a82c408247416e474a46071a2b62858528b3208fad31195d9a78fa69b6b474a0d9fd03815420531e6976ff8fc7300c73ae3e34cf7fb5666a3beef010ac00d845c6d64230890e4a69cc15e44577bdbef8de7befbdf7de7b7bb87abe7ab0f470e931eb499bc133e32701348a300d239cb57cbc589f1c06b0e57681276b5c6b42ac7044018aedd822628c712522c5bb58e38c35c618638c311453860a92a18c6428a80c5595a1ae3214176f67a8af12361e566919b5d65a5b0d695114c58aeb991f5488418c900a505008caaa5204cbb972ec2c7665bc414ebc0bea489860151c862856d10ab66888ae60e482b26d51e92d1eed62231da2e3ac88070d56d47448d921721a65567cec10668c971d2045ae6a74b1d508237341d7109dd81c1b479f544c4676cefaf6c03123737b1091d949c02540d159962f665e538844b1227643c8c287c9d30ca0ac265850683832cdb405a690d1487b627664eb4918da57a031202d40654067d6a80c638c477be24bd06f8533a00d50d4c90e93ce95c1bae35d8def752723eca4849d7e602727ec94e437c63b1a6bf56b9c6865f4bcebe574430bb405333bed4995e4273b6d1bd717d72380bc80ae9fcc243243579a8e21ac2f6a4dc06ab8405f2900d7306883d6da799ebd011349113921021279dd501721a9a2281649e1222635880551bbad1af5d6ce241faf9949433ac9884c52ca3d082c34e1a1c2501430604c7a2cf1d0e38a64bae2f483016dea0910b5eaf135418f2e2186d41d2cd0300cf3111ad502c0c5027ec05d9ddfd5912047aa7a8294f530adda1887e9d878003dec830c86d91813196b18865108d0020b4090f738e79c8524f19a29a59d90c09c97d74c21633bde084c6ce0615624a8098dc660c5e3d660f5e3530547c33cd78acb91243946650c8aa27e4c303ab3a18a528589cde0825e32c119ecb2730dc32c2d59777505dd2c2f3a0c432c3400a42b80ad1fbaf18fd748e9ecc8ca1e3dc9473e62bcc51863fcc3e535f34b63f19af0874beec978d81d1372a521923ccf3967283f5e3349f4a0e0f09ab97d9a7c1adf85e0670b17ae2b5a5541e854d89e00ab0c8ba288455114b1bc445114c3a2829f0420b85505015b51138c0036ca18f6f1aa01a009d6516cb460890553066320d4c698c88cc21863a1a130c44b4df56b82289d71be39e71c7ece3967293ef7861a4b872a335114c5a31be3cfe6b694703351146cf9081a60ab290a575c8e24c9280c3ee8849c78c727bc62473bc69a95802c2a0f1e7274638c31c6414ab5269c5a572c48d951030520693d2cb571d1938d8bb06e605603525e58a12c8c95928da8b42dfac5d29188000a4803170000180808058441c12cc8e354f80114800b5284445c4a2e1446239140200e8442a12086011088612080021806a22890045990cb05c749a9d360df20cef1557e5f6352f8a13110295bfd3bcc5aec6515ac49d39e05b40119aa5172a74bc47148c02ac8e2ee0d13d8cc01fd24eaf0192ac68e699292fbdb4a331c43649143ae515906a3f5050c3f7095bb9b87feeed46c7d5eedd9545072574277ff302bf9f8d6c3e7ff2225487bdf330ad0e545ded77d7a21710d0650d8e5e822cd13acfc41165dcbe2e837daf514139a4f521ff16cfe995763d13e24ba15fdffca03addddf6cc1dd2fa9058f6beb18af4e7321d8db344ca3c64aadf0ea306f7cc1fa1c6b8f837edc7df11442fff7b285c15215cacedd49908b659b7445b66e483a7c2b244e3d046ef70eb80abe43a77a58f9ca1fec7a9661d995d56bc3c6d2a930d042910b5de5f0853b9d0016edb969eb24c215d120422d8b05c5577cc4b5f055b6e26588c4f08af4dbad871c9a26eb2646cfbec1f706d3e712f2ba1a3746af68fd1f142f23b162c8d1e658a7f9d178cb13d4ab8d23681fc5d0117ab4909b8604b829578676f3d5b791c546f2eec8c5b8bc0e68fcfcc671b79f04ea4809a5122d1dbe5a88a9b7359c5283b21fdedd5b427eb14ba446aa1285c1930dbdde8628071743fb96e46e1110803b7ebe8962b0441f7e04df07485479072bc81474345cb0994684721138c3d758e85c7bacd70ef8c03cc7d3a977d52ca3d767c90f39e41ac4bb9f02f3d942ed494f64f41f49fa3891ff8f459b6b1927a4466a4e9bc6750b804f5f5f7767e843e4202cebfe64f545028317896559062cf4ae8e660d626f6ca403f1a37047ad4df68d24d74544840404a82d76ec144b1d1b4030523b4a2880929551689bcba70428b60850914028d6da3df0057f5ce6ed59352e1d744ec359a4d88afac74af8e8d3955c03b27466a8147a1923d5f2fbbdf5d2c2c0b369b6da61110e159562728651c4f5c6661a3b1c458cdd93ecfb652e4e1d9e2f42e9c86add0b90956819923849be01d3d8c561d9041e00013c71803eb530690a16001bfd71fa989d590b29e2172c5770774d94401c62993ef644f6e58ccd594096e98033293ebf07c55c6fe21f4e0b6120b37d2b2c1725cb86b3d177fc5beba80ca8952e487e4b70cafa2332174224a774d90f526ebdf5f810bd24d8d4c3c014dcc9fc73dc29a447f97fddd8c81ff808f77a275b98770be0881ff554dd10dbac9e918f8a5f15a83f4c06c96fa1583c3527d0aba22bd585752491655f33dc43cc87be398fef2ce1a49a321c852e48c67245446ecbb6ddc301b48e45911e422fcb17b0f6f147bcdc8e61837fd9fdde2a3059cd4a0d98cc144109b56583618a6a1f49371cff374065084af9bdfb002470549a72b36136987304258aaf21902b7c69623cff0ac4c45f05985b81962a95436a9b2245e326a1510071a2c12c1fbbc2a03e02e41b3e0eb1cbfdcd326cbf0d386574885cd5ae138b3c73117f8f40e77c79287870ddc48adb025bad750a5caab42fc94c29f9f8749b305d7a9c4c04f0e4fb10078f2af71c69ea975d923d4d27878dca2696df79583b026c2f52b22011efe49645ea005572d00f0261f58968b51768c2f8ba4939f7d241c99e3bda3366f017df5538b8994cd99f29060af9b4b385e2681e8a6c203ddea48c7b79c492489000672aaec21e8c81ca876f12ba9883008cacd26b41b4e9a72b7728dd02920a2e1534b82a34d1348ca99609d7a9bc4f5202861815accc4e251872b8b8e5edbb95f59294e4897ba640457273a4ec9be4f03c62d58f8faeef2468b46e386508b646a919730c927a31844618a9a0185fb852ddc5fb137cdf4c37ed92ce0b058612feb8c64d6650f9783d668edeed08c87aaf4ab7f79722c55c11b5b120b1ecd85049fd6b8222a9e6ac191074d1fc72d655bb0112e96133cb354305bb7f7957debb7c58ac3b3ba7c90c6fd279ea7adca688cc079e65b21b502785c21ba3fb280f44833523d3e8a62bdc15b51ba952c1e9a87883774c115058c90d4ba29d89ff4c1a7b4f972e7d22adcdbb4f0631937dbb8105ad2cc73587b029f88a92e07472b936e8c34a40f08d02dd6d51b1f8e2255012557b586913744ea95ff6c2e495613a5d569436e559c38754978ed735b806c6e5f23f18c1e7943822ead00c8b93f2c3ec3411f0446d809ca77eaa492b26bd865ed21af343a1b42b4efce83827bc5ba382c421492c333799af6f636c722f6a40702b0e86c4524aad6ab87c162c43369105bcf546665ddeefe3f5c02e70b6bd708723fdd54b3e0e6963bd445e03a4a04abc3b0341b438c589877b893496f52197f5647c37a78b92b506a48c2f745600ecae2b5e77c353b0980ed203197d29da69450a16e97da6ed2048799208c6a3a04c432e4c58a47684ced7e2816d9558762ce22e25ec24d8992dbc892c2f2d14285b34cb5ede8d3302e05ad1605e0a2dc14823204af827d1a26786574ff525475651395b3ab7acc06d78d9278586dbbc7c3ef946845dfd3056a8a84ed3bb1085fb2d0189d08964c4717870ccf55e597bc1813b9cd963a8c9bf2f6cfc8fbcd97ae41c3545772a07a9fcaeaf8c81f9f64cc7620820861cd3249a2f70c4cce109cfb37c2e67dc04fa32c2976aece41d470aea4e96b1e0cbf65055944d95a14fc5843f6eedd536b93b2a126fa246770010d200c5cc4214d5fdc170e5732a870f1bf51ba556bca20f51ea21fba985f47b2ffe9972d228299292306289cadb1f92da6bf46d87ebb04438f0fa9abd96d2286b807bef5b7520320e29392cf733267a693ca3870815f80c4457db72ddedf7ea084c435a9029cdb98debb8238592c04e0be68366a521ed532e7b59cbd0c5a828f41b9934d54295c6d7f0c47d47ae52bd8ff46e0c4450525b4aa652b35cf7de6a1313d743394e9f98a6de103c6ce7b10cf120a31976d863f211f3f9fefbe230d144a99e9b960493cad3c01f8e1f6156e9ffa6aa1e758fbd35115c73e779267ff414aad74bec2437978b6fe2007c81533f9fb06ca790417967fc9429607b910a93d2a44d5c6233847d0bb1cfeb859bfb80636a48d2d07986ef88bf4e48823d04690c2486308de17fbeaaad32c21e608445e4f9e4bcceb3e67047bca53114528156c1ed1613ba0ae4300d0f2f90a01bc0f7cf1950bbd6aa9e12581d05e40613cc9f8fa039e974ed6bd3141a4d841543a7aca0ae1a6bdf67b2bacf1e6339516149a69293052fc72e2a2465d11b2062dcbdb865c849e6ae6a6f320e5e1a7dd8c1e370cf62333ff18eecd0e2a191a859ee0b353682dbc87ce2b26c503f161e470cac4369e7b59865369cff52e66b6a363df00b6954b8a63b0015e7be73d0cfdb6a7cd7d477722d74345cfd12f73f8195ad4dce20d8612051b472c1eae5c9ab6fe5fe5e8615ae07b9f98c5ced300837d2bc831ab8a8c983d2a65a48606cf1b392716008e74dcb6159631efda5cc1b1097b8ead25310ca533550c4d8a201f4d9d36943c30d6cfc30a08caa0e72222bb6608e39f0fa6563daf1d81252d5cf1675ef9b5185baadce5e614cf8b5950fd55de8096b5f49e4d5134981470324f06559c43edb412fecf2e00a4868757121a051830dbe3d8915782b55e6526c6a9e976fdaf3fac1fd015438803491c2d81b4ecedefe378da10b773eae8ca9071375eb5d43f17d690120f74f033d8bf79a07e5e32853d8e90d56d6140a539b4af23e973291de68cc098917303756ea28446012cd14560fd2856c1bfec28f1c92a194db8a3df73ad5c1444ebe69d2448af16103b77490194e391c404480010c1196387ed8da039feb3f11e45b27ab455aabc24f326c6b52e5d0b51a7728a134bdeb0aa1fa7dbaebd33d2ce18310b610262b50dfb524fb6a0596bddc41da470105edb5152298e0d867603a4f8fce818b107cdaf501fb88dd10d5cb0b19e40eaa90c90bc4e4d390429e877e554bcee37e0e954817f7f04c64ff2b1935f022e63302e097a8974fb6196e6618d53980472667d2599aaed58df6e33c28447c8d68951b74f0a25447a4b55db7b4481244028fedb707f8b7aa42ee03a0a1d8b94c52e9415ab7f9c317450ab707ffbb5b571af845ac41e0bcddfcf806689d79237f08c6fea7964a2f2e7856575f1055bfff19d1b9ab64d7bc2bbbc6250ca6ee5b572194a341f1126fab7e430430d211b55caa9ec538d8a0fa52d52814dbb84928758d2cc58f52012736c3c71589b0d2dcca3c6f72b033ebc5ca684ef83757e7d163c59ab5a8e32d3a1be710b8beaefb4fce438f35cfa3e1d22d75c3d5760667057f09f9588ea4671c81eefbcb5ddfdb38cc9922cbc25f154bdf82c7ab443569dff83629da418eb7688cf68c2d22c29174b6777c46b6ef738119d89557e2376f0832c4e585db1c7b9c24f1962b7e2ded07f8403b1a1147f0d9141dfa5dcaf1dca31720a9c7289a4475da1f6203a2b008b23a537a4ac7898e77873d9115935a983181240af7c48aa154e4b4d82a72f3ad6fe18c914fedb6c6e824c1052d2d029e377cc28bb55636986926e730d7742819eca3ce301f33147d68e51f1c206053b246ba35c7ea0447a8be64dd50bc6cd7493f092fd283ff65a5d889b25ea9b5575873b9d63b53fc776142d4e738aff0385a56f616951dd8f6729fc598b7b7aef028ffc9b5354ee7933e86f7b749b16285c68b6d8f1caa84bfffa0b85d966015560c7e789d7d986590f40ff8497a457b9b7cf2a44aae8cdd0050c9e16426944e71c6a8202f6bdd82c53bd77d4166e45f52926fecb92a07a45665c7f7cde7c4be195523c6eb6fcea3109d66fc1c4760baa7e97a6d5abea1cb7b1ab05fcd8ab41cbdd906b9130441901b0b7b4d44491112a094b022f1de09814b76b250eaf1464c72e95762da71f06bc6f2fb9997d9c43f7503a71bc0ccf346e82de440f51b9aa1dbecbcca32e317c56ef9488dac2809440a11ed29c00b73702da9eeb19d780650d0f17e9496cb57a281c587b2cb26170e37a3c4475208e13ac5391f9565965ab3a6f31597bac27d75e4edd5cf1c2360115806b8802fb3401c9646d253063203d83a0c086bd9df27c7cea58a1a57fd961688b0dbb04604617a9b760ff1236bed2af830919415963e36c27955835f7d1c0ea29e511d12a0d587e4cf985262608ab219d056de35466e57d5b304dd320bc8182981c80a66ddf97c6597d1d5b7da94d3d503097b385eb1e2c88519e37aed199e54929de561c627c3fd81eb413508aac369ca755752448159654fde1ac91f313a2d86152b1f95e15b4ce92401fccf7a35754872e7a1506340f3086dceef4c5b31febb301d508f89e84fa0af8a42f16eb630f56fb3cb2134ac319762776719ec35ee7129af06eefeaae81db6f29a15d4b0ab8437d37b854af873815f6d1b0c06879498f5db416ebfa86bc9f8281b819a2307d5ee6320085a790b36fcdbde2ad439bd02640b540cccf2d122677eb3c35ada399904a5e64507ac9f97e30e0edb2d15e9b00dd154235e31c1bd8cc920b5ceddc98c5f785fc5721f88afda1183fabbbd9ba283a0e603b863a730a9f57d324800d47809c117e69c4072c9e91388a80e96a14f8b3d5840be30aae4ec3eb3524b0f1ec50fe96c4f22b2146ee2e3571dfaf33f2c3d8cf46f2c3aaf0699f99bd06d416bed66b82132ddadad36854fbfb7ff480b18fc98f816a76c55cf29539f176e82433b6427811c0440d44cd575c3cc6fe69adbd9f1950a46c5cf4d308a8982fcd04e0f280539fcb72ff873d1dda9c6c24d87c791d41fe9791ca82980aefe919ffdd9777eb2bc90bd49cdd5e6d7494e9b5475444ab4164d954306fafb3afbdb7c73f7f5eddee26231ed52966b7678573199cf5ab1871ac7c13835f4c39080d365baf1c969328ba27a032beb14d77110317df03abc0bf50c8288554a1657d3aa007bb095529eb42359a90a89161d4c0391c80930f3584ad8925141515f9454a9828475a30a12ef7964de8b087dae7972594fc7ff3480292ee0f263073665522ade3c7024f8ffd4d1e1b0146ea924090dd62c39cf49897a7eb88659f64a03b0686940589ac83d49f7130586d64a64f8603cb3963d55937dbcad158cb324cd133498a2488ae45284594ca6e05bc1f465ff102a660150371799344f1063d070291b4fb3b7b8f78cb847dbf51d99025abc78ea2bc031c3f39fdd73a4f7877a88749f901d9c175de1af4860ed66514b3a6acb3a26de221a7979e73f684bc189812d2a205bfe06ea7bc61c2f6cf641a5efd1594dd5606665c12049a4c047e743ccb268696789f640c1209a9380d7523b90526a0068af9687cc75ce08194e9316bbf198065c3eb02d66e231d2ec052530f11efa5799cc56a7cad809dd641717e8e1239e5f9881ea146201b520ab10d1a8137b60f8624d0a04a7106ba0e4fb1f4760749723d0d80b22c1dcb107f2233d47025bdff356a249275202029874807d46184d9db8975c18932195b490d780212568e3d27a0445467f251d99f38bf7f9a200af67bc2911d38a1585049290dfda010b02c50b68bcdc11cb219a8b432f79a39438e3b1f0ca90a3335d998802b0ec2d599c36cf6de31e80d08f1e06b84b2da5f64d381620c16827f48a391edd0e7d7dd27d02030df2e14ff89732c90cf9b21a98a6ef26e39d472586a1b9a78d41caa469274a07a29e4e387ca59e953245ee23680a321f4505ec5e47b67371dd6d344cd33c50ffd9007f9f1c1c6adc4f3dca4218f335d11a005c23b0dfa694f44a08c961e82be136e0769b03bbf79a4ef088c0f0502af6552663b2d56ce5768dab00c4c0e9a10e0c45ea9d68220f63f19f94498925c69803194611fb3a4af8aa82b0b5ceeba9503d59c5c0e631d00d314e04a59f868c88b009e5974354d86adb9dbd36eeb652528c22e3c7eb68cb2a655db710b4666e87f14ca275f19214509459fa8d69383aeb82854adc3753abd0b0bf1277759460dd622d1bef805e519b1f7cadc8a034f25d8aa74e6e534bfb70c96278188be57ad518880682dde4e5a2b4af353536a88b27d8dcb7b4225572a805e252b3b1a41f580d515df37047ca1be3dc708fd12bc792d0edfc7145291c3dde9df20d5aa843efb36e90dd5546a8a7615d2ce21e3b1367b191e9cec803a47e271b5569db17cc656b4a0c819c4d4adabc4d7609c18206e7c09663fdc047e899d53f5946695c86c460144a496497b6412ba41d5951e218a88014bd1d176e66798a6462c43d294e219ca1c212cf4a3841e4e30026837b0ca58123ed021d21ebbef92210ec4ba535330ca5b2810878c014b3b40b1cd0fb3284c17371c7179281d68904e4bd36ab2256634c364816d1a2958ec2caa8151050957b2570ddbb5b28d87a441e050cf1076d93323e9a463a7e0a8d2ca211eabdd8a9f5421e21b18fd4de36c15594b72f028e12af9e70178690e85118b78a96b61bab213a8c2a52340d1fb2d0ce61ee8835a7c59ca40941cad34ef58d96bcfb22bb57336060fd523737b4a865befe6148c98288cac40502d8ec4c75769476cfb957bef66ef0198ef7489da7cc1c4c5be199626aaeb92eea4f1090029f51ea851ba9394c9faa1f7c5225e50c56ca9144c71253af9ed986e16723485efef64935d7e4791e3fc1e5425c15e7846e0718949df5ac138289d8bafa33e5d03e47224f41d21143ef2f3635e1915bd61b8bf750dfc70188097994bc9a3b520490f0476d525c6c9bb925f397ea75bf80d03c7d513e3f6f84356559782e4d68f2d7866672dd4fe2a949fad9e65f2aeb2128228cca2a1678923aefc9aacfe61a660f695f31e0361f6a55a6f94703c0421f694bde042892d22b5ffa8bf54651fb41e13f4f520c491dc9232e06b35411de83f6111273e6c0d63615d6ff296ea1aa4310b7920ad5782ffcd1d42b4ddb283c69dd9bcbd28f2a55ae5448ba59ac3e030d4cc8641ec97005bbce5cdd2b8aea165d8f462d119429cd2b04b53c0a2572a4081d2c8bb47f928183ef59ab9a66ab2148fa0a99d8c7884723a270b1993240ce12c64a75b5e7a7677123b9dffb4b21c34919342043495d098c3387212a3801ccb6687be2f1698413357deceb5f40fa0927bae736243bc61b208d352846cdc15ac737ce9665e9b6adfa5c653092c4a957834212a23a2cb8cba1fb31a49c3154f826469474b696587d1482737ac7c62c287cfcd5035d1654d2fad5f2997639ce91dec9933257da2f2f24ca844934b45d3c1c913c69faca675cf370d46603c4bbd2eb226a5e5f5fdaeb514048d80d102cdee7d2cf4db803b0628cfd2aa41e58eeee55d718f423e74a94986f74219bc96178f15403c44feae8726a329d854e000278c59a1402a09613267fa799dab953d0866782518ce3fcd98e511365c2448e6d192f003afc07aa15574abfbab2a59d2c2891500ceb9c82a970b9ad0f68bb9c00af5ee9d12beaf0d60bc0dac2a5c38af6ee23e6f6230253528ac862f7dfee637a8e9a2d44eb8f126f2a87c8df3d2d1cde9ae5cad59169510db1bec270d2655a07a08f039777d6f8856c7012e8b4d43caed7062013db1913ba44ec703d7654679912f7ad16737c53f553ae30425308c918f2f32cd0cfe15c470df5be3d7969c7041b1fb9dc8c00a052311168a682c1ea069fef1c31f39bd31e556a06773d7bca791c7c0c050bef1319f84772e7bd20b453046decc52b6f543c176224699f0578bd0d00787a5af91819472e9725a6c3b97e0522acec2edf849404fd46fddaa2e89e2231199460a2529a6be3969794ce64d01a70ddfb80f29bcea42a0fc4cdaf3736a01a1fa8badc2d06946d66cc57a86bddce69c9a99633ede37a850cc19d5c08dbe548ee9da5947b03b350343a8408e802bd58aac20b9357f41dc2d16a0508788a11dfedc02a908305872f5b028778305a72c151ca0ec11549d57fbc2bf814536138356337f614ce06c2e8d521589d6b085fe44d9c6050d5bd0be81121c3ae4c0ac1d3b02f92e1f007c38c1843c8fbb54f2b952833103c5848019b54f83879090dac14c7d6f19b972e86ebbdce76f57d5f6af05af0d11f9ebb5c4793427b6a0b28e49909b5bf34675b9f23bf416a8536eb2a061632d4b20fe94893e7126b8eabcefe219ddb2e5333327a9d0f61122bee2563c113c0619e1ee547f91a96c4e7fc36c478274d7e9c1a9c49f71886336c0391caab3ba88bf4227fec108610e5886d09d18e7d5f844535fc1476f3442eac4cbce1b2d5722161e150be2fe480f3306fc75457b617db83f58f826073a232892b343d3233d781adc7bad16012a35fde79c15916ea54d5ff72d59f8ecd5077c297420c20935f17f4cdd088ca05fe90202329bce190fdb04751e0f1be88f25e6ed2d393a952f0bc39245dea857c0c87daa51cd59b7b6a44c4bc0b984ab927ed1a63a5ab4f148ce946d120dd0a4322e481655691b483fafa63574db950388b50dee484b5aebef624fb29a36434f6733a331fc62138aec043292dac4c47172c6e7bf2616d578d5ba21cc78d8991e4961570acb55bf6615d55703bae29b3767c751baeca26b3d34222248f916ecd0a6f512c4efc98b104ea5d4bc7d64b2652a1f88422da320ed825725059b1e443b82bed36a00fa864793359fdf5b50615eaa0e6f0f7fb46c22302414e382591422c8c4259b2ae009ba9e969ed35fc807b29ddac189c3083399d50bd1bf0aa0fa099f4b16608c3575903d6c0494e88a9aaa8ce76ec181034a686335b6fb453565aba42d61460626d23d07f41de64a33d43939d9281045966163d5609ed1bbddea4acb83b4b264bf806b6e1bd54a2969180babaeb696b70e1306a9d8dd89dc89b59c916ab3a23e5f0a4c5b6defc1a4e49ca6e367a0b1f367761922c59393a29e1b003801a8b1733bd308c4d09e1d936750cd320246206014c86adcc10cc14210cced872ec4d5b4d78e4dcabaca7377012351a530debd0e7849665dc6911ab09d17cb98c48b23670138555a9df78e29c8d80c36dae0842033d6620bc88935cf347bd2f1140158fd061c24df9f0639e280b1811ce39542ce4de65be0076b80c6eb779f01e8c709594fc3e173b240ed8abbbb31dd9af64770a23247fff54329c5433cc247e3625bef64278d89ca941a6be3ac598785babf5e3ab96b4dbeb32ce0be8aad664c84a16048b03c6ff80aa088bfae4d6ab7707bc6edce6a4fc225e1e5070055ddd054321a63275bcd27c5c0ea6a0c2861b926eb641610ce0b5b1c16a100708c1175a85b23025672ca78e076c46b7e5912f02806380b7eae32f00e493792a6b98fcce099a265cba8cc1f4aad44052583055f22f0855b4095c0444c27d0c78eefa95361cdcb3010aa48097a09e646c7951d55cf46ab3325d62a2eaf318d7489a03452ddad5bd2dd7f528568341e7f73ba6f2438ef040e2231959a5be39fb106d1d001d46a2a75486da1be970ade30ff7e905066903d33beb806d0d5a5863c7bcd53ad42dfad2d0a34703265ac7a61863284c73f4a03d7d599286709198b075851522b300432e0336ecf946f44903ba1009654dc65d8c2ca3040eade94831a2930ac58467a5631933d6c3d008462cad99059c71e3d55d45c226594924ea146e3d39039e6242f00b8da3dc24a68cd749713bd38ca1645a46172675bec3adddd0d20daa373beeef63c49592828f85c77eb7605268fbdd21e8e42f5337625c2c8ce0983decef2e1aaa284edcb998d9018689b4d62ef5a8f89e0b78a81a6ee3bcec86c0f51e8a298c759c50161211f22909ce35abe6c2763412365a6a350b50059f2c70ae899acd8b01ed36501274321e77e121c11ffd78c40c52c56455ede4cf85f76aebbe740f86f840e7caf22f8a94c41c0c1e5923847035c298cf9a8f359d6d39624e473f4a165440bee0570fb630b5292619e599445db2c5f9550c664aa50ab4312e52026872770c9d85e61f63738dd5fb2cf8b19408a64fc7993ea0b73ecd03ea1f5ccba8116f09131c23c228ad4d8e33fc081b573c4db5c38aa05118c2a3bc2b6e625372438fd27c9fed59810dd247bf020ecf917fbef1f190ce11862c3a9dbe85382926e6efc2e26daf55e81c2e9ccb6433349c063929d4277ab532b16e5d40252ea58ba0c8350e31d782dd5e9aedbfa0b62bd657d9c0dec93340edd795e4ce8f23f8eb2c83b2ab918b29041ffa11aeab89ea19173231e7c6c853f4237a8fad3f964be4278024aa908f593a82d3c8c3934a63e3dd13094b6303c8abba13dbaf3d7e970e03bb7425f2e642eeeaf975d6630dadc510e5603469d0f0f1fc91a5ea6638224120a70cc982cf6ba7c2f3e2d24ef039441f91e9c207c54632ba3fd91994216d5a23ab691c2167f530876d70a44a2c159fb3c601a03b46ae9acc7ebd764a8ebaf5bc7df27720525b91ee85d85a1cf65a8d988c2a0bfbad7671649349a4ab22107da8b10642210db2ba6a303b2e3b23ee2fd036b6ee011ce93de5062bb97a66b2b3e57263e991bef28cf995f62ab0f97138fd8d5853456e1f17aa54cced0c9a10e2ce6c149153cb93f86726ca3abf8ca0ad70a5248dd543e854b69964af78ab1462d2f5c4a8fcf14b89cf0419009088d0b9d859c051b792084673ee3394ebf73317d3b4054d81677081ed1f393257ed8204358490ab9f1ab956cada8d7e7e65781b32eb5027563880c53a1cb62b227fdf685ae8e923de27bf10fe1fb1bb16d1b0c5f20e5dd94229d3f6d330f5b64586c91075f818c9641e606fe6c83286b81c9d6f17d75f09f41618823dad0e3d0f157c4be26fc3788ea6524f2a5f45fa3c689b0368b37668f2d6218eb57219b4c31c0befd9cefda8b396e2926348632b115550b4dd5ed2b73e9ffa30b2276b633d5c7c9bab3d72c412ac13cf4ddcf400119ffe01cdd57a25332f9ecb0414f30cd557e8e37b63571316f67e4f92e3c6d5cc732437984e236e786605aff3da281477cab69e76a9b0b1b899d8005d8753b441417eefb322f4404c48e177c4f3ec4851ab1f667fb23cade35b10a9936690d03d2ad560903ad1de96be40a468add06471bf9ad60f95395617ed3c4878daeded58dca4aadbc5ae93f0c920f5cf794ec27f62bb5542b34f089788ddc5111334907696ddbe02ce3da8bea0802a0ec9345ad00c55fb70700a85edcc53125207d71dcde20498175e754b73e647b168ce18fada99418664e33016cd9bc4b2c69c39dceddeee4130de4be7c4ae87a33675361cae87a38edf4cf8f20f4e4d926eb7dfd262b35cfe91f9943c51ff198c4cd3383251b78a0bc8c58138684f0e537d7dab544bf6a8c61198113ef960c346365d40d344aabe0cb774181594728ea537e335f390415cc9ef5121845aa2e46f44e10045381c9e7bb42a52a09d4fffd65a875ffc64ebbb91f789e78566a114d86c0f388c6da6b8ac3a52bb28841b8266ed327f8defc03555c9af7d4fdad40059452b3fffa7f89db001ce19342e5e7d74e2abae3e19a9a16c8e3360ec19d93dbeaf498798b5cc3c3896d39e41d1ad3f96dafea9db94475fa8a165d0b113ae406453433ffb90c116e602f605720a174319ddfdaa109980be42cf5d7fa057cb904cd05ca3f33969bedc4a2e83ae18df4442c52c17233a982066b8fe81265a88cfcfba1fc7977c7c13376b2b6c33cb2ce742ec8f4f00fba685a0eb12415c48c6e883d02095ed6724bec89e4b1e529cfcd6b3c964673ff9dfb13af9cd0f0342adb44673da5b877707352544bf5e940a163e656f488a7b922b868eedd85ed1f9634e51cfd9e50d192c2ed150812f735bb1b2c385e327aead821ec812000bfa7bd6619b2ad36ddb21859d9639cc6f62a805626380e517a6bd6166f21b98df858809246087aaae212dcfe6699b3fef0acba1683eadca736e2094cb0814593665d6c4c67c8eefaa7f19c4c1a0ef69ebb071572cbd1ffa4ec9dabbf4809336e9e51a01d21ae5869b619cd93b7ab9741dac829ab4ac5df5600165f9d5480f8be6321a6d351ca44f29e4ab2d21c79a6a50663b78e7a6b69be7b0ef8d0a7b12d62711ec05108f5abe79caa80e6b0c41bfd30002c97a93f582018610ea918e0a61e8c36aae0fdcd6ae4685ddd0421b22d9afe91142f650036a223d101b9e74f8f03c14f51cdd94a81b6bb48829fbf7e1c5cc85ce5d7b4a5a19213e2be5eccc3be798c5a0255f02700cca8e7d1ef9a580af3ef0cb5f4a130a256ef781890f05e6f401404704337b5d4c7db2e49d844e2fc82f4d9025e649e4e7f64257718b333f6ee5f3f5274173de351869341a320addb683c804687ce236562b33aa4a4e72b9165e20c47de8a3b9caa862a348ec5f2af701e722e80272f529442104231c449e51ced38429e9e9d7db94f5041a06ba7fc9df0908a907202debdf34a95f6c704a71c8f401efb59809cdffc2a4c016b86c842e62e4624e5f1bfd2f93099da434e354416f436f645d712d7108d157e4533eb698efcf033da50b075cc23694a58e73db6b8ad970568058951a5da714485004fedae8568458c35b6a40f40a2b15b4645b62c25e0ea81ce7c6779e680125cce70849b42ec08e408e56000c14573c046a1ae4aea3968e666a111b3eb6c60f9f6c6ab2a0650e711f326d2253f03f3adc5b517b167a93e16a22f5d63b0929876f09315686ecf27c59bfa74118554371ed5c27812179a5b06b045825fba64edbfb479cfafc262521862e825fabec49d4a55c0af7f8c86293135aec2eafa3be38fc34002f5579aef1db4881a9a10b7859359d3efba51065df85121110633e31817f7b0061b1883c0a5606ecec1278b8601a87c299215a4510720744e5366a5255a16dc010e88f28b2c54b1e52b6f42d0e9a1d4f71cef769ab6aab17c0928ddcd93ea62091662abc3e9b1a83f7c1651ee9cf23fe40859d54504699971279876a453c560288068ac4c392dc6649d133b5f76c5b4183a63a594b17d56603938c2373c31921410d86920e29d00b43496172b79c4a88ba6babbbfa7aaa1fa66a69479c94b573ae8b559412572dc5030a252db65b4daa6b79471bc9c1e2a1f36f4507a7f5d2ec29538ac69a87cfa9caaee4b6b418700da36c940977b4136d6f0c485cf185824320417f585208fcc155e1bead1f4fdba04c130cc3b9e0cf4dc0ff0eb818d24aeaff98f585bd88816a49b9b7137786e93b0653a4565ab7f7336c6f44a3b59633f2c3bc6a10c7c84acb0f1769286ba7a807e11494a2d6eef4e8182b179ce883171f4068297bde7c4e5708922e2845e7508f2e973a68bab7e39915d7bca7b615e574e152a973fb8a32cc861cd8c7c76229885d68db7aaf5a7d86dfec6b5e78f08411705caca512592b97e32603728b5991b19ac18dc7628b62baf21402bc5a6395eda9903a30650a92488b65d896d272b69f49369f348028dbc3754beb277989ed338d2d3afb5da658ab6f24a60678ad9799cbda257e7af36ed59009dd62c5d2ea96e4b332804fc5f51187502436e2bdf5cb3b6a29cedc00cc20ba38cfcb96d2de9f24fe7e5ae8b4bb280c634f68467e10dbfff7fc0631fcb2b5e506e6793a779e8b41094d0326785f26adaebc9fce80b7a09941b53561ea786013e8b40c4c303ea925a817eedc86cbafb586fc49a8e92d21a7e23c0cc596ed5beaecbec70f9ad3e265d00ec1e45fa103e00e7f964b6c4ee8f7704ff652be44fea5ca784b6a6babc3a4bd72e0b8c1175cb1264d446239b2f238e07d12a788aeb41d8e1b9a60c9014e3e2671baf00de7538b2d9be2472301caf94929100f8503b06d68b2973104a3ad4013d339adc52f1c01d0bcdf85220e782696a429991c2747f2faa70f403fbb9a364ddb49c3b484c40a11f74b3012447f611db84ac1e2a609af74e28103440baf263a2376499a99bfffda14a339f3ca2fd3feed1b0e9b537fbdba93f530a9652e1f4e2c6d4cee21e9bf8bd55e2218e70215ee892b35e508b3d4cc1fa13ed84102f65f7950cbf91495389d906f6c8d87c53ad3d05074502a535e17a6f64e3cb14c1bf9246822a56ffcdc5c0b8c865079c1f04d3ef3bc82709a841588baf6b3ad2ca92bad899a952f8832bd4f7ba03658309ef5c31b1c5a84992ade0062a584cdd293af78dd7a630bbbec857ba21b304f33f8109aeccf40a036aae75835c6d098741005e39eed3d2046558de98bc70cda871476ccb4fccb1e5d7cf3760d5c88027abbc1565830543a4a070538d1708dc7266ad1849d54d1035e10d7f63ca836149771ced2dc63ded6343c88422f2c713a2c9031c40cc8c16c00e317fcc400fa9d379509a74491f19f9c37ededab290a9ca4f14b42f6de644b29a54c29a57e0555053905cff950dfe4207c534aebadcd88435e0ec6f862d421768c5246b8994cdb4c3d99460df3b0b1385a601d01588eff650e95d6ee48ede9f398c091ce2d35ce7cccdafdf04e8fbb623e865f7337b7bb63cccbbdee077eeb27de6a9e572fb8e9aa5df1ef8a9fdb15bfc796e3dfb71cff33524445ebdb8cf050b13a6b7d9b1124b5197b51a2f0f72fc3c710327cdccd4ce2f6b5a73f89f04508237c91ebf16c40e30c5fe35f31c6e743d5608d9a082af4663fc5af017c11be075f843e44e1d3e063c6f072af86d7bc381e121f5e1f7c879b0ebc36bd435718e1b61049879c3d9a73295f673a3040a1eff1f0ecda2c0e2882f8171bf13162eafbce7c344f35e23abc3f326baf8dc75e8e3f7621a8fd5ec3ab6f87f65e3f4919bccacdc8da70c7cd6d7cced4cb410d7cbb6be18076dcc6447e6f369a9ba9f7aa90514a195bdb854ce6d4c175f572ff8077d766bd791d7a7155f1ee5e3399fcaa50fef5125ccced0fabccccdcccdabb16c2c8ec590cafbbbbfb618f62cd7177777794724ecb6213bf2c8698c5405b4a29b92164d8bd59465c5bd33a22ace1521dffaafbd6f25800e3e8bf2267af3948e3279a6bd20b601edc82c5d17fc593172c0048066142434040b2885acb847e7c6f76ec0cc440330b90fb6b6e9ed8d3096b12c18a186aaa4b948445130aeb12250d254129a2c2dd757eaaff03c610aaeeeee38f678c21371a03a946f8168b5555a1f256d33694f8fb24d19fc7e5cb0896b5da4ba87d0307436955cd1531a4126388480c271c0d15101cc48f5442655d22314e10cba027d4250a438c9c822e713b61287d0d37535f5da2309aa0e27fc5da18cfef309d6f06f80d5f77ed3ea0efaf8588b8ba6e4d47bba0fc11b06e901f7bb07ef6202f4ba3f9616d93e647fc6e2d6ed3574cf9884b07aca68b465a574b0a030539ce9cd355aa7ef62d03b238ac37802b6d2ec8855ef577f3bd7c1cd3cbadbb717ddcba9adae53cdc3a1bb5cba15b65ee146c5b1b9717b85e70b88c00e1548f07c8fb39fd6943f27a7cd59f6d387086c38569d357d85fd8b6a7c7306c4382fd699ba7bf3624d79fb6f5d5fbe9a9ed3770047827db747cb58f99dea45da6a5c8d3412018544dda2bf295bf17f9aa7f4e4a55aaa58e818a1ce7090519a9e8ab9e162f6a47aaed4ab55f90afda971808f2f0f0d48b77d94f4ee32376fc71fba1b647a3d8a7313242196110cee4947c9256ad98a6606484dc94eae8b48e359dd06e8784e8f4f00c0d0d0d592fc457fe3d6854c1676d3abef29d4905ede6fc39dfd6ec9375d1425da22421d4ab2e11186048a12b108d88440446972e7e4b551181b10596f917f056b58ed405b8b8800b1717dc3050962c58b83811638c31c688c9e026c61863ec09b27a84ac1e2856cf90d55364f51859d64d8cef0b0b4ca212263540011861baa4c123042f6a5080d30a4b00b2580993654b8d2c42534e20882b9098520230aee4400b11aa8c416fbab78ba22e8ca816c7bae0c2b4c50a06c5a985232a3303702f5a823281f04cae7229a59c42084e01658a21aa32994c269972c2f20190cc42cb8d2b29a0ee59a494521201cbb2c55d0928588c3122e1e3ab9d72c6532a4ae90e1c0b0aaac4b2d41cb960852c4feec08aa3124c1113052808485fbc5c599c052e1807042b28784a4529dd22cbe22daa4bc4021e1c5162111b038b0e245c2321ec6f260fb23909808551438821c68a189c40084cd8828b3146ac8416d924bb01e6c5cfd269880c65545483e6888c992cd28256b42ccbb22ccbb288508a442cc51e9ed8d3137b7c62cf4fec018a31c6225850634cd98087d2b744529278ea9631a509262c7ade23420059945041c6cad1fc15c492858c5b96e559647609858a31c6a5ba44434852635c923204313e843156171b8cf11599454208214ca2a5579b04692649a249945449965e081e4e889e1b9db6260b269329c5450a58ae4441002b6940dfb5afbe78bb4f1c65b63996828698ba8ed32a67667ee661ad94304fd41456756bb4c599aec2ead0ba6e00099b42edb90949826edd220748607bf0d6d954fefe19b686d5e1d96edd72e1ec27c8959bf3b70e76982b9adbdaa3757413aab5310b9a88a9376e7234c7d3099e65086da9a2c4f5689dad4791eed1e5062b68e2034310dddaed507b8fd6b17575e67337397573aadff0b27f3512a4f69317e35e0fe14537c62f4214418cfab6203908d9d2324f464619b9428dabcc113d30429717c288696eeb836f95a8701e3fa0c1d08eff0f3b759fbbb36a6d7cb5057dcdb50bde7bbe595cac4a3da1d807de7bdfb9bb3f777757adcdf2638c742519e2779c74b313c05f5573e955e3c9bc97aaaf4b55146aeb88d4b77510f550bb869c9e6766505f03a25e06857ad4bfad4b55d45ecc3646f5fd0943f57d0acec0d853827645ea11af4352ff1ecb6838e1b38e7e0affb60e85c76a785b6ac809fe75d2e25fcb017e9450457df66400d5f727f890f987d580fad30e2cd40b5ef0da687a6cc5b6d33f28b3efd903e8cbbca3b4eef4d7765a424f0f3fbe19661ec5190df0df4623f3efaf476934328f3dea54e36964b6ebb113ea2fad23521ff6922bed503fb39d9650d4a37f6d43e8cb6c43b247bd4e63dad3bad3462455e90cfca8e7cc47b6ad6a3be2f584878fdaba14b687c276c46beae1095bc735f53a9d3a3dea2f9476d25e7328aecba16235bcfa68e8aed75c77c46b6aeb7ab82686653198fe69fd68386daf39d45fd8d6c13fe2471c3a35fd7b5b77c463e081eb4defb39ed560aad8a334d3d7d03a22b5c6fb7637ebccd33ae33ff3d7cc034e1beab721849b8c8c56e34f5aa6c2cc9f7e5b26b575f3446766b62e55e166a2c1d4990fd376bd8ed7d068b02a66d250bf198a03fcd75feb01ba4464f0d4eb5127ad23524f906e1d9dd1b6b91a5b47ebe9f450859f99d9ba594f5fe355d0b657353afaf265b41db8d6c0b621a819d807eaafabcefcccd6114955d4ef9f3419ade35ae331ad235251dbe9a1867d0d4de675bc625a47b7a9a1b4eefa0ffc50518f69dd6610d87a7aa875442afc93764a823319508fbd8e5f6f3da6653d66ed88a4ea69eb7ce888a46af69b55938961a8402a911806154b6851f97707ae1c06154bfca09e501985ae210584b063b8d1ecfb3fb8d1ecb61bc6042f49f606fabe9b4b74043015fefe6c28e4728f4ad7b67b731fb5d0dcbb437563706ddde74e8016065023804d7b160200001b342ba8a04a0185135233356428ea946197c99a32c2e7eff9b4c1eeaeccda7473bb1a5d9b276a6763050ad025f2b2a4762a1bbb637798e0d200dd75f6ee82c313629278228c1f217c980bbe62079b068cd2ed85280c239c2871e58a251ec21671898c3006e6007e5d222370a9a6ba44460082507d463ca0fbfcbc0b6bd6a83613fda724d4ff64047d1f03fc7eebf78ba0efe7b355bbf75e0a182f35ab2be5cb12ea663e3888dac96d6e439880807f876a7c9d5ee1b404e520ead62b6a4c7a7e07890f2f290a1352769c348d8f214c4260fd0e55ee71628401b587706284017548ffcab0434f5c3a348ffccb3f6e34d60f73bb7a167ae3383dc4f797cfe5380eefc88de6c7933b9763525a16d4de5752ceadcba9d2b31b7807fe6a5d69f68f70e5ea1acd6e3fe2c3ef016e345c73f26f7c2591a03438c41efce3ef8e64221f90d6925bca96dc9994524629659451ce199958cf7a3eefbdc7ccfcde7befbd4793b8fbb69b4c26d3739f7463773b18cbb2b45dea5b4008e185c5e5265fbddca76bdbb599bf717fc3bae157280f5c963de8040739d0c1179eaabd5cff7e0df3b0d1c2ee08c0da74cf30634924ef74bb0eac40822fc2f7e08b30c2788319aee6f6e1f943e2b51d7ad1cc0347bf085f8c2fc247df151242f8a68401beeeeeebeebeeeebeebebbab05850f3affac7c4fc6c740590d02194208e19b396234e8fa2a08de8156eb3b01d4ade91b8d83bdb63c7698756e39b8d74ee3c171b8ebb16fac47d6c3f5d86759b6c518afebb1c7361b80f4903df6a78dc6fab5347ad2542c639c60d06e917ac03eab40966d42b0adc72a0549810d570fc7d176e762eceae138d7f7b5adaf0ac01da0b2f5fcadef7688c79ca635994c265390c7ddb8d55815398ea3ada8bc06b898ae328a56d2dddddd5348484848484848684e4aff5948486847ca7f967f6d5d0eb93afc65955bb7f2d5f130f7eef64cf20c76c7899974947e3e627182f8aa7f2681d4f3822a459558082d1214128a33b8414f94da2f7bc03d424272e31e29a59c526255fecce614129a5372376e4e5198dbdfca06747db2a75bcf04af88c9cc82a678492ceb2ccb32ceb22ccbaeab09b4a2495e13b332d369d7844d29e8bcb06c774f7d3a9d4e7c3a9d4ea7c6b0ee6d42bb394ddaa561a92bc34e19aafba714f4c256cbb45377a3502814a35028146ab38c77b75d9bde1ec5e6da60445c17afbbbb77c3f60ddb96df832c274baf02c403d7017577674e8306e16eeee66eeed652bb59dd0789d74413b5e7b5226be3cf3dcc8337238b63bf18511de91b2788af3c8810ce3e4aa03e67107624204b0a77e626eaa788c03ca9dd533450902242c078410ac23f3fdecfe18b50c629ade618fd7113f67e313e8c31c608659c31fe8c31c6c831728c31323363ccfcf3289d93b7eeb9058b63fa7efed91da7dacf4dec8ede1d904d1bfff8aa7f897f5095676d9a81601f26b58820be8a8276bcf403055dca0006967310126477705d2564757b85a8c5ee783f25b1440b66ff9cc1daf4973ab78e9ef133f9877f1ae8842ad8680bdab1904f8d9779eb59a83bda2c44bf9ba853b78e1ead86266381c681a157fcd67711c46c015304ba7573a2b68ed2d3d6a9feb1ade36e7274841029c2f180c448e280953002ba8c43e986ebfe862fa5d63b4da0cfed0e186515b05275118277732603d7072184f03f46c89180cfe02af4da4941f330333333f35e6cf198da5628e5e4cce94c812e3309052406d377393b725ee0c1801c1f3c1faa52a92ccbb2728e983083aeaf66e65ae876777316438c31c638678c36371e0eb80948d2b22ccb62422190192d95c9c4ddb8c991e3026462045dc6d142b08ada48cdeb89e6b676479917d010d3dc9e96cc097fc063905a053a435ee3428e1b352a15843d9ae3e7852fad3bcaf957bf8bad6baf14ed962df2b78f2f8d6b0110707d3f370db03ae4f7e3581df1bab62e6786a36c3fcdd769d363188661d816f5c0b0b736dc8db5b9692e870d3f805862e8fa4a49456da4e67477771bda862628cd6e59529b71f43f6f37cc377c8353d3396eccf9de2b09f112e8328e2f74190733812ee3e0ebbaaeeb87eedac45fc661ba542a0c6beec64d8e9ce65cd081a1397e48440341d757efbdf7c0d0f5953f958a93625373eeee11ae33308dff10bce3c4371f5df9bdf7def36745ffd299c276577828a0fdbd38bb2ea1965538537b4e8d2b5fcbcf4ff02dcbb2ac6d18064947045b7f230e61c72e727ae8ec949c1c08dfefeec21c2a37398ce538ce9c2854ed72585bedf60f08eaffa4ccc9919223d4b81bf10b56511ba9f95ddf7bafb3f7de36a0bba98f9fd902951fc88dea0f5d839a119e7372110b3672e1e66677b34d824f127aa424f144ca09a64849a2c973b85a56a1c6d90d3cdb05d28e7c9a7971795ff8467ef643122121a876373a57d4567a3285d75737ec4edcde57cdbc3133f3cd0d84fcdc4a72a9b8bf51a372cc993de35fe661b1f7896964e834e8aa6cd4d8b0914a61aa186d70f114da325852bbf690132f011fe47d7c7cb888ab5494fa7bef1ca63b22355567f3663a625365d0c95c348db4e8f47cdd560c68ff9c2b83b82c2a738bab80eeaf3459d064fa228e93a49196c42c89018267c94bed7fc08f1c1a872269cbb44575559b6869a89a1e46ab76b39bdde4210edff4706d989faf9f5b8fe674302dc989c7887b6b414e4e37333b0f1cc893547ff65faa5eaa273537e4c4ee602bce022d8cb4b062a4e5c808080a6ac44f588883f8091f84ac212323ca4f8b94aabb51736ec6ee601bac4d19bb03beff0a5bc5daf8f315af6277b41186c24357404159290deefe3e6aa3cafc53b93908e55733c2ef2d2226c250aa3f24c250b050da65f4636666769e5e02dd7fccbbe9f0eadb8ec8eadb148376944e317c6ab396a3e6aa34d745cd318176bb548daab40e7f24a4a424a5daedd251fd71fd681d1bc69cacf1b07163a51fcfe79571ed7b4942da72b475f1614f8dff3425d0fd073f6afe9ae3eadb363989d222ef11615ecaa1b7deeedeee5d0821dc25da3f9940f907e717041732a80bf943659833a865042fd7115334601c02b5823aa152a812d4887e0d8715d5eec75cbd64ca2ca49b11e70a6098f78aa0fcbd56edefdaedb06ec6b53936f3d76914ea129d41832aa53842aa178ac338e24a1664ecec7311948da8408ef3820bb40c4541041a8071b2c3fc45e5ef2b76c7abecdf01ae0e79ffa4f27740d8bb83af8380acd0ae81588c55a396f68955e2510345280d1487ba5804bd4030b5e152ed8f3ccd5974177dbaf80381207c314598264dc038dae92210116a1783a250edf864096a59428249b5fb2150ff7650a8f66f07a1b8b04450135a74c922460d763a38d4a57610c22323a8c4082e415a42823cfda014d5ee5ef7b4bca4dadd53aafd0f4cd06b12f49e40793f501e9023f9a0a78041a2044aa64003ca08a620d5eef53c9fb7a4f657a97e54dba10cb14f515212ff129efeedbca7f676fe93030b13591c5114458926ec740e54b9d4ce833a17f227eda5f65210942eaafd5d1b757d54fba7c786a520abb10cf5112da2493decd3fd94a974208c0a8a946862c69717ecb417df27a05db70968d740955f4ea11d2f91413bf6c2ff98a01d2fc13fc1d0f9855a55d06ea3a050503782be1ecaff88e888548728d1d5dddccccddcccccdccccddccccccdccdccccdccdcddefbdb64263ed3e0ada6dd2be104f5a28936a8ef3894a2ec6824e1e3f8376b36e193bfd9ea060becc0cad0d1294ff865bbda652c1aa385470d88cda2c46557189cf71920ac6433aea522a5ad6460549546602a4f8a0f6cf3320361d07e6a8fb504565780801759fa92c8e10b6b241a85b929077046529617c1e3bb2750292540c7527c148596ad3cab1a14a99e085a5e8a496c9465539d61886618c611886994c3a3084ce3dfd1cbe27d53f0ad5252be3b4584975ee591b87dc136516032791586b1d2f619f24cdb9d59665596c59966549c93c6be39087e6188991d6c6bf5b25a4feda5612d736d77d9d04e59fb207b209caefbbbb8ec6c3f629a1fcbebbee2f0b7eeea694c3146151322f2c25696e61e55da3355a23ad76bbc483e344c07f0cad766bf4cd535f1ad4f7bd88fa57a143724b12287a23d03bf3e686e3e606831c17624e4ece72376e6011bcd43deca58826822ee3400277a1ebab0834b3ef26f61df51833ec32593347470891228c711ca36370045dc681f405656625a518638c1d04a39491bb712305e986cb71e3df3108e1ebc9dd65d06eee8e57fbf94d7589cc48a33a4308e13f84ccc9207a08ba8c830cbabe2a832ee39873ced94bd05d9be6a665e5c8f982c3dc4ce7b2eaee0c21a7c102238ef059537e9b90a4ec42e8104277e80edda1bbbb4377e80edd1dbabb4377e8ee0ea1efaeaffbbaafbbbbaffb7a991aa8c46d9904ad7468060200000316000020100a87c30191602c4dd280fb14000d698e426e4c3a158823419003318a8230860042883180186300200819c55c1a7e75e50692021794c8052e74331415e6c931852043f74c690be5e682b496e19d4d85886989864420c4eb99584becc0dd66e162b06da5e0c156dee1dd8d4f6c0a8f1464f91a140010691a24ee8668f104735e1822427eafced65d11fad4b20c901e3fd368345f3d2d627ec716743a46462deeabcae063f2d4774d91ab6b241a9705c39518af795bf297893d926e54c07d99daf5f092d5528681de33ddae6d00a137ece32a16c84e3bcdf4c6a711af4d946ec7c53bc9bc54fa2c1a6dd64927d4c2ea9c868dd55020e1a66abda2998d4563110f35da9f2dd566e8bbd0202332fb396049f1cca6e3e09b682d41644053af3961087f0a66dcde3563674568fce1f8d6d600d004cf5074d56a2cc741a392f7c44477683adf81adca9d50ff88e0589435ab82da6c3ebb91859e91002b40f80bae21f5944931ddf46e0094a0dc8345190e00a24d2eb3270e27cb1d9627d22afade6ba1f6367e5199012b33dbca61109d9b1a26296666b49d4b3c1be0651301c40fe293011941e90a5914acab93ae1c7b603318bf1c5966e9f4cf9333bdccaf2a4ef22273c06dc38352b55125220c14c52d340d5fac5bcd7ae810e654500ebc22d8d306c6ce0c8c9ae7c12a8470e81231aacd0c0391c84cd070c60df24e3457034e0a5d23cd464b5f1d9a9c4f146f30e52efc625988d8470f8c5fdc206ee7f5c572b39836e65a2d84cea7e6118c07aea643ade2a681fea12afed2b1f2a41b0b60606fb805e50f9739cf9f166372e06f1785286325766d32ab2e457f20527b88194eec337ecf34d1fe89473c09c03546b1707445aab7c73453266e1378205761361de55770c54cf76d16c999af0e44b9c55104038c8d16277179dc2a6ec565e4cd2f08d56d1021ca09a416c2f5c21e5969bbdcfe8676c21b18c0bbfd6277373472e6f9522c29b77c97055c77370bef7450bdeaed98a92428beaa44c460f09df0fe599b8eacb8193a0a6e7cb6e3b8135a9c4f89704b871f5402b75314078cb8147a134f0a5edc61c254514e8d8056d2635da6c641b0b7b431b5a5f9309c413ae45a5922c103716dd1a98d8b2c6e3cdafb2feaf86506b48adf6324b67d28ddc41b934b18a6dfb784f1ae3382e780e593be63380308971d85864d3945a62606b3e04e68b08f3bd738432998a00fd6c87814a0837bb937152e070910d9a967711f688f9720d313e8d3ad3d09c8fd0f10d414989af6425c0d8a9fc68370a9ba582a6d0bfa14b502d219bd8c36ffdd5eb9aa3c76313b205f7a8a6e9537d0e0e3e60793b44a18cc19ec09de46c7e7e8e9ed6bc84fb3e684c96441b5aa0471857f23ece24386ceb7083aacdf1adbdcd02e85e7722c11504cd92ac2cb49cb5b38aadbee20d9b0101ea33ad5759303712e46a412318d45f6c26b888740aaa0632be45136d06bf1d13a018392173115530506a0078786dcaaf404b568e23661c34c40250c735c54afcdf93ffe02367089d4c009fc581bf44453530c4eb8dc3c8203d022d48dc9a198e29b327c49da208ed05401fc395ef8b0ef244e69c7db80924437988cb9214061db194d7dd36b8e813a36b597c28e388df97564051db0ba1fcfa8fb68b985be769f37d1fe6a3922a178535920bfd3d40e53595f0ee7ba3a54051a4e0351eb11e8ca017a52b5f73b8592fd519943b3a8c324287feea7e255f4bfb164e4d14a381fe9dddfc55ece28a6ca5507de8e0481cb54283e50ba0cd0f04a4d1f368a295f15476874067450902d8958f838f73e50dac439f735a602a04e92639558d8b1846e8845cad7ba3ad67cc1d4bec072a341410be59322e42086469c1093b89a06ea48aed7bcf1ae1426218c6634b0dde2d0322b163597b759778341e4f26ef2e5ae995f992a67dd9d929330bbccaed254585962f805a8f7e9fb3b0194952d2d4b6b2863933582d075364132e30db8e9831e1325b0b71c51b37c5d2fc0eff8ab7dba12445a20a8365882b8985db0701a910c10b79203c2f565d8420fca77422d892d71412ecfe97ad6e611c559aceaa0a0dbf6cb388326f0406b8a21a07b4c212dcf00f5ecc2a5acc7baaa85161bc4a06d9727f42cc72c5e65fcb1bee072dd17505b5ed64d7a4abbf94fa458c06adf7da66c92d5d25667625fca9955560969177717ec74ae6b20277b68192530ef7436c76782327e53d93953d79a5806f605ea633855d733751f5f00d3e46e0c01fdc0ce485a0ec73a88b256d9f6391b74244a84480c22cced8fd78896c15a23c475d783a0dbe5ff9f2a30968261a86b95e84da86f24f475de87b38a279e612247a9a78cee2966f8ca59bf570c46fd2aee5f5c98d30b741589a2db2f199a25da4d868e1032a34718800e134823d599539b8b391bf3527ab95808c59b9bf5a5289524c35b3a09670f3de0485e061f19e1eab25b309a91729d6af8d1816923dce18cbcea49405f74badf80d89134270e61b9a1546b8b18659d3f055372a72b76985bdaabaa2055cc537e7396f22d6824dd386b89145b785a0e951d1fcdbfaf50599f9e08a36a30dd0133c73ee10d91189fe8fa223a11494c0392a21665d78c2e2b45f15a31d064a5034560c047cc32d021c51e3f61968dbb42a5840a92d817921a1464cc5dcfe95e68aecd7557361076eab7ab591e11bd1045ae004072e8490b33347a93955a00bfdea18e2eaf20c959b61713b114071f8c7670d5a30804318d21ef31b7ec14b2e212cba1e004a49f449968e95efc40b985ce3ad2bcf66bf2d0c50199a6a873fe4393e3ca788652ed8c0b5969d148c770503f7b5f96533ca0ef1b80b6e422fde2fce055a68c5b9e2d8067be2e02c2474d61507741b99dbe58c8565e33dcdc907dc5a51426224319cb15ee41e220c863b4fc14961398f0004f388b6fd4c43087d03a86689bca27a303304942b72a980acbd0933602ca41235f69ccc7253e7dcc60a1ea49ca2c69b852d1fa4933189515fcc351b06ad94a39a56b23f8a34bc3b4983c1fc71034101564841e54c633d8c9ec3ab567ec8cafc9cd2a4bab9952e129afac58b79b6e5c378596eeaa27f1c6a1392ef2704c11d780b8c224004736d82c410b6108518d2415c1c71bc5c188f4ff0134efd5c84a90a7ca8e1c3c2ecce3e9a9a2965e2affa1c9716e7f30782073cb578c2000d635c9c26eb49b9e85bd4a7c1522030e86fa0ddc883c8fa524c2635be0965a5f16ca28edf325a5398dc9e34ba181a4708f89444f86033eb5d8421a36c16075260518e9de896acb374ae093dbd08168506c35880df899e3ffdf59c61026250fa0ac9a1e368af83364e2847487dd76ac02caa750f556ff61de6570766462e769f5fdf72e3264f37e9c23326eef834f60455c26ed04f80af801d4e70337621384e78a7af777edf6efbcc75cda576f6a4f03121c54c0e060603b6bcdd6e9f9d16deb0109c3b1a37db7777b232225c68c3077215e4e1321c5f653bd3a76871eb8dde381bbc0fccaababae4f69bc7ae2ff9bf0abd8e6ab2cce44400b6ac05922a7a7c9b6069626d33bc0b7bc9bc591604fc5ea41c24853189dfc0572811b6f37249134f2646041e5bb8259e17d88f6c9532a8bf693d8fa0aec0c6fa04092f2174054eed8240998ba7c4330f2e8efcb5fd00a36d3275c67888362310d9e461c26d93810ae69b2d37cac04a0db154d285aaa1b63e128c20835a8060d3616c56a745310fb942c17abb8665d366904e83ab2ec8154dce7edb0d7ae6640fcf0622977dfd8d0b5e3694a0ebdc78f4962cdc251a663b09b2629ad8d91a199d284c3f53733c239dd86dc05f4d3c3c8e9ff48dc0afd3996cd13c77cad3b4c43d58534f85840c112611928a30eff88837fb264e31e556b1dbd9c7794b04445145a42891ce93c3c83e1e08008bc5117e78a3026134e4bf1e8a2974f82465e7a3a586de2542529742351dc75017c6a325d1825653729255e570f6bbcd0cbd2e3e80dc74c1666d08a491c7fb257b1e44b85be40c51ed7e82eaa6ec11ed1b1e607dd3f2fec9b38b1b1c9364a75f1a0e510aaec9ade58f672d6da5951d4d57079dda8ab75cf4eea2b80e6f60a41ea4c36aa6169dbef9cf879a64acaca54a24096d2e20a50f366549b5dc56a91dc87d093b6feef748a7274c45c11e1df1d2c5adab48e3b7f3e9217c895ab44d94b49d9fd32691e2afe89457df871a74f3f33315129f1dc458bc2eec44f59a17cdb1927d6f69d6dd21b5412a52c523c5cf19ae8eec9b6299d7c7d368e063031fe328969f8dc5e9467524c11986d48d2bfcf0a3d1cddfc8acbaea812ccd6d941b5ae1e76b5d1b40add5b7b1663b2b0b369aa10cd704eb24108f5b33be743f9882a49fa4b08a60fe9be7a3abfb1817df2f54edf2a1f4ccb0781bac539c4e28d783c2cc9600f20af23f2d60cb74e20925755c730d41afdca6382b8520ede481a4915487fca7edacdf206d1b8bf34da6f8db287e5e133b2b41e15d7a269a171e5eafe472cee96faae297dba681071598b5fea73bd26ddf5d3ab275396fccfee8e4cb4d5deb2771e48d5d53c68712c58d48fb10d563fb11c0ae01ad79eecca5d403a361cbf9638594b156cc7544dd7e3010aa4ca3231cc69896fd5a2cf62347be5457dba8733b7bfcefe4e4870e8be4846a06c3f0bcf3568e469eb2a97d689c1fae8ef4d376fb4c8a6bed1569b53e5fe59797906a28cfb2432e838ae76237d5b39bb21d8a7acd90f9cee21dcba4f882e08baa594ac85e0ca25cb8a239626743ec2f2ebed671668431dd3f0eb0372dd2dc477a0d4db19c309c8383eb15a03e5c5f4c3f67f0b29cbb7f92acd7077ab4e1323879cbf852cb305aaf11a90efc1d443a2988c36f50ff22575ac81ba19a883c778f061770583701e1e0cecbbfeabf384c200bfb4712f31d688f91288950c77453c83d2c58f62d65e628cbf47e5e1f768ad0e35be42dffcc48aba1c68f837050795d3220865ccbe6101b6e1660df4092ee46ffc52c6afff3a4b72b83eaf8522effbdbb6763161749a2c0bf6b98c431ca0fa409c6866790c2dfb1ed38e665bfd538dae3c56628f03c739c98d5d6936b3ea9e4481d2ee51518b86ba0c4be4d2a3e0f615d15d1790885c6e28bd1dd5b74777520662db6b1af94681486fe063301cd17a728fe6c5aaee111bd7ab9ad8639b4fe7da96ed93e82a7d2d6f43caf782a2d9f1d24d694a85c356902b7a5d27df3efeaa858beb55ba3f32e33c1fe66fefc1cc7cf1a7bbf7eaff7c53e6f7437b38edf9684f7e3dbe180071d1a80defc1e3f2749eb98f664f9d7756f05e7236a4ad1f6c720712eb1528382e78ae11165fee2cec05af0faa3a33fd96a243feb619f5d63c6f2827327f0bb54598faf8f12b00344d802d0bb6a74e8e2c9dcdc929efba02ece7b8023cb62ae28b99b18a4942fd66bb19301f6400e00b80a8c7b1930429c477498b4f19339baafde15b80dc0a0643b713e2374067914dd3280a146b146581674ab7566bb9e39be98fb40a80db9b6f1dc965811d938620a8117e9b30ce0aeea83cb8b4780c1684994c5a33591a5d84789f7c7a3c320f4113f8d5f3ff6885555a928a321a19018b48e5e630726ddd987b09211c493ea6c78ec9a94c96e18754d59585c5552d50f540baaec7cdb03cad0bd861e014b1c5eadbcc1b0066a9ae7ad71ac78e034d44ab3d1c57aabf62d0e5cdab59273e812c6f0dfd9b11d818199daff5e33173deee09ddbebeb1f6a02972fc3d09fe3d7e58a2ec2a58b21845d7d330012dc27dc4b4bd624dff2491f55e679bd15a52f4633d306bc42c0395b49424ae4d76e30aadd734529d214c0ec3bb82e6e7c8c0c2be702f0e62397122622fce8d8c597359912241993f922a4428654a040c40628caa850ef45df2ccc831b9a525a4510cf270935d22ecb505f67b55a40a47169e89ff64bf334fb082e3984cd94884586024585d80034bc0cd099e8396d222bec4eb30013ec78f745d0c710ed558f395fb619a96aee2e5d25d7e6364797fb3e5416f33b55bb387635f7082053f98f9f9a2c5020b2198401356c611a7c952660163a6f56249192e64dd1d7b5aae79b8e78045181f8ddd140f1e6da8b8a3126d4d79f466f42756133dc2161bbee5140023094856e06d00c0c36c68d0c45c364c6576aa38abd98f581436eaf93a2740baebf09988a7f29ab05f087c1668337e4f71b90038bbea5b5c711c6c0a9a2176cc2909e12b5f73d3c933a96a5c435f7d255c4708284ebda67b8ba46b451ebfb1f8a9433df4eaf93a7abf57df7ff34eb02c2520fbc0b61c9fec6ec77441cd9fc3e4b845ab0cee2c1fcb7e8152d3d24b0b2e18841d4bbe7e0149474db7eb091e403a200822a94da376e4fb8830cb151269131e5b0807c40eed51028eca5d532dce1dd99e198c292c2ba521099ae3e688dae93afedf38bd96eb150766256878395213d7a12fddb64265343c2e3a446659ac0702b01038e4e29ab4b391c6ac23128b1dee2eea52f85db84651f974190d6238d0b08016d3f540a15e745aa4cef0c137257a76652b0ac46093525fdab74f92443a9e9d5075a5cc6ee40d813782c000a49347d228de9bc3b1df86d33f44cafccfcfd4b47ff6b95e66d93a7a124fe47173af422efcb65acc48f522d2e66e70a6e775442c3d602fe3b96a1ff1d7e38ac45b1a040f1ac9edf01c0b65f6b4904df213e9ff58a5d600ad4378e528286bf76ba4e81bd6fa6f24937c7edede1c5c355b34648e67c89f36a2861ac42a4897a0c7de148f21f5ac1979d8133aad5201847c3a97142ac23c4708753f72e4e1c7afba457636ae5b26176aaca099ee58a4fcf0ac83d9bc9bbb2895f345699b3498fcef337e3a0e0dbfd50abc909046a0023dd8ae5eb62c218ba1f9b2b362861a97128ac707c38ad4901b1dac986e33f1ff77272a845dcc332841656425458b74f56c8fc712c470918df08e3612a7caee3f1fb66ff97de14e6d9c8c88e7872d9a6038e35332aeef94aec0f3e658a38d4314f5e7704c5035866c12626724db23c2571b5950f8731381d7c7a0f8ddbeded5f0d9d218f2d4232314b1693b2f32cfbeed17d9be6aaf4596c2065a9a3faafcb47c145b0d8e007d14b7866aa946c9d721a0120d5af517b938a362a793420a9efd30f0af0d435a85d11b055b3b227bfe83eb83894b2d43dc97cc101d8132849ed168b3f9dc7f04d792a6327b8801878d930d15da4079b3b6ded671d75c58343d49e12b1195eb9496a562f9da99e5bed76d0280aeaeb799cfbae4daacafcdb934d2ed7da8469e00bd7f4c06321a914f128471458e874e510b19a88e4e38c8d84e557b10ce2ba48b8f1e8acfa0719374fa1130cd528d10ff5c41a2260afd8851fc521b9c84d1dc0cdb60ce2d34cc797f4abe4a264d651ec6369bf055a40395b824f2385c3b3eef258878613148c45d4b7498e6d1ce615bf1bd5a6d0813632372194845f87421db7e9dcf6ad697fb672e61da8093a1b26e8a3c54a89e15a9bdf5d37405dce6bf76c349258cd256916da8548c741e9558e9aed9d3971eea188fd68f54735b3cffadbc6c44c324c25b1e585a38854496c36f9d23a6bf3adb1ac23b86a5027ec2db272ca351ec57b2d0fa09932af5839e55f9c0c1ae581ed1601d36e04423a63f589eb8a825c786b08a139a94768eb271e73c3be37e518d3b950e29eab6c44d9a3ae2833d1267633175eab0a4ff53c86b5aeae841436fb1c96494354f2c05b34f80a00b0176cca5526a81628222f553c457a29ffdc08b27bcd1d86ad234a3b5bbae9d34de6a901411c9ac27a6c4757593c3cc7af5e5af25ffbb81d840c27036de26d2c00a9f7ec15bdb50465d08ed968d73aa67d337f1334165038e739a2815fa3a7dfb304446162f274c252a45d3d6901e273d32859f270a663b0599d04ab1f0d10b68f3ccdece0340c78ca8c8df961d36dc2e60703adb7460c9a533157bf7b2f15f9432f50069a68cd763203498c73e10263c71791c19978eb1935f72ce094830b3846984e1eb4cfc26ac824ca7d5fef1c237e54a05f2525227612e04f05a110af1d0e4a602dc45121cc3835663db071c1a107b8c316d15eff06c2881ff0def0aea7d6d2b9e792966d92dc2288786e95db4a61c7203bc604add56a37f572b30d2d77045fd0ae6c3b7bb6a81764ebecf6a37bc4e4bfe784b7e570bf75a3fdc210ea331bb821efd878bd94311100292ffe6b32163fff209b3077a67177d2d9ba31040a60a2161ccac7c09c3b48ce659b5dc04e99c6f69728f5ee10d2c6f90232c274777d7a2e4701818a1a31501e223a04d764e0c774e8302c492c302305778c9a22085000002b311ab08f0f80ca6888fb00f297888c206bf4bce0b0afe5679d20cd9e8ad0d82e44b7806b0ca19b95a7f9f43561b05ce9805f5b03c721dad7047e544a9af70b461dd1785a41891ccae049ac57195a5d8bc71134211addbd31ac31109f1aba60024a1a01a07b833a2106a78be5c2a9947d3c8e9a1197c83b67e337c8f60bce940c36b3e83d25e98c868be50e35719d935f0eb938c7c05f250424ef478fc833ef3e91f01ca55b4292046926ff9dbc7ceed20af30987e20243a93dbab226323f1177a8e25c8270e5cfe5bb708e51c503df5e57770b378af8da48aee96b77d64c15bb6379d5caf37e3728813f3e953d756ab48b574f026c8d924b059a2744982a8ae5d5062bde735297987651780b14ad680e7d5557795b030730a66ffbfa97c21f30c5ab1046def4b866e69817d60e705518dbc3ca369905f495ce64d5da52405404f36274772981d8609f671d29844c873b4b4809393aa90afb371c768245576d2d039baa134e66a148e982a6a11246a49a532a3f90e66ab8a23fa480a4a5524a8ab17e3a57290a7e467d2255bfbf7079772973d502429051d0030acd332fd06a1b2776ad9bd0a456c97810e0bfe5b10843363e3ee0fe5b25584b5711fdef7590333814c53bd385efe2c7f26eec6eddad9ada69a89ca6c44848e215ba626452d9f54927bec4c085da11244f8c8594a872d07081542b4a754c05d62e17fd80238b0a8934f500f11c3f8a342deed2586adecd6bfaef49a581a2e90a890040de56fe97335cf4e6fb839904e2f4a18e1d8f40bc4c06764d3baea9e81f56112aa3961a6d57b541eb895a5efd82fdb8e2b38958dff3e85294f0722fa246206ce8240a911888b85057dba5dddd7fbc9e482304a70a5522a3cbf1eca2121fa9cbfc4c362aa66c3c7c96db5349dc426d0bfa65ce3cdc2f9ef85844448ad97b00718723db4021f19b56e3084da54f6ac629547b89d5f53323950505c16e5d322d67ccc42ebb2daa92fb5cb04aaef17e94569b4181a2863d2a5d175d1ff3d1af36af714ec1ec789974eda81e3e78dcf168d04eb3c9b0d2557e6c3eaa7e55233ab943a4ab25bae87842789f9feae37088ddcfc4c884779895718d09c3c11c134aaa2c6b68862a522d2a92062a28fd125e1f5113b1475fa26a4df2d24c4117ee3a9d7332507187b192edf16474a227e251ea1a2983e8c2f6ecf604138a42f20421fe9dcc55a88fad97c87be3f576848526252f9b408da3a9156af9f74b48c974412d082f3780b2f79a0726b3b5fb89885f7cb2d7da35245116980870d081c2d8b9cd0e32b9d5319d8f870baf02ac415b4354797842b1443ebfb7a2a98fd26116148009be2a22b417c52297921e8e87d150a0d6d517f8639faf56c56e7201de072f147a7b764a012fe32efcd3cfa974d0d85ee43421ff174242e1ee5d1f1b478df3124761e727ff78510d98a7ea62de3acf4c418780fb912bf6716d4518b893ab4b144b548f3c91e8c78dca4cbc881e51c01963e0e40e6cfd63735946ed191450fdc07e7a36b3521a350d4f70bdba194650cfefa2e17a48dd080faaab8e34d370661fed48e938ff850c93a31d428f6bdd1ddab1633745f74ed465b0ccd3706b74393716c9a40972314f3f93ff5ba5d21c2808f9f0c09137c63ffdcb29c04da9b6763b21c12c17f8fd6d6c8ea7d81a1ab69ba9986906af4839f944c3603c5e4e23013f6c9cc906fa3707b44513a820c5a7d0b541598aa19be061202b08023bc039192190e6bd1501dab9a4b7433207290d872c87b6981c25420ee911c6c7f88ad612feba0dc75d59cb59773cda327ac8e95e4400d1a89b39b021997f0c87321825491c0c634e9acdb1321cf3df5b604c5ce3a40580eb298425b5baa2c562aa043920c4cf42ffcc963afbed3318a8b9ce92803ed755cc781a0b3d26aedfcbd9a99b0ea6845126c96044feee4e52b2afb55617bd55ccc33bfaf806e0fa562a414a449a21f1c8e7d15af98c650468be246ffe5bcd042721a7e39963a568deb163e2fbb9a3ea0b968ffd1ab2c47619deca0bc4049680f19961aaaeeb2ab8db7e81b033532852ea6419c46887763c1ac56e4a31e2dc64ec7ffa78a9abc10c635c828ff45838aee17338bd959d82324580c2d423789ac3c42a3c9be1dc2caeb25399915b281b2b052c789accc14042c59586c2b1b63e125141a5210334024c1962e7c0371248502a0020f679a2ddb055340312aeffa89fc2424927f8fd117339a31c62cece6762f0575072ef228904ce53cb06a16a49ad9bbc834ba23d793c45603e214e2a911939f4076f1ba99d6f3eea92c727857c99bd7f3bb1ed04a1ca8804cf8afe3d8c567c787b7f1f7c26fb7b47305c76ff43917cc924df7c8a11212805d5a6d075a51ea6a592e3e979dd0c93b19af4df7ee2611401ab5017ab4331c0e144d5d8a549577ad4f413899b838d91384ea75e362a273318ae1984c7ec8009bf7ee71a9b03491c121d734ec3511157e204fbb8d48c443dffbb9971033c558747d1b7afe438d0545a41bf608b4538522a1695602d61a3c6100578f805173b6dc3da0c0aae38ce17382d021ef6c6aedab02242c7273c0b420e9be13871d4e31920afa8162b0864cc6313fe57c57a1bd4fab2439786eb9629fb112811ebfd5b976a2a520012ea8545a58ff00c50734d12590c0c90e564b21ddb139a3125194a537a407227c14c1fb2cbbb43d4b4ab65dab3d1351b7e365093f56bb09008a47d80ff94a4766b2d632fdb963805c0c8f762401bb6021f7c347013c254aea51ef88b9fba266ddc2a8f310a6aa52b8dfa310a09d43f84364fe427bc681a0cde4388f85121c25401c9cbb8a53445655204533527db3a0c10599135412966e3642c86525e90366c7743e1d9551b63ac29544d1d3f52e22ebb104a2fe60c2f545761ed4195cfc90acbaa10e4ebc0da302220448bf901b0f62ab8d60fc0311fd9f20fae3d9356b6a557bb1aba1484a484207af14d33d3c7defbe7ef05cf6c85506b8b41353c22e6fac50c0ba2bd63f213c24d4fd627050fcad076c9e1a2e4f5145608b58b7822ae7b84022b5d0aa7566c8b9cebb715b367e1a6e09aa2255927e5f7f31c05234f5580033d34704afb9f71806c29629da0c780c2db12ccf412eaf9434b158e541ddf303b20a34cf77ec3fbc4e41f052b4d20cacff731e57022814a2de709925f6205fe810472e0675b3a4f0f43deec7af581d5ccd637037c7346c6688ff546113db1bc09e1741455cf8429ef35d9b5eff157145eb744f07bb88a438c43bd822ecf2246af5e405d76dd2e6bbe3857abe25003f4d2f1698acbea996d57b16d3c6320c0994e4261d218ff531af34eb8a525291d3d5d3aa0be5bf14a752b765d3ecd88b406c690ea2a70985581a073aef41a6c67f8e52384c684c6e68629baf47295eebf7590d9b88aad8e731bc89ec381365751b2759c8e467e91746b0c67c11a1ff6f040a280380ed81d5a16548e2cac00405cb6563e8c2b30132f9e40678e6b4e659bcba71297b42a684177c38495caa9ad2aa3a07f73565c1145a9168d24d8628c9f330b0182d5be2430da15456891953763c8af873fbac94e8e526a5670904b709d8e5a589dc4a15a04a774dd85305c4fb55da5c880312bb167ac065d4363b727f52cd1d5c23f32aed8a2b45bc4dee0ae832f0a0b49db5a4bc41822dcc86ad3bac085f08a3ba8b4485a4b4e3a82f0c154bd9a7d965d78ca4b8abd30f6f5630eef30f2d0e536823e5504f42417dffe99342d0e96b4c4a02297eac566d7af7c527a9e182245b820db6ffbb659e0321f11ab45a27cba12caa8bc1041d091d139909e4d3fd853176bfce8531987801a633761b554f5d18242435e5eccbba96a94e1a415a996d8efc4f9c3ebe53d02dfaf80a110619016a8d03c433915f70b76c063d7573d5336cdf067d95dca4a8739c734ac05c48573ce8500b7918d241da2f89b60e03a5b9b438131047c16c5d0373334c826bcf14609e3a0641cdc3239b081e253c0622e9668894049941e03d3e2e09874c955c9ca0a982e5fed7be0aa019d1e5758211e4d21740b5ae20918c39dac2a95a2616a4a7b90fbdffba09fe15638dcf6f52436b3eb0fd4ed08b23aa09b4779420923d31d854724f79c50a790e5919a7ee221084c73cc400ddd7b677e7b7b72342af3740041f0d672c4f0c6db58d78583cc9d2f9599f40db116ba046b66c7a65c0a9991be737c9dd9943c7ffe9c733a1d2e5135c6727acc56a27b1f6a4052192472e4ddc5fca10ef62bc682cfbde56751267829b0a688d891ed02249c1ea71148d0eb41318cf57c4ede55974d457fb1e207c3cff83608d15fa073b85d433fbd25c61ce3adb41ae0bdc763308c2f681fb88824165eba3fcea0c0bc9f4e299195cafb55499b40531132bbd6045bc5c2f3a9096edf8079974d6a9cb69b2417d60a8a8ae096c5881e2f36bf6bff273d08c2c68489899ce12120b0c67fe343bea86eb262e258e223269b0a00399de25904ebdc6afeab8facc475dec154b738d2d0841f51ce84b96883eec1ef33c0c6e88af4125e365986fdaee60d6d807f7445d88ec8eeb2de80f066fa61208f67b1206016e251ed701f5556bc00fa08e2d51d95d52580ce4a2fa6a43ef9520365163bf91a0459239a7d996aa94684682ef57a70fefbe1dacffa36f155b91005531263030e6884f9066c491de52fe516e85abfc4557a5f0cd1972c4d0060023bf389ced3ac2de5a31221980b6bee427acca0d35cb2b310bed2fa1f662d0ff22c761a373013d19ec512031b97418bf337f04d8dce9c93938fb83449788a51e977769ce3a9388b12b20a5fdcbb9e77a1d6f194ba18b1bbeac7ada9df752ad91f846d71b1ff60d66668408f641bd175912b8e592357a59b94024051bd29bbfa64638dde693d005a26ac1ae4c653681444ac47bb5be629f8415ce38d72557717e50bfc66209936b6c9e64890ebcadfffe89839c6e21eaeb4dc3749f1e12e46d4a00c2f7d16fbba83ac2cdf39240de26d01aafcc4a014860ca1b8200a560f26815ece8353323a857b80792111195da94ffcb94d378a67a89f16463a601be2e37e1c1ca6fa67fec9515223e54f280beaaa60a8fc96499fce5d8471957d3c8f9f27133d88c0bd25e9d402360b3302f731da8e9e600c5fc09cfb7eddee52066107f386f974e08a3132f70a3f62f624bfd57db02f879f7b07c9f5fe4a5da308e70eb0f2013244259b74601b6ae3ddc8e7d249f92bf3dd5dd602f24ce475fa0e0ff39c14f310f380f24c2e186a89cb75af5e784c29f19196f28ff143a1149e1e2e732d535f4e4bc2817b986c8b1e7c4d5024b844a3a26972dfb1b27e6c9fcf149eed9ef895c6eabb5ec0543f0d8190218f3f3efccff80cb1c6925741173736db2b977c00d1fc3b1e8b0a286f7315015d6a7e691d6972ddcd8e9abbb684fae7ec1ac639fddc7a4b1361cef5e418096e7c0c3dcc0fe7d9c2ed5dac93a5bb18ec8e6d85fcfa4f969b9728722dedfa95f6cba76aa5e9d16422344caaf7245d2018638cb751419f9c7d220725ff61a45f109e0c8869b7d361b6e18b16d1337d9eba09665ae998f9ea8f48afc0b0efa3cdb9700baee4cd270ceb02da4d844407c80f5127467f8875a339171241eac7af1573674ad2d7d53bfb3380ae8a6141b0b7451d92e15dc6c5b0b6c61175a2cad126c1b2bc3b64105ec9b55c3624375ec365582657365d836a9847db34ad86caa8cdda64ab06daeac622fb8378de50946ce4d40745b8ac7a0683aafc67e03b609a2360ca73415331c4ee159e3c22d6742525763042506888894538d702c5a693679320a48f3a8b59fe58507bad0839096123c052198829e3407ca1aa8bb562c9b195958ef52dc3b12877ebda3e0dc0d0f3df106ff86f47bdcec3fe0f2e2da0fe07b7b4be0bf543456aec5893baa3878b3b90d17da7fd42861cea9f87cbb6029478087ef01732db68a99eebb8f1398eb07fc1105a3681a9b61ca05e550bc78ee2d2a5bf030dc51da12e59c51426ab618e10fb57dc5b89c983eab52294b54d97db4aba30d86497c9e2a4c57658b9ba8a70d038cad9c978aa7e7d4dbe7e9c590dab68412ec5a2e8cd80383d8924acd2e0fb9cca9a5c039a1ed6e75840eb65a8112dfc2fb855a18df5404e5f5d93f457c3787996cd63b70a460b5cbd3e0554a438d56a9fc0480f7a21b4341186753bf644c265a31614efc2bfbfe04efb11e9a593134794d5c6f7a440bb9b7202742a60b1d531643a2032f36aea9bac9749180af8989e13f3e33c06c98387aa33dcbcc0619f68aab41e9203173fad0c7c82c1cbe77b74990911b5370e6ea3fea7a22d3c61fa86cdbcb254264d6434e249fafa5537902b66ab871104037e80463543391698e200d2e32052d6c84b799a8f551b26d864c26718743ed7fe31db19740ec70e904b905d08092553bd18e55d377a24bb90a8cf16feb2e5174d083406bbc523ee9b250111fa66b0415eb80a9b9b461b653f6f96cfb0ceda6f5f4add5f7463400d685784ea307c7d52108f518ff493a4acd450d07828a79290168cd4fe6215aea3935c9729b218ae1a9e55c134b18dbd69b0ed9306b508231e9c9959ecfff66d8bf6b81ee4d6d1857f53c4c5a0a6a1982ce352b30d9c605f53b1990ce67dc08fe5efaa0c6a7ef759388c8b03c4904ec266aa0e11b58fe28531a723b8068a386d3ce81f0df127bb91da86c2e3750a2cd3fd8d7ff08900a915f607878f18b907ebf8ea5b3bb18e8162d0c6e07868558fbf4410fdb3a10fa1e00ec5a3c9af23a0409eed1eb42fbcb3c841a2fe71c5b3b4280e2e5ec7724ac9cd840461fba5f128a0f391b1756b3d20102866f183551da1804283c172a3805c97dd16a0ead50cd0c407246f6a0cba20b18c27a1fafdc04cbb69fb67f55d6f103667ca6d529b96ac0556da312d3016bd4d7ccc98285ed1fd3a46936609c85d31a134e5843319a339d7f4fb07199c41351ddb024266d2fbd25c19f478422eaea2d32c9dba31ceb672abd6e83ccb345dbabea2fcf62d6cd581cd2d10d811483282daaf1dd4f2e50f592a762ad6d82d0bdce32fecf970437a18bab945098fe49d73943bb6f78c5439827f2f2b3141000267cac3dbd2a6936d1485f26bbc9493a8c485f6bd16378b27c917248f28b381f0546acd1d95898f4ce8558cf6d1bbc87afe08231660a697fecda5bbce9fc51ccf4da356335d4a610a82bd8b2e552d4214eccb9236a241e06f4020f07ce0e06f7311ce80a793e6eede0e6e26a1e71aaccec5f0018d4b88adf5bb1fd49008b522554cc9b18ec72102358d0cb38b6e981f6958e636726c0681e20b74b9ba0ca34ecf361c2c1e113a964c71ff75b813b1e3384ecf8fca0c671f03a881c48760a60bd46787bbbe0945a6477103b9cb97e07c14676747f67c414e86c4f275525289c65e2007e1202320363cfc17ed20a5462090eae684f84d8e9c924ce4f617304fd812e1f503b5198d2c908bc31c566d11fb384fe26a8f0fc85722504809bcd1ee1e48eff1603d82dcf041f9c155fea0bd4d92d0a22e067f71bb0579b8a0daf30f8ab3049d07460879e5509f05805cb158905ea21334c801395d50dd22c04a6cc9c9ce3204643e73b02f2fab01322871aedbeb4602067dc6a2a4d693e3bb0fd9933c6ac1888f71411d37ef8fa47359d26bc8419d8ab12c9c78622b8a822ea4e8340c71f4cddfb43d948e42db0d6c82f53e83e5ffdbae07d2acb146e6005ca794f15ce02c3218518d92452527e15e4b695bc3dfffe1b07394bdd19149d938c54c159d830eabf9a8df7fa24905c2f5dab8b44f550910d01573e2bfbf0461b15bdae99e9ceac3fb8f1543f03176d0ca99a3cbd44c721d0a1302ae3882e669071282e7c6108cc147b0829756f8ec20f9246fe05322240f10d7a839ac2bfe0a947a9f9d1bf18f820d620dc5daf3bc450fbb7e9be61216c579ff016e4e42a43f23a818d739c7e79afee02e1130f301e86ed2a79f94874952d78223c849d0ec984b1a773ac9f4d3ba7a7e269d8badcb1e5a193769edcf4e887c5fcaf59d4270815f868940c229c212b2881a21042b77dd0d7934193bcb128bc77b582118046391ad8ab78788503c0e2a6db2db8fd4c4f396e3805f599768a619269cfcbcaff760e634d5c00a371c20ec64176a87d32e457d259272bc2018d2d3648f78b0bf08f089ebe10b557f4c26b67df315db67c66fc32c06d994f935fafa3c30c0eaada8a6c21d51374f26860ccd215eae930c1cb181074d2c60fc0180048ea1673d19cc36b794dd538e9c24246ff8823bf4f673091365332aee04231abf94a5f3036c1b1f153ff7cdf3ae94a8d9b07caaeccc225adde09599c0a15371d7e15cfa1c0eea6d52a04b583e1424487e21a31c171ed028d38be88c39e5958e10ffb95bbd533319e41cc5ac5adf44af0cea9adb054300ad936b5e1c2bf2f8856ec67b9ca0f57f76c6d76ca3c8b2750129103a24a9a6dcc4035213f4a1ecffea125ae48e8e0fb6a97ecbc13cbd773b6688085d9aa4a833b70feec2f102a61c929b3309c7c123b6969cd724195b635a1d630890816e8c657ee40e8dc249e544095eb85c87d054a25a0d7244462eb78267a820e36a24aa05643b78f3e638246e2040e28f1afad42f927114e134bab0add054335ea334307f2560af727ad12219a67f0a465617de944363c848c2ebc8d0ace2486027e2b57044bbf45c51dfe93996ee12e06947931cdc50b4aa35a933a0c21895a81ff0bd9680d163409d0ff0f43a3d92c2775a02b04bcbec561e449329dd9d4a938b47298ec29bf99262085e54d239ef5ec0c249275327e29db483a4b636a7d353cc0a68245e4282dc9f8f9f52fbd85498c64a28cdefd2cd5799e7726e22b0e3a5485e1b329cd766797a26aa704b1d5f43afd5a1058984010d8160d61d1e48b62ccd5980f41cc2e9677b05b3bc458969afb7f4c88c5be28c126137d5fab7f0832411a3b189cbec12dfb72eedbe21ba9c4a56209e106425a90f7771c7d820c3b2ab4c8a826dd3e1bcdc224cb153d7185c096c2584621b7fb438558c77c54e1b6957cb8426c1d578e7ec1111968621d27adf2cbf0d2e185c29f875abf5ddd8617ae1050c70d3db4c40271169851ab15a50f7a6596bbaf9a42086aa46fac3cdaa6e070a451f1cd3f0224de412bc13810037dd35a5606924b1f17cc41f08f658be6240502518881ea976dfd3e5c2aebc8600f7e4de5a823803b1d1322ff83280de3ba8b14a1b3ad391c9f5949faa91349378a4494c3af8786a2d0d4d751915666bfbc6ed4215fdb848408f6ce4f3ab3155572192bd608f2ab1f98acb3d3e7ddb8c1afb067048ba733e8137f1dfc45868bb056b18ab2a2a5e320d8cf9065ff6101b8f5b6875c06b5740638ef8da9f2aa6c7d2416269ff879e0f9e34d2677443660e5c23ed66789c9a9c2c818ef6ef33f1d0ce677299749474bba164f4157ba691c686070289b71693173466ab8f08fd4e48ce008cd97b442e2d700494354b69102ee48d031fd3195968aa1674abe5f8e17c0a3bf4a02868da49a431a368fc06752075db41e7de9bc7b8cd17ef433fc44c3f3b686c59d6749a4a098879a278ca2ec084072f7ffd0578d8d0f74c6467955967ced539d6a8e7141f803730888ed7ab994abac4f306feb616ddec30022c465cfbd8d350415b3cf44820489c6490122f023eb4863e5392a4352570d24c624dbfd11be696152e43a823ad2f3f623155fd8d3d46e9b220a3db7b4703e20480035dd2e01619413fb802a904a719bb2b76698e0045074d0968e12fb8af824b0f8a65cd023dc0a03b27b30765e9639a023a89d2382865204efbe3994c0509e429cac7f7562cb0b44a342d644a4c74c9346028954330a8c22920982f4f9ce037ed286903e9545159baaba037efba2b067f6e08579c43c7fb595cb3301cb764775a8ffef1be6208ae591ce9cbb55640a34b37185a2228c2a8d279f5a21948f4c37480b147e5c8f6d4c9095358b94a0b38bc46b100490b587122f91c5dfc3e9013943f6324e11458fc9f590ab61278f00ad158e71d49ae62a7307659993a0029de24c4df25cdd65ce9951629be1b3cdc66c624580629012c49328dd7a41749012f9955719b8327855bebd398690ec70716e9bdfb498b1ebbe86af5d83ac6f0a30908967df9fa132eef9f8e4ca5a22032179deb67a005329775959c8404caa26bc7febe7b6623b86b1fd2e8cd9ba7d3059caa684bf2080d5614464b092167c9068364dc19e500100b13d83b8b78d8fe72f1a5c95c91b52a400f908181211a4703ee61260cc049c77c78d0e72901e60085ffd9cc6d3b6fcf8d5275e698e767a2b61fc5ef6a0fc67c1c669c82a22a60969dc8312b9fb17597237ded7c83116ee1c7a392ca3cbd9f7b49808b3e259a0666164738fe8f0a8fcdd01be398072feb46cac9c48f3461ed2914d40e8ad81912452f4aec554b8cd0fd3edc2df086ff01313ce668d348d1907e7561ca140ed57970531271a00d48121399de5bf0afbecb7165780b512b4fa6d692ce270d11195e6fe9b418bd992abc57dc66c985adc95a28710b5f87776374875763352f1333101530f60a16798f7986b2f30cfc8cd719e9e21f2e0acba3bd475e84950bfe3564ab2f724485e82445cfb1ac7d84109743157b43351d730cec4f41047b97da1c37a740af10cc2e6c8be1bf34f26f0dd3aa532007849e55a0d1f9826756f0bd0321d11e77360accadf03cb293bf8380e86f646346002b1e8f028eca5998bff149e85723ac750624855222008af703761ea40e089a4e1d89624f1fc77878bf3f10e619752f0c12cb42f860cea2b74e54836801216b9cb73b371e92e8e402ac84995845332d38d5ee149072ed1e479196c6d08bfaa5f0e217a2688ff6198dcc71f2102418e40c4770bf97de532851f5e82eb6be948c4e5dad4c6850557c8c8d78a24a8228ffacaa3587a1812517f5e1d7821385b7767d856a7d5c06c8541ffe8767e41e908ac4cf85c3d1ec57137029d7356710553a78572905c6fb93af602b300bf25a4ed263a7a678a2e1ed998c9d193652cd6bd650e9441986c730b3bc2c242d0de7b47e7684452be2ffafc875db1a9dff09a764393693bf257c3d1266d616d0fc82fe2c2a92d05a89190cafd7bae474ac89b11f014488cfa5a8ae2ba4e896173434b773357bd8b688838c253fd892053e4938decfe10f48177fdbfaed7496d35156deba97ebd60491fb4a3173028feda6d2b2aa982250a199efea68b7bd01e96c4a977f51fbf6d300fa517f90f29905c8d19db68c8bdce94bbf1055011b055235edccac9783ec284bf72aff311e6fd952b94a1d269d8adbd0ba8d9ce505aa3b0e4793a277911f70984374dbdf882534c9871a06f28558220e1a3eb927a3960d1ffe4850f9b542b7c6406ae8881d9f34e4256a26ed585012a01870b7a88e5561d844b352076528f98edf4c8034577ff932116314eda62bf89c59af1b12d026ed7c80c7305f1f976ace3461e5f24c2357d92ae1469caf49e2e9883637f53445247375698ef3e3633b2faf318f7eb45034b9e816e4c5f60e58877202038e6c75f1b8a97d278d4872d78b9e4b7ba543081b06572692395050099a53f4d117d7099587bb79df443eb2c93bf2929c456d2642685a039669e87fcb58851a49a71b57d3bf2f389a22d5b0263510f9665f7642487c4152f5c70f10b12afa032e8c8042ce8a0bef7abac6a858d6aedd42987b521440d4feb5b2dbcc261860f3ca895a02f4190b2808a7a2413e0cea51a699f4a4f06133cd187f4bafb954f7a2805bf8c64c55bd8a7f89aac2cb04511f08881236c7ef42655a33277b276ec68a7f15cd2bed24cbc3f71b3c69daeed88c0310c7929356da9cbd945bfe1974da8faced9e662261694e4642271d94ada290af1b830983cada90fa48dc28b45999abb4aea868ffc4336d794e58cde74abf6f0f3c9e038f070cb536e883516a43e3a94337543dc803e37cd008569acc2e69a50c4d45286ebe9c28822d7f2cedbe77aec2e987e610ea4c8b33f97e0e1772661707aaa592b93210eef92a54d76c0f8c88bd868134ef800dc70a57954401f005707578e92226812f46332873a352a581bd8730bb20f55267e7e7eaa37f531172c04b85bca35de0ba4da58062045cdce95996f79e554b93f801b7f8d55e013accef3781636df77153b14422b6e5ec47b56e0d78d41a85cf1de056538b18c5e5d7265b7942b19ed8fa0d0a749d0a6c0c69a0cc10d2c242857948402571a72204bec1c093f892bec89f670f7f5bd27e45e827ac5a9882228dc114a6e2c64ec53dce860680047a646e56014fb378f153b0926165d6af8079b61955c96ab058609ce057fa54915fe7395f5bd919a2821460188668bfbff8a15418136d519281fd302b6416fa5c026a33c43306137d5f69d34d07f67968941bb65841104b0d099dfc70311c7d97e44ad173183c493df8b7c6871f5f77a7e00bc7466fca822785ae49c75a6a9c5ddf1dae87322c435c9bc51f7c1bc37f01b6c8b6f9c0ae194fb87cd6425ef26d04d5911261bbd9bde114632d12ab5b282e508fe761d47e44a4f88733227b86cca836f003e7a8d4048d47392b9c0b8b109c43f5498f0489a7ef3157290265df8041c7a2afe272944e11ea8e78e07f6dc84fd1ee636e786e9364c7ee90e8537c5aef71a3b015e45a29182c03d550474f37b23b8608563765f424b251bc82f8a33c6321073dd1512d5960b75c348b62aa885c9045035746a751ade8f0eb8780c2c6852b6bc664f5a0f8dff944eb418cd83c6ae71ec15db2dad491f8854556f1ed740746f4dfb766cc9bbf316d7bfef97947b79a1eeb4ebf4e3a01f3f6c41fca4069e1e07db32571c6748ff7ba681937dbcbbf540ea325542baac273903db71659be1ab9e734994407b9a1bb79b18b26feff21c46ac5a579c4c3ac0e1ec96846e38458e59662b5109fe2a5b232f811d6842e9714098b45d1bc55e4c0c23b5f6a8a97f35fe2780711c7e5c9833899bf9a49850431061bf542526e6c0b4c5b12997325683480030ee52279c72bc497ba24fe971626960db94e3c3ff4dd30464f303da096b975930d619a5397da2a6c93631aff96b0fae386697de9eff0785d5db6e6336b8c2ca61d770444f367a22e2e11336cb3328120605ec78da6b379fe3799d550674c830496d54e15d9ff74919f6614361082db1b61be9464d4444880cba0951092f09f2f14d718f53d1fba8368b4ff22e9f74c4cbeb58f8a463fdba75a77a350281d9fdba39d066fe58300c9fde7bce144e8a9e945d4a87c875ecc4e3c2b00eb499d7ad72e271cd5f1807dacc2b056da6c3164e3da199a40601e2b66479f7c26085401b3fd08675f91eda864debb40d5a9643262a582764f9cd2609593ed536288e94584a6c45c15e9fb77e401b49e9cd0204b491f751f9a8aacbe014ecc30e7fdd86367de281fdfa89c796ca4de712f7e2106ad77477bfad4691d8ce271d775276d97ada0d1d22f41746419bf93b4f71439b49e114ac8d7797df68ea55916573fee43e0abb681c765da6fdebbae055b1bb62a677f3a152bb71ed703177dfb86fd7bd6e5def877ace6e5dd775e12dc315c787b222c56e6b372c1df4f3a4939ab7700e0f35635083f47968763fa71dfad65f86fd438a06c2068536f2f20b4aa56c2904b4818167823672a8a538168b54c9148e109e87923f4f10438924e82a891319238346eed50980b2ad0201b0c85913ba0b907b95440a4c58c956497430937be5c4916ccabd62420a117a0293094450a2a604544a30c5d2627bc584931260c9ef9d574c2c31d1f0783f2aebd17a130bcf5fb9c3f372c444c0dc7fc9aded4080dcb88907e24d840f6ed9d2dd5bb6a86693f9d62680836ae28711456d68e389cbf2d60da223ea88b179b89f0bbbb6898f1db8ee9318a38b310261e409109cbcc303333d2afba8aed7d674c0e3cdcd224f80e0e446808f96c3273f552c628b2ff016d4e0adeb3e21e25910422a451413f13a32a152fbddc4eadc9d7beeea883f0e12f11c0e32dbc1cb8c8867fd42177da67b588c80ca9453ce39691eaa9d8410bbb7959a6865b110531cdd15fbde5a0e9c142143fc28234b531fd1c78f1f437416d3103ee08d997601acd320e0e36e4f96f6a11ee44116bb447640d0464e952a9b0907c7f45e1182b81801922fa21746f00305db3dc3cb12c613eceb19dc8114915d845e80574c31520763843042d74eb0dd33bc07b3084d81b02184104208db0b28022699161100497894eb87df04bb99860086162d43ef870a3605e4869c9625b05022a06894b1449029d55d83161df4b2e8a498e4771a7274b8d241a9772116ec89870eb479341e0a06a89c37b44519f61d5681f61246f27bd4428def9a2d2eb1c02005ebbebd25a9254b52a9a884a670a8a4a00a0e419b11d80d85a211976cc9d9707094c092df1980d341cec18b2282dd70dec329704a0459085150b657e54991f26e7aa5c413f9fd4d794bde4dbf286fc99362ddbb667b5690003dd4335d3fb056601193e98bfc4e834a3100756a22adc0010a1a71848b58c3fbc5034bf350c7b6605d032145869ba82257cd3bc7d3a9398f70d03a9979a2a347d8d4434181281af1a1da2ad1449ec4bbf7834a7d4d9bc5a6877a2f76c7a646d8cd66d41124f99d062a3bcd5d53f636d4abb0dd1375d7c029540edbab134cc9bd85dc2b1350c9389486608cfc9e5147f32dec09beb3cdacd81bc14a41994c21a031b91f782608a9208cc20521a88213857408014e0e4ebfc855a817a58b589a1e17cfcaabe1bdc213b824767b559e9577e332aacae302c2c06e514808da784ee8371baf689bf72cf2dbc2c621e4f7aa48e91a5109acc26ea9140d3f548a25100a94102059e224ca92110c8a2fb0db7392b77bdd73e29e4079e30705bf29a6e81aefcf0913ec06a1408150ec7b82c97302f45051ec46f3cb60904ca212791265f7e516eb9c730ec3eb40f350af86f717066b75904fe5f0e6c30de53003db5936b401b24f0727716a4be55419f99d06d39f16bb7510cdb379a62ed82d8593b2a51fa1fcdea385be6b9e0f082184725ad39ad69c3d0ff5f36a78a75fb01b8d485816d8788435429def9a4d5a815f3c9bf75786dddc105c8195522414d8877883550e8786c68059e01850c80d61b9f250cf8aab026dc8c69b1312c29bcb92a1d090fc7fa41526370d25bf476b7ad0f46a7887b70486ddba0a092816bb592b0f09d5a9b27ce1a0730e3a175bb66cd9dd8d5d96cd6579376164f9794bf27bc9fd3823eee788dd7a8c9e77e37edcfbe686fe93fda4a03a55d76cef094bbb8cbc39254fc9cac812d452cd0afbfe7ef2e6949cdcfb4f48228b15ca5f582551e559216fd988c893f8aed950358f80ceab011ecec042fc002d43ef6635022e32cfcb1c8899921ea0e58b877a8096301eca65af8b29b9f303b820f46e7a558425b9f303b484f16e207e809630605063c9ddf07e66a008ec96c2d91ea6e1a120b46171e00fb5581a1258978386dc9a049ebd92bbc165f82094890b470515e7fa653fdddd4d3b76777777777777773784738b6efce094f326195de36e29a5bc393c947439d3c52ade122647d53cd445c55aa734dc1faa5e0ee6bc20313b777d08f7cb3928b26de4b4737fc00f6ee126d969f62a5b3a28da7cf21fcad53c1b77cb08f6d577aed9cdbf9b79e7be604bb9b5206389cdba1cfd8d4667afc6764f94b32a7225cfd728a77535cdac8418066f9098b7ce422f2cb3eed64343748dbb1f6a7516f7a381b601440ff10cf44d54c2154c78909d1715e8be7660b70ecac9c16537b2e42abd1b2ad35f7743e5ebd9b26cb6e8e88aeeb35bef341cd7b1e7c07a27769f76e37a6bd6354f6f9daff338d9d11c2f8d37c91adf12b2d796e0960cc944f98603b676a30f9d7babda7d234f2184d24519f1ed87dd107e721dc137846f54ccb41d6086308bcee7273b2d4ee5e090045a0577005d3f11bd3a90e3e1a4104227852ca114b542c4bdfb892cf4f3f36364eb2e64294b9a2cdf5b8dd6842c1fe5e9115364b93a424b96486891e55124c8f231c61919beafc8f0efe6f96937dc10b55db0a8c0f20ccd1060fa00796b2ec090451361a04186ff11196e3142864ffd4091e10f1732d4820b4f64b83242891bd008b4020719420861a408d0020c4057e477cb5e618329929835652479111a012184b00a2a8ec419a490df6184102e000c49604106464c0108220023432b969021bc0e841046047ca18afcee200f30c61001158e58a14a132a58052bc480105e54ec174a5093dfcb5240ed755d7449cc6c1244ad9031c06205c114a1189a52b0a014d20ada8bc10fb4694a06199993284ea668f18522350a28514829b2028191ec0a2f6c965b8b2d786121b134606cc185241e00829f15112bba70831c73af885091a2810118e6e2927b9b99c98db10dccd9bc37cd4a47b0259a5e22872c450303acfd3b48e7ddfc1002881c1d22c003033e84f4748c400ed08608673ae4e410d47fee0271193bbc14c358d76042d6ac562e2cf14295f76a667a375b67b1251a2f20c9d9312bb3b41bf2cfddb81b1ce6506e868b647aa81decd9370cc359cc8e3df56e36ab9333061944f23b0d1a7288001016b83039bb0ad4b25bbf6e869b888ebd42ed06f63011ce32dc0f55f14651d76a5e7d34044e6888218618e2dd08f16ee4bcc30e0a162c562010d08633d4f64a1536428102030e6f77a3b9be46adc7cb5d5c70b8b77db5b75e300eb4913b4cfb1c6887c0c043c991e8ceabd64a29a53ef9ba9e3da3b9bb602f21591644ade9a854760e8890ac6d45643f786475b9381905c2c56cc3aacb3297b9cc652ecb5c96594ec86635bcf9381d1eef06bb843ab092322c7b6b2e7bf4f1ede0689665aed667599665599665d9736025d55f35bb39e8f0509292b08ab7972beabab0bb48897dc149e0a0229de4e2f22257b9d422323da52457dd25bda10db526975d19cee102b13276511dc8b0f23fde8d65e1e3cc17302ae95d53db4ab68eb2980c651ab9574ca8e49f6cfd02a217a3f68ea0d01c4d5d6e8f4b767bb5572a5f3a749ff3a61ef0a597956eab9e7643a7f4faea2823dd2b8b2d3dbe7f80777925693d485a0f9f5e92e92d0783a0ac65f59a1e6afe12c3d66b5a0e5bb1a9e7e558a4f5289d7491d6c347457a1d654efba174d29de683d643c9621e98add7934e61b409c0c33cc6645203021e0646badcd25bd3293d469b007ce998e643ab64c987db13eff2bfdc56b90489795e7435fb824ca47bbde298e9afeb0f5bbf30112e5b39a8834f64e156bd3e3aa3e4eb1109d47ed07af8a8464cf2751d108844a2e73ca7e9881eb51f5a357ad480107d241a391c99f6fa6e5fc1be6b3d46171d6a3d7c54a257983d0d473f94f6874577f42a351d108c8ec48fd1486774a7f9e054a23b0d88910fad1a617f1ade7c6c3cd8cdfaa4b4114bbd4eeb1839be322192e51dbc85adbbf1c46cbdf12d5a371f99949665c9a943843c1845ed6e2e6b57b2bcfb22673b5e7c36f1a22436091c54a28be29fb6c3bac3f56f4776cd59f9bdfe85e9454e588a3721134e9a25060223a4a2ce28393b6592477748b23d8d96e572f7727705eb9ccb7db90fca36914a0f2545871a0f0e8fb2d34dfb457fd93bbaa2ab6515bb9b8fcc599da735b51789d427718c44327d7cf514dac85eeb63bd7d055b9f23d3706098275a371a6f3f72b79b01763c96d841f807c6957cb5675fe4f83846befe6eb26fb1e52b949ac693ddc6dab1775d5cb0f1ee27d27878258ab35e7dcdb2ac62792d8b32c9f1b371cfbc3e219e1876ad5ff76e3eb259e7eb84906618851a0f71cc4724ee87103a3950fa12488795b18a7baabcda6120d60d3821c49b900cdf6b0dc607310feceb4520d42e809d2df8e274a359ae5c4024c7f718395ed2d31331f222575d58e7f153489e6febcaf7b3ac28234c4d5922c402a15c4727f9f9213d4f26260225f6a15520b0ae5df421a23a9a17e11ed1273ecdf920604ee7249df422543e41bc5818dc2411be158d44dffe22543eaad11d8944a2d145e731ba6824128df0909c524a34bafd961a59d1097af9e8435c6e5fc2435e3ebabd3d4f78d88f462e1f3d8897977eeac17e741ef6233b7a6914840b1ef203dab8c33c88172cc488dee5f65b0b2571c1a225863ce151bafd9097bb1c060f29dd7e732eb7e709cc5df049d5637f9238c8a98797db07513acc4f3db8dc9e87cb2dee7922df33ef728979d8c3e01e7afbf9530f2fb8a18d0b1e52c4fccb5dcea3841dd00bfe9977d1a9a737bb8b3be9ba25445c10944ba4a15b223ae99b83a284eda078fb91bbeed217dd21a283c047a57d7441e0a312696fd5e80e996f15e9b64a44baadb257fbe8071fd5d46e124e28f63b8b94b08d9b9c24d02aec3d1da4f2c0750f60c7f0a96fcd0b9f1afbd02aebf343e6351d22d6a7b5ee909095d47b7a9ef0a09fb8679ef658d484aa82c5598d81f47dd6473ecd09b96a01951cbff31e5b7025d347fac63eaf1df49775eceef0bc7c956137abd21be7dd5cc49e03b17be167ec47c67832f64d48c6def35bcffc16e73c1021395ad3caf1f069f34d6f6bef31b3b46983cb9eb6e33d66091f8c5de81be407d1b3fbd0aa4b693a45894f11f7d070864f129fe47553a6529eac6bcf1eefc9c23d34dc9f979707468ab71ff97acf8b5415bf7709454aac082781832a7ba6692ff2ee2272d39b03a5902c3b8c713ec3c94d04cc8d3721f9c22ecb6df3c9927696f563ccb2169263f6225765b89d02faadca81b207028156775f4c2e25d8f766b5dc3445dbaa930ec5ed14e0de2af81c08ef3c909e1c77c0085f9cd1c13b0895607f28bce22e2fd07793d15f126f0fd2d07835bc37c1e875cb3a10971d06e2628cd9f505bb75900e39b4c77f7e300cf7d374306959267ca2163e51dca3620c9f2c6ce980f47a3d767a4f17ee513176770a0462775eb765cb9630e4f876f289e2371c68e34d309e28e3092239d21f59c5c1eb08d3b22cfb5057ac674d2b7032a7e5581004cbb22c2dd0a6f39c486081890552a42d99562ba822cbdcab1540c99bcdae0417111ea002285b90a2ad54b003ed2ac21537c610524358a5e0ca0a055d5c5cac9e30028221ae524006bc82b04a0118d9cabd4ac11717106a2e20747109c112caf0f4fdbc1bf98cbc9b386444669143320c2bec0ba0b32094f2cfb58d2ce3dd384b08ea00be18217cdd475ca1d6b480ec957be5841154b056ee951232564ac46802862c827c40852fb8a8c269610926c896c002975932596b89b004237309239048564aacb052a2844ccabd52b20518de7bef169672f3d132c677fb8c502ba531d7f7de7b1242e860ccdb7b4da10d843473ce394ca711d68a5289fa0add7b19eca60f155fc386dddd0fe520958aba0942082184d67577141b5001450653c4c0891550302979a1c6ee86b0fbbd6ff1e63d481fea3df8beb9191a16a19b0b2951ae2cac3b9ca285d035fa403ea8c2032a5a188911de309f9b114608b10b9a7349ff25794dd820748dbe0ee6bc61c67e281963c46e8a9ba19ff3208410c6484d312ee83a279ed79c4fbb71514ab75286bfdec4187901b9574a8072135f34d1c58d2c470f562ff851c12b4ab0d85b0b023b3482d574d03c9fdd7a5b358fd8cd3ed4a5ca317174a3b6f9791ff250e2f86a2c26d877107d33094a6715abe55e355145b6ac979e6559968551446c967bd544141906a69a7bd5040ac200c011244e9050c60d9a80210b4e80e10416ae14982089125491832dc050061650908445411327cbb22c0b0554c8ac8924326f34564d2011e55e355124bfe45e9d400b797baa491dc2d2521c94763f42d6166b8b18b56be0bba10d392964628f934ae990c3c402a1036d9e535d4cacbb7b4e8b95cb0955672dea8412b47022090698d1c509b820758d8eb66bf4e994244c34c25416a460032834c0420d86bc70435578a1c41340de4a48c64072c5e41cd60527487290c4912653dc1560505748ae38e5c7c50a054ab24cee150aa06801c6179926b71660fcccd7f773695fd39d24d02a13b4991725b1adb2f08ff5eb143fd940104238034731c2907f0e8cb9a4c35cd14bd73af6973bbada754ede6d1c7b6da89b6d861c4b6dc3bb7179d37c1c68e307b48977db0c28d40ff196cad785af1d19a318960bde52143e88ca3095a1c453dea5f294b7ee9eb9ec97bbfc75a2b73e9d409b38c30c28d46f32599be3f549030d5b2adb6b27ddd67264dfb1b8ded27664397348b0b172ab441af2f511165d2dbb5ea318ee86a18cdd96534a29a985a3186e86494f0e00588491b33c1f24254aae21f7ca0665e4950db858d980c9ca065dd48b6872431b2e6dc568241a8946a291188dd87a248eaaa659bc458cc61aa39168241a894fc468c568c421893b313aa06f17d0ac98e7650bf3bcedc23c6f6a9731c6f88937176334e26e8831c68d04b413278d246ce98c369bf1c8915ae5cc31c623d1c8bb2ed76ef679e547f78a1808e7369e471dc5f04e7c3dfd2de5eb7676474435f2bc9df86256b13be266700e9f2c77c4dd4031edca3059dd91234780628c31c623ce47f4ecaaf6bba6df79732b08421555302417c09848614ca45655405945b9023bc1f4429eab2a822866022a98d4c0c99521d55911c2eeb410f236dfefecb2bb61dffb928a27f27b56926d25fbe4da9a8eb692b70e429e5307daf410425c20a8dc52b949f66d62f8b839285b0be107b4e96322b0dd98aaefa47495ae6e897382454ed8b6927528765d3b5ff4a26224a8d9e08f4fae2e33e1d8d486ca4a0edf4ae4dfd5117311b803cc2ebf0bb3cc450207d51000502b325582a00d23f7343782dd10b481a7a174065b18f3e5423a763721192b7d7b4a7672b4b673dd27bf5479695eccebbb3bf19deb56448ecf7e83f090e134e74180cbbc6570cf139a9f4e002c43cf09f77c73c772263acc45ef1c237acc458f39e9103dbbe8d90d202e7f7b581c36bdb51b339ef34c772b7d73437747be33de8a08c243e6319c870c97791284c7e932972106dc78880cee91f9890637f677657680e0b0654b18f24907cceb615edfb9760f323b99db2a37e30271d9e1938efa98d7c7b8dc1ee602715bb64cedc649477df6fa0c1301f34e09f3c07c1d95a57663f4a705da101a1262288b0cee79c243e6348734b847063e00df5aa8e65b0f09e09bfb510de03297e6ee0621731adcf3cb7ce6f6f8a866eeb4530f34977910320fc22d8136f0a78b92d8996f6ec9100745946406bb2007e59779d00e313bbc15b143e687372b88ff055f1f556fa25f58bb8bc544c0bca1f2cebbfbe8ea9859fb00ae13c07558a08d10b4810fc075bf5848dedcb6e31efca345ee4f87328733877f46a08d180e6538bce86e31dc9e185ee49e0766f799db33f322f745ee69ee95b94664fecf5c2332a7790cd788cc67aedd1d19ae119937b471a7b90d6d78e0ccc532f749c44070c80eefd077503eb421b1e0772b1238a8681ec31bd7cf5c86634668f01000bc5532ff33178b2081830a00a7a1224a7e375980320ce37dd1b0a32c116549ae323bb431448637cdfbdda777c7801fc365c06d6970ea39d0069ee631609a4ad2d04606ebc0990f2962e627fc337399cb9c66066a3920101c32c54500f0189e611ce4e5f72200f8ccb7ebdbc3828b00e032c7f0f630113353dc0400d803325c5a78192e1133cfe0863298526b3b8b9db9d3e434737939f3666868b009dafce41a6f07e5d3f540439bd3f5c0cc0ca6d0e627cf600b6d5c6eed868ecc95c1a6fb7ef2c827e6c98851dd185a9c76c3444518f9bd86067d2376444e6125832ee4849ff33eb9b3c683db921dce348c887dee866a6554b68611b1316f5976f9c9b681fea1ecbb917797da0e70870fe4a923e69d79225ea6af57e6341d303a28ca12038199c65bf3f07606f5bd78896f62f71bf59f7befdd64b40668406414e3c39b4f96b9efe206a3b6837c7cc4dbfb0ec4563e265632e062f5e408d811577aae9efc20cff9d29b3fb69fd0e0491498115bb67481257b945a5c6473ce39a9502d1e0e2c2dde9c734e9a7b35c5915a8395144bac6280e4da42121c68f12ccbb2ac28518aa34e060f4a744c2c29a07b225b2b295a802f2baca4c882145f20a1be3ce66e3d39e655becabb915ea39575e63c48d248ef77f383f6a8f1b0915e493726e675da9d976b3f740f4bd360ec48bee0cd27bfbc3f8a81d9fa2f42ab2896e4f819ab288072bc8b15a9cbcce38c474c33d1654c8f7447fbab76a38e981da46b9777746b1cc14cbb5bcc9ab3b72f77b76f8d876ab5bfdc98bbd50888fe7237ba864548ece8f2f325e6237752143f99f4ccc9508e9ff16a65a4ab5ddeb843e9da49577badafda253d5e988feecbabebf1629ef35e1e83eda5c6030cde7a727ccca3c6c308f764d263ee566f699f9a0e2e8b4eba1be9252c42624998e765ed7187e81ade11fd05ef6427bd5e9e974923ccf3b228236533332e4f9231b9ccc09bcb2e77199df44a7219914855a47d74928b6844ba56499152ed0a8356a7e9c0b0c3dcad27c3bcb51ba4587a39e935e2c81183379f1c43fae8231afb9c17351e228e79f497e7400ab4ea04f3989b95aec557a9fdd0d3966eef341e4a2f6fd1354bd381496d07f77aa9bd3cd4cbc6019ffcf298eff0bc1cf3adf43ee9a3db0f457a39cccbda6b6b60e5c44aae972f787bf9e5f6a3bb5121f974d2fbd908c155a39f6e3f1b18d2dd4abf6e2f102be64fc3e1720c26c265ed2fb733cc2d5da8e11d295f2dde7cb2bd76b79eacd95708a3d348d7ec739edcd15e493120924797af56a6dd7aec7676f23e6a3c90481f9de69e9e03e5a5af34b3cf79da7db94ead07cd4f9f5a0f1fd5e93fd0fc74a27979ce832fb81fca056f9107921dbd3fc25886b1eec90d210f343dd3139d1f0d2f3ae1efbd780d3d58e18b19059ef3dca1abf0c207a9d6c33a3d8516a638226e2ca618e309509e2fbbebc2271d24e8cfcfa9071f247caef758247ec4672352622f4c75e11c38dd7c7f5218037bb6f1e4f9a697198675508c52bb61bdcf7b089b3e9fcb4122f0091716564d943c30dc61ea020352c884f71e1860bcf71c64021458322af70a0a281a7e962646f275ed3dad47abb464f937c29b903cfabbdd2412e99144ba25611f9c0a8adeed6cf1363ad5de5754facbe3df312cba288985d8e530df6c7e1b0f1e611f59769113367b67515f91f5728b5cf572985be42af8d2cb0b1e026f5d2e8477d35d62d19b3da85d23793c94d49959463320100aa04c4f97e4eb17d470d4bb65b9e270bfae2c96749c7feb20775aefc42f77483c49c260a842f193b5d3571df3f647ae3b34ecf496c643e3c7132bde7c643b4677b74279beaff7b41e3eae62596b3ae6033bc0a20af3074c80d102106c410228981ca1e4b2c208d48bd516506cb104cb120421276421841054b0415617c250454b9523aaf08cd8c208f5629425572634a1094de841141e4ce9a20754a0125c72af7af0831ef4e0ca1292908419f4a56df429bb1d0c51e902127e1c210a4c5e10654a184fde0f0f7e4646e041923c73af784002318e70e54a2a9502420a0b8216596044a0324544c507f407548870613fa052853ccabda2b2042dae64d1451174a0c517910b2a4ee42bf78a4a0dac5368e33d7e3355f8ee57acf6e8eff63ce191d55a7f611ea243dce3b487ad5d221ec3f0b5f69bf11898cf38cc7b9ef028fddd3917ef7e611e2613d6c13d4f78c4dce19e14b4b11ef3193806f7e46018dcd353c2f5a32bba7633dcf3e4d403f67a1ed82bee7972bd273bf60b3b207787b38a1feea918e8dde19e877b7e7a9ef0c8ee708f10d0c67af69edbb7cab5d0fbe67e5496cb5b958d6eabe87db93d4f78d4bbe01ea7aa1f9d74ed1df2844786dd9221d945494650a0401beba225ec94181e22dfd90d1d22efab2fa8fce493cebc045a353f01a792aaaeb1f08e0b82cf8ac20735353ef0410af3810fa20ca31304b98accc9828b6cbde13b27370760ad55e6c29b2efc8b162fcc13d9b24ebf311343b778cc5e594cc9d6611653e4ec71e6f1ce38bca31dd77daa96c33accaf0b7397bb93dde5f66ed9ed5f5e5ebaa79977369d924eef69e632ef5c5fef6987eca7673fe1d30cc6e1934b3fed2073d2654e3a89e6f05ef15ea65bfa8cbb897e3de6eeb8bc738519bcd761f096fd2add175cf350f23497ee5cc72ece43c98fee8e08f74365d7eee693337987df093b8db76211ab873c435810ec3c3357a11c718dc7aeae716910f0c182c6f5a9f140ff1c8883521cffe483bc198f3651ab9b71526a2afd5dc6bb18638c12c6c30b048796497ac6a12c97f4676761e31010901509945d90d46ef404925246a0188f00492019c3708590142af95df8cd070d9d63a431c622b2dc7c1efcc4efc230f27b62cca0295840c9f5b967efe6dd3814df8d43f0fddd6c3e0fd7a05eb5e4f2763116d86b068b82c60146ae7a0e7887f53ed96b8625f0ae619f7cdd8ddeec6ed7a575ea93b1db58b23be26690cf8113c75f6154bb6db0e2afa537b9e91a06179d34cbf4e5199f03b09b2cc4302aecf676d7868687c25e87f410c643f84aa61658f7ce9b7597852974980c284139d9d3c139e79ccbc105f6478e1c1cd9214ece6e66c58a151d28c5f066adc9f45b7843a15242a2fbf381935d986cc1044e4fd778c7a6b0a5877a3ab09bc9f4de734ab03eeddbbdeba64b820ae9b1f006cfc3c4ba1f39d06579de92c5acf80f1e21319a1e0ab3c26eb722543b03b657582cc987efe64d1cf3560d97838eb641d43638ebf0f4ee583731e7a11c10b725d3c84f179a77339164f9d30ef4b2043597cfbbd16ff322ebdd35623ccc463fa364a903265636b51b2ea7d79e66f479d75d63b3beedd076d4535cb5ac06874f269de6dd44edf19e5cfe9283960e73b1d2eb4bf8e4f2d30e2fd7fe724d0709a732d28d808d1e3ee6a4bb5c1c3e9984632edd19e17ad1ddd1f0ac30cbda0697b50d1c7059de454a5b6287d78fcb3acfcc5790eb3c73871018fd65d1b0f036351d72b0f9f899d1d05d6313391d5dc3e5229c4fe951231ee3052b84d7e5ec968b31f2c4fcde3b28e250dde362147d9ead0c423b82106fd6757838ba7d6b231b47318e62966b26adcf17dd1b3918052c8044b66bb2e83439c688455784c48a9ebd46bb317a6ba29114619174ef1aee19cd1207bd054f6b24bc7b3519fe693bd048c10214440e8a4129a516b52c4ae927563f6fdd39a99c724e1fa92c7f5da97c6517486f9192faa8d17058bfb0b8819abc63fdca5813db0f257b2631995e60b2397a950326d9cabdca810a48b957395862958328484c292353eacac8104e092343085d9cef499c6152ec66ca449eb817b9eac93bfc9f92da3d6cafa64cc9bdc201959c80dc2b1c309185c0c048521703db0fe53eafcb905aee2db1f18d2966032badd0edcce3ddc41b5c960f02b459286f08b55f6d1b3b3aa594464ed16065da3799335b32e14845713482108a68d49a5d6bcaac5dd735ea9df4d6affad6689dbfde0e385b37bb30d7ecbe5c6fccaeab41763bd792e9a1e2b128768bd694b40abbd164f878ec8a9058fa69518b3e486175a64f1cd419cc89d5de508e3026cec97577ab265bd76ebc795a29a5f802c121e76177cbb2f5ecf6359d738e5ad354ba352f101ce6250e6ae3ad9134d9992c769f75637635288decf046ad4843a6fd5098cceebcd37880b7f0e68eb90c7b0eecc6b981ed6f4e895342691525ef34bc1beb2f66e73029ece6ecb3bf5b6ce28d3e521fa63ccf4343eb79a83f14ed2a39b00d6d3429d50289772ee2204180382fb23c0e1d984e963ef202c142c78c0f0b72bc6b2fa4cbb4b02ff70a074b64f786800f0d79783788e77b47c45bcf09473fe7bcd8a88ef0058eca537ae488158dc42340f4d6fcba14dac448832aa5c0baa75238dd45bb23405a6c80243ff7aa0a2c44722af7ca8a22e41b94910d907b75c55096d6b56d71286608218c116f34f9c57987e73b8c76f186a3d71469bac6fb433d6187a0d45a93e947a12210d0466c2cf9a572144261892ea6e0b5daa36e62a95db355bcd5e4fa68f4bb215d5afb6eb4cbcbbbe00c6f260c0a982be632bbcb5f2ecf801cde8dcbe57588b9340f250f73df828dbe9b1998d2bc9b186c05b633cced5cba99cbcd4ebad9edcd2eba19bd5bdd6a7286b76a1fcae6b0b0100443ed63ddcfae36561f2f90de402a6f3a7907fb8e758a611f7dc34076df7cf09050b62f26851d5d5bb32afafb4884dd32e55a314623b5175736be5f6f67f13ce772f0bc3041930f1c17265b98c0078e0a208490473e4c0510be1bf75edf8df14ee62dcf397172491687b2a331e4b602c85d23770db91b90fbcf96da06d261dee7654e3431c830036bcdaeebba785e7677b09e715d99e9ee9a31c34198c3193043d96d9d040df6161976160b700221de7cd0706d10e20c6e36cbe9d353849cf00e6719cbc8db3ccfdb99f12232bc1bccf282968c63e46dba9cf7669478be0de5f9ec299b2b43ba6c4079b96f0f0ba192f02198b6d83a8b6d01d92120c77c4b6537dd67ccb8c44ea08db6dacb61862f8cddfa34c9fc84377b3a0dde4c348f012786cb8065f80cde66d001dd0df00e5fcf81f982dbcce7b738b4b91df8191f693bb22ca5f57219f036430c7843d1e0ed27bc9964b0094399196c92175d9874cdc9bf8ce435f92a4f3793832759cfb41b965c02218401a2d4a8c44df594452d99a211010080003314000028140c874442a16034209304c50f14000d90a64a764c17885192a3208590318410430820000088008c0c9166034f2f085852969f194ff8e7476d0e1b2a3cf0452816d062a8d4adb244a1ea7c516fdd4f32cd66c7e4dc47c776bea81b129bbdd4c81761ca58d89e2ed62f453e4fca58424c6e1db847bcd3cdfdc27d2b80f219f231897a03c9875b9fd697719193345ec6d7b1d7068811e903b27d2592fe7522334556549b5f8533a19914635b49cab8834094a191d2a27da980e2e65b13f83e4041ac79d24355d0c81c9f198bb68bdecdc0957ed94972b4d44631885043d29bde8781a21620f62362a44be4cead1504775ac98e22c22e79364a6e24c1d770e01701430102d6ce454b3f678c88ecb9bc874133713f2f130805e5ea7ba665ccbb792ea7d4c9c425baba28113497a9afbd66e33553af71947ed8e7e015e777017ab13150d203f07d1b6af923a1d74bd3d0a06aa5a76e07f6c7839568906fd5294add7f4bbfa7c24be9f3ef2aaa0ecb496b2c7a12df0ca304b5bf9bd0e02ed4bfc3185ce6a8d82ce858f5d2212142ab6b214b8f4501390ac0ab8ebe88023a68103057bb5377dee3498c879d013701e52ddaf09397fa21eb8e3b582f2287cb2b5c19111579d9e8d250efb8ea91e7ae5aa208f366ee591247621c3c15f3547ba6863ea2a0c52f5676a62e3fcf44afe246c70f548e7aab99107bf3b2f12d80d4b3e3a533812e77b7124b664ff8de0d17486a8e4b57bac93d4b2bb6740646816f7a0c8414847d10bf110381fd1f72227b39cf751b312794fd13a1471fd127b09debfb61c7466375f963046d77b58d24d7aaeeffda6aa2e756c2846292806a9b8e5e33ddaa248d6f346f459368e12cc1f9b56cee922ca9136907250f06bfdd7d9372387edda7563e73be21efdd741bfef6e445b57d5dfb1fc0433a4c54dd0d72235af9dcb87e4378149aca1a02ade34d0a6b7d975b62ec806f75c4a4ad140a3738f0c660b99b72676375d69e85c65af39c988bc04a04ebb5035e528a6fabe462b2fb466d278607f2a0618b4abd0a4e2e944fe4613ea973019e1732bc22079b42e347a36e7e3cc7445aa51540cc47811f5f8e9f4e801261fe2c24b65cb517596984f75241d755a85c5d3ea5daefe20197239b1af2856d3c1263485b9382b8abddce14a28764e99ef24eae836ea0926fab39b88702bdef9a036343bb1de0cbdcd6c8e656a0816bed534bdd62a44e53187e7a7ca1d2cfb05023829be65ab20bc1f864a5f5f39a69153d3da8e39681e12c595508cb3c9d7a36a54f598fd18728db6e702fd6cb423d71e769d39e1dfb303996e83877082373f27fea4c94f5f130660a1de7f5090e0998b216fcc8369d5550b27a4b6d938565567bc30d09ba6aa120a5b1e36d1a808e22f19c0e8e802e296a9e7c922a09b62d46b3e113a41e3048564dc726f1dffb143672d1948183ff0a76fdda8be503ba6f083786d85d2598606bf3dafda8a4a0701decc122cf59757e8aa2eab2a5ca7eebca8350306ed4ae060adf326b9b54492f9eecaf89b95b9ba2b927e55d8def534ad1b8fac5d29383084f3e590bcfee4174a3e976dda46f1afb73b27ba26fbb57e85f42b3047ccc9964b611cc6c666242583f831f1f5406daafb237b90becea317337370147a2e54d490bc1745252f176dc38e76966e614b9a8cc3f0b9c5b9cfad455e49cfdf15fc35b60f3360779b34d3aa864fa726c8d98f89f899ce029ad32f1189a07513c5c79a561ff11b7d4707d5a2908c9009cbf1c1f0b61bf70fab2877aa4bc709b8bc7a775cbb3b2fc1e7b9f84db04bd72d68e9259e1c25f65a299b6fb473d5f164fc2f0232ad6ccb6077ba311a0ae6fa43dee75146f76f8b57c2758d53a63a9d8df23aca276fdfecf71a471da7dd6fcb8426b0a22395d985c2d2340ee92901210cbf6b568391d8dd0c8c73b948a202dd5857cd8809e2c80206e5c613256f77246e22d37a4c14297c142902cb257a051e87e52e423b39b5d960b16a270ba0daa967f5dde8660996e33d3d4b7a36bd069c73fedfbecb4f137134135b8d2a8e04acdbaf00e3aa028da0ba281b0d3cb18b251c9bfeed062eb542adb61836c5097d89fc6cc99a3f943fd6c9ef98f4af9e250d38be7322acbea4283adfd1c390cf8b46cbd4db0cf2d554462306573845589db06ea9f71d94f08d7556c2cb5d411fcec5304df3d3343efe9108a7072f834d657c5f2ae1cfa0b31345c25dfc8794453ea3688ab1d46deccb74662b452ea4cb7aa1ffbebd763b7dbb3d47066c611a5896c4dec2e127e6c2cb3b4b6b386be961cf10ddd3a8228082ad599bd04b52445e7767a0d33ea40b3a67a41dccdf0a67328f5ccc8ab39876800c1d6b1b848a4783dd171e87302739d3273d27e3c43622a2bcbc76167b42cb52e2f86afc26403a8f6068dad1761680fcaec0fac4967efbf37a7c958d5503cfb5d5f6f8b45419a9620f4b93f53a3454eef25a87e8ef7e7a0ad73edc4f39a01cabe76c3c78b084aae0fb6ec790cec2c48bacc9b97b507462e9316df85b28b5cdb412c4882595dd1dea3de2aa48c5165d85279d318f907913d0edc66f98592bd5e4096b04dc1d937b92a7efbcfaf60d13e621fcd9b440b5e44297df8ecd27293e1924153b4a1d1c9cb6d4958a2bda87568cdda6f14c8ae7ae30d13ea4392c8089819f02cc656c542d2f7d30d1cb48fedefaf426bb70383e58da19138846d49b89236f492e13bce439dc4d0e4bbc27ddf9f54f7c65752eecb88d1b1a91464bc27d425d2d5bb1d84720960980dfb7cd6deea619d5126f46abdca8e7104c7060bb60e38003f28a08758340cf9283a27c383fff8b371fe6d2aa7988874ad7a4df9751b023e33684346a6da65c09a60a445e19e61c613f2b4e93678271395bd36a80466daa18ff623820011a09cbc715e6c0e838917ac0027e21dc35c0941f3811e3b206dc36ddaa47ec6c99ca206aaeeef662610fab185659cc0310f6006a7f599ead7be64412cf9b942dcc63c837493c5bcc4267db666a1638816c7ed200bf35bcae79cddb464e44c42c4a85f2c7812a2533373bcf31de36c18a2d3564b8ef6d330fa358858c3e82fde9142d1c1278d3187d174aece982fb21b7d950eb409277ba14d2799a13a40507c4732a0bb9184388cf1e18181783b08553f268085986442f48cc98e51eff1e18425b4a8a032b42494a30659ac9e6cf58067dd237db77dbdb4c7fe7f4b3c7d6c4815e06a7cfa65aaef7e42f8b70b1c1e6de7e96c6801b868157788bf380a89dfc130cef1e5b0e6ff2ed1b7a0844504ec82e0811504329cdafa3a76cbdd0612807209a90fea5759f6fc0a499914d7ae019b79513244674699c135c3878110fdc06e495208f6a03e58bedeb706fb42e599ee5508fe7e26bc52409f4400d3c05851b66b0534fcf0bb934ecf023c0228311abc1b470d20e13ba63c74dfa492bb6c8fc4ee4a57fe08aab420c6dddc59dbc8f0bd72daea9e0d7d670f30f42fd221c772d72ed08b83ae35bcd0e30bcf6e94b840a4f3bf3d49805167e209831a49079fc36539ca016452d13f7c5a8512f63c0ea8a2bac6e329f82dc3cf57141171a5aa65bb4dbe1b25a9a8d8ff9fbad76f4a7a79c2af0c60e87e6828f60fc09cc707e5c58ef128558298c3d305d95f539bc05646e730cc17bdd1b5ada160802dcb67e2a2f23be0a48d839138b7b676e077d38f194c9c5b8e2a85e10e619e600e722e945a1b6e4df08c45bd757b0c9b4a6f17dc3a7edcdf0c2ccc20e094610956b8e6a1c5c73d00a78443f46043cc13643b0bf90aaeff0739f6a27156a942b0007744ab4e732847e7f292f327fb3df6e3195c2f0f6ef78a0432079d548e42ef7f127399d9ce3a9b74c54c9b3729cda81efa7483ffc4e22a6a0ef39ae0871203af71d0ca2192e3110542c3de0c9ba9bac017fa576ef025d000e2f63558f77d0ed7f1d9dde919bda9ae79eabfaa6978e35afc0b80427511174f9c295c731b985144f421347373bff1525b08074e3781b1a58b8e606d6454ba656859adf7d0c3eb9ebef8458ae3d408a895544d467fe4a166cc9e116f87413ce0a1159c61eeca752811af730a016f642cb7c2d99dcefb82e40e31d62bffe38c0b8bb115fdff67d2bf1601a71d61d8c2f32d661b3555554ce8228a66c2c460558bf6d052d98fe60f4b52aae4ab329a5c93d55e5f87526b29b995ee575aa05190c4c7ec93774becbd024a5810e0be543c88638e8338b135896565108906c7b7f79db413dc4ce72fc04518a0c5ef528a0b6ef55a60ff5c4f5d4fcf113260fc01c2f0338eba129b2f4a6e3eff277bda93453cc934483f8a5f9f40e2256b1c5fcd41416ccd7735e6eb5d46f22075b637d26bd14161dbe4ab320f5d8071e256499ecf89fe888ef0f5e0eef13fc7af0c89abf128bda950cafb0d809c5525eb95a4ca75ecc1fe24f91938f8a923f996c835bcbeb56496e6e36ebb45614c3e29676ce0a472b4e8061350e563f2e68ad723a1ad16e4118c929b9001626828afec505884da42fd59b0a87af1db99757df5bfc4d2a19c0b0ad8a4f694b23d93d9b612db476b242a01dee520114eb103a1b6099a83d5df5743d22c21af0acc3e751d49ab9fd273421c61baaed9d2aa0eafebe262ce145a4b615dd15ee91aa3be18068e88dbb3143d17eb4c66fe898a61a6622884479c8695a171a51edaafc133104e5adc2ddd6f12e9ef667fd8c92bc97294631a7617077590ac02d0b6f977b4da099a0bb5a5bd36cf363bb811e7d39b101c46242d7fadf4d219a4c206e6095092f796989d67eb69ca8dd0a848209f07b4088f6334e112d503c9ebe0409e06d701fc003157d3363d7337ff425941094326d67d4113765591dcaf4dd6c8e116cd134747bf0cea65664ea4104c22653da4e9131269e23ad4ec77e553a1c67b71b076cb52fa25a3aa46b14e37af54bf39a4dea87f6a1fae46eb991e0ff59b3e3c91d0ac4f4f0eb02dcbdb257fa2db62a675bca7bdeacaf1907607f37bdd5039659cfa0043a0a613da5ab648e57762f84e63007725eb137bdce5f42c17ea536c88028faba30bc920c68216651652dad910eb4ce47181f2d69ad36fd1a714d0326d6c5ef19b26122b34211fb830a053ec23d2e656f103f435e4305ddb4964ad46749b53b50f5961bbb1cce8ebb73c866d440201c8636e217c6534d891da5766ae853e9174e8ddc9cafe5459cd25ad9092113a2dd55c13de5ebd1a741fff2215b8d6880f2376e1e0a033b7893d80f306b6222b9ab7f459a69a285e84fcd440451ac3c851e42be297adfdff4ca556c94bc0073904e0dd7088abba5ba64b01c3902904461d847ceea0ac585b248a0ab93d3b4bed0efa77ccf0f5f682c40aa7d764ce3b36bf07c169206e5882f161e58d2631d305a8148c474ac6b61e1b769cca841c59130423aba89bb7d2368620e9ff72d1b586470f7cba837acd557a827c1707ae2026d3ce9598bf129c9909e0c70c18cd6a5144513df966d21350d3dfccb90ff1bc02408990a274dd37d3b5fef5fb7d416413e4312925b165f47e53c79e1ff2661213d97364467f186a7c8ceedc91e33836c212b8557e5f36524f169ba01541da9a29e3de3b70ac8b7c8dd7d824d469327161ec3419601325e006c87c28fe91519adf78d2ffef46a6699b1ec365941dd0eadb64412fe148596aeec7559de8b2f3115d519fb458ae7617b849d03b504ea8dadab9e9a21ad2ce8ba95f228aadc9859003d6b62c10cda17d98eeb7ec194889145702ce87fcb662e33fef45ef4055b3fe36e5a4cee69ab4d5b7060948b00c40dca3a9441bca2b3b04c03675741e2062d776e5a8f3cfd44b476146ee03ca810e7d0dbdd644178ea4c07adfcff5f4f6c6b464668ccbf69a6787c7306c519e91c77506754213749271edb13948e5192a5d109b2e60fa3e514e6d9224106cdd135dba2f50acc2688becd3cc2f11d324402d20cef542128b284eda05f9610d4aa7fb7c19d827d9ec137d8107cb2ba7e55f4ea38e3d66992aa141d450a5502727e025e169a0fa45d7cc9bdd53f2ed4e43734584e812be125de2319cd0199ff3f21863c8fab3e1f98d8283a0c676723c1fa5998d49573dba88a8e12bac976982d5092551e311f2cb31e38bbb060480d76cda0d5c3148bcd1a4d456521549e36d901f94bcefeb4cad50cf644265c6686bb479b25c129962fe53f62654d23b6fa8832307584aebde20caf3d97610d954708a01f7db78cff9cb5e29211ca67101f18883be0d9b8e70982ee0132328ab34b866116a6d7f84cc04a97a7a0cded79ba61a233fe7050e0aeef634bf3808ee8e1109e9462558fee8891032aab6bc2258177a41e6ec301a5696176e702d979e98733a45dfd7c5caaca948c311abc4f39a926cce12ad9a3a19f6e60d015798ba77a5acbf017721515add353967d064a536411f8454098787b88bbef8d66dab72fc20e05db2c0b741d101554fe283a1f27e4c885f692fdebcb5b23987272ca00f4cc86da582e0baff6b517b6b78f0abdc2b1b8da813532651164b6e9e1fdc2b7fa04a8b4d6e39e4fe1171b4de1bc2a30569efee865f5ce274b650d6bfdd682b47cd5a47ba61b161cf4552b1ef8816aec58a2f9b5034de0b38bca289cf7cfd361262005d48f82b72f28c2f602c982de6537c20d1290a38a36c8a789a3a982296a64d7ab0cd08d074cdb96e61c1c924913d3caa0b6904f3d43eea1665f89c8a5fa39523bd87767249f858b87d4b0a79a2af9fdd369b079576a09c6405fa91b4a2cde6d796d1c6d49763d92ffc2ca3abc1268cb1ce4965ce26afdcd01ac61050eb450a13db2f86e48faf10b25ef9f47d57d737e19fdd439ee4112c5f2df30d7d18e445ad7ffff2e079a5bf90353a662464a9a86dec93be7ac6dfab35ea95dee61a66dc652ce54e3b4357266f62167d43a2b4f16b7501708a353134459121e9eef769872d3e0a9c003021997d91eb505e8f25f7bbca91a6dbcba85f2dd122926084b36fb268ec49bc4f9206f13ec2ae811247b91c96cb4a03079b3a2822740f77a8bf43f3b447c29be82598b2342adcf76843257c0a65762e922cd5390a558a6d707981a82af374ddee17fcfd004d7db12bf2199062aba6cdc60b5f79b7a5b96780e535fcc7d7df0db0dd6810afcaa662a2e363bb0cb0c5b86b19c7b453e8007b4dd80a302744e2892160ce500f8c562321be1e60fa6d0c1f60e592a9af84ea0790b4d17d88dae1034c4b3dc8e865cb9e3cd02d98d92a68f10a3ca86d30db9e2c34656b83314f347a0fe67e2e993472f75133189b3660cbda5ab0fcebd47153ba1607a706b5d7c329ca3cd7787c57aecd11bd3018b1b9132adbf0d46245d1ca2c1e410a2675e87afa9325e55380dcd62fb7597b1dba260de1d48b9779df106c91e69827f40bd05bcc32bd38ab89d289e1e64a3b99821811897126563fdce19ed634eaa2d8ccea3eb0be0624baf0b01ae74c87860726ba89f8c9bde50ca1008b8bae0f750efbcd10e3492f83458a63e7ac2283fc18fb621e6270943dcc310a78f5d37cccb0b768896a65cc77cfdff5463f36325fdf191b22be9d95e341f25bd2062f30bdd07db384afb82e77998ac8fa76cd6f111c1129f4e339d72add4abb5519ef764f2ca1f14c1645c85dee8645407e73c8e7d16d061a7b5522cd9c24236997dd94e4d21c8e9692ad9ab10f3a00cb281aa89ded6bb99f5f491547b2f376dfddc2109c16fccedc30a4a4855b60200329b7c1acdc87eedd1b977aeb5cc390f09c4690019ccae98b452f7a72ccced29960c730840cc144c5c4001ed53849e7cd711ad4186658df11ce16d4d18daa4cc703c468f9f3582310efcf7220ae39e879040fa33e0be4e0cf8cd2fec212e73f77ffb2367b7bb36174aa5f950351c8c77e534f271be043b259adc57f50fe36381be02f754fc9e69af92f0f75361d2c4a9084ceb9f950bb10856c038523e45897378aabaf505842b1346b1d6507c0fc71e9ba296b67bd78d346cf62fba6aa9f19ff14d6d6e1114e5cc920eac5cbc4a043b466f8bcffe5b8db30ee841d5f95781e7c75146d283da606ad7998e692024cf1bbcef0a7f2685019b8e10d698962d9b62faf0abe830ca38d44f4505dbd2723bccc80354413de2b9a4049a3090f883dfb48c54889114fd7cc496afe647b3224736aad723509fa79c545374fc5ff62b202f45ecabbc34e531ed5285617fb80c0e7fa4d24f554286aec74034ea31edd55f3e1ae6f6435886237ba7e8d94431c979b82c934feb5a74714c2747c7047a972eaac6e4e4a691496b80a804c0f59fbd2e9c1085b17ce22f8ba386e0996af5b903f66f507dda1593af1aa17d6633d9516fb29bc6a6b81d4fe664be09ff0c2bbfd3f80acfab7c8146c0e010b3f080b52100b8fb1306ce0f0ebf15f56a985ba28028f55203bab80cd85416ac49d2346abbc0372e93a05c4a6a6e66e72ee1cfed30a4fe984da1bf778f3b56cd6046d36e5dc2488bacbae399b4963f8b97e786a3e44af86031359884a404239bb42cedab917b57edd6434820528546a8291c1bffea3d9ae4acf7c0aa27b14f06be3f86a1f44450b975debd87fd59d3d074a34233ae3a42e5308f761431a5571ec3848285b7b9040d04279c6aa0ec5f3bd465bc6515700b1c7e3b0767d40f498b5f8ab8fc11a56449e9fe443614ba6e6b9f8f483c29ce1ab59471681c028f78aa22dbf489c5b375fe90a623d60972b56240c990249767c99a4455bcc56e2ab6fbe2f484e85cb73722771e52877249d303a7b3242d7d4eb6324845e9a6c54714e7b550b30e0ba12f9e778c48022e1f7e76d23616d99fc77084951b2d3415bcbc3de9039c022b6eb258644419a43b6757c2c22f6c8870cc776c57e16bb1cfff3780303f9de2056b363ccaa60abf8b41a6c40c67b115465c41be5f0bd3945670a01c161a0202360794fd529e6a3c8c7d6e1754d8e589578b6b6104cc4ee51b6fea2b4e0cc7d5b0c06fc31e11968d2a4739b8bdae194a8fe9432f399ecdb4c420ad2720957b875a0c29fb90776712871e3be09c4f9c68a2c2c759c2ca5d6c1643e4d6cc9a27fcfb85f2954d1e86450a5739b0afeaf40c27ebdc1af53b580348ccf4c683c9e6a484161463ba46e036282bdf943031d1aab3f89117a85e7b0b95f03d1989620a64814a46d2f331f56237836fe1ab923e12ed7c05ffe8c6cea199758c439f1321a7be58ae233eed329e5079e2ff74f5a057effc0ab3c6f07194b8ad74630479c76084eec384723088c5caa52cc5ec1083a6cbe3d179f43a0391a95ebe8094d3e34327b4a0af33f53c6c2c0f77a81a8411a67ad915aa54b799c4c1a8b6ed3d24ae3235a69be67d40cbfb6d2580ca6910122fae78bd854018a1a114ce32d783bfef57b62b0c9c434463633f149d2f0918b09c03f5e153b7349c1f40d2a6408175aaeafe771c3d3a8bc37da8aa763fdd9bd4638767cc70a03fae065a90cd8f6929dd23434f5e069741f61aecb76761a85c43ec639ac89cb23e10b197386b6dc8b1a472f41097c714298e2704523adc50ec527da201110cbcd0b4bd85b41ea50c93bddb8c3dda4def967c39e6acccdc9e9b5d7d7cf5d6c254fe9a34ba50654b618e042ace43c53c386c8f81e6b9b3fad7f322a9364662488ec621a99de11a16d83c13a39d7664f351a424c5def0468b7b0d0da2d3aacb1dcb09fa4eaf0ede3575c58199ae1c2afac8d3af41f93b33f17fc3159c8318edfd298ac3df02259798c532b10834013eb65d028e2774ec88baca4b203fc19897d0977195038b7691ff245eb447f1b2cfd12b71d027770b3cb9072b51e97dc9c5137fe20a048d6fb0a7b6fc8507d7af588d6072671385535a93a520cd8732f7fa06968c0cda97ae9492848549a380fc5d55096fad98932f7720095d4ff0b952400d398bf4305a667de9bf9006b9a1da42c7a2a9c0059ae9fbb384ef2e07318a482a1c286e048a85919067711f86c96fa6c2e32e0b8784d787469dc20c93c5cb4078a52b137e276253228d8b3bed64848fcad481b37c36c7e01d27a4d6e8a6b990ca2229f64b80362868183d55bd328438dacd9c6a2bcbbfc0d53455187789b45930a76913948f724e48b4964bb8f4cb31140d86f8c5df8136dfed1ab2082bdcec2f316ba96f871d456bf72810d26eab99d305c547b05fe1210a40ade5eacd018cfb2b622f73a73d31bc1d88ce8143453bec38e9f9566249420c8810b29261faaead7100d06267ae5bfc7e97938f3b8470fa5d427f87ceb6866f803a5c2315af4c078da273691cef0547c00bdd2c00754177c9be4ba8aa7816cf3082ecd9e954615fb296310c0e8afac692a4cc9a7c99dfb126a171a1f4c17d2a3788011d2a4cbe50fbe3226fa4bc22648134dd8121016bcf67383e1afd18d329bfaacf6c2201d5d3428d2ff8f2a94b5de04d4dc1ec8857f94fb5fe5163de7f591ac673b076b1f8802c5f27a6c054b45a43ce6d4cc004b80e63a245af130f224e7c615f5f4a1f7aff211c1ba2b1783e583e295f9bcbd607f9d89878a29e47bbddc61392531833f13570c642f2f366e61549cbf4171be050e72b51c3fc58dffb050d8f07e8d8e8c6d182c3d5735c49ae59c6060e65073bef23a6591985002ad846029fedd06456fe3bc30ff606d7ca596943d2348d0862935ab678a7c598ed51dd75a3a109c669cbadfa0aa19c89488e1e41ea2ac9842a140e00e51cbc999912e1f9b3285719362445cfd013b32d056aa11395bfa565c1157b8bcac06c350fba3291f02c02466615c3fa42286972d12ef216e8f6365a3b6aaf24296af0eb55ae86e0bc03e4e713ebff317c545a307478f42fa22f7d71721b2a23f05b32d52bc51d469c1d803b94cc49b1f3c958d0f44d4c2531029df11a631c8121a10d035db9941f5e0f2312e39b3ac1ddfdbcf601428975d433e724f0ce487d4a9a42161f127e17f96df9220a2c691fed7c8d75b1468f97a950f6ea0db103904c8418f0c758f8de698e71b5e2ce58e3bdb54213a976c8a38a7a2287ec7bf7a41be8d1e9afad8402a73407f020293114b5b8a9133f3303257c296b8f84c67858e17cb2b4837e2f1c436106b003eca39f5fffc1d0e969494001b82d5f4e4fc9b4a3b4808a5cd62531481d44e7028c4711ab6896d99485d82a60ac4c098c52e1513f48aeeab16e513daf6857cb108d6c2f4ee4a498fd827e160d1376a128ff22f047b7c6161cd37820de2ad6387290010ef9249a6c839473a71b0cbab950ebea3d5d6d7d03e527ecf9075ef034b696cf6a64543e0a1032c06fa30ecaf0294099838a16fc2cd56f2ddcbf17cee4fe159fd603b2239c8c9131269a79755dccca3acee0de81c2f6165b72ecb6db9b3fa9933c63924dfd74b231888882af9501afc26ddf6f5271d8295f658bd1639beafe3330d5f9eee4cdc898337d3cd99fced000f55328ed83a6bfd3c8fd7016d7142e9da83ea02c559dd7dbbf327fe0fd392454aed8bf6b78fe5bb3247c2d1b92edb12bcd2cc714fb8371db9743d31c3b497b063f79d4b4ff4b4c3a50164b90d90f9128c528066990b1e5973f608b0d197870522f9298d7a4bd129e727f87ff1b59dfaffd53c7bd4e8054cb20eb625b476f8ba1c16218c0fb29808cd91e966f6bd14251ab1d4d042ffe83bba7b5acf75728e9a5e11b028e5e7453972ebc1a67f4bae3dac8ac991cef91959ea2673005bbc28ee5f09e67f54494ee8c20a0793f34e928011c4937c6ef07f32855e33f145959cbbe6a88bc34cf6855e87c9542d7d662ccfa40869daa929d0c5c2fae535e6cdc4da76169bed774ec5d01e54e3f9a72c4ef904b3872c7ac56640356a801762079889db91c1c483af1e6ea9436aeb0a935d38cd3f5e7a915a853d2082844013d6eff5a3d47bd278743c801faec32a0e31b0ca08c6cbe73a5ea4f078a7d8e7624d0ad55ac5ae72a28401821810c60b6b6834b626b2723368794b721256c66c43c04899cb9c8280fbca169c7565872ad1a20c7f35d4a73fe78d31e11a25ab9d762b52de55f69f4d67b4767e86dfe7e1715565c8bab03b7104991d062f3b5fcc3a72588fb50d4bff3260c6b5364284beeeda0be48ca6ff49864fbc5f37977042318a58ddcfcd52c67853648abf848aea466cafa07dcfa2ad5a8f9adfdfb26a81a607f9a63d9f0346670a99ca6e2f1bcba372d9a110473254219fffe0ae15bcedf63931515754c915c3edd850bdf241f09d688906ba78e564a4e0c98a5608a9373e884781cf2a2623b796cd77517880696b7b9c49bc59146221c482596a7f8667bc662eb24e72c78939391fa90ef7f095439ccbb1e3f96b162834890d28dc36ba4c64a0a6d0356050693b7aed2aef3f9e85d57d0f1513bcc9ee10a564cd5663c10987dd6e109a3363d741b8e10a1be001895dc89e56c54c9e880edc09b750eb2d1b762294e9ade18a237e4ab8fe3238e1be09a936e44da250225b128bf8df455c718a7c7d47851e9be8f139d5e964afebb030234a34882225e575178a2e6e9a9a193d2245474248839e8e31ba554e22ef42e70c15fe5f5833b495f2c6557e089aa0475c173079c539abdd86e75254048af1667cc7d48b69eb2f4fb16b3c4c0fa36f6ed3628b3f00a6ef235c74407dec3bcdbd23edc5fcdddb8836a02b84d6e75a6a9b54de75bd8d6447db4d09ebba812d90f452cd1bbd683eaeec937d8800c7a5cff8cbe21eace5c44f9514fd8f885560410bd416421f59b675e4652924ed06a80dd20e2ffdf5b53f56e41246e9afa0e45505a9d599a2589c1479718abee4a9186f2443cd439855a260bc374c65867d251f0a8c1f772580050c6873360f84b2caffa781da5306c8ff906ac4138b0a8a8b2be132129e96247df8109d3a0277a899d0ace3633bc0cee2df040b7e9ec6d2eab22cfe9ede0ff591f6b79a404905de324ef1523c15b4605117bc016a9379c00bdd394822bf39cb8fbac1b0dedd14e922858016668bd33edb7456a9e1d3b5c7b03ada3c9f9aaf2f97ed9f423ab85c36d65e49aca8de5098fd81918793921088bdf59124504d2740098d48b3d26a2c5a4ef1ee8dc6692c50e64c58e1e1d1b173f8a621f32c8eb4045db1288a2f5ff200562bdaf0871f12aa11858095305c0d092c3d46167874674750dc744b4804ab7c43e87c17889b851996fcbe589b35fad617e4cd623a16c0fd72f4db33bd1fd404f4515803804e444a7806f66c7c5952560807af67d19262a75a7ae77b97c0bfa1820baad7e362aa737706f1bac0302b66d90e2b4d22f64ff0c8cbea8bc70053615a2fd683724cda128dd7cfe45c4332e4023b0c7feb7d68a96ce8c587c349e5d8aff7adbaa24322fd4af20adb97c841d0577d16dade87650704c291a9090ba6ba12d5b6f79d02be0f34e52216465df05856887b245c56b4f40893b6b1f24e24ab0a975580871ce255042f6458eeb5454961f2bb18e18d50cd301097016b47864586c4372a2d116f1c8b240dde8b7732cd1c4084a0e4dafdf823605852a4bf12c5c9f61a4531da53fc9251d9283b8ebcfd557f88ab07cd38afcf633035202ccf5810d0da74edd6f50bf4ddc0412aa8ee6d9e05d0dc9bdd4c32f32f61584bf9899e879421478cb174e6802d354ddeff39b42da74f8ecb182163fde77376ce80b741569a8930c57eeccf730110b1d0cf3f999c31587faa4ab181815751d2ef21a90582d1b4a5ee9e79f5bd444b82d4cd515a530bcde8afdf4f3e07c3d1327f73d2281d9a4af8997972f81ed31f21dd564c179a264da0560336b1df3190e0260555a588f4872ee290a35cf1fd3fb2fd1f46a9664a7cbffb3dbd043efd9fbaa9639c28b9702c4395e7f70b55c0900681a85b7df14b61b7a73fcdbaf36aa3bc40a335b055658904004acc687c53fdc8c0ffff228974141b2c51e009c38d383c29786ae56cc08a2620846614fe6f4467328300bb40c62df921f5c656692c50b4ca7dc0eab31af35b98cdd6ac31e853058abbe6e1f35bac22d2971fce873e2343c17b04b27520f8e016fae36df181333782002b186a2477905fff527d581246a529d32631cd2bb06b8c2fea75e6580fff0722da5c4441c43f18fb20b001209027c0404cdd5b8ab9ad0c8ea300f074295d42530099451acaa3ad9aa8bf9e8e897c6c628e62af9b7916dae0348b05ba37b13cc0aa9e9b53173459dac22f62d0386ee207eba3f5d0817b82e647072fce093868be9f09a36f56389aff5d673023bc29a9c47382cf1da8e14222bbfd9383aaf4897ad12772692b4a7f827cc3a64fdbb524deba2f913eca7e729816a4b8d36047bc365efa6569d98259c49b140f1e77006676f1daf15cf1df8ca1a97110e6184854755524da8a5a9aed5736626697491f191e047310baadee62deaf0fb716e28371a73fd9f702084e35f5b8bd3d05077207ffa5eaf188c953a0020bc10a00b83640541c28855f0912f8c961afdb1ce581450e302cba59b580cd2f9b25f645eb48c191009ce9748fb8729ee91de28cb2e6fd75c49ce115c27bd1b729e97a1f38bd03cb475971a41f9b04525238de8cf84268073b4308ff9ad1f0e50b8ec0caeab295250b4db2471aea6be923464cb733f684c080cb1f8c156fd708a67a9ca9ec47e74da99c1c34f762e760292f6e36517ac10a2cf84e31b871767c884022195b5acd1d2c93879ca37f274d2f5331cb633b7c3cfd50f2fc286701c11d4e64858a97670098198ef4b91211593ebcda60957709221d068bd033fb2f19b380bf5314d9e5aa4452638049f8eaca83c5335b6ec559950b6569ebe1632ea1581902f0989450603ac595ec60b205123ee422a65318fe17d109d68a67f552b4cf8e641008d5bd4146913ea328c3635f4a27f7ee37c11eb0b7c6adb5d29e5b5cdf3e1f73ae94fc14b3ce6c188452ff245959821481914e8aa1b16f649df7c07e61d95be2f4c386b5f5b3656b48bab1a0cb408878338a3a14e96db1dae86e296cd16b92b44fa5db4c117b1e08f045c14fd54ddcb31ed751ef48d94552bd8dc76975fb50ba68d6c29c43c95945f82e05519a4c1cefe0f6d39ef90534c29a09df81029247a02b661e5f69a3233108aef424799f26e4708b7cfcbb294b2d5e386f0ea9007d080a1e8c1e1c5b38a7d1a6f4205655f7fac856ff26e490e0a541f01273cbf92cac0a8469de8ff35e72c7183039f6cfeb9cd83a9f7a9ff612eae15015c495a465bd3c4d43cd6b2fde1cde6d3a18a6f2460d67c1116b1a6f345510a647200744515b3746d862c2aa7db00d4bd4d7dc8466f33f2094c9bb94c0fe2203a0e2a299dd3c28cbdaf8f2fc4684d2da6143a2e0ad87edc57400648c5a85c6cbee25a36a6a50108fcf4014970e1359a1dd1bd0de9bbf7531ea3fe37ccf61835d007ad730ea09d060f9d3ee81ded9ee55ca69470f47cc997091b1f5c6650429dc28375446e22ac34bbcbe3f5c70e4fcc3f3993de4e1c2cabfb2f3409d86bd670e2e2cf6fde1adba4d2267242fe69ad0d72639ceaedd96506375137a659aff017e0c7aac934b461a4a17e81c374739a70afd911ea7b6ab947f6521a48df03e220f2b866053f6aa5610553293eeae8b79326bbf5d2b3beb64b1fe43e7dad96621f656bbc0f2a0b39e18d2908a7dfb25ff8d89e805d623d5009f83c83717a6aaa7dc2ccc67d2be645e38b463d03e919e3d19e665d0b5e3412b13a220b0a4d6f611ca8df30e9bd959fe08c36a4763891485b1ecb543837d561944a8b590ca2e1c0ae08d8c7ff5919edc6fa77a443f43644ba22c3a5c8b584643d74aa5723194741d67e0c6dd775d2bd2c73d6fb97924e293c1518ebfc44e72c7c69dfb6189e37f5744e85778135997327e2c555b7a0756bd1564f325b242e2075a847a4444a9c5a1dd7cd8b59f0ebe6efe83d6314dfc38f74cd7cbb61e76ae5cc1dd27b90f9012a3110c2ac315b32bc24d7c4e759177d24b0e9235d7081816d213908ac78a2094e8ff46ad045b90778da11cc7f21280109054894da9264bf980a1eec79202978a2d5afa799508f4ce03cb09b74af7f235bdf91d850246802b7f4903840392b72efa252e1efb0f5f2027bf504d6cdcd265b8a88f71e6b62af1028e296573e582c16e3106cec1fe05ebe03a9af1e83cc1a9cfd54eb6dd5347c23f3d4da6078ffee8330999cc8b84a4f8a3112ea12c6d8416b0da895bb085806de00d22d628240bf35678fa8ad9335d60f496c7b66e8575b64cac161fcc1bab9d97bdc29b7e2c0abad76d24a089c648edcbce344e226c1030c3d7f104f2cad5c2695fd641adb718bb584d4afade8b49520f0a02afa2ce60ebc1ea7f7605c13ad28d5da547cff7060e5edd17fb080467173b032d9d4d15ef63ea359e169e3c0ffaa523e347d3a0e59b64134dfde50a61754ecc845e92f56d5cf8fe6cdaf6a02c21c62d14e844141ae789dfe591ad1850d63218f3468ce8d29bcde4801a57041ecc4f281228577ed8d6d9b10ee428d4909fefbb7b24c35cc4b110ab5958a2c091070caffc131929c3e06e0cf24ea1b19324f01abb9d4cba754711390ca8ca89ced83f8283992bed24a9fa4971c938eacb74635a16c05d648d5adaf7ed743e80073c9e580233e770e0988adc60f62914c634a8460d8808d48e5d50b1c2fd2884fbec2ab62e6bb9086932ff69f8bab79fd85935bb6f22be63482274424797773e4596e73aa663efbda9ca240d525668033932f1e6443b80191c13517f5f6e4d6516da7dec91ddc287ab05e96f5245876e4400396ebe56d21e248d16ffdcbfbee2470983ee352e56eca5dbcc4af4cc23997bdb957b6f630b11af0087b2cfb5b19959eadabd888c04c9a2bf6383084c04977000dcbd336306ad01c5fc3712922924abb8cac28bb625a8b7b420296a19cda72e2aa456b49b76416be283e9db37c174999707e3838626d170101c67ac72f09fb863a2c11b1f67134bed056ccdeda1f8557d9be7ad7e88598dbab64d6fd07ecb79757bf53e58ba378f924209e0aab03d41be1197a002c6948c7ed62b2481c93efb91186a7477ad26baac5a662b3c1255a90a4ff0f21663fe902a85ea19ee147271e0074c6703c54182ed7cc26513f1b7d7b2db4d43bd0c7773419bf4667eef6f7f7f9a10f2153d6af7de61c4b92766516ac876b867256437d5b9965bcbd14b4886f1908577b99f5609e2ec4b9c870d8c6b990599849d859271a1995eb9a373d2844ae38bd3fa1489cf513acd4388a63e7914674bb526fa786dba21421e2d2f695fbad8607adeac4e292ec598236a334e6cb4312191c0625da83c690706fa730ed86835039a9f69e782ec3a0a9df4aa4256120d09dc2fd2cd62717d36ec6794cd3eefe2dea1f2eb0980ba70d83e0a03f2461a5476472577a24f42b69552dfd874a33420813106e9ce43d28af29015775fb87eb4e88066cb861ff93cfa97f6dcc0c013ec9a389cdc273113e5b4ef4104f8c67f814fc987462eaa1b1c6d169b926efb011702ba21fba5567be6acca1efab754308f24e20361f8e82d8493a6b0174d92a93da541a828d66da456a5344039bf24e01bca6ee12b4b25a3ee44f361c17308ce2cb16a6dafd16515d354ffa1289fb13ef2f48dcc8ec92e2a550836255d536f0f750f21a3e5e577ec57131a2d96b13a4f4522d636c879f8b2232e07b1753831a0e9240137ea94d74d6ca2d865a81daffa4b52480bcaf05183cfdf5940c9ca65062fba84da4346644c64067a96c1bf9a96983308790cd7fcd41e38b6e18a168e205fe0188b21f605cb1cf7e687ca8f8bdbb95a44f435e950aa85fed451aec14f2f3ec602886de300c7963b9c45392a0ef17ee816a30437b9a873995a59224cceab10aea4bced8a239103f24ce77f03197f337fdfb58c51ff1d644c5b25acc6abf43a799681bfc9b63a58e9e3174de0265192f56cf445041884a374e9ee50e459d674ba838e32fe98571778061ed81782efd0edef30055f670f98b1d962e1ef1824f9960d27ac55dc6edfccd9ae8577761c269c0d2dfaacf6ce04e1b75543da1e4e0e0729b41a1c2f89e0b64d6141eda3c75c990fa78ae077a3e7e1d9085e985e3fcc0a00f65bd54abc4697fa0ad27dad1c932bbe6583fb5c34f04eb414487fc09e23c4d48e49da78c3e83fe2eb567062e18dc16bcb5ad3615b97ca28481825626fce8e796352aa098d1974e034a71d5a57cdf0741406295533d2399252003a2ced14e7d16216238c4c61c12214be557acef813409bc842c36f24a4935bcd12b5ee34a4f1e0277456ab7d32841cc20d7862d2f116d041d2fa9ea7d08575050cbe2b077259060f7ad0b1680cbde7d8cfdd90caefb2a461bffbd600eea0b7745547dfb7a5c242c45591a622d1c1ca28b986b3718d93b91e7ead7a412f0ae5185f18ef0796da24464ec882b99766d0ab62f4f0cb53fac026d488d56f25d20a6434a5e792342439049a0706d3dd692c56f488e2e1a3a52db443a51bc2af8cf067993d9896a7503964d3bf13cbb63dd1469b8e8b16eb0803e46a2d016b2c558dc60f422b7b3478b6a21174137d3f095bb355ebd665b2eacd78acc36085cfa7939b7046ab4a765fcd431cca0126d7040055746df5da0521f2f6cdda0da5f1e5d11804fcae89bf198d8081f7f24667c2d4f464c2c5408ef2544c0c372be4e28b85eaa596702d1242c54b89a724921ef42602a65edfa14714e51ea65d3365060763dadc3e2844f0439bf0161e406d3398c09ec3f1c7bd2511728f1db1b6b9642a79633bbdd3ed046109219a204b52aeb38b0aa3f20de8deb97efceff74ad9472d0355d74423a9d5ae0b4102c251392da55e22b0f08419f0f67fb932d4db4acbc0a3221bf2fd60c3d649c5410e93b12867f06288c58aff7005b24717e318e0c400cda805ef5350f28a907572eb1660869d1c434b0291d1d56334f7b00dac4ed2299ac6a8b298d78d827fe969a2da7a3a52d580859e7f848e6c705f0a94b9afe15465df3800eb3e4292c183e6aebb26279ce4210d8bfd4a5c14d1deed787e69b54dc4c68598017cdf39b5e914d9ac9098a35607c5ec95621d98d989fde44e17b417446718396be8a82e9966953a88922e8356e9889a503645909043ba13e316e066474a69754a8702f772b8150a5dcafbd8f7dbecde736a19af90ac9da98e7ae98775a48b5ad9ec3c2f8f297f7a2b8deab85f9ec829bb56154b8046abbc238803a4d8fc2896c20de573679724888ea624df807e35483ce774e34af227758c6a35edbfffb193af885c507f8134e30ed009b7de2fbb10bd91fd252b3deabc03fb4b274afa9503944924a608f7ed9801f31bd135cc2975a8125a6c90da500694a79a8e67d593d6c96445d06379d7118ae74ae9b2dbbd5073e0427bb5d9a43fd1c91cefaee7b647c467cf76a34be08e6cc6967923e25464cb5132ab8a1caf96d0c8989da909b289e158a21c58ddab73520e6b4cbc521be3298bf1c481aff6999b0c73def83cc3e1b40afc338f18f47d139f31c3f1006d16d7ca36c097e45dda880618989363676526a9fbd1eab40dfa9b03f593a1729a91815c3cd26f4d41a37ea8f39894dc170dce45b86519dd297d6da9dcaffe5f37e801274adfe2c1f54206078c8a50436b84fae34edd38485fcb57512e3064c975bcfd87cb52ca27cbc415d235a80f9ee84d70c8b0a32f6d44fa415333f60e27f4730d6ed04e1ec90050a7360b2ffa587337807cbc9d616aa6cc1a77fe5472c47e28f3c018206215e245cbce8e6aaddbe39741d1c8cf1e7d6b84f858a75c2d70362098a104cb3dba6574ded32530a7d33cb57447fb3b27f5747d3dcd0b959d414965a02fff0f24350bd2fffce29bdd34be0856db036656d404a20d144bdd070aebff02323cb42205edd052fc047afd084dd22ed54ab556292f37ae5ea4f27f65c4c650154e74d493be9064b8e339064ca89d15161d3065c7a0769f9c4ce74a74c58631026b1aae1faa100356ec44e8bb1df45de35b2fb357b24bc6da36ec13210e2fb2b1fbaa15e00a6fceb978834a7bfd30c0c55c7ba502e34231a4fc2fbd843f06ad9c1112f4699a8e281ef99fde3f1e3f0bf8c28a0fcf07559d240db766f6a4bc3704dfd69c720f94a8de3914b02c5fad545030d55bb712985f410fb9e8eb88bc8ab6fbe07d3a5c7a82cca61f050add1fa5702498b9f99b8913687ee2084fe60aa1ea89ae40978bdfc623722783f01129f71df7e0f60f4d82186b856fbf98d76316bd48286b776d3a6cc1617a542a858dbabe21afca6a7e18c828e7ec048b9f87fac332f742bb5f1fe2d1bce47e092f3870e09700b5c350c8a71b4d520e6125ace65eb97b4de55ae53bb390bd8307555d64cd548effe9ce4f3037fea71c12acce3ced2342bf3b40db36f567a98cdff7f3191796da01db8a922cd07ce8da380a3210bd811a803b4f78cb9a45827cebdb263e936f51cd81fc283edaed92c15b5442e63637503b462fdc06d0de01ddbcfbf91cb0a45aad7cc185cfbfdcdbaa9244d121eee0d837a994a0ea5f7743f03e758d21d00501321b62117940fe354c2a75b92cd746e0bef99ef23d14a3d62a9490f9244f0ee283be26757c03d19305f337f5d54fe69cdd9a980bd518c808391b81aced58ff54d29c5afe6125c476abd823389920cc36052d450af7bfd9d869d84b1eea4e08b9e6577663b2e4eda49ea3906112012edeb6e6d5a4ec71bbf26d20d459bbe7c97735af153b0d72c026b86dc533d9e1e0a52b05b55dfca7e672129319d57a66c82a37373922403cd354311e67f280d03a119c5037e10f628d0b910e13cd27f0f8bee55114269a3ea8e7bb8bc4cf1e5532374e1b2c916c962f7b80d9bcc5221d4998aed4ad43867e531d6f79960076c9642bc315197c0099769765698d2e43fd490ec1b3b01e900a2be6caff6c6040612cc1ea299399fae74e09febeb8764399e670473b44964ac024574aadfd37acfdd2db7595b2b20a82ab920842344228bf0fcf7d2179fcf49549cd7c2a40771d3208aa975ea389ce76d8cd10b5fd16ebf95d32cc62b028fc5b94fbc4ad1eb878d83d9304ce33da55350d8bf1704625b337d59a0c71c6c9685db3c466d3cb7647b762af5b23f656bfcde21531e067348c80a4f63b8faa2d8bedee4c4d18f2e2021bc67218f7d55098f2aa858f5b563c85c9e85a292bfb570583f76a168bab25018f7dab6bef9c96ce3fc085dd12769568ba18d94bea5abf8458d3e6f47541eec14ec06ed5659744e8255b7110fb1358add2eb74b9b558a21f0c08528c1efd045c7389901efde6944397a23ea5f3d4d3b49e4e40a92c2c27a699c7b6c326d386bd3ea2675516356fccb588aecbfde5db4e045087c73c58819f5e37e6dd8b0a05090aa18ef55645d8c05afd79c6c3643aeb493a5e96eb7d2f96e06f453050fcc185342c38f9c2d3c4628d14e05f78c0fe80e75f0527f84392bc3768470cb71c52e404ddacc8b1e7568d81dda724ba5d609eb49f5b2a79a1bdbcf2bfa3b5844483246ece53b80e25fd14cd4995a97848b1522df0434d20d8df3648c265d3230250563b2ab679e9507a4f86779af441752433fa6ec3ab1f92aed57e6856d2135eb3e1363b85b33f879d6e16cbff71dd587142d7748a550a3707d6f7e7d3da3180b4c707dffea22eebdef4fbd5e506f93a5185aac144fc3e910a402ad7a181748466de6ef824d50135b59b2932e9f8e787514c57a1861cc645cdec0d03a2bde80a8355fcacf6a279c09e8a4ea627d373bf4e595cdc051858e0eb4ff143f2c21a3b6e2c88de6b3578212a48c0676ff240c967ed3e1baa7d8dc8b4fb39daaa6fd6b4e1a3da1726b00874681e3fb676a3b79446875ea14089dc31780f0861dea0846297c0a02cb702d47a826a716d5259bfb1638fe9fd3bb7a210d0e3aabc6ce74763f0459045423a597066b0cf789183f170830e15fe464fd6c15c5616dbfe9b8546e3de7f22ff8fd3aa24111ad9116e7fd820dd707c0a10fe00f3628abbef9aa1b2d5f61ceecb5558a380b0ff8faedfbfd16aa7fa98bb062eec217a8bd0afacccd62f69563237b4b6d654490710c32291ebfaeae98109a2c081128c3d948adb7e8acc3bcadd65c47b5ca99094abb6cbbdb97198728454d8c08805a5b660b9cb517b8fe35b10e1ce23cb7ce134ad6e1ad5733c00b27f8ece214f8475440d8a3821507721600fa4d8455b6b64d497e3efe1745b7fe2e3a9ebc62545677124003e45e0a8803b3aa915415e9a8997a0bf3b641ba358e00aa67929ee43eb1b481cac045c1f0210aac5e0e311a7e4e759d6af4ff5c5ac1f91eb6c30d71cbfcd5ccdcddd478e88092b381dca7901a905961d21afc094f8a2b7fe541cb556f517908975b1d54ea255c3cc5c7f23f0f09ff1eaff4d62017a62cfe372027d1bb07c01b9bb6df0d01af6969c3f453357455b4c1a07279436f00524def4e6cc8625c0f081f6fe43dc3d7ab8e808fb695134b26d84f0274de2f3b608a29e84d84b1cdfac8684ce6566d9264d0c4d2b3886150c720ac7ec46a9e2fb42e8c903a91d146e8ae9bd887fe58c7524bb4df7b7d81f72b61c2411b93b8b2dfa438c55f84ead89226a82db424a943d4493094c38b160eb6db63b8b69562089ccc88d7e48f7e1286061e9b3d3dc65fa3c2cfe4467ae5bdc5231eece63dcbdc8041b0c491f45598a68c65637a0e2a89e7d03a04ea18f6aee39d4c118a538617005783e84b9b337dd4445c05647975e48c38c2bd70974edc6ff6d5d03bd878fe89eddbb9a65d78a4ed2cfed3b5832218619cfb99c465d6c46549ac966b6cf8a7aef98257b3402b49ce75f23f8d362b792e3f0511cbb97ae8b04203888d0335a50388301fc6b0e4b5a4a5528f678fd17d828936a437f0adf6e10265fc4b984eccca8adf245c40f79c3436139ed6a00014c13ca44e7cfca61305a282e84acfc03c76c3b2b4cd7e8c8b9d3889f4bb637b896ff1cca63542c6afc0e450fe6f3c9904380394eefac79b33ac16879237f2cf012cadd6f135c16bf2ae5644e72b73831f64d7ccacde9e2ae02fff81b0a0da3989572dfc23dc35b43c4a2fdc57ee159ba1083b6805f2c5c8f7a7c2b8f8ec857c63651d6acfb612dc5acb315ec1de230cdb0673d411c549f8806a351059aa19583316bd5e731a4d79a981eb6f6eace8a3b4c2493aa079dfc7d970bb35304db8b30608cf708d4a4c1459339e49a4b5f5168f44cc5fea6d1ad4d6346e2786e5de36d26760cca211100faa37772230988e787b77828a8483315f5c2a093eb484f798cbec883f3563e3d9eb3e76676520b2ece14035cbc12ddfb03d328571d72c8eefb92e9e6328edac93208ffff6a065698aa714d71e0b4cadc8e240498223e7b1855eac4cff2ee27b559fc43f412f3a9d9847fa738bbda956bae02901dc944619430d2198385353b5d07844b7a6ee6c5c07d3775bdd11e5a29781a580a0ac53398e0d5284f65d21024125c698a7938885f8e730ee8ca6b23adc22a35c5cac31e41358ce0f32d960cb60144bbfd497afdc4ff4027d2c44190f594d4af287d5a8039eb6506915ceb0a94b5b4fac4631ae4e28e5ab91a03b2dd2f073bc63c1740a10c880a9389d161214500ac4fabee8ca2a1c958f79091902c5d6849ace85aba709f28236a06f6c90b3046f09636aa6e9f90f6abed9cb3a9a442ca28f2be5241ab09ceb227c1139e46767dcc4ec5abfced7bcd846749e597f5b596d434073f27b2a6ab62b10c6d01581aa38a56107eea69b141a4122c334b1823abebe2c929ed70ee10a4ddb34810347405aedc39ff10411ad5d9bb7590fd046b9ed22c2e638a5c6f6cb8ed3a971215962ade5fa584b938d716062c70ebae6c11ddbff6db774a3f378eb9ea0166e2aaa10d2e7679e6999611385735037536f130164b2de18395432b9b92a3c40f10ede1be716921cf583fcb7a6f4fc04d209833f396823b4d3e0d1cbc58c1e5ab1b07bd92b2ec05882d5f87ed318cd24aafd5b09c93f57f53bbb31ff0ce3e42d6a13d0218249b702ec96fffdd7eb349435bdac54a3fb84e5de1dbec785dea81e38872539b80a30e0acea76295b46400c27473d3ba0d95ee97f7d9de9fc75644d734f2db4353963bb96e99810e3a4bfc8fd788318253ea28eac225ac0554b49aa10482fb95503894add4d6f313be46a72e9b5d8c55efee3a9c9b752e7616a1092af202430f298750f351406afb2a533ea6256080941d2cd173286a36f1bb483ab9b4a2dbab001709ffb8691452241036ab42ed239a846b4d4325e6c1bacac5bbf84bc99fb1dfeaee2d3130ee249d6812861a7a01b0e949ce3ef0672afa0b684d109f16e43e087a0f35a90d29a94d9041cec09e03827b50a057576992a6e9282fa5ac35da5f5c5a9fa0967e87cb4a88fdc93e575f0ea67020b0d32860808fb1146daf323c6276249a56e2baf406b18a9218b633766e9486a0c8e288741f4603079f4f332f49eb2b7284bd11eda4db437504287bb6eea8f27e8a0a23bf4fccd84e859a9f3b5cf9f67aeebbda763824187ef2accf6dee291b5f5be0873621a9c63def9ed213e27597d7a675723265eba978325df4ed52931728a3a2ac3e13fb05fae5b591eb7b9db5b9b72066b51cdf74d0e6e137b16bd29b591f31c18f3b8bfa4576d0c92182235e9333e755eee0c10c6e904b84682795330cad0c7f3b0e31031d07821b3603661eecdec994f41b94412e84939b856b4f98b189b9376cc813affc1db89372cceff2da58021b62921b7ac2899de5d225e9bcb68cce3b50339030497f1e347249c291d7572282983e5c82fbb19bf7fe56b5b0c07279922c8c5f7d704a138cedf3fa4f2a34caf7dffc242ef67dd79c13cf9b288bea46ee64f224c9935ae3c097eb547bf6ec3ad665a587a0d4f4c98a7cb17bc3843d2a8acfba9a2ca5a0f0391acc4fce19478449867002f09f040842668366133e0ad4362f2cfaae8050fee7780d003ee83d11fd9c853cfc9110502d1281bc161da957a27f569f8a0b11ed2e78e9ede3b1d26ff0ce1d8b8e4e621998ff3d000b6d1e6ddbbcfb0082dd0bf4ad842ad3f3e8c1276817fdf4aae75676437c3026f0afea8d5cdabd9b82a18ef2d9d14e903f87e49bb71ac02fdc7b32515acd4e00e107c4c5e313f30808971a472646b8beea7a927b2b90d0607e2458b0a36bbabaa1e2a6d4e75688fc51d68206a93fb3e99bcfbea4d6ab649f69835b4508e0eb09e46d7610d0da51101f97764d6850e43356ae168e5004ca3efa94947b873ac176645fdfe7ab47fed11b1e4f9b5028f3bef979a2001ff764ec145430110718b4fdb5c2ee7e08e350e7b83521d470177859c6d915f995001e31e91fad14b3a9d99e6144755f15500abc08e2bdf2bd703c950c171801bdf516583c3f6f2e30efd5634581e158d6be6dc81aa0e89ea7bafd285de001dfcdf8f422047fb087081d23a68cc447b3a40a1c6e027b139276a20a828eac4548fdaf483653e3682b1686e9835c8006a11f7a33b61119b97efe09d4447ec416c20df281701ca0c15c06cef7b07f97068324bd9f8cfd3960224ca0aa44ccb7aae3a7e6f12768aa80df0b2933a61709972a9a8d3cfb02d2c851cf206357407b7dd071be697c8ec12b67af7a59103f7f20d93d1b886464fb921b128a519637799100247986c096ddcdcea26c6f61ccdb7f79f993d0050e60a7d68d9999234a459e3d0597a5e19c91a9aaf3b28be33f36af28f5a3962272c111d136d71392f2b81a38489301ef6d19c8946f905b877733c7cdac1ed5b29180d863fbbb62a5d3fb1c310e6691c335cabae834cb65e30e809ce223e4b52acee8676c2f60e6c77393927270940feb544c76ea641611296299d0309476495bccd43a454ebb523ef1f858495c980c28ad963d29d8e3282660e836fe35a9041741267da7676ebb5f2a8894613ed2cf247eba6cf38ec790971807a2928e928a064958d67ab11d50a8c5a8cd4be576116dc6cd3f93a77a1f5c83c913da50bac93e31a503d049ad146279a6cbe037be988e520c82ec3129ccb41c50e318f64850182880027d698b924c2b2de8f75a100196e33ffc2ffcf973fce742885920b98cf78ca6457111d046d32209e01348d3eec1a64fe3404205fac60dfae9771d6845016baa81dc1ff64f2c25031944032f5a4aade6c6f4cd3a21105bcb04c3fdb775abdd5bd56b8dea695cf60029a391f4e6a27c431b194e4c363a07d4a8065e305c9ab3206ce5b7fa816a1c7d259a1b05271c48ef5215a640d470df07d0990a390df6f34e43b0659c30e790511475b1621d78d189841c5cbe8bfc9c4f06c3a0440d683c105aae138373ef122ac9471e9664ef059d0992787dd51ca2b3aac443b5761c172dc691029c36223638a4763fcd79587e0fce7726b543e7e10c6866bceee3d3634e0df8d4170f841542d0798bf721387d02dfd51c2be0c0a49cd6de4239d3d82cc9d4a4d8ca28e5cfa6ec779b3a9d7aa6618b18acaf660c12941949610e5025c35893f6a2f116c2d4c9d852364856e5b50c4ba32ee94c5cfc92b0a1582339a6f9946a10cc29621baa7cfab9b4a5ec52e9c3e8806682f0f76ec13f5a4ea608cfa06ba84dfc6a4b4756f92610549a4bd63e23adf8801fe8799a4f0d37f6eade414a60c9ee9703c3ee0344c1880475d21f26018f6be434b8334c64558a3e6b824d6c05b4f02bac9e60b7e93031e99176dd49a20fca60dd2839489936f794445e5aecd38c7cd7aee99941e8e8ce836a0734cd28bd0d076fd7f4c0038afdf1cf65dd8b90629d0d4eab31ba5769711bd3828faf546a402c2105c0209ce030e3270a7fad651e79e1bc0bf6b028956038117c8b19af3c403aed07cded686ec43a23639116a809130106bb21ddccedd06cd843153b0fb99c0bdd02956c57789be1f76882f6ae0730a172b6d7f054807ad325488d0622fc9d91d8d241d7778f89bdd70061f7d3343b853d62ddbe543a1259cb14bd4752684817f098a6fb81294f0ae7376a1c459e325e2c625495fb2cfe7fb56ee701cc7aa2f50aad8c7933272b812604e6acec27d2189ed1cdfbfcd508223fe0922d75b2914ff579d08ab2bfcde3160a9b8df36848225d32258c36d150292b29f90071d209405e243578861e831c35ab1698c8cc24c96361d7fd6de1707a665cf8dd666b4b49a0906be75b3c1bc6c542c12c23bd28c90cef87222fc27a9c443eca16c9d230c6538531261e4400b12fd091edb43b1d254d44ae28b7483d4d3488b504946277034fcda33166cc91f986880c551ba589f4618368642fa407fc11dc38a0a0eb2a324cca8a6ae2ba0795a75902f1d6f3f34b20e466d39e0b76e72d41f29c7dd70bc1f361c8a5d1463a9bb11a1a7b588cb8d61a4cd0ebf183d11ee86ff8eee409544031afbec9382c3b7d6003a2ae662d69f59cd0ad4ff45f5923c128bc3875c61a95ea5e75a000045e09e85c0531b35da24b234a2885deb4721782a6c50e454ca1ba1d7d2a5c4ed1c98ef49e1c2f55788d13b97907a8f2308bbf53d1b2b4af882d055501aba5667b7759ae6de0225923c6dd45e04b3bd54b36e48251016f5393fe2c2e56a4e4fd6dc933304059a6fb27dc1cf7156cc5e88185412b641ca9cb5031ac369cb9f94c567a78166a8ef5d154fa6baafd35bc4199d6940b4bf1e6b1bdc15284a637a242435c52c3ad38e13d7026b61b700e74c363cabd6a94ef7e79f78f435245f0d1c883285d08551d39052ddce0b839b198f4126403157d8adede86c8beaa62876cb6797ea7fb768a978e21e1dea43d5256af8d5b075b03f7aeec6a1d1a71d1690d49b3fdddec196aa49b0a656370d004265642c00e3acd10f40cae84feb373746f9900f162e9034649244b337d284c732d2d8ccbcdf604a9d8d8866dec2536b2bce42645f5fb0009485e54e104dcc2fc9145d8821f8d50410e0f40ccc963b773411cc8fc56cba3a14eff77bb54b8843f6b9c218e53c72beb7a80aba9528af28873fdd0994c27bba7188a26b8ce6fc577b873f843fa10f3397c3e8ea040bf3b4158004bb466e8e052d337ac423858ee155fd8117fd2c41a2578651c4468fd461ccc04f2ef1f21b0f26f3ad6d6ff1f41d0b21f80759054e48b823fb4711f7b946f3d854f1fbd11076a1994b2a2035a7b9f8490b484456f640528b1fc532d763ef975115b514b53b8fc9c0b949191dd712f2a802a84e99f438d92f1c0a1342ab6ba334334ca4203efd6f0a5a5ee295ba8a312f2f92b2466e0a225a86da24c15ea7dc9c75bac6e96b998368d77226e8ae971aff905cf9da7accaba0c69958043ce106df61e8da2579df0f5290bdd7f7b7a58e3cb33fe1ea87345011a2eae9cefa2b7ba1aa526f0d514a62e5096663d425432293d26b74a1f18b1a0f1d70d8ea60202d392484d6a11617b74467dcdc8968872fff11ea4c3782407d2c849b012f08fa3a5378693ed271434d2919a7484e937faa53b15c3ce46049f056c76c0d182aeba9e8c4e2b6c8908c184571aa69d18c3f685a8b0be57e5d763dc97c060640a73bdb2c653df1b930d6a4e97c4ffec50cec48ba2cc3a1e725f22dc8f346fd65c9ff2a89564a8cb63554335a2fe906d7d25f1177f4d70e1b036181d5ffe9f8d82b4e169c817f0eb410483982faa13bec2136cd1d860781a0726d74c50d48d308c00acd4440e38ab4b390d9d9958d5ad23a673060a04324ff2c1db718b274a26c6717845d1abda4ae9c817b18426f337ee204b2879e74f4059487f9208619ae98bd2edb78a7ad68e9d386afc22a528f07c510a4f97156bf7889c30348aa46221dc642f046f73e595a9b583665e51a3caab889cf0bea48613ac6ccea805be060517a080abeb21e6c0574e5a9e84e8790bd7f43418be0b0c47ce84c4b856155457f802fac33746ec888817760801f69c31153cc21c6e8feb637e09752c87b499c85e076855b658918931d86b61c1a317b0d096db9dac689626d0fa34fb98d3e4bf8f966bcd9ad5315436deb8fde06b3d8888ce6c4e35e56d308f4b5f15ee0d6b74c5ac23c1afb26590e3fb7e0878e97b01e8b4e1bc8365264befc44b26ccc3ef887376388db6967cdf738c2a3534209b8a3e461fda440cc0b256178152a9e1096cc8c48c3e844479fcb84c90cc30b315afdf6c60face8e827e546b1e5f63051c3433f0522314609a98bcdb3e56fd8e86dc4abf039cd095857512857576ebff713ea279d878f8abed7f6c483020f6b161003d134c70541dd7d6b452742713042ba0334ceb32ae6e6cb91288c5f1ddf0cf12b6b7ac0ce580687dcb5a9531ee82e31f0de698102e4e44d2cf09c18446f0be4fab77f8b46551e213c36350d8534df14b719d504ce602b94842bc209ebe1f619527973eaa5114f5e851bff17510ce21bf281f5060466ab1f3c5d4532231d4212fcae479f22843ebebcf382837609a91395319c5b4266248283f37195a6d620fcb983dccc76241bff96d0fd0921366bba710af7f2e71ce3476bd110f3b1327b739b34672d5d1f795e7a21788c9d555b51db78e002d341318e3a999751425f90e11cdb73e2a697ea8de428c14a9f1d851c5bfdca647db3fa882f8df1d6297bcee805f6076b4ea5d023fe347207e510ec97b6ee817461133d356c28e3051c8a02fea0a19cbd2f4c24772e66a0c66d3e0d74e7e76f6d8b496ef859a89b4f11826f5ccf5a4c95c3b1d513d247b3d390eff1c477e99685fd50b97b84833a82513db648eb75abf7346cdf591c87e2a7c372352907a54e979f578d448aa907d263f271fd63a98dfb54ec1923700c63a558ac5817d30a1869ea83b37a5fa1f28cfb5d0a07869c9fac5aaac5cf20accb3d332dda68cbc100b7f28ef62f24c0b8e2c59169f470ac1dbf81d39c6d120146d9f7691f6b9a5d4dbf11985885bebf5f8375229824c3d105cbb4c32a06013a6a056468095d8e7fc08edc4347a985673030f59b878185cf1d0d12fbe5852e90deb0098c01024361e0be6b332beb44c9eb905cb65c92028b359a8280aefff3e3a615a5a15981c648a9905619d401a4033b62ec6914a29257f10b5de05fce4668ebdf82a3f5a2d427550075556db06ca798b04f78a67a48483041e47f498346cdb62c8134702acb930cf91cfef7c07f5c1014309acefc094038d9bd87daa160e58af656c805f1283d09d83269c6765e5d6603549ae938eb6ab5536fc0652e796f1accbffcd61cb084dfa39bc51ad4d4e3ed25fb8d1e4a170bc8683c1a560e4b7bdfe00076b8f0ed851ac2845339e86383af1c68334735d8dfbdfb1ddfb8abdca88e44a6f49a9179efcea0eabb319d81ec29649620a8bb4da5718583191fe0a47b9748f5c76d02a43d6ea9985c6931edcb8338b40cbb8229d8c6c8157f9286fafb95885fb3524cbc653994970e9ec5203e42e4d228694ee10b97e2c07cd719bfae02d04ad063d00c8db14d2b8850cc1a1deb13364dafc7d0f0a66e8d31ea1041408cfc6d94630fa5f3a44804b106be6c0280f4216e2ef3362bcaf7734f12d3246c71f4d051c2823739173b5d600dc88dff69912f02fb50cfcc265da22b80161062298ac04c133b0d1738731e309e524809d74234b9e6e2445bc0519f6c550a8e55071596f03bcdca8cf46a64f72d5b3f69e7a84c789fc9ea257e53ee7200903b2f77a578320c5c1be53dd4b02bc4595f63d746ebe609d2710076830721d3d437910717039a2050fce2d41f1671f14433e32a81381ed5b31e58c5743fcf9344dca899737ce7bc29473913644b0517507151f6c2d673bf30af55c691ff29e762862b73c0b6bd64f2c576d74bb56a76bbddc0fc414d21d35c71dd4eac905bad9ba4e43444a315e7da7adb8ea3cb438f23370fcdb306290e31fb03d40016c9f128681d3b5cfcb26b56a27dee68f0b62fef599414cd0a4f9120f023baa92566a916f68def1ef0152260ea12368970360909b9cee864b25ef28c3d70cebe9b0a5262864a95fa6b93dc471b7092fc3206f7054764ee184b83a08c339f07605d0bc281e47122a98471acd3bf6866adda063f91d9eb201aa80412391ea515b1b5f515ea5cd44b5d63728a922b700ac26783d74458d410c2cd650f5ac9a43408f6eabe327d845068eb48b5cd51d2f22e959e0a53414257ff858a8824db3a285be5028b050e82b8afa9f4bab7c2bd2b9ca271c0f0a572e7c0c08c471730cbf2eabe56d779041a5da239d7af36cc63b90498f60c9a5175f0af7b80abf0bb41ffe9b2c8b99eb3790df289cf98c51e0bf11881a569970a95bccc4da7ee1ded2d340754f56e52ce4c40057ac39d7324bbe15c19d6a323bb7dd1159601d671aae0e96a32ff051b211d0c00531794f8119ce88a52c3424ebf3c17205b05881b929c29b5fa7e0f28a38c9373df43382b2986d7b02796060a38622056c9620ff0b94050bf972c16e9dc93c9e85e10f58ed8c059d27e987671d1f24c98c27577806ed73998a495d6d344fef846ffdac77935b600eaa0b31c21bc300e220229976cd0f2999ca19f86bec030a704070b301d7c4740e40a8743106e90019f22aaafcc503668d63740da6d3518ac0869c1088fb00a16793809b4cbae2556804ca01564e7fa37a165ef0fc8c2a4ceb1ea08d80e330f0a69f5712288772003168cf92c01acca8c45906ae9e2a39443ce895bfa98d9272721f7427cd5b9cfdb4ae892bbfe720adc9aa3ee29b3c2918bc99408b6ee0966ce1cf7b0d3160ece46fd26ba2ccac76585dcc373656baca36b53ed21fc1c0f210258b55b9c7701431d2ebe4a238463ae36b2212ac79c5dbde569dc0b3ca53c33239c112236d8e519674b0c65b91afd2ee1bbe3ab0959529f139a00bc61c20266a22a69d9a69ec6981b99869a39746a89c937c8630d0f72ba2d5c751d24432d19a67ae83dc4728b0ed46809b4cf4445e9dd107e3d18918c3a2166799e215ae493f66f99e3d743507c9fdd7493a899c0086e16888c10336a3af777adb82e0d3e4891029fea997ba82f04d0665aeb4b138c45e048e33bb08147bb1ebdfd0278d9e31d081ab5859174899aaa997016b2b5a0437d488d4763045a108da369d4ad5fb0383283c5e508c2172a9d2de4604e60f708ac1abd3b6d776eeed03c41f020a575a854d1010aad5e66148a93223e33486a3da2b8da043fa1be0b90ff5384f3af0d7c822e338d4b297c36fab43befedab4fbab1256713074ec0fb389f4ffefdd991ab16954ed7961f1bb6bb6bb7b4b99528e0823089c084d8c3028fdc2e74c1d35ca9b029c4183f82185b243a32d8daca4755840f794a092ed7942670f13464fa46e0aa57ffaa042dc4103c1497b5867695411d65931842f796e50ad6041e3bc9076a1921231454541cf13b58b9e26e89745e81088500894fad163870e2ae84d6a039ab44c112d7e8172434a86459df202fa86ca0e53b424f12405934413f5ca22940ca22402b5d1838aa163c41b4568d245452a5fd4176ed0014bf185740e9514d014239078d203124dd0438bd42a88d21a02a138e829001d366fd03368922892f972c30da9152ce9f0059548058d624a6a28cf93f44a9e266a0e8b584084ca09a4a6a0c7073a78deac802615a2a892f085ce0d7ab0a0845e4073a002678aca04cf9394053c4dd0318bb8209a21d0087a4ea0a36679932642b3a6a8daf025f572035a041674d10b81a8a81e4cc972e7099a3b4da058cf454eac27221104a16cf8dcb9a3d67044aa860d018678c20e1916ecf6c317166ca33cd8f1c282dddc6f36aded6461b5d69acad95bb68d41800a1d30251d214a928e9a74a42327d5e3e92b9fd4d0f984594778be9163074d5797e38786fa2ae7d08c76c4da664dd1acbfa659cbd2560725c7d3d7397d9246a525db527652b49a2fe5073a0448e9c440c6e182b6808ea5d3503b72fca4720eb19e9db4d62359a6b25e94b3c84ccbb22ccd13065a0ab474369bcdd41eaf4443535dfb3960e67859d3afadac17e7a4613d3b7d6d4e9d14e9cfe9b3a648484848eb2a9ce384d24ccad708c0fa8a5449dda1a4e44269059d2c746240e5ec60ca1193fe9082f5a21c34aa9dd234cd928c1b175873002dd334d7d55ccd365fc66475ccffaab484a6c96de9d736423665ac5a872c4b75b8ea49cdd461c1d6744b1c3834d68b71d0b047ac17e3f83006f58ccad9418e18d6b3930ed4776db77a518e0cac5985f5a21c292cd829a78cf5621c2758b091cd7110c5f1c3fe4ed321280aa614428d9c221d3e740cd179e2d5a02a124b4b2a9925b5a896c95a8c95229c2f2b580121174df0a0587c4376860d7a04e63c83e466444975abb623e8680523f770b7d144337753138cb1f62a1063586973ca9a46933eb346837a5e5fec58c19c9a581c4d4b3eb0799feb1079f480f5ad00adcbfa270b80a29b22586f025bd5432b0ea8e7b36bbb50daef8609414537855440d53829dab5fef625adb5d6fab7fea63b698ae78e1a66a9490da364c0076d88388647b440048288c0700cb111f19da8f03a2f9e58aa21b2182e22be0968e2381a248884682043841ae69c193d55a2b8e0a584403ab889b2c6801b3e4134443c605885f84e90119d8676fecdaff02ab0210e305c1a3e0a4cecd1e3c6a3f8c10da3268839108f86828835c418be104f21e2fb0088880e81704479884fe6f99421ae188a2044a0e14f81e20c201688483a867ee238a33e586c20ce40a41aba40848258c070e847f8a040c404864d8875740882286318c417f187688668a534bc338cc1f44259d208b714d1c88192831b9a95444445af8485334f68e9e10723bc7cc1819195a661232fc2cb18e2d430cb175fcf5bf1348af82c221241fce179088198426a088788876887e841a01431642d21aac32fc4304417432388358651437fe206e21a3ac4db708911886988308620108bc42700711c7a97a119831c628b61972b2f3c226f62871733fc1edc500a4c5ed027f1c40b507a1b2a6612114f203aa12112c42dc4f56a38a43d14cd870b0b6bd87829c6d083973642a056fe94402c00f1c6504dd91efea197e346b812bfe7711807407c123e8a2588b5e1104e78123214224a2196b13d1334869e87387b03a6509a4233e43924e60637645258c9311c444c5a438c427451c2a13f5bea8869e1031bdad4418ae11c0ea104f1c1b021a6c3aaa1cf8143ece387b86879e367c82e435389092cbc11214b951868d841cbe4c29a6da4248de31ec3347421e6dc79229c8e1fe27048848138036b8896ee81fa43a98e597f13e104eb453582582ad68b6a7060c7f6ea09a033d6357ed838b363260bd7c80e1c375009ebebb34f3e90792861c79c0190370736b646eed5a69fa05bd02d32561fd1182b9996acf7129ab7eff36acd3618eb7f0e9469f5313b95690aeb8b95dc645af4b87fb880a14455c369c90ae684e5e463bad404cae3d91649288bd54752dba258498d46adab3e37b99d482b729fb6152b991673cf369996dfe7ae399bfd6e356ed374b2b5b490ec2d97419550152b9996b779aec281a6485a7d7b897a938691b553d57062b2823939393939d5b89da9d2ba8e794628d2d17a94b7bbbb7b26b3fb98be622e2da96bde4ca5edb42da1b092b6cc07f5b1fe7614f58cead6cc7a714d1c27367ba99436599760d4de2fd14f7d4c13205857775cf9b1a66f6463aed2bab481c56c499224498e99cbaab5d6b998369d274a459bb92f2d9d13a0e76933572ab45c7d24c7263067e37af51bfe50bec60cebc5353b3cb12b03d9496f1cd9496facecd46505c36a004b5f69ac156c5d95b28ea53256efb1b8a60eabaf946dadd1a6b7a03c9ecdab9b288f677fd57b85bbbbaf645a489b52765aca557a15ae990cea689992ffb4a8660aeb3150f759d2024ed4c94eae060afb2cea6405dbeca31eac6986525443f66f42d6ea0e9d738d15ec64c77177413d5b69cd94b717d590b1ce8e4af4e74c537de0df679266ce5372de6476775af3d6ea031db0243be66c2b4b1f3d469d5925e5ff1dd47d289237b1bf949d688dd01ad67f5fb3951eb71e9b72202e642cd8ad86d771331a642b323fd952c213162c6595e4f1942d6d4bb75593ff9b5c9dacbf3de723ad37b9e699b3cb8dfa7359bd03f649b31c47f507c9aa749090724def726bd54726b30381fa39d69c6ccde70edbcfc55dbd98a698860d7bc47a310d19f6f76a26e757f2629a3c29582fa629a473c6837a76534c534507d433d6077266209e602511180ffaa44883421168cc9c61c3448ab2326648f0e2a410910c4d1ca443681aa179e2cc183f6075629d02c0ea3daa3bc8bc75ade7b4c930880c195282021f1cd23421e10a9c354da41cdd616782e09481c306bc1df3da37de5e3f5e1cb7be17d8b5db5cba3b0c5f7dc9dd937c752a772702ec75bdee10e03612f374ad58ab83df279d3b2447a47da1742f0c0565e46d5f2e5ecfdd493dba7bd68aeaa03e4628645deb27a4a0e8d1084ce5d65a5ee63dda5cd320758da9dca3de2312bdf37ea6d226c4b5f2fe8de4b76b99801e49c96565e32e1b6d4e4e7d75e44892bcf54ef27bdc47b41c212bdb5ae404b34dee24e7a66db71d71b2b548db9124e5a6d966b624a7ad6c6bfd263769d3db7792f27fe851942312c2c81ea44d89fe0750cfb53134e76d1b3bc28eb6b0c7611cc7ffb0dfe736c7f491ff3ddb9ec7234362bd8fb0e55640939a3c62ee73eb1d468691b6b073e75d962458de7fdac6343bee71d45a8f7a1cc751eb9db7eff73d03eab9368692b3ed3ef4d7de781c32d6c696d8b06c73f66d6347d8d97eb58167cf990d6cdc60a76dec48feb2040b43ca3ea0de74856e75832d9d6a2da3b5d7cafbb409f34ed33d9bb9166ddc343224b3fd79a62a11d69c24a0a537ad4656f65a7a3b49406b1cb7ba27d0295a53d54d4377d96bd1c8d2d976b274ab331b122d13d0f66f24e8ceabcac4c3dc659869bb6224efd276c588de099c64e82edb5ab47dd6c8d4fdaa4d68236b822489de349b5093956d2d756b1503b5862489dea84db8473502bd75a6d5ca5e8bb667b5b2d79a6db446e624012dda2e7b2d74a3b9a6eeafa55bab3b982218b7ba31d03bdd678d6cb64fd5866423a1d590a028ad56eed73a7ab28e7923b6b4855d3192f71523e30e9237a9324530eebc27d0fbf7a90619b7ab41f4de3a973c2cadde24020ed4c93b212bdb4e469b96b10458bd7d8c05e86ff74131cd2dc1936563ae00896fafc0f769631afb23a4d6694b42d33add7ca66ce351a6652c01f6b712a16b4da042ebc84ccb6dabe3704d83d4722d27c9ad7d90bb0a7ae4e48a6fad75cef9b3ed47bbe24cc8af8125e1a185be47abdcbe4b1c4c685a6e13d2b4caed389024a1699536214dabfcdffaf5952242a150e8b52be67677f38335ccb73fe07bb3bf87fc2e2214daae98db1d47d9d6326d49685a5a48b3dc5909f4778f2c143aa0840dae6cbcb28d5874d47fe4c13ae464c7741442942c75a34663ba5159da78ac48ecef2c2c53577bf850c18ee946eb589265ee7b26a0609d7d16454251577bbcbf8f7a87f3187f8e13a3b1daf6a3b10e9684cdac0eebd1d8b7fd68639acd3b3611751f941289c901df3d5ec82a61d1b7318d3d4bb2d9d6a3b199e96d3c4ef6f7903193fdfd42d65915b393755b8fc63a0b363a1bf4dcb5677d6c9c0deb7bb67b387b6ed4dcfa682b6115adc07a5111141601d68b89f2b04bfa2acd34c84dd66e6c79a6e7d81a36dbbec1669b46fa1e551fb3ed7bdde9463293465b9a9dca7dc2d04c79abdb8b89d2b04835266d6eb40c3b6d439e2d77b9d7da671f8fb6db18d9b52c4f96e96482800d43b28185cdf68bee748f6a311111ac1713a961cd9aae8d8d2c18f699b06506fbad57364ca335a6bccbfd6c5a1b43c1bca8c80b6bf2b8b1aff2789add58771bb412ad2932c39ac1196c1c833d6def3830736b736bd3f66ee3c696a7dee58d2db71230749b0f20b1e8366b33d476ba0dbf396dcf6a4c3c343bf647b057acab3bcad206566e0fd3bb2c75f9a59eb1609e6805f69900f53ea1b0cf844d94857d26ecb95147cdd455133dd3b206b6cf5df2789a6d6d53205f2973a27fbf1427c088b044ebac81992c98a32604df843d376a966ab9b5fac033f1716e6d03cbead89f2c9892ecf9c864ca6c993eaa02ead90e2967d48c10258148501488bc1cfa72b4640d4d33325733b34104c489288d66b482f5e2212b87aa2c81028aa57f5591c984cb24901b15ca9a48d427af31d4875ddab26ad68b86f6b060eb2e040421225445867c500d9d316950e1224b1c26b0700904c5172d930817c6174c435352431fa4ba10a224436480d44245cc8a85eca82a13ff86e6184ac382ed1b96dc2dab1966b0264a212c422041484c9adb2dd14d4574436468055bf352b61a32838501f0a44da21c0e7161d246abcc68d8b28d0c6cd9260b2133777cfcffffff712b901d08b78d99acde9f9aa5aaf79fa6dae3599d6769babad3ef7727b58799ea9a668b84bcb0284ddd713465d5bb879ea93db4cd6de3fff99f02fa36ede399493d7e0d4cef7f4d6eb0fffd0ef636342449880a4bbad022c99325499224d3145692f522a1137e5847d68b844840fd100221f5606dc4c47a71d09e41737497adde59c16eec0d6bdc990aeb4d320f22c36ad68b83c4b041e4c082ddf80cd2a2960d8221cd33880a0f83484034080417060955159af860503a049f2575d0396f4838830e5ae207b5852576b8b3c416358625acc05982852a4b949082b0c493434b3059e2894485d01efaa44adc21024e13695040828a6104f5909609eac248900ce913825c100a3a01f5415008291741647116a5c44368983fe8127954198e50792022952150193381c0bc0974c30981685041083454bf044a014d1328ca0b819ca060181962226d21903a431283e4a06bd8a44c0009810850982200e180d6019445d501d00b6200a9e004a011d401002a5371681445514ac31269147b503574b0b80961881cfe8c49a5fcd1213de24f0d4efec4f0e7cf0a40fc9902c39f01a47bfe2c31c1893284502894509bd8418128221d83868f1f3230f8e121d5e3870b8ac7cf9512fcb490fae0c784347ea0a03af86932a5005b0ca13bf851a9e049913042dd414d2aa80f10aa097dbe34e963835a429f1954e853257dd20705554a1f11847d3c5069808209448500298144ea421cd4ce1ad44d1029133ee0e0a5092c1f10fb10205d4109296821253e50c144714322d48b2095007ae4cc4907d026d562668918d54b0eea132d691f18085151b190903ae103420c7c84281201630d3506560d425a05e96eac71b424533191148c5fe22023ccf18381a42679a48d3f6834215482c9016c963c894b9aa1c0923cbed036c4bea68344008eb02c6704215b81a4e581eeb1e403a18b9905a952aa24001e2bc44e98a64888239ef5d8226135675a25c34610a02c39c7a332c6234d2de9439c1d6004d9e9040a471925154832b3c607dc0cb83051413ad178d00a2835180f30b398f030ecc18486282cd61b99aafb3100208468b08b4a13550ae92b80a16f982eac96d0ccac92761ee92b1f3d740c1a0f45429ae943157236c0b483076dd6402a64878307618bb18615adc4a9504b1abac3c6811521ed6c4006d71321a957f2cb084a1a79a33c4087161a9c19703650d24c172a03b3a611c8ac85128c025eb038e044e001090a58423b9fcc2975eaca4710331dcd17244e5bcdd2d180ad803933692314f9c088a5c0934cba46b2b8cd4e9dceda7163e9a4b1985d2d5900084a753a8d4d63534983f080a39101120152c6ac69848114a56330113e41bdeed236464d7660e3604a18e06f4b0d6cc670d942030c035082a4081cd6e5eab638d000830054408190203f1c68008735e554000103ec3188328d0809ea93800e026c2f5c9855d4a4b96225821d09e062ac48d11208303d6891e10a0c2f0cabb040819301504f5454984048232898e0c280d020313960120951785d3a6ed04c194690723593136706197059562d9450da69c260d221070ddc1d04401cf0c163470b2280f0418a3c553e6ca981062516547065b5a434b66183a8021d39ac6666d9c5490f36104f98f2a403175a68210a044074dc3020fd45ba00d989b1d01e94fd8c7d3212649e9f938fc846cce0a445e436794d0693bbe82d396c04fba69f71c62061a82e728bbce2959254902972d2afe35146495a9e9d67364d328fafff4b579134930e231c43108e3fca03f69b8996ac568424999a05cc59320ea3c616290c58ca0108c71f331634184b4d84b30b460cb40c342ed393c78955d2f48d251a894678ee2054d260c9ec4ab37028c523241b09c9f48511c234b68511ce968eb2798384224b285b81ba4081c90845885443aa691b4950e75252232624022796894323691c270c420d8028a8925666116ab47c31d640a3f48fe95883c68344c0bca07c51d24ab05429559a2dcd58981768464cca179a0b13d28c9000b0d1b77304c4187129529934beb6a1171883a42dca6c5a60329940c815475fa29a1c53ad75d6a63ef58c0663d42605660aa420b424b3345f9b23d50835fee942332d99cd20b1088fb22c492a47a6930126231e26b6dcf64651f49113a70d9b21cc04bd848c206587560239ae803f4283a00c58914e33030c2000d2888822dccc88724b01050554c073c41a21c4e8b0e50ad453012704e328f09bc850a13a708a28838392241c688041002000d283c70c192d5880f9a2644b0d4990e462c000fe661ce1b933e613368aa209451a64d08245d2114204f183181d72e812450410721c5c600104407cac212208353ea4d0009d396b842823268c15189e78d0c450a123e0880903c60632a1bbf3b123492dc4670f9e21ca7ce9c1cb0c2384e0ee721c689081053e76dcc0d14852d71031040b54462073771c38e083c78e1bb82375743e7bd6107126882f3d78e932030922906de0808f1c37705754513452248dde88cf9e3b678608a2cc971ebacc40c20822844096446e030d12807a8262d18f07908eecc09002e549002cb811437862a4889659231d4566442eca0538b18c34e419f29597ca51faf64fa80d8d80de2c666418cf64bed02e7e696cf12c662a52a4d0eb88941eadea8bd4050a83963433cfd2cc654992faddfd853b6d9d8dee0ebf4f6e8fab8d7fbc570ccadd0175bc5f3086fb9b671c2f7d3d69584fe27a1ea1b19df676773cf8c7db306fc7bc7dc3c8dbbeed976eb73b06fe36ccdba703278ff5c472dfda5e3fdecffd22b269ec1c38b6530e1cfbc7f372b9ce733519b96fed2ee086793b36c2b5c1d3d353063b5d597573272bb0915aab59c4e1f7e9f7ba71dc8a41bdb18f3afb5a625032e0bb9f1bb71b5ecba6ddcf5dc943649efd157bfddcdeee02eefd02bbe2b78de1ed7afddad7dd0d70f72f920bfc3ebd5f3a9feb837275bc1eec3327332564cf857b6d971b7c63f87dda017d05b83b01eedee4595958595759565955595459535952595159b7ac2c2c2cac2b2c2bac2a2c2aac292c29ac28ac1b56d615d6d5d595d555d515d5d5d495d455d4d5ed2acb0acbeacacacaaaca8aca6aca4aca2acaea6695558555755565555555455535552555155575abcaa2c2a2baa2b2a2aaa2a2a29aa292a28aa2ba51654d614d5d4d594d554d514d4d4d494d454ddda6b2a4b0a4aea4aca4aaa4a8a4a6a4a4a4a2a46e52595158515751565155515451535152515151b7a8ac1bd6edea6675abba51dda66e52b7a8dbed56e4e6ba3b93afe306f0fbe40bfea0625f97fb02751bb8fb006306b41a4e3427241bc9ee60ef1ab41ab40ef6de7bbbbbcddd6b3eca18cf8ddbb31566d53654717717beea0fdc7de3b6b6368e56636b6be3e22eb7b7376d8b46fce5c57bf78ebbdc9ed3d6c6eddc1eafed7a5d186e73e4c85183dd2f5ddcfa66c858750441760d030cd871bb776e988ef76bc5b4d39ab3b83b4e8f6bde1307b9f68679ee57bce638ee4f4f34dc1f10c8d6fe0179b11804b0bb73e06bcee1f0fbc4c5ebc52f90ab75f715eeaee4ee2adcdd87fb9824e53eea146eb0d70ed95befbb636bffdafd03ee2d1a4e62ed4be417c4e1e25e107625d8da3bb7d76b75eebeba3b92bb1fb9bbeaeea9bba3bf82bb6ff9fa7b7d182fdc3878f70b02b9761770c741e006c6b998d71383e1f7e9f644f51465037663235c505352583a378b6b97952565a5d3b951367c40dd1357f02be67919796398e7e5be5040afb7d53d41ed7abc2fddcb6bbb7ec0d7f5b53a28f875610aa0760e1cdb7de6eee7ea49b83b0b5f3d8dbba7f0d5f5b87b1177f7e3ed5e3b04e8735f1a1be639ed9fdbdb38b6cbb577bd20f00bde34dc7dbbe79dbb2ff9ea30c0ef53eceb2addfd3dfb0b7c5f77ffdabd737f6f3076dffd03f2e236feb93eb727e60b6b5310ee8ee46b930e4d333411a0498afbc6d1687bd7d8389d3b24d76b5d38675525d57ec1b95e2fd8f505b75f5456c19cabfb017fb45d63e35e97cb17fce56834f77f6a02b236f968bab96f1a7be376bca0d3ce950088fb13d0e4ee1f631d006c1de0c70045778f7f41a04ed7eedf26e2fef66ed7c2efd300b60196d601ce4234366ed7d8b81fcf8bf702b793bb5fe0ee2306b61a6c29b8fb92af366111f7bd7170dbdb1b67fb6163c0dd11d8b89ebbb3b5586d4735287c6f9c11372c067e1971c3763da02ee7f5e3fd8800dd9d88bb63b9bb05bed6a85cb7d64c586b4f10d8b89d1b7483637be378b91d2f18bb3ff87daa8d7b90bbef8d1373633887c3e56cacfb052f9e6f037fdb83159270e3d6cbedb9bb9ceb03ea5caeddae75c3da18fe018bb470d0fd8a5d1ff015cb71bd2e9c8b5d78b76b5f5e70cc7ddddf0fa8cb85bd4022b1fbcbb9616d9c7b5db8485ca4fdf17c6f90ebc7f3b971fbe6621776c3bcdad71dd2e6bc7ebc21405f0edefd805c46e25e50c7ebeae2ede29feb8be120d8aeedb5435e37fef162d7eb6de3ae2e601cd4e58cc4bc21bc58d702bfde564cac0d8377bc6091d6b77389fc783e01f0e0dc972e6eb9be622fddcf0d8eb970ecc2eedbe3c5bf3638967377c19eae88ebfe82af98ebf5ba4378bb1c1cf3bc80bd9fdb7b8171fbba44806239226dafd5c139235eaffb3302ef80bcd715cb198979bfa0cfc7cbbdae580e7675bf3608c3bc5cecc239e02e08c37110f815f388e4be74403197e773733f5efbee72616febfbd2fd5caed8edf55a31a8a9212f10feb93150cc7da1e0f8e7fa823a20032596bb6fe0eb8b3aeeeebec1b875bbf616fc0be686846d6df70b0eee9ddbe36a7fee76b9760c077d7bf76b77af8d7fae8ff7ba6245dc0df3f6b661c40d73bf8a7aa27abad9f002f670b8fb8df58597ac5dfb7a895110c3ed0f1f02e0f5da1f8fc70e233d3131166ebf824c80c3fd08d3017b5a3a76ae9818ef8d619c0f6e89e07808dc1dcc57173f7ce78eed1bdbe5dac078c770dc0bb65dee6ebb3d2e1efc7385c0ef131cf37cbc9ecf8d6123ee5ee5ee4fe5f5e3bdb110097262b0eb6b7fb1ae15f30117c0e174ee0ea8939a0aea78dd2070ee75c58ab4beb7d7c6c03857a4f5e9c051a4f5b970cba5436a2a88e3758314697d3fdf04c0166cdcf50185b83f209009dcbd01771780af2d5cb060c2e1f7a9d7f6da9545147f7a7a3220767b35e2d67dbd760c04b29fe0dd6bdb5e50d76b75fb2be611d95e3fdeee17dceedb6ed7025c8c83dfa72fb8fdfa82dbaf156cdce317f8056f2030fe82dbaf8d73e1d71de2befbe7c37de70e795dd895c0755f1e3a98e7c3e1e09f1b068c71383818b7b11057f76b7bad166ca40de2ba5d39d8482b06c36d4f67c10fab92a11e2fc815f534f5246503d8f5e3d9707bad0e86bfe23688bb433d01bb7e3e8cbcc0b8ddfd5a00f4dad71583a0d7fa5e2010186e7b0b30f0a58b5b3198b7838db44160f7022010f87d82828292a2eaba9ac28a828a926a6f57533aaaddd41415d6cdbdb2e1c6ed1bc35c41180a8e83c0309dfba5838a795e3bf70b6ebf7e411c2e7e8162624138e7fadab7cbf56a5f600ce370ae8f17ff82af8a302ac0ac2a64f0ad8ddbda71bbddb88de1bd7363b1e0fb02174085b8fb0f5f55fc70bf91a28ebb31ecfe72af1bf3723ee0db95d3b9623094ce1d02d5526175b5515952ee8d6a579555c585c565d55e65e9aaa6ba5aaaac5d563b75db714d61e9a8a074311c7ca1d61464e220d048fbc30b09b7dcf4993efa98e3ddacf23b32e94d477a92c51ffd29355eb92e619461e6f32447fdef2df29367ff08c604decd1ba42395efff4f925419cdfeff3a37f06f7efed7a500f2e737330b09fecb373dbffaa473593ad1f68f398f527efccfb44f947fe6e48f59bbff60f93f9f1464a6fcb91cf307caff65fe08bec5bfce6791d4e6d1b3f8ffcf5979c5a99f85cf5fe6ffac739f47da8c2aa7afe37cb249e77833e7314ad3727e1a4856dfc11885f2d041b28f9e63f8513b8d647eeda4099fc47bf6755dfdfdc7cf3fe8cf7cc07f2673d6ff530ff5e7a739e75c2ef006e43ce6bfe927d4c559c0ab295e64149ecc2e7e7cfd9e83e44dfbd133aff4bdf52168af15df150822eb2f7f345f7d9d42ce274e3f96fe9cc79dd399d393ffe66d8c9a71fb4c2bcf6cfee33e673abafc591ef590b7e5e0bfb6f13334ff697e5620cbc832720cedda1fe9cb9c5f1d573d661739e72f7fecfa59d3a3647ef28fae98b20f139de5e73a53337f047ffeebfce337f03172568691d16c66f2f5e7cf8ffb1819cde6f89ad4ff57afdfe9679ffeff8d979169dafc73fcfc8ffb2f5964a51ff393e36765f47596f15ae73ce6f1b372fe1799f6b33fc7fcffb88f9169a6fef11cffff6affd201270e8d00f9fa673f2efd6c8643336092f1038c9f73994df7d71537b481fffcf9ff6db8e9628e81922f74c120df170cfdb0b9a0b64790056d12355080441e9fa036c7850cdbb842da352ec1100c0b12d480104430042b14a4a552e0c75006a2065a18b7ace11254567b84e50a09b5e9aa467b2b249e91d556a316ac7614feb4b548868d2d19728d3c568035f6ed0004aad42034636ba733768b145e45c506be458ee07b6ce2aedd35700fb21e997177a79fdbcb01e39c583087d51553f002df7787bb1be0fe80405c1f5008fc3eb571bbcb95643402d008cd99391b8f4e2b73a663a4132c01623b9d4ada8935a6483fa6656e24a2559d34c669a282b03ccae62c8ec3a844ea57d27212544a29330332020800008316003030140e884663398e42312c8b3d14800671ae36563a1e234ba2802888a22086611880610006600000400004610006a3102efa01d3ecf624ab30afa5220885594123fe765a1d3ad6b4b027922dc56cd8e17a5ea1353485b128abea4d880bbc6186c11f1068839300aaf080f8be61d2943c157fb85a6a6ea7ddc3b8607df49086c3ba438f723cb06eb7eba5a78c435d0e9e35ba4a37a6f777092f47b8617fc6edbe32a8b1b9e0fd9042ffc9b479d701dd0245bc692db5dc191ff397b4535a7ba996a114dbf1e5a2e007f7812f7715767936783431cc9a02dc21e0b91ed61b7322b2f85e728237e4f88203f356165ebc3c662bb766f13dc31eee1f87934dafafeb262bc1af80150d0bcc385640422c622106cf2d62295a105079d89f75740e3e8b5ef356c39c3f68e2e6ca30461c026414d984576ad0adc2e534c28fd9bdb409202a8b8715060d801a402354aa56470367eb775979d4d7d1b36f88b0abc5a3ae78589391f0762360e98984a28333d3edc223ebdfdfa8f407a1cdea0ff4759844f03dff6ed8a24907694e68352504997ce893a6affd3ba51a3754f198a4d874e139e13ba440ad16f87cfb4495c55a21f9016a1196fe174a7877dbebc0046b241e44cc517118559f7fdb9ee7c0deab22d5253afc1d450de4862fbde8293471a08f502af75847c96e2b727b792e8097290d0fa505f1e7360180a5515eb4b17ee41fce7b599f7e5bb789adf9c6bde5bba3f7acf5cb245ce63b6cadb776a84d4974283a0b263e0db8475fc7c94e68f5aae83199ba7dc7355edfda0ef338f26ede635a3bc4f7fad667492bb433e52e3fd7096977e0f31701baa7e3e51cb54d1d9b2e9710e7ef053dba02f77aaa3b61cea61723daa2900dc2c49fdb50abf6535477c5dc73d1f63aefa5ecdc553f5d0188109db69ab08d0f11529457e720cb064fb87df2bed8b2831ebf11e58b97f563deb16dfe70ced2e65ee7718ad22aee7be33f55a7e1295bbe30459157e4131e93d0f3e164b923969f178bcec647cc2fbee92174dea2ba953325f3bde9b2dd75ea75d47402afe80b0e6810966053a440e15007bbe1fef3ff5fe343543ee808c7ab2f1d256e8120767c57df07c44bbe614f3fb4cc3aeb0d51cffe92ed7cedbbe50b1ae505db5182a860627728ecb1d52574c815b578d896ff7f51ac88b9a8c93b47653397417e6d90dfc10ff1a10ff7cec31f540dcd338bf2de878951f057404943ea5e7c94801b0de56ff0f72265e7c4d7ad5e61f5e0afb52e7f42a6d7ba21731f8691bf43827f1931d60b26e5f26b4c3e6b3b54862b753bc3424bffc96a973e2a3176739a415e85c735f7ab26f35da95f08d244fd4e00bf6085e24368cf6155f039aa1e4f5779bdcd558fb104bf6fdd5eddab029cb8ab50497b59abff5de0c3b63b7a3e3682fe94b08272fc68e034c3bdc332c89bae0d8d6c1c68965daf84e32d475beac29a73d67a45f317008865287a80c2e84134ffbaf6add0c876ca2d4e5e2be6c24564fda6cd039ae9205643f937627685789b3af3e9d7bd4fcbc8dabb6b63994b711f7a777102abf7af42e113d4621176839b12e9e5cb94a974b980effe2b9d9e4f5d8bc43426ec09e3d8439ee162e73d37f4f25a1a1f751aa8f521ef572f177a89e73e899f5af25a7b9bab24541c0a3b542f321a333c56a8e7ca37097e30c56208d804ab247be69fd84ffc1d99d6d3571daacb17e5460532b89fed9c89d6deaf86d931fc28c8975d75f55d31041dff66fb5b22e8f1d3457bdd9cebafb6e1e58f85ae1bbff2d792e5635857d758a580fa1f9956bb9c71ff098920eb7a4559ab8bd78f8fc1b80d1ca6939c82327768fea30b9a7437867fbc93b97a589179ba0071306dd5168419e95f0f51330af8f6c98e7f28b1c52c7919d8ce12e6bf65a7407529b6c68faa68ceada256740518bde264c4ab1caf9f8e7b55b306baea02ca3c3b2e8b560ef0db5dabe45493b430a0fdd13c734396a0e046df43992860f55716e2ad6808781a9516780277007f3f1e69d9c078c7c3b0d948692b2947f389a320ab0603450fda37a2255c427ffeccd59ddbb799c9abd38aab8f85e5b5743adc0996de2f3a2bcb07326ccdb2a10e73a32420f819a49b10c9992db48965abd7d2061fbe033ffc3a7d22e48ec8a40e39f853032a0628267de0959a96356050d79e9243666b44e152b0c11cf17002302b6cfb88df0c182a3e6a1aab3f64f9d7a1c1ddc9cb3ba29d15f4227c4a9c2a01c9531d6a60e0f6970b9c119272a57dc15ea90af545beb990bd309e4002a6fd0f21c62913f5e454a7ec7d29c23c9e6a68cc70f8c197264ac2a2184cd487916f8987fbce33f56b87e44e242918b1b59b8c656c3fae59a86d7585a1c96390a514c1c252ffcbde24b0c43d64d926fe65c2416032d6507ac7eb4d8cd980396d69bdc967c3fb294398ec6584b5119eb02eb6c80eb814b57a457d381ee6e17871476e8e46cb3f8f42fbbd4071c3c4c8aae3f83a90ee040a6957c2b25e9e0cebb75ad3b595793b4b43fac7b2afe6fc2739a7871680f0d0d5e74f8ab6511c560fdc412f8ec217278158d1783827601c24de02ac4533f275cbc82adabc3529aeab8fddd4ef91747a77af0490ca6f68a572a6ab66984128a5cc79fcfa25b82bd8a985dda637a293cf0da36f70d1f444855428d20dbde825fb9779e76672ce4ed62c840b090aea4b7b8888e7f8ff180f9fd380999a0cfda8c664485a1ac81359d50e92bbad5c1d16bb5ca6a1b5c49b41d9fa37df107f03cd183f01dda42fe95ded82ff23e385e100406caf75349cf8d196408377459327e953dfba62fd2efdcc45f9ea9d2449a5066426dc0e4429eac78a1cc00aeab1837d9411593c5428dbc8a86fa59eea76505cef81d517adb3167e5503197b6989e2e7edd2ea9763a5bdf71c059c5cd2d2163783a8ae6e82316a33ffbcaf4cd163f3a8a9742735779b712ce71cd0226bb9d3963fdd941496a3c6ef6e4e589e8cf0e98c6322f59ea64ab462e9b27fcca7e817d71b803840070e5f0530e78b0929bc8cbcfd7db21c516f0d09312f56a5942f6817a14934c25c05e93fec296c007c0a7ef78956185ac66bddba5415e5b9fb486dbf116945031e055b50980db4952a557ce004b2170bc3220b24c5c5d25788eb81f848faced73a04ea03da1aa28ad0b315d39ca6acc8137ec3446b124e5d7fc37543eca463d128ea28a590465263d99ab4e46bef9407dd4d524fc6d4909b60c8b9eecc7e9bb066f7497f5bf175b238f5f0f93f921b0ce8702457640a29a58475f5c65f2e05bcbd175faad1d6d924311caaf4a8bda260668a0aeee2f71180a478be638d2f589349d00d3f34fad8162a17724c5b38217cc2592cc243b68e6e50bfa9833ac8187c591561ebbc88064351586f12ee86830cb55cf54ef6f3ad5b6ede7d8bc8e0404699576b970f27fd3a2208e557c08e5d78422ee199621a9526915ca208dd46d47a5e8e498a1a4a14fd04d8a3b11c1f46636aa33e04c58d0a41e02acc7cf9cc4f3bcf56eef0e6713f82fa447a68f4016a20fe801654e53c9800ca81ee36ebc914023e877f1719a884df42673f8db9d6c97f10ad8d4079d27972a38a21347ba54c26dccda06cffb38c634c349848404b5e2982d3a9e89b1f36b5ad110121f4b7f2e341f529f1789923c4f63e487ea4c6e7d453b17036086f2eaa6b2a1b32537220061ad28b5ba38e7ff4016f88d55b8776fc399d76960c9ac843a603d7c3de0a0880695f1e3a012fec19112ea0ce2897720e482a200e25b5ecd8978bc0fd740b7b68a28198e315af69b72ba96b914884b9584ab4e790c17cb8f869537958bcdb703994e1fb60347fcdc055c2531c7c324a808c3207b06f49c93fab2840c73157df030a849ac5214c718395f954c8fa8d22c50d3c5efd102a1fd1f8d9af866d1fd33c710b4c1ad6071a78927ea49152cf02158b973beec712c4c9c2a8c334fa39350d2a63d44322f4773f7d36f2799c12c282485522cdf082c42c22ecc4bc4d96798e2e4414909f0cf1aa9cc679b8173c15f698cf566aa67a38e0c624f285beeb45266b50a36b9fb5bb42298043b651a96a025338a9922c85b0051407884c6431402815d4ad534805de74791f54116fdf5f660f55b42e1f0d720524ac33abf82117fd895940f3504424587b865f1f0c41ae3790b0187fbf75734ddec53b835ab89ad21323dcca4a5b525147696683b5597942050b93ac34734b60ea08141ea2feb5f9b481776fac39849392c804fea871dbab10a4932758b351d6272dc1e703ab14936645eae276c1caf2de4ca46d2c17bb957c74604fbe8140a39945bec5bbf2d3310e6bad6f5268a02598f78f3c0935f98ae4d392f72ce011290bba9ff6b195605e4a446c80613f4f96b9bac75f7be3395a55afa4703824debd05bfc971a1ba3808133d846b05b25b2b632571cd915dd49b6d8714666eb802d70746b2b3d8e30534932c9ead8641e4ed3f28fe4314cc7a704e3d8a9cfe87aa79fd547b9d46d09a433330bc2da55ff9c1cd3dde730b80c8d633b85d1aa846e6fcbbbb8701118ca7e28264ad75391705fb45378a29243ca3dace7a2cbfe4662310d0e48c540ae29caedba741286b3e1c6462c489888666c6de61526a49398f7837392fb37fb5f9cf733b3e41f83f887508ce27d02f132e9b9f49527f183a939631d865e8af4b0dff16ae4f3e9d59be9234f831d320f32ecb3b49b6298e478e9765623e72319c4238e64268b7cd82c796a704c325f0a33aed6776b630e619754a698b6705120255e888aa41e31fba1a7ff6bf4e3b3d3f9b65698fc99026caff9d7d0d35b4f30330fbfdff8340db5f088bba81c69e2f8a357175ec24d47807a571a8d4980480dc57c4f19224f41ca3d5a9bb097a11321f0b887f11f298c8e183d7fa39ade219468e33a0cb982af92a36d110c8a1bdbe25744304dfd4e7f82d4094068df9456c4eac8a1a0564835385e2753e425169dfa2e3ac79d8776479eac4217411f876659a2e6251866927feef00df0172bf2fa6958497878f465db100c840f570b43d30701a7a69dd0d15c3f7d3cff26c68de38fccf98e08630c875955f2fbb279550f6b6a8016255ff43265dd7147b216003e73cb757e7274a6b8db45e09d14ee1f65a10c4039827549d907aa7b2c4d49b91e69a2c87b419c926daa09170eacb505007d65db0964048276724fac7938c6ff3a03c94ee3300e26c1eb812d53be77882c629b3413206afa725e3ba129bbd21ce3aa59459710a8c1ad37b1325e395f1b76fd99586165f2584ea7c4bab6b7f29abfaa3c3ffc445049bd63a5a8ac80c4427793fe0da1f9cc9c874a7255c659ce06f7fba9268e5c5a65fc80ce1a09b46c31a397958950681f68ba0b4ab3133f9abea46adb5bdead86ad0b41c4291426d63cafb13e8ba7b8903679b68a61e4539c67cd2cd2297f207513f1a4a4a04c6d507f6712ab8a018948f8745173e11eddce10709b5f0ed3fd468d9428ff6c17af0bee593adf0d659c7270ab6808406a84bc38e3093947d83e68a58f7016b8f793eee0c56bbc192605cdabf5033ec631c6e7a1f96fee1b531c18c7d835da8ff197909015c36943a8924d8ad17ce201d02bd1597c687c7a6aa931243c455778b8686206f1d8b0f85e0b3c682f020f8ca68e1e449a5bffba229b9ac63998900e2a17fab450b42dd759ddd5d88ed66af790ce4e86af5f00bbcd043fe307156e11a3f19c715d7be663942547f7a26dfca02a5e51fd5408e95eff0a238f54ddea711c3a553f95c0138134e3b88bd58d16df5452d45ff49b584a55887cdf3b5a5a974b479773c3bd16745e8532352f8209d119e895b2d6b8743a24aaa8e6050c5c2ce5a20ee5e216e0d2f877d74a9af1377daea08ec82bcdfc84909aad2668d213d610b57a7c7c1b69eee9eb4c01d5f6596c2ccc40f5ef987fb08fdf656fc9507d9534eaa0eb06c380a3bc36bdaf4bba1c6448f45d5224ef4c7fc323c49e1d19a549597edec6b8601a9d7645649ec6a90eea60317aebaa71dc6a5becd27e545054d6fa91a18ac74f39c0b60eea24149834a46be648e67c6b54db51b72b6b3171d96c1a5fe424fb891bfd1ac7880c8c5e3e20b36b98b97d6976384aae2c461cc3095e30688be508b4d612711fb44d38358c23e7b3e4aa096f55194456a8bbcd39cfb6f6d391479d5bbf6f01502074ae554736c03a1a35c84b57f311aeab09526764f60b1a699c0add250729f874c7188879ec4d5519db80e5518b838fddeffbb09b944aa584cb8afed2d8656069bf508484c836b5c62306ef48ecd6f29c545255ab8d4ed05a381a6be99f179306f8aabd0c023fa2e92afe6676843e813d40572a3c9b0fe45e29ba28446a7bec5a4dc003d16e9c73efc278ab3d9c49887ff049622d36843269fadee921a818cdffe1704f49d65b9c912e706c7c6c0100fec3341dec1df6f7b0b79af4e028c81df7b0b444a3116299d643ee76aae886c6983e4c830130150fce823b98c0997747525939b7d478b33eac9bdca333e84d8eda2ccd8a6afb99fc945f45ace38746b8077fa714bf5b0fff4ddd2478766dcff8939b83408f3ec0a631c66b6955ad2e47f3ccc882ef70f0463fa2d3ed5c08e8b4cbb122cfedb7ee0e5492d9b0b10c15ffa7b4919ba1df8ca5b7fe407eabc01d709c8f6dc70325329d762f1e982a566d957ef277ebb0137abb5772607c4f10e2380c57cdf5bafce0c8f7bbb06321116f8a432bba61ede3ada40f613ad5a7a99d1dbf493c33ff93c856bfbe845f0bcb6dc8dcde024820c7720aaa35e84339ad5dc36c188be507f59739dabc538cbc33f0bc890ceecb3f833409b90108c2689429842157424bc24027a04381123fabc344ac47fcdb2a76413dce171c8cf556519029c449298caf0a46f11a624c666701439068b5f9441c66371d8da6f223a2290b37034c21e77244b36f2092c56694aea81588a833f1f5e874c5582571541b74868e820092226a0c2f5afc99d3bb41ab42526a0255cc37a4564fc353d31d854aaf8f8cb04fce44af68eb6bd6d960744764d433c550426f2174a468418e5236e797d161d1e0339540b9392a17b278eb197f35dc0495d9db5afe174b12776c1f7d37986e8cca7f1070ba42197b657302eaf66c9d064be776d1fac591ea5a14632e2277e2e1036a89945774f6304c38f80934d73973f0f5d6df938a19a2b604ad0391b4545587864097d75ce2315c898579a2db1bb4d31817dcd3291df3324b07563176ceeb0ca95045960036a7ccf07096c8646ac4ee85542309303099cdb5d6b083b2409deca3f624919ae99a55a4af2ab615132dfbfcf31bf9e47bab2f26dfba40ea0d1e6447c1b0711bec179a44b24ba861bc3c0e583a0cb821361926118368bcc4aed6eef5dbff1960a591f8cbe7b85a85fb0cae00e3c06bead49bf383f457e90e4fd42f2a1c278260316459a7717296ad02cdeceab96f7279064910f3d88e1f54fc9cca1352a584372ca2b783076db426f83b99470396ffcc34f51516dc7c20a521c428cf24c2a9daa906c8ff71194a939541cfb4652ece06b25cfbdca9084ba903062403dd99b06c17859084edd51bece6b3630236b0c3abcb7cfabc9d0f523dd2eeba242e948b5f41525a5d100f2f90c110ed9c4a063e3ad102699b4b117a0d6f719ea095b3aebafc4a97e1c2ccb7b3723f7441e60485ba94791f17f3b27dba399e6d2a71aa4d57b36b50a8efc44159f846a123611280d31b8c72ab84a23b1c9a508985e7880c1adc0ccb8b6b8d0fbc0a9b1f2d01e03eba62cdf9bb26f336e1dea51a1f6670df112b479745bf2c0106f247b4151a9286db9db608174e8354a351eacb36783f47ffb248840368152a9b94dd759ebf582d505c33682815b44d5fd06b8453160a9bb9bccb8c18d2d41f98ec2379970b9655cc793250ce3430cd36ccc62487b9ee68f711037bc4f1c9d7a12541633e876dab37179630cdc8b191a0338389c86118e74428cd058cd0930ebf10880445bec07ca0c68d5a43c90be206e23a91b9526c994735605e46429c57e2a314b092b4df653249d4b9494bfa75bcbd4a604fa30f1b1fbe0f8c1f5c10888927aea0ea5c992e2e139c92dd5240941f041b67f680ba4b5f8a9816903d906b22f5d7109ee68d9a435552423b9665ec9ad7c7cae208aa1afa4092b7563f32499e273b65549a9243e2a93ba9965efac29119a27f714a3ac21885bbc5437691224659c2a1f477d54faf03448482c52e54b9379aaf352092f555252807840c6209420503e9440ba209e2083809cd4537d386b5352252c4d6e29614edb86a97c32cf871242ea98a406a11864092525b1441fc4ad2ce53769b24a0926db1f640539f95305481b1fc706d9887eea83d2c49eda9c02903c10d5a8b425633e666598c2953d200b206710df0509894eea2d4ffd30b94c71e60c04ef072d1036c83f9093a229d7a6927dea82d2e42cc56e9e94a7849dfcefa5442e4dae29729ba96549d597613efe04629b6e4a9d34314b2137b5c54f95ce0508fd402419a55a9726f529c034ff315d9093bd53dbb91aeb0900564a546ee2c364bc521d97dbfa9496ca928ecf188c2053cf49f9b0f4641a0ea77f2e8ff6d8d46d4d0d4df60fb230fcf074570279850c4c874893f3297cf6f82000a11348fffb94bec44c6592d829ec0d5f8a34157cfcf810ffa8f621156409e22e90b79f14e2335ba6a96ecefba01c486ddba956d6c087493ec5ed3c094ac971c67691c2e18c6b9f6a5c15f81b0c8bec3db28e26d494840c44c9fc943769024adde13c59521c9a4adc5262a4c98114b11949de54c9d26491ea97546249ed4a060159624e2526fb41dc5252439426ba94d4040ee4c49372cc54724a5d579acc53cccc257a2991a6db55aa2b19d714c8706c5db2c91c8203c9cba55a36c75a23201f1ef96cfad285d929bc844e6de7a961a4895e8a7c2a407e41463ea87ea08350a8f8e59705a1fff9948e1cfee3ac424c1f48ea1fe83f16203a7ebc3f3c344936d49478906f9043414eea537d986e55a941c9762a25644cbb680a53c7e6494faa4b733655aa4bcae2c36493eac66c2b4c65128f0fe28f936b698ae2afe21f99759f9b073979a504366b3ba7c695266629fc045f75a274faaa2217bb48e0b4a43665880f331ff73eb87d3c7f88fed8f4248e069379a2a50692e63fbe0b228923a5b834714a71e71c04f7c70d08d8414efe290a4cb7f694e6147ce4ff80cd879276ea434659eab6b37d912b35df312225a2f823d444eb1ac4edaa155a9a9ca7083701c04b2d4b5404599b973a3c911daa05f48a09495d72aa1839201bd4a5f2933b402c8398e3a8daa2a549790a37d10339494c1992b8068964941a4d9ad8a51c904a4e537b25cf40a08c4bdd4ef01f67103d484e10f78f8ba8805344267b49eee27cb1f4aacf3e813b1a1dd98b16a843e7316c1f104901af826c927374b9a1c347da27b5602dbf0ab01c1fb8f56b655b450360f962411f391872b71fa5924b8abe04013181e8fdc0e9830fc809347523d32429c5779ea4a4a49c6d3fa97632ca87c98b4a9dd440810c48803c47bfaf9c7f18579ff79e29a69c68e90df2fae1a95541d731a68adb151258f2dcff57c339ccf987996a69da768e193384048f4808a59787e97f8722b45a43bdc820d560b878e737f5a98b0f22085ad755d9a94247baa74fd33129e71d3906266344686e06c2a1362816c9969fe771f27f25a47b8e9eafec03ddf9f05c1b817039fcb8fe05b22a525819c083583692d5bcbc3588e81655bba2984fdbf8c47cbb88ea78d11865995e9015a11c13bf3faad39358f19cfaa5c0993941584640b5338a3def4e93f2e2373171cb139e6c9e7ef1bc5a8d0d1fe11123cea0ce95ad133ec07c13669403a577ca9c195e5b204406f921be1945836528eef41420d7b9bbf98a31f2ab7ab6023f313a4a7a32a8ebc5715ee942293801e6087e1bc69d8defc1a4d0b127d1820ba73772bab81e666adfefa48d12a9b7d41a53aa5fabd07506e475c7132e12eb8e8154a08177fad1e6eff533de47682cc276f59810c4c54be2fd97d89fcf77a1493379bc9ea4d487732db526cefa6295b55f9b6bb89865a3668d9b57da1a26f3a62b130a073355c4b43096269cdf4d3396c466bceee8424de1e7a9ee06b5595c4e2edc2fe965aa18b03011911f20e4c5b998fe0867b49e3db21450f3cc776372771f6971849fdcc18daaf32934e3fc836b2c5f20424682b05538345d7877113585e3e086e5eedae85c18ddc182df18325f49a7bc20a1bfaf2e971fd3226f841bf1b5979c9fc63226ef350ae46deeec15c0b2da88b56675852648a41cc607c4412e77f54b973072ffd8bd23d54f90db12f69022f416ca1729c5311a44874287682a8fe8541c54c38832ccc4f593d74b11ec9f56399fe9be422c3fe85541e812e1099f425ec960e9a678378e074eaf266c75df4493422b716fc5502158a9d12d0af3c4ca3725b290722ca82bc8b46c8d4d564430d04866fffc2cfb089bb21162618c3e34143cee17a7841200bad1f1f2e9b0463a264c2f2f23143725a9da1dbf352575e3ae3fb5490d98d5911251364436703ca64758f3d44d2ce8af2f06b7f3e23bbc8d5b2283ee0565df951f7a868d459824a2355e9aff4cde21428f649f1a1b407033930a5c0806a18333866004222e56c84764d82e8c8bcf14961227b17a391b0e22133746bedaeaf1e3b877403331bc4824b84fb6609377c395c0f968f42d3d87c54b817e6d1b404c7a12bdf8c656cc5a7ab15833ac8fb88a4514687be0b0bf4c575ac4bf6425e30875cd6a9dc06a39eb83be4d87b8151fa8cabb4dc5ba591cec386ba284db56c5e57c5327baa2ec6cdd2a39418458ec4418574cd62ad2b1a2f703adbf12c7aee04f10b90088777376ff809129e7257ae6b683d35239d9935bbf6ea039a6bd48c71269d6c7d2ca1bb4f25285ca0c438b51ab39062fdbcb4804a4872b865f6852e8c5222c8740d014a818af194db27c329abffb977ff8eeea2b93bf7e8d3504f2897b30fa7dbb2ca33ce86d42477ae10f8da67e726df736cb209a4437b637744e61905620b0039edb01ecbc7787d5c6db1c05083cff68c5146a0e2dfb11a7048592ca49f2b8fda48272008bafc549d2b5413084f8e8ad61166207d13d02c862ab4979ccc7fd176b29b7c2ea149cc23dc23b1fe085293fe07d4e9edabb9a18939fd9cbe1c9e793a895d78606ef9687b34ebc727c8b3e781a2718e222f0a115846427084f117922941befae9dc69e60690dc12fca102a9f34a7aecd449f3056af7cc740f6de3f46aed6789842f82538acf70e13deddf6b3bdbd7fa36d225d89e75a0ac57e7a3ce18e5726e713a3af92a4d298f0af4377938b674729c9d6562cf55d191766823904d46e068918b096086cdc7350878d72fb6bb50959fec66780aad38b7d70c002f1c2c2f35a35c9014dd69729957f879421b8a7f9fa60b2983decb9ae7b1a8c903988d4c63a24e5a1a2c2566faec1a4cb0a8ddb7258255aa9c9d178643fb533ef6b81dd5202fec76ea9ab5cda2f0239d1b912ed8d505a8b2c1d137862b5333ae06870e01576e8634a13abe32f704c0d5f90a5bcf7f90a7e1bf108d927479395f9e60feba1e2debb9486df0c57eb65fb8bc363ee46fd33cd1d2bba926d9a1af52d96878e8db07d0e46a457bd363c6e9dbccd48cb7dcfddd7d020fbeef1625f01fa2838b2efa633a8289ebcd3a9898a8e41ae58384757823c33b83c101c433f72b3bcb04a5bcd3457703168b3209da7766423a221a030b78f5b47551ffed4a545272b2e587992f3103013e1a83c39cc7e60d3b7d43f900cd1c64f788ae9f622c755b1070565c1ab478db57401e048c64c402df67484a80e89f3da389e9b1b13de5fcb9ad0afe3922e5dd814eed26c4ab6ababd15b8d72d21706881297352356b38c614ac3ed7e780cd76e1ef8bf85e074116ecdf5910ce3224f807e9ccf3c073046e042c7684f84dcd91ab46d469f5f47f3d23d9f76aa4e577aead2b2489fa2814ec840b0cf8607958d7675cefe8e3501054c3c57a9875d85b181404d743fe74575a049bc6fc90f17a0dcce8f7dbe9c44a5ba4325b32cfde61bc8853636f5727d467e5d34b21c36b27197df0062a96ec162d78536a17e1671327f52801d8f705e5e1ea4aa0b85f96d1b6547fba5f51c525feb8d66c0f85a99aa7428f24befa898903c4dbc2beddd2b5d53dd9c254eee5847aac7b4fd75762f749b91a839a16899850cf5826a66e79cc597655b565fb66e05549ea21e32b0255368ba28103844ef70859e4c138b740526ee876ad3d4424062bb40fbc9991f1b2dc4a1ace098166c937504a3644f61e858166ed401c2a0a7f65d8e2fc333678c23fd9771f67c25eaa72e21a4a8033c303be39c148d522613126603d68ebbe96694a291c08cf57f8b1e0c9988c97d9d409caba8313b4044b03c54063102aac1397c2c255523014c4a04ee9405fc104310a0449336c3300cc3304058954f8250ca782a19029a7ed408e98d90d685c84aaeec9e26686336b0ea4305a1587d439ce616d9dddddd3d466c3f1d96577ecce0dbdf018e121a12700f313ac8441518639865747024a3031b50c57f92fa47fb63352b371c375cb678171bed921a8793b715c399a82de5001339d8420e62725059f51b477451ba75200dea9d6afc2729bfe4d662fe48e354e5699c8aaeef5494842447af2da96345abfea290b4223199f33c0b1d97250589c92b57de702b8b9596c8ca11d52720549f24aaaa7e83032f3cf9ab141cd888e1703e1d07d31042b8b8dca00c8c3196720332ac0c93c218932c73832f20fefd0603e8e4c9032c63833230685b39e2d2963e2c36e02fd5c7f3fbab46550f468e899a5a6e96d462830fd48012109ff2472d991a0021538311b8b48a6a2e2eed6592aa8f2e09439f45cc6b01e3278b98d722e68f48fa794e6d1a10e2923a3f84656830d1a0f1609919008231067d0ce7af5c7df6ce8d7a182be68fe88fd5a8eba2a822319cafda77d1a8a0820a2aa02b4757e670f20cae4006632e375ecb3151ed4816c218cb826566408431c8933f6a5607aee93b1355494874e72d2b457f1733b0c11883381c498b19fc4bb63a9425832f64647031c6ac188ab7eb43c9000031800463cc6533651796e7d476699d725171695f7d04d35c6eb0b42a9a8a8d212e2d07f49cda303fb1e02c5a74ece7c2b2aaef7cac89da56be32b77285359a8bf3b1b2e0405a746c3887628cadb04c189c600ce2dfa58fd52ce98a988b7fb42f09c622c117d553fc638de08bf6514b186c20c117d508be68503b4251296124a17d74c1fc9405879355b268566e2d3a7424a17670387967d1a2a30ff17c8e5054102a8281c1236030068362fe08cc4fed860bcc4f2e361a079272bd9581810dc698162cf38240bc400c68bfc7b23e8de21f968f2e9effb22609078793a18ba28abc60c80b7644967901025e8000178c8231c6c2322e408331e8c6b331a4e588b9a424cff1742895a6010d409b8d257dd4023d12e9a5651797e9d363fe48a3f8c7baa68fae4f0a091cccc27e74ece722c9f3890430b62bec67042e908289c1180301cbb4a00c66515191cbfa549ebf583a9225e4b2721015eb53b5585cdae29148d7f47b02f2d61653f5d961597ce271593988e72f96cbca417e4f128bf5c3ca415e2549cb65e520f9ca3cba1842e4c8948b549f1d3c53412c6b4b1eaa658219625d497ee54aa15aac9567e140a4a8c5e2cf790a633b52e005639465522081698b0c0abca85e26697ff857f979cb4db3a5172a0803b8a862008498031fa630d94215a6104602dc50851e2c8000638c4dd1b2033a30c6b0c018bb021a54608c4d81312605c6181a8cc3c2ffca527565cf17e15bf07e595490cbfaf0e00f43f1098331801831804c4736152449160275a4cbfa8aeabc5dd4ca94e4a1e93d54e39ddcf877f24a83f257bdf109876699a8d6382d664a6990657d5c5c5a6ed65b9f166d298269385ca8abc554b9ba266a73c97a99284e6e2e2ecd7a0b87d53bf53c5757b3beb3037271a12eeb43894005ad6591858e7c651e0d07af88681a8e2c7468349df32c3a3a52c4e2c942fe2563d15a6b28682aad3d47f274a84d4d2e2e2eb869a041536e9daaa8cbe2783a39c93724d2b5f3f3962febfa50ad53d0c7b2a4665dd2f46996545dd61789b9242e7d4e205dd34b5beafd634d40783c128993e2f9d7e2391ffed1667999a4cb7a2e5e26293f0fabc87338921630bfc29f532a96f57b7f58ac3c30664981c52ac2186344ec646191da741dc99d465dad7a5e516d92289d1b0c351db1a4365156fe220ddaefb1da5799b39fb798ab7a0f55795a067ad869591968cd4e1696c6df7a0e591d98a4eb7d34cd03fa23149249b274be32ef548bacfce3b22aa9376ea379fe8f749a90065990a5b138d0a0b772e469df3e72f92aba2e6a6a5d6a376cb4b738b947835c60fcb8344b6a2e2e94d5f8a76a6910ffa87d9a8b8bcb57d4c5d2a0cba238ed75a7599fe85f5ea4eabaa8a951497eba266a7769fae86a2e2ebc4bedad4fdeaf9bcb0d1c1a4d8b0d971c5dd6577ffdf74e752c69cad1fe4e7b99a4233b5797b59f2371768e2b7a9138cd82a1389c4c715adbd265491c4e8a8e4ee51b91957f68342dd7f4d6579eaf6cb42d79a8ab658f270b69d065499ce692458b0e9729ba24980e87235d56fb89fa0e65b5b7f2b63e26e8548e97765153126a4783622aaaf39d1c93d4fea5ca9c96db23c911171491f699d327a9bd25c1541f5dd49ea41ced36518de7afdaeb563d929fda5754ffeaa36bfa344b6a9dbf240d6e19b2c29f37ab4155f6e0b03e9484e42b4f64e51f0d72716931ba5dcfd220fe96f59baa3c372ea95197d501cb933b369ad520cbf2642138aa8fae7fa1ae767d3c9c7ca34d928d9606f100b2a5abca9e9627a971389ffe9dbcb2dfc362e5aff604846ab1f8ffca7ecfa65abe43a94cd40affea2f8ed483c3c92a00807981c277b2f5d6a7fabc487da236a756b6903c0025275540ca4852482c7a44d4888811cc43b51b1bb0d134cd8a766303ade1d840d378a8866303cd466b1a1c8d0423784eed280bd11058d3dcb091db46633d128993379eff231c4ede54dfa7cfe6774575c6d88d2830f692adb0fe257b369efcd1e67acb7a4eedeb2d0e473ab2892e09e62b932998dbb04c47c0955f2424792a79d70060838645166d7acee6eacfd9700e75420522d4f822007eb82cdeb0c0a2ddb0313d07da95a5031b64f87c2d47e7bcd5b0c0a245d0bff0b72ae8393589a005884a0a911495a6a325e1520f8fc7450040329b02ccaaa26b53bd674bd7771e8974491da9f214813156030896644957b6da454d578ef257fb3d31578b9972b32a4fa3aa9d3bed0a9724b983d274b41b497207c58a13d8e048bf9b15ed042e8c3122943104219011042030c64a18c11813c232d90e9687e25226f2df230b812ccb2349315de2783af9615c5cacac9ba7f3d12d5b4ff116c399281c144b83fe9ade6a2e1ee9a22c4e6ed64b1c8f27b7eaa58fac4f7b98f67b92ac8b5a692e2e2eed8a1b55b42b1a15ad8af6df37132559361a16cd235d9beb3957dcf048978dcdc97b83456bd773acb0a27d454d2f128ecbfa542e2e2e0dcad1d5aa7f9924fee1ec496a31bae1b8e191ae464d365a8e184e95395247b25a6ed3a7ca714d54d5db94b78b4b9bf277da05a441d64465cee7e5ab3649ed9a7ea23cd2d55af557e6e428b749ca97a61a753597ffbe7169dff8733ccd921a63cc25e6922a4aaa3c2ead922c8aca8047ba2a094906da5f1f4f87da8dba9ae5e272499c4fa7fdf796d2ac0a9ad0835c0e6ba248d0802f2ac120ebabf61e6a5b6d925a455dfb9134feb975aab1b4ec47b239921f86ff7e2ef8f3e943ed3892bf0b8a311606cb7cc101c6a60fc2e1a44456fe3151f862798e47aa5a625e8bbff2c562fdcb9423959749e2d31311528403b93ee59a3e944aff22306e41b570186aa57a89e3f9bf280ecb165ce2615d540bff5d513dae24bf92e39abe13933d1b1110823156220324b070c181e04bea74a8fd1591951f3157ff8b42f21591166b43550fc666a2a6e9456261a99e4fcf42556f64a256186358b08c1774608c415f7dd4acffde1a274bd7f417e5699ce75f695e788101fe40da45ada4641ee1029679041258e61185e8c118bb828a0b30c61e701b9106cb34e20f2ce3080cb08c23be601947a080651cb1049671c41b58c6118e60994794b0cc236cb0cc2372260449bad801879355844096d526ca923e146faf2fa9e1c8d527a555548f06bd40df20abe5f693d4a9fe7adea8eb4845c1b4180e27370e478269cdfa58d35b13b5dbf41615ed567d281134c8b23e57aeb2c793db875395a73d15fcaffd9e244fc55b5295617e4783ace91fe67add26a9d33e6a691eb024fe2f8d4b3c1ad43ebabe235d4da559a10a96d9c20c93d47e4f54e3473615a47aa94bd7f41cc8265f99c717d9441d325bd8608ce5265994a70d2d9880a6af32919245b484813148ea52f548da172d2d80a4ea6a2d465a3e705154d54c5a5a180f93202c6130f648a4ebb33fc967588030887fb4afdcb13a7f49edb2fe45e2ad536d05f38ada5f245f9907d55b3a1b8dc69a28c99afe4af21acd5551428eec8f548af8e07a4b118900057f2e21a12acf2742908431a602c658490a580c275f99b7d6e23cf9b23ad2e5e9e4cbfa8e743502204002086840011c000102642a3045039803189041c002ea172c5353c032550c96a966b04c9d02cbd43458a6de8165ea2358c6028065ac0458c6de6019bbc23276b38c8d01cb581eb08c3d02cb582db08c5503cbd83db08c9d04cb5c01b0cc5500cb5c0db0cc8d2c7379b0cccd2c7393b0cc7d01cbdc1db0cc3d8365ae1658e6be8165ae2158e67a826534076019cd0458465305cb682ccb687cb08c868565349d318600c61ea080065c4058e6f28265aecd3217182c73c18065ae1fb0cce50496b9c4c0580116c024208182654c30009631c10358c60454b08c097cb08c09322bf90c715605c554d7f4567872c78affead2565813055dd6730ed5beb9ece7c265925e379767db069a4bf5bd7f747b99a4aae566e5c6abdc2ca95952d4d2204e96aabf72a7f17fab352b37ebdad97ab65d56e674a8aa37ab7a9e3f6a2f55cfd2aad79d1702c49aa82429297f245f56cc6b61a21182834330c6249611b2004be7ea4a82a7178985c3c92a1f8d664bd54fffc27fda22478e1c1e89d3378f44baf67338d4de58d64f1d8a4fb9da7cf5d96f4d921678608c798231f6c2325a7c5132001020823146f11f15630f8664b5cea72325c91dea6a9cfc39c18321595f354b6afc1d84112617e140923c9f3890942c5d9c4f47ba381f2155858f0cc12d2634608c699601e2b9a42a4f9464494824eec58f462091f8e6b904f3a8d45d662b22a4db3e4e6fed9c4bd973f04f2275aa7a305a8c6ecf2524126fd4d52e4fe7af2bffa001fbe1c50f213f7030d6344da3f34685b192153af8d0e10311ec868d1214647c70e1230a8c5915e423023e6a683a36e841864c0f3218630d87661128c04ee8e1a387c4c19817d80c3d1ad0345605611620c114f6e286c6d28117c003143878dc81471718d3fc917ce181078731c643f2a000631a3e031517a8442a79b02ae848be549e0a0518631afe9ce7cdeb0d756d1a8e435461471a8cb13ab830c65a586647ca0e1b6c07006c5ccff22c166721c232514952ae67b1ae67b99ee57a96e87ab99ee57a1696eb59ae67b18e703e3bac239ccf0ece91509d2f92c9a213a3a590428a29f85f97d4f93682f6518b14524831c5353d12cc18ebb00c169160524821c51450b3a4e86a9d92420a29a6b8618595c3baa647f256589be239262977aca02408b236c5adb0426379ae2bb241026b04ad435dd647450a29a498228305101814a3dbeb36e5ddae1c5ded393ab7ea3ded9ab2546524ed27aa599394af895269e170b2ca57d4667999a4e95311b1a22256cb547d76581610c6d8168c05400299099cc1988a2525c13197c4c99c12f29e98160f10eb2bce533c402e6a25e6b5e01c4a85ff95a54d5956ca44ad5017d572512b9c0c932b1e5d521744a895074362e1e4cf09f045adece7824b5bfc95b7c4f17c7870698be8e5434df86592ae2315d505273fcb45ad581c08d5a272512b9cf3a154f8739ef247f2c592240bf9972917e943aecf1096eb592e6ac5ca3f284fe75bac685f59c845ad584ff1b0fec3c95ce022fc552c2b13b9a8152ef1e011c3f3579c94cbca3d28aae56592ae956b7f55517b02f21697b6b0b8b4057ffebf93604be26149518b1533652114d5521b0545d3b4ea89a88122a1697804a126a03f9aa67a2256912459084dc08404634d13023cb930c7689aebad2f523d1197ccd4d1342c5300198908d6342c5590625ccf4202a9e37a8936e5c1528a4c640417c860ac69c09058a27de51df125fe60ac695284587ca276440980a269625e0b2e2c39c02a34cd07f9e95311e102ba8031560196819c354d9b2421100703927283651e25585e1acf094d533d0c450426076141810b634df3e28300973e1697b6484992853c820e448038331c0d54524ac08335cd24097112c838c69a2649169292c30cc6d88da6a371e9638513ac48c18df6a2d164d1a243a359d11f162bb0685658d63497d49182fc958b2469b162008c35cd1fc92d3ca043407070c218ac692609f2405fb0a6499285401ac806634d933d9eac850058060a408e4c4cd5474fd5678755411647ba38d2958393e30bc69a868aaaeb59a8a808630c0763260028f94409274a3691614d4324470dd63498134d832748b0a6e1c118fb82b11118c19146d3dc2882858d06a40bac695a70a0d134485a90c09a4605c70e7030c5141a161c9c0dcc4f3882340d0ec79ac6baa44ef59e0d0e14d6342c1407b3aa20046b9a76e556051c26205564815d997b3eedb272902ac8a88205557cc19a66aa800d66920563ac08cb50c107c6ac0aa2c20c2a6ec058d36ca86bc3a5cfefccf17c655590c5a58fc5a5cf8d51dca0c42118630f60991b70d8c0fcd452b0b992fcd452d0f8f41e4f66253d4aaa90b961e4c6ca8d0f549bfd48a07664531c493fedaa1ec9446dc6d81965f8c0c6188c311bd4468e0bd8c8d830e9002558d3f06f1a1c4dd370344de7336dacbc89ac1ce1681a1e1dd803634dc37f927487da2f5f75a00c8418842002a1f22ef087143ff4e1035658a0840f2570b8c25ab18102a048220f01f1f1ec89d0c6556a7eababb1c73773b4517db087c6eef9e4ae5f5be5ed357e549fb5a1b87cb3ba2c9af19e9d52fad6417df98e37aa8f93ab179e3df0557f5deb82f3ebd049076174b4d66bb7d860eee2e3976a68ad9c574bd74509b08cefca1e9f96353e9c9004b853c869765377495fd7f70b1ca3ebf3ba6da5eddcf538af4e8f239817d228ebb4d7d62ae5a3d7ea295dbc11dedc9fee1fd537ab9f26202c4630d4b54fb71f4e5d35fffd1ccfa7c58900cfddeb865043e85eaeef45f571ce4b7db2bc67af4e8f3abcc0714a2eed8bf6739e8fbba83e4edec847e7ed02ebe9fae4f9716eedd57bdf149e8958ba6264f3c205ded73e0a69779cd7dcbb8c8f7e456db8059ef36dadd1e918297df4a6dcb96264e3450bcefd27ffafbac2cbdd3f28578c6c7cb06018ef768e72dd699534febda20896b7ca07f3eb13baac698feab3ec0b01eeb3ebe7aafbd7a5be5047f56de29b44707533465d9f8373ffce36aa6fe21882e1ec3b6a1b337cfcba68dd0abefa56db5ddcda42f72c052204f779f3d6fa73bcf672a83d1d29ea20f8f9df78ebb49df23da3d5025f5b73a73c7facb46ef912080870e7f5c9bda18c6e423e33d527205b9a3211088a75c5c8a603c1b2ba7be77cfdced59bebcb245df7035f39a3ae9172fa32ceade39b28e9f25014fac0bcbb6bafdd7bbfefd7e9a83e2e6dde13735594161f2831530a04c54cb985c615231bdc03531af77c5f73fff8f286517d930318f3cfd0be6cabad93ee07a3fb8207769c5e9bff72576fb79c26e140505a82a0401004591e6af3ea8a910d57c11af23aebfcab2d751cbea8beea2f202a10c45ff2501b82acea2f4fe61982aabfa41e4550a07bd5e529a9c10e1cf9ebeecad9ef8cf9ca17d5f77ba2eeef69c8d4835acb43695230d4d3e96aada477ee0a6954df55513a6be003b1c25ae99e96df48757c20f080eb7456b9657dce73943baa2fbe205944e7e87e5d472e1da5fbc1a705168fea676d872ff608eda49c53f715d1a1b3fb9aebde4ede2e65541fa75aaed0d7ba9af59592ce99e5e49c62d140ad2be45f6becb4bac71dcbc5afbb9eedb456ee9e6395517d1c4f47daec47f2558e2c5bad948e3fe7a6d46ed6e82c845b541c6d7eaede68e1c7da6b8dacfa28fdbc1fed8fd608fb9310521daaf23cd832319f324bdda7edddf27a65549f87aaf87312bf77ddedbdfb7c36f3dca3faf623d950577c11d30a9fea4b5de67bc3dba3fa7ea2387bb3a52b5f167c0f622ee18d71ca87f02bef3baaaf43555d42d07e24282d41503a54d503827640d05d31b201e3e16eebddb1eaae7bacbcd3a83ebbc35519b785365ff86fb384200e04c5595d9e1358f11c8cf1d949abbc77ce5b257d5007098703e1b9edefdd3797fc6dae730443b23e15d55f3efa79703c1d09cece678a57a72781f0969157b8e7bd73463d6554df263e4adf7e24407260fea08cd3453b21b757eaa8beeaa749dac0d7e9d5e991e2c0d3f1a73b8fbb4269a3a6517d57653f5005be3c4ff84f219d313ee4517d9b6aa9c0794f9e298f9b5ac86be5517df6d51b986b18e5bd39bf676fa437aa0f86aa5e1eb481e7bbd0f187506e4a2594517dd56575003b171f5aede6edd5b9c9a3faae4ebf5b70007f7fb1c61babde7ff9b6517d7c7ab08a600398ba9ca573714ffd9b5b19d517591fcfa6a2fa86a6a0a8a0b4a0b40441d9cf054c2e725f9d2b46364150709f7beb6ee9ceaf3feb60545f95a3bd717f4955171074753a64e52e501cfca05c9fcec6cd15239b2e3480f78e4e4638a973395e6ea3fa505a7ca0d80f4a8f2228282d41503693b4f15015afb0ba3cf12b6acb15231b2c3250fdc6fa549b8d04309024772ed02ed0010b5815a0001513c85f4940021688c04b0420408107d87040061a50e5e7ec0930806f16600105242003997e7d2a8080cd95e42970000354a00004b0c014520cc0c227800e040002a0463d31d140093bc0100a800729947c274f350a8087430c80000c0d9c80440ab302ca81a30a2a6ed8e800073690010cb40b58a002149880042200810738a0010c588002129041c0010cc032122803cb4c40878918046068d00420529827e48083653e01b1cc271c63da512e89638cf52829521282375491c3059d312ec407634c0ac638cb7862005605c5648ec79238cf5f49fae94410c61812cdc6637542009ce804271871a36970344dffca460b0ca5e2b1ec673f1796e449b1244f7e99389e8eb48941704e7e96af88e4ea7385e79c73ce29a594524a29658c31c618638c104208218410bef7de7befbde7a0830e3ae8a0830e3ae8a083f7de7befbdf75a6badb5d65a5b6badb5d65a2ba594524a29a573ce39e79c734a29a594524a19638c31c61823841042082184efbdf7de7bce39e79c73cebd7befbdf7de7badb5d65a6badadb5d65a6bad95524a29a594d239e79c73ce39a594524a29a58c31c618638c114208218410c2f7de7befbde7deb5954e19e1eb00618ce1f8b08c24a6608c591594abb7c2e2d3a707b5f2120fcef9505dc0509cdfa172bd65a54c0f0655f5e03f6dc158898eeaa36b08cf3b182b0141111522d48a0a10c64a7e3056e2034f9424652479da22daf98b541f9383502b8c95f4f8976c59df61ac0433c678648b6f8cadfc8333497d82a1a623d31637acff98ecc91390af3ed7a767dda2ab9274cb6dda0d6a53e67cacfc3a739a956152207ec58b6435220c8c95ec60ac2485b1920fb04c23c6c8c203593056a283b1922b182bd13056722d09b011202c1b1e98c7109515bc2932a44811bcc141b6e03144c8c68b1421d31fa15aec278bce5f4074ece7c27aa13859580ee427e965c28c880063376c9430c66e7c55e204458082591514a33796953d3bf71c2528251ab8a63c491da9da96c45be733bdd5b1324ca37ebc4cd255513aa57d7294e49b22c260ccba2c2909632553305622c50078f0bf720fc64a045012008f276b3489c888c844b6f87efe5f91404e8a88057089e242a3f90a46eba238d645715824fd1c42c8c6e3c91b8fb5258efe10d168ee9b1356962168c052b61842556f2466070f0bdf16fc93483dae49a23c1b8d66e3b186640a1149dac2b7b1aaf7703eb2f07d081109468820ec0783f8438d3a081e186000c002009a200834308be565922c4b12c2181b4528c21044904088c132eb81c083c78a984b14a1b08ab497b75a74654fe37052341ac634500562020c10941f38c13ee1093f8080d510421fd0b826e9732197e39aa40f9df0870ec65a63267d800063ec24c3873138c0873198d507093097c4f121148c31f8209723478912f8c000c618dfc01eced8430e327be0e8684de3f232492eedb2b20709cffb39a5a28745402ea694c785f545f4a0802429549f80c0673ff47dae8af2c0f891f493870d30c6033c54010f35eee0844d68e2e400539c0860149c43c1fc64d20566d205132c5cc10a26521293cba432e198704c4c5051db4a8089014c085072894bb0124420a2c40a40a0a2e407338849f229c1c2022511a8440418636418c317980f7a40070fcb9d3974825b2fc1cc418b3930067139d801e26f6de4800306f115393480c349d99624e4bf471c3a7c608c99ec813176b10c1ccec8f163e3b1a8eac1a05a9672e72bc6f6e60d6c60ecadcc82440af257455d2b1c26c9d502b94c9276c1800bccc7fa6060fa50bd512b930d18684318cce5a2382e1cabfdf691a4e55faa1ca994800d84b05e262986c369d4d5ac4bd2ad4b91d545660d1c36c880ff09da152e9600b6c97a4e4ca7ca44aa4fae3e790d7fd8118a3972b01a6b90d61a28b0860130a6064c300b104c0d69fc257136131a8c31ae0632d4d002c6d4e0a2861f38d490490326184b0321581ad8c018dba4e18c1c2c93060e63696861ccb23450200d5030c6d26004b35e4a830e8c75814a830861302b9346c4ac4c1a44184bc3a661235f99e7eb3b69642434708231563207347034f081a1010a8cb1465d3cef40c300ce9003c6d8192a4e92854054f560583762a6dcba64e36592ae4c69cb933bcda541d7f4d6a7492d4916d2200e55b9b8e4f06248172d0dfaf0666598ff2313cc5317f490d470585c6ad75b2f5d8dbaf2c3e488f92315d59ff330cdea34eb92a8b8a6cba23a54f00092d2204bb27efa97d6a99681ebad0cdcb05eba248ea4256ba2b6d4a1266ac770a65cfdd526c946b362385586fe6582816ee0b0d134ad599f4ca4c844512c14d5e291226b88260c7c608cb52bb7983f12863318633762fe880d763205c6c210858108634dc3a96d858765c040073084c1367f51d3736aa2f8878548ae3e291e89d343a5482318638c584422105152e2bcd0d2709571438bcc07068cb19218e0c108508003263a2d608c95b88cc0086620a20a45ec608c9d88c053240a7960c0282ec1183b7182129e406401606043658c9578a007231822460d8a1006c6d849262251822ba0910621448231567283475ce04605001d51608c95e041c3347c9183130482316612047b201137e8c00d36c1183b69c00ee82089209011063b30c60230841f70a2040f1a517c80315682884cf02005a008c2212a63ac46192e021042f406130c82b11ccfa9690e5de8026356163af624c5a10b3a18eb428d89f2e48f3456056d4bea58b12da9938465b6c008eb3f96255911edbf622ea9ca1e2bda951bc7d3aea83e8f44b2a6efcf5bbe02256af4a881590d0f4080b11a57b01ab006ca16a407c808441980d85a2002b32ac8d3d94972a7c392851f64c1075960019b281e560ac53f2c3ce6b5e012ffb060210e5838833146f18f95afe71f0b0b8d412eeda3962e287e851bcff9704a08c53f2c0d878e8a52d168a698a8bdb19e6fa6e75c8a3692276f3c9645f18fe591ae2958610a23805ca808c645ca1e2b4738fe7bbbac0c136d09c7e7dad9a5a54c81a3e1e040349a2c561a8e1f3ab8b4b9debaa6ef5042349a16186a42230e6888018d4723028888021ea21046148640c11350480463909006b95c6f6daae7f029476da25c5a9ba4e6f247f2a5d1b8b44ed968908dd6792dc78dd72ed7b34c435c5aa76c3417202e2e2e31bae57f9d231c6fe5c8d38154eff9c6bf235dd6653de71687dad445551e8b4b9f2c2485d6918a82794fccb5f14816be8f47ba2aca33551f6d3faf7acea122cad3f98bff112aaab2a0efa363c3d0f7e1d387aa38901b2c0d0717367e44568efead4d87aa3a753521435d4c180413c4608c41964571daf5a1a4f6324938ae8fb6a80886095e3006316100d31218c17f094e806e50978d9659820b74630914b861e38c3b9cd1024fccc5f9243568a2f65b975549cdca302c67786165983332ecc3cd2004636638a17dc68c242cc7579e982a4b5bea3ffd0e332c639c5c2981138c316635242d450974600ca21a455d4a3003a659f15ff50dcc4f94d6685a36309f8420244127a10048f002123612ae4002018ef009c6580858e6085038028b113ec118e382658cb0042370ab92ac0e546f7d78feea88c46997f501416bd08dff54978df6d794abfc79c9cf69578ea91ec9b71b367058396ad35b2e1c4e9ef273284ab794a6c901491e697fe57259cf5d5a0e0882f6f50d87f52fb9fa9829c768cf579ee943c5e0e09f1bd5d2aad7386eb8fc4772b1d1ac8a6a38380efe53b636cfd179d32ae99abe089b2842178a6015a1038c311f2c43044210210c0c72b928aa72699674c3e5f744add8685fe1f82a86e3a12a8f27b71b2e2e36a049aafee264a9b24c19764041192843a8c4109a308496210c410a8111424083108e0881036478828c2d90f1c990f101c6586b5386b15c5cf8f4e1e4cbfaf0d0685a5c7a910dcc4f9b89d268a6211a4d8b4b6b4d372e71abe5680dfa682b26c370324caef894f7e6ab8f35bd4bcbd19aa608f469d4c4e1e4c9e29f7340cfd220ebfa7826c9aade43e1e0e42d5d52b5db454d2fd5f47c7acff36839a08a7271b9380f43715e374b6a56078040d523917e7ff5550f170b9a82c0892504a18c20fc60504ce6548f84d27f49154ce63cb71c10ff5d7d28cbf33bc9371cedda3949101800044630a865c890225356816274668c399601420c209601421740f8f1834c30c6200d703a574404baf6a7baaca681ae29c3b40f15b3817ba18faec6e1484732095c5c46f09dcc6259d557ff7d7a302ca923657ee0c2a0cbca9c89a2ac46595356f9c10052c62843748d118331783006dde08f44e28c51001fb001bad11e89c4791bfcad4ff6b4ab039325b5ef7cacaf527c90248693e2830a4c39258b2c1a0e1d3a92503b3a455544625e0b8da665d3830af000123c10020f78884109318420868f49647610841dfc60508ce64c4320989f2a8ac3b9acbf3227ba1acc4f2e1bfe606c2eebb50b75b589a29034989f2c8a8a769b3e9a5ebf4cd2cb4be6746a82a1b8d55eb798dcb1d1feca558b73eef17fa9a81c5f51d347d7f42259dff958566e319ceff95bf422453dbbb85057c361bd4cd234492fdff8ef66b57fe1f9a5b514223c56528af878ab05b0573de07bad74d9699a5d9f56cb3c44796f954f4247e7a3ef4628b7945f399dd3de0e25b5b0a2ae7c4df080219592bfb99ff64c73ec77c050c6ed608cf54a7d2dbc76c0fd7ee66fbf7d5fb7d7be3ae0beaded7bffaeee395d1d7c49b21014159498cbd3c1d3101415143e495ac454ff09018a0a0a8a0a0a842fe6b540514179d54756fe81a282c283bae7c99d8d7b3382c1840eb8daf830effb9e660bafbe513099039eaf9f9b59bedc56e67f8fe2ef092672c06ec74ae3d5aff353eab60d4ce280fbfb4d69e591be5939cf6a308103d6cf6b95d7def71024231a4cde80ebedeed6685d7d3bc23e933c9f5450e0db3cf8bc60e2063cdf845adaf9ac8cd7698821a806266dc05157fd76d56f5eab2dac6cc0333ff77f6375efddcef91af0b3bad26e2f8c5a0d983be82cacafc22be17e390d38cfccdd84ee592861dc350ddcb57bd54e48bb9e5d6b34e018a97bbadbdbeb843d4a1f2667c0fa3969e595f2be8ff1763360cd2dd4b6d25967add67d193094bfb5d3f3fe8e745a4e067c79ceffb0da0bf753b7c7807787756e67a9e537caaec52069def7cdd8f7d661c05c470bb9ee7557d7670703a63de76af584d35e38e3fd028ef6beef5cffdebaf7eb05ccb5bb715eebeafc796d1770a5dd5a392da75b523d2d17b074f3dd7bdd94bbeecf5bc0f9e1835f1f4af8f8cba905dc61b5fc4647a1e5506699055c63b57d7eb7bad6b8391670a715cabc61fcd7bb9cf10a98427ba59d2e5a9be985cf0ab8eeebb673b2ff73bba7e7dcd7c3a40af8e6edb8e5304ff8dc9d920a583fca2faffd66fe7a9731aa8f6fe2db984ce1e9fcf9e3ccf6f237a5a6b2ba3c26309102bef9be84f76ab9e584ba23451684c449232668e09756cfbcffa5eb994f4aa77d2b46361e308902a675573a35e45a7ecdf1d53d984001bf989f4b786187316abaa3faa6114c9e80f3db49e7ecfcb9abb3eea83ee904fc7ab75fa78ef33def3330690286136a285f573d5faf950998df2defac52475be1b63baacf2e014b1e69878fd3de35fd9ca5c919b8eb1761ee51c2a7ddb5d70c2ce77bd8b97c79eb3bbd37cf4409f879fcb8eb9c77ba19f9c6ca917d49c0f3b9973672f8f2ad12421324e01add73bb9fcd8e474d4d8e80639fafff7c7b75cf7c3e132360a7a39c30e65d04dcfb6ff97e767d9f83d6840898cf9f96ef0c33bccece1dd567520676afe59ef94208efcbf5a3fae04b81c910f096905368e74799fff17b4d84801fac54c3fcfaebd36fd2584dc8c0d57ecf32eaf7fb5e799f45824910b0a65bf71aa5db99ca5a3310b0edf55eaaafe6143a2bef0fb08d99e72df57cda3eb7d0640cfc9cd7d9afb690f6e8386782890ff07c2a3b77b1dafef0f617d547cd30e901a6ddbd6d697e92db6bebb4ae295ff30b131e609bedfc49dff64bb7d537aa6f042662e0e8e89edcc5bf75dafb32aacf6407f83aee20ecd475dee1cef309c14407384349e775fa46ad7b7ff56ec830c901a6b76af9eacb2f532e618fea9bd20407b8c2e872edb5db9a5fce30aaef0678c7da39bd8fd7cf10c21bd577e30f4c6c809d7dbcde17b99b9cef1a601e9f7ef0bdddd5596ec730a101febe6fde53eefdb3f327a3fa6680addc70de2d7f6a07e7ee517dd54406f81f4e37ebdb3bc7c8258cea8b018e3ace9b9f8e96eaf8748cea0b03c70bf7ccbd732d25774f9ac0004777e79b3dc3f94fbe00c70d69ad4f43e87e4f68e202ac2774ff76da27679eaf9ab400eb282b851952def393b146f58d02cbf7249d3d5f2b21bf5a47f58902571a6fe71cbed8f3d397a3fa5e28f0a4f53a692b9cd0f14b79549f8ba0c0f7a18c136aa8bff2c87754dffd04fed961cece71e7dee63746f57902c7cb77e5ddeaf8609dd745f5d94e607be7947946c709fc3abee55bd37fd4f2ba09dcefddfd699696efd71446f569027fbefb520db5ae55f3e7a2fa280d4a3281b59eb43f2ebfcbd7e374517d1313f841089da56fdf1e9ffd1ed5272f813bccf73dfb1ca734ef4e3f57e69ecfe67a2b5a02df09a39c4f4a3769de39c24ae0c829e45cf22c37a79b12784aa7ffc549eb8e0ec619d53709fc7f6da696c6fa355a57a3faaa24f084325bf7667695f27f4b57501209bc757499eeed9cdc53eea852410924b0938e7faeb3424b2944e1f583f2d5c7a2381b45d70fcac642314a1e81fbbb4e4b5b6997efd90ba3faa623f07ef55ebdb39bcf6786f6fa6953517dc3a50f9c41492330cfb3db79f79c905a3863549fc5087c5deeb57f94afff939d47f5f1f81631f3acf5d968afd67ace180d637c562851049e6ffed3dce94dedf39fd3072589c013ceed5c957aef0c7ba4517d9b29ef1b841244e07adddb93735df94bfdf286e210072587c02ebad973d43fabe636770e45e91243600725b5196e38eb7b3d7b54df86e20e16e24125282104b6f7767abf5a591d8c118c9241e0c8fb769c3b9edd7e503e8b4b1f89821241e0fd56cecca3bbf2f54d5d2f10f8b37bbf67bc55d2bb5f8feab326186a731f0c4a0081e1dffdf123ddd0c9fd2eaa6f3351d3a64b2ed63fe048f73f18a5eeae776dbd65654f8d7ec016de87ddb9cde5ebf0eda83e2a83923e60fab5532e2fb49a67296b549f75fd77369195a3076f50c2077be1a57b5ea8e7db9cbe3de0dedf47ea7a84fbc9ac3faa6fea01c32e1dad9fe7760ef2bda3faa86bb31fc9832e28c903ded6d1bd9f7c71f697100fb8c268adadbad6f7d5cd304aee80b396d1eed75a3fc7738f517dd40ed44e5be3bdf5bea42ff70beb80b5cbd972dded73554b5ea3faacf75a5042075a379cb7d7c9afb312be2694cc0143a9f58ddc6af9e6d72a0728ad9536522a9d95917608eb831dd28f1fe58c038efa594b29fcdc73d63547f54938e0ee648ffc59ea6ae59ddaa8bece571b241287aff34590483c9e4b5d4010b4b1f07dea8a91cd094ade801ddd5f3bac8e6f38ef7b282d3ea67b2c2871039e9dcadef35fae3995ffa2fa9ec3a5cd44550892fa447138906908045947282a7243491bb0d39077c79fba18f98b3baa8fb201f7987795cf3dfa1bce2ea3fadc9b8fbe15231b21256bc0f17138e1cd5de6dc6b8cde40c87fda6209256ac0af771e2174bd776ba7fba8befaaef70141501af0ac3742c8adbb0fedddf7af8c44aa3c9bcd8a910d549206b6b372faae7695426bdde6f409821234e0d7ef9cf7b95b39ffb734aaef85e2c41f2567c010f2fcb27e7d23e57f6b54df0b15449ae1ca59699cd53eebe674b5726abfbb774ec69792e6a8be7f79a18278a815087aa138336e4aca8037ccf742e75e7699dafb517d9c4f27aea0b40441e17c84401004591c8ea4ab270241d08a914d554206fc9ca5bf63b4f755bb79d0062563c0f137b4995bde7b7d957708aa9b175322060cb7cb76470d757eb3532f664918f0cb9f1dda697fbb5be38301d7bef5ccd271c87b7dbb21c88c922fe0c9f5f3d7b0d3c869e5d46ebc802fecbbeae878e4b7bb596d0f4aba809f9d524b48757e58b77eb3ae18d944255cc034ee473fe75a5d75cfbddd94e8a0640b585f97bb73f67ed574c31bedb85ac031f7ec9c84ce76492ba7517dd626ba2418088b946401ebfe3ae7badeefce59f828c4c294af52fa6caceeb90b29a5b1ea5c2994eed99b7b74517d1325411004dd095372054c3984b65ebbdd9ed16d6a050cdf5719fbdfc8e59e2e46b525088a55dfe4515205fca4d3ddeefb35763be3a4970af81fb4f7e99d1f46e89eb6389e8e34a780b7bef6d6075ddc5b535da580afb63bffcbfa71460e2b1a985efaf09f7cceef75744701f3bb21cd5b6ab93b7c5fa180f3bb933bfaf657be61e64fc05d52beff6aebfac7c7af1328b575ca0b5f8ccebef85cacee3dfc7c6174ef6bb4af0958efbfb64f48a996d5b90741907b9bb72da923296502ee5c3efe648553f218e35d02deb16f9d65dc7fdfdf074bcec0f5c5fa667f36ebf899ba1d282a2829282d41502629472a1044dd2b31a356f82874f25679afabdd5aed1c95f52fb556962801db9e9f9ebb7639bfffb3f6072549c0fc358d2f6718a7d4efe405420912f09479d7b7ed96dc429a5f541fd5272b2613812008b29b674b72507204ec66d753cffc34fcc89f47f555d4e6fa17f96c8d6fc5c8c6478911f09e3176e79eac1788bd45c0f2e3b58e6b783fdbdf4f049c37a7d2fe7d0eeefdf345f5dd0e55f5d8ef6181a01acbc00e7e9caebaabe1ec5a0f01cb99678eaff37edff9d542c0564e9ae37395e68f938aa0840c1c5fd716ded7365248f30701f3bb3bdcf6f298fbbcf5052901c2bdcec9e7e0b5efd10b69cd2feeebf8853b576977d5a2e407b8e647217f77cf7ffb65084176c5c80649c91878bf699d943bbfad25943ec00e3a692b8d37660961ec3dc0fd3a5867cfddf2c9e7ab3cc0dadd283b9ddb45faf8a6babc3275e51e282a289e2c04823cb90363502206d6eec16cbba53b3e9af79b24ea9a80f07f3b28d901ee16c6a7f3e5ea7a960ebe27a6e56e1e0f4a7480ad7cee7fda870ef2f9ff9d5c6522ef896989b9a02ba32407385b3973779fe1dbfcc5f9a45082035cf3bbef733b6d63124a6e8033d5113ef9a0e5f5ce6aa3fa3cb1a288b81794d8004fe8f677cef7b4b7f6fbf271a1525203ccedd4523aaef5cdf4ea6c1d0db09ceed9fc7e77f7ea841f535d1366916894cc00bb1ca5a42f7e871572b8af295fd644a940f2218120fadee6c98724ca2994c80043a7ddce2f5f68f3d50ff9fca0b4a0e8f94199340658d678f7be52ee6dafd69dba262010445d9344ed8a91cd551206a6ddfea419be6e25b7f16180e7ae0fbe8b3ccb581d97282d3e503c54c503823c54c55dc90b30af70e64de9a372d6f894735eea0141ee6d9e459550e202cce7becfba9c6de602822028e682a0e9dee6d5980b6e9850d202dcabac96e6fc5e9430d279ef8a914d890c58806dadbafe8335feed74d7a83eeada5c94161f281882f6f3fe452008da6cdc5b31b229c1c10a30ddf34eae63ccaff3a5fad215231bad029c79df2ed2cbf5b4f37d872008aa2e49578c6c489002ecb8bbb5f6bd6fdcd5718b02ecba96ffaab5f7720aa10403db99297c6a61bfd7421d7dc4601ee5967b532a9f8ed025c114c68f5742eb1ce46fafa41a06e77965dfb24ffe8e5e98edcb48a295fb3da731ee9b25fd6cae4eaf3f3e9866fe1eff0a3ba4d44d3be5852f85ce7d943aeabeeb73d7c29bce4e5feacaffb9d336aa0fe6a7587db4abfc2cd5c5f25ead2e2b5618f2997b846fbbe6714318d567fbf5b996ad2e2bc61360deaf7b51e7fa2e7cafdaa9e2c154ee987f4b28e7ec506ef8010ac3fd6ee7f964ef5bba0c2b9e3097964639f77ef5f178359f5eeee8e0daabedd95ea9dfac59cacdf5c1c1fcd6871ac6dd75a4d5fef49c0d9c3c8ee0bd5fec7cea4a5d859cde1c9900ebd7f5bb9b76f82ef39f517df455972561feb44608afe34e5f0e6754379604df8df0856ebb9def8434d25e69541fe5d3773ef2e4ea653ef82a085ef094b34f3b5fbd4fe1cbf9b23a8eb1bedaf99494bfb9eb84cff17cae891f5fc939fd4969ecd9ceeca2fae2e6e5a39f53c8317f396ec7e9ad32068fc05176c95fbeedf2c92bad2370df32d29f2eeb17dfa68dc0b15aaa6d94963202d3fbeafbbd2dbf55efb9881d75efd072ed6055049e2e4ffe70db7eef7eae26026bf8b4de5fafcd88c0934a1ea98dfdc9d79bd3436099b3b53d73c7a921b076d46e9bf373f6c90b6921309dd36e1725bf7bbf59272170b754ce3d9fddf2c27ce520f0e53dcb7aafdb2fc23aa520b0acef729d1d3e6ef93f19086c9fce70e7e7fbdaba770404e6fb3d775af22ca3d432fe0167fd75d2bb738c7ec0b0daa81dbc7ddb4b2f8c7dc0fa412bbb750e6a6aef433ee047ab84fcf7d3fdeaace11ef0532b6f8c94df6ca7be500f183a18bbb4b45a9e076c69ad7176d7e17ec92d1e70ac70f23b6bff57ddbb03d6df5f577aab86df1db503ee8ff6ba79ecb2d6fc661df084fce6fad155692d9474c0fb59c72be7f53f5ffae680eb753266b9f594393af7e4803785ba4659637e3abfc701bba734ba282bd5f7ba0d07ecbab3bd4ffde6d4efd937e0fbdcddece6dc33c338dd00a1d5af6badbfda77798d36e07eb5e691f2ee387f0add0e6ab0216b3ee1ebe89e7d2ff25f03ceafef27dde3316a1761dcc454577dd6933bd10835d40037cddfabb49bd758d39073dc6f6ab7dd846f529d06cd0f2be7d655fe7ad4130dd7bef824bdd1c5faa47416decbafa453e628637cd093638d3360be1f7ff95ccf5df7cdb51930a4915f4edf736ff3dccb80e7ce8fcf2865a6cee699c980e7dd9b4a9e1d77bb6f786b8c01f3df5cee58a7ce97be3a6b8801dfeff6ed2de1436aeb8422a81106ececb3b64fb8e37ecd6f0703aebdc32b6f9fd7d6bbfb17309d1a462be7cd74f24db917307dbee3cb953a289ded02e6f2ddd355c64ea3a6960b5872e772dd76ba9de5837b0bf84d5b3b8cdab97b9fab560b38bace6d96f0b91d2d97350bf8ee0ca77cf7d2d6f8926201d7bcffdde5dd59fa68be0286cf5d58ddb4f9e55b37ad80dfefaef5bdd7f19eeb5701dfc8bbbbdaca986394525201674a279d57cf1863a7750ad8da2a73dfefb9376b75500a98726b69cd53d7b969e668604e5db73cbf9b17d6ada380e59e356e0b339fd0c9faa080f9a5cfc527ff5df8ff4fc06fb7bb5acb47b583bb3a01df69a7db5fffda1de174359a806de799d3db5dedbc6fe76a3081d279677d13ce4bad75eed6c8df7bb44b37757eaba1acb1046c7fe6cf54dff7ee67fa7050e30c7c679ef1714d9d95f1c66ba48619faafe6d6cd7a2dadb286123094f96ad8f99557f36c6754df7b60481707359280df3d8c4f2d8fb2bedae184410d24e04ca3bd356e5a798d2360b8679e90fedddf2fccd008f8e9d68eeb2c3395fdf2579f50a308585e5af9e6114a7de77dd60935888077efaf423b697d0e5fbf25d428035757fbbbbfa5959b1a43c0b9c3b7e1a337760adfd3978b1a42c0f6c207f7d3aea52ca30619f837dfd96da7f7f3fe340d02a6d476c7e5472b35b4bb4a0a046c73a719cefbb8bc8eda18d5778b6478831a3fc0bfb5ee3146d8f3d563e0c82597356e373bb79adfa83e8b891a3ec0baeac921adf9fdbe169abca0460f709791d29cad8cf42db78fc66c088ad99007f866fbb8ec345f3a7be7683e0181a0e768e965faf4178a1a626009adeccf63a5b9ffa3570a357650430798d6ba9fcdbafea6cfcd1cd587d2e22386e3f1a4409075492a36d6c8019e50c2f7ec9dbfdf557d8b50030798cfd8e3ce4ec6d9b5d643a87103fcb7bab7ef94f9d1ca6d80adb679f6bf95726b77fd23d4a80186f04917e7a5dcd2b823f7a2060df083b2cb9cdde6f0dadcdd1c5263063856d963ad9b3e5879504306f8c5c7279d7f3f563d5fb41b316ac400c70a75d6736b7765a5508b1a6160f8dee58e760d338417baa83efb520306f8bafe68b5703e5e75a733aacf9230498d176009f3c7cb6385d4bd3749355c00a112d468017e3b9f9dd759ca5f3e84484e4641efac4fd67aed83f252f9dccb52cf9e2b7dfa96f6fde08928b0a5f3bd8dd1bdb6aeca2b14185e2ba5cc4fcf5969859c1350600d2184f7e5bb87d2e263c24f60b8f794b7d35c33a5b36b279c7802bf3ca186fcb9d79fd595bf77d2094c63a72eeb27e1e6b6febbe08413783f17f73fcbe93bfea2d4e2ab961d6e1358d7facf5afa6ab61cf61ed5b799282b1d104e34813be533f74b6786dbdec9632670bed5390ee9ae5f5d9bf760a8aaa276c867e5e10413386af8ba9cb47e95df7f1fe17c34274f43f84f3043e625f0c748b7cc3c4f92557df6579bf72f1cf7fc415450ae8f1008aafe48953bcfa91508fae11e7c4e4dd284082796c08ff2bdada43bee77e3e42795985f5da64f3af9bbdfcac50925b0e5d7eee7cfeda655db0b869349e04ddfce7eb3b656bad7dd8392c0393efc1739ac8fdfcc733f8904a6f1abed96f7796faeff487002094ca37d0d37a790f3fb20ecc1c92370a7fd2fafd5beaa2b8d3aaaafe6e0c41178e6db21bc9a725ea3a4f006278d3861047cb45e596fac92cee866b577d21b5dcfdcc19e6172351781e1d3fd9c86103a281d7f517d14cae04411984faba385b3ef6cadec9f08dc23bc573e9ab97ed54e4ddf45c49e97ba4d39d4f52d1e4e0e8127effde548e1ad7df2cc0d81fbeb379fe69b5bf8a2a67b3829047ef1b3abb3526d2b84f15dd3efea8954f7e22304e6544f2deb6bcadddbf883c032f37bdd83fbf17d631604d6f9f10977b4361018d67e79e44fefdedd4917d547517002087cdfd2e7d7dec7e37cdbff806d7fd23969bff2eedee37ec07b5718a9bb13f27d6f857dc0b346faee84f7e9a5efdec7c3079c279dd3b987796fb961de03aed5ce28f75739e7e6d6ea01ef0ded9befb9b733bf9b79c0734b9ee5fb1da37b282d3ee67f3819c96cc1091e70df195afb9e7e1cd26cc9232777c0f376b96b85d6e57ae5fba86e310d81af235d139cd801cf77f7cc72cbabf37dbe592008822775c031de9d1f84f442dae9c7a83e3ae03e67964eded9759e395a3b07fc2cefb33a2e2fd492be2a07ec2c747b539e277cfdb91ed5b789037612de7ded753adb2ba9f4c4091c707d9cef191fb79bcf1aa9264ede80ed74d4e55965b4927fa65f79a0ad74ca089f0a8ac3284482a0c015239b924c9cb80177d761bfd26ab9b5a6bfdb80617cdc5a7da5d5efed7e3660fa62edbceb99e5ce5056151447044505c50d415141712b282a284e088a0a8a0b82a282e2b4405141712040211204850c276bc0ee7128ffe60d77fd48f3f5d3144ed480fbadafbbcd0fe7e74b3f0d75d7fdde8b555ea7671af2ca9db783d7ba18efb37008dabc2c9ca0a1470d2b8f3c4fbb69ffde859333dccdf7e35ad299e39413becd7b90122766f85eb7761dd6cf0fd29f405054507ea0a8a0f84051c1282a283c5054505450546e144ecac05d8f334ed8e7adfa2fbf689c90613fd7f5b355f7beeb7e790cf2761e6ba47ddbdb5f05058228144ec4609fa513d22adf75b1be5bafbe32422b5dcc1d12098242777012069cffc919e99d0e424e69d5c10918f09ed2f1bd337ddc5a8bf2c4eabe80ebb63d67d72fcc9cff9e5ec053f397fcb5de6e76182104dd15231b159c7401c73ab7d55ceafcfcbef99c5d31b2f19c7001d77bf9767c435a1fcdf6b780addbffce4907ffedadb91670fdef59e64a39ac76479705dcf3a50fb58bafdebf1b1681a05d95e0040bb8d3aba7b592cf4db3ddf4e40aefdd6eb97452c60afbcb96c772929f58016bcde9cdd6d6b935efb95701db6b73ed3dea19a77cff54c03bbb1c65afb1bee4d1e5296098abdc96fe734fc6ed9c074ea480e5de5672dda5d3d4ee97d1c075cf77eff27399526b6f143095f9d63c9f9cd455ae35fce2040ad856272987bddfbab9adf3e409b8673d5fcdf31fa4dd757ae204fc5edeb2e70edfbbd03a77d284eb8409b85fcdb7769adeabad9e3aaa6fe3e012f0e632cfbd65bf3dbf8ff60ccc337dfd6dfd53ee5c5f9e9881e7a5afbeb4d5fe7bf63faa4f09b86afbe2a51b5e17b5d3349e24013f776db5b4d36d3b9c946e41e40409f5af7b72671937bd95a2220e97204cc3c91170ecfd5149397f34ff5317d537cf383102def2be9495c6b796f20d3d290276aff7bcefffb6d2edaba3fae2344425baa42e2c0e27054505054505e54af2d744a9a0a8a0a8a0a0b40441c1d2bda9841322e028fb6749298d7adffa550505e3e0a40cecf6ccfa3d7f16ba99e3b636159e0c015fbaa3e439bed4fafb9377c78910aa8d6f5a595d9d36c638a3a5b55e5bb5bdb9664e29dc9235493f505a82a06cdeac76382103cb69a37d92c2d8f7dbce421004bdb779f339359c0401c747e98c165ebbadbb57c65c128467380102ce52f2da397cf93a97364310273bf885931f605bdddfade9b372f34cf91878dbbf5bc20a6b7c59cecf57e60141659cf800bb8792d7ea9e8c5fe7fb962028473675c738e901a639f6fdb2b47bbe5ae543514141690982f284131ee06861bdf06efab69b51cfc9831331b0cbb4d3b7a3ce7b7eb730aa6f0341273bc07dbef79ed5dde6f99ac3c915a2b4f840b9a48ec4d94497e7395d31b24972a2032ce7ffdcb53ff7f6cefbb8405141b9a6bc3317167a7227cef7362739f8ec1ead3ceae72e95d2090e709dda6a972587f6d9fef6e40638676e6fad7be668b99ebc7362036c9fb3306f5eff5dfd74ad01de565aa767cc795ecb2f3da1017e53d29ce17c34ef4c799f01be5ad62ea79bb2cb00f39e6bcc75e78770cab96380dfcb4e66fe14524e21bd6160aaa3b3f15dfaa095305a18e0e976e7f5c2977d5e2aeb0bb0ab746b4de9edd405d85928e58bfb33addbdd281f4a8b0f1408b210ae18d998e8e0a405185e2ab7e5f16559f7837ab3d95c44988c02ef6c77a4f5f69e9f6ff7a2c072d237eb9e17667ef70b05ce79be97d6c9ae1f46c74181f7936e57a97ba5b7f6cc89144171455054505c0850880441a91c4f47a2cf08269fc0f97b8d7dc27b69e4f435f404a63642eedecb31efdf394210046d5e27f073fb1cbefc5cd63cebaa33e504be9f699533f34d2f747752a805934d604a2b7daeabfc08b37432aa8f6a22c6b8e9dc7246a9fbd3755697c732c9047e5bd2c96dad7a5267697f1e748ac10413b85ff9dd559d9fbc5b6b9eab2b094651414151415141d9028220684b5830b904b6aec7b7ef6b1d77d5b55b02df4b2daf3af61edfd5c82b11a18599ea4cb5a47de647094c37df9ce7b7f0f669a39e04b7d455d7a9a57b572a092c9feb50d7bd7b9d54be1c09ee7e3afd648c56db8704b6dd5a09f3d4f0e18dfa3ec2554a3f3ee554d7d7ad7504ceae463d5f76afd6a7ba3642da68eb6fb8b30923b0aeddc138e73b5ef3c3f77c6881d21204e539fa6ec16411b8e7b7e9ffdb2fe1e47a2b4277be5fe5bcbb870eda98081c6dcd5172a7637c57538b084c3b87f05d47ef3cc47655d36b9f7b9aebcc9f21f0b42fe63bffd949df5ed55b9344b3605208bc2fedffa2fed78478f3e6aced7bb353e8a81e8483904679797f53f21eb72036fdf8729cfd55a9b70c04be177267a3d434771ead04047dddc15c5f77ba4e0bed1f708ed5ca4ba19b333f3aff255ac1c40fb743aaabbeaecbbfbbce3eec77ba6e0b35e596530df9307fdb8f556afeeade83ceae42f9d6ee97039b12a8f44e6a8699408a81180431c620492aa3a0008312003030241e11c84362d97cdab53a1400035b96708c3c18c964b17024876110034114035106100308310010831883680f2edfd1cfe73f452e7ce0d3ff71fe943208a6b37278fe027d0a0d4e0c7ff0cfedfd2622ef70af58fe5fe2fb24bf6ff3f808efc7f87d95c7cf7b71b7e62d94af79b9ca8ff37cb9e385bbf6548eec3394b1ef13fc7d8400dcc2ef27fcfb841f9ff0fdc67edff2e35b7ef996df72df7ec49f9a0cd5e75875859e2ff92a88c45047166d811b6433f95ae98b80d1e8526954561a3cfef0fa819dd8738853a045c466745da7441a69b711106fa47df126180a5cce83a8ac72206654cb4d5ae485c5f707c65bdbfdc55fb07de6bef8b5c5f74d7c6c1557e4a93e54957d731fd06a24fb46a62cd1f6925a6ff24dd464cbba67cb9adade5d5556ccbd59ab9debdb4dd955dbffd7353229ef3491c9e338c3770de46611cef97e3ebd58d9901f804f6907708176a98419d0007a5e8cd593bc301a97087f31a050d1a118217ccd9587bff9f658210d074456dd2bc10bdf865d8e028d0126c49cf78cdf695542e9d3d8fc96ccd129cbe231cd56feb313b7ea11b17fa0b50a7461bb5016dbac367ebff9f1c3ff4b8a44c7d0572a60180596b8c409406ef2451c14a30ec37e54e84a2c2c40f1b573db2a01709afe516773dc4c9eb4e7e6a3e6f4216fc6aaad7bc1210e654a4e4ed7315b793064981ef7e3e2a043f8b151e169511f5c9a701d7423b4ff0ed2b3a24c3d2c4f6187e96212d25df32e3832bfbad10cc9bcad8aab81311da64b3128064c2141750f5623983eb5890608fedaf13d082106af2ac981e1f548325fe9a7e8279f22c31ebbdf459d2bbf0d5505275b5a62a4cc399b04ceabd81002f46e45b10bacde4757ab8869167dff0beededc094a15311b53a14411f858d75cdb4527a7de3cc4631c98dea7a35900053692b0f4b81a4887033dca3f990216c92acc60c0a21b6e3c0d62440f0eb31a329951ee52bb8703053dfb1347e961b6e6775e73b15e280690b1a5d84a131e7ed3f1d8afa923403670403417be104a2dfc24d960608f034248919def8e18a0c17994413ab81b2e6d28372ce4ab940b981a26c13c8812e858739c56f86676eec4359f8f5c0ea3502efcac328b6a2485a183434e81ca1d081b429d2b23e92ffe34666935b379e26e9b22dc2fc1a4e65e00303bd11d5b3ef0404549c2eeb318445a71249492a4758223bb805d4dab514a08508d05b6067a187b1fdcbdee950f6c96452b0b0ecd361e83e481ebcf1bfad9c99b09a3e1d6aef8806218237dc0c5e62886e294c4030dce63d619bb026a29f810acb22be7a9a47afc374d39ca4f976e3c68537e18134f8642e0b4bbcdcaf97b9ce24494c5a30ce102fc51ef5872cfa458c47e9d99e069ffd09265794d1b80e58052e62b002e59d7bb285e1591e6a6c6e855eb9d13d5ae0e5ba2b9603fec5c5d3ce28f7605abde4b4d83e390a44819a6124da4b310f5cd7416e0d84bfbd12f46ab24dc524ab6904d013a3e84ae38db827ea9dcc57c477314469b7e26971225a8ee0a7e3b21dc178594c8521454a3668df4db2dc42ff718a800db9480d2ac200af965ab18c8e74459e6d40dcbdabe3eb8d8ea97ea6beb1f74228c4a3f2230c807ff59c498651a69d495684e0aa64d72311a498eb4b404ebfcc581155117881903258b32a7d2da6902c86d5a6788aad1f4b9f9fc8dde92d398f46ee46cfbfddc45ff2154484cbcc92a84f2308f93a1b9667c47946430e180af6452e5420c7e95462728a1902cd7271cd0c4a7d459bb43f9aeb8e79faa00f8589281591534109f5334159e6a29147d5e9cc99c8ee358077b9d1287a14fa94ecca731437927c988e3ee4ac37101766e8d41c7cc465c19018a0989dfb9bf62d729d23295afe37cf325ed602c162d20114363fb4e9d8ed3605020896c163def4d849ba67f61889f8ef66119422c19b497027881fa2bee5942ba64ea47a3683f2f666e3740017e384ef2371a568e80d9c7ac7e10942e8e69c9bdf31f3d2911ad65f49aee9a6cd75bfd790717d5a5f133c6d1b06580ac96e09eb926ecf5005127c14eaee5d5aea846c6530dc697d69c82732a70dc2d49b5268d3cea0a296310d8c5c48869ac615946d730738f817b260f18ea19448ff187d32a0e180376a0c32220a97f381577fa905fa71bfb2c82bd66a867efd0a409a111faa6414dd998d8df9800a4b3f640a1e04502a5e0416cd44d2891475d7b8946c888e12d256833779c4b64080dd137cd4774b5503db4b5ba25141c1d718c8733460e0e2f49dd14da7d306fb8a8776a263f676dae291e4845cad4732a030f62049d7289264be787a09bc51bdab1865857fe5403dc7b7395d6d9df3df170008bc0c9c7bdf0a10e367723ec790baa9e72e0be8e3f0445af000405327366b4392a9dd485179dbcb2880ae7d3319ad1e9aa669f808c3685afedf19813770debda769bbd7d35252258ec52ad7876ec6254ff5d1bc67879a4e065d6a92e081e47f0ce4772d537b73f06ee823a70a12b8799a02cf428328733e1440727b9a2fe2fb14b3d0b75cedc46c3bf44f806fbe6f62ca709612f6ee3c0f545c0cdbadfcffc97d743d58fc1952c9787803b95058e451105a53dea998e0be53e541b6b3f75d078ed6c910055a73967b53e699017364adf809eb8daeaf5a9c02899cde1470c610a3e7df1ac4ba0ba4615b54429132d2517d1b0d2d2524588903b76861429c83b86d3d6072f5b50a0a40a3f8c3905f996fd33d7e28141d70abe0829728e56e34f1dc2d401cab18d1a9245a26ba10c43a98dca650c1dd6268350048fbaf23fabd59a330169c0ed76f01170613ef5f9398b4a63908540a4175aa4e758c28f546b2649cc72155d910cb30b5e975dcbc6c7156dcd4829195fe566ae464a044070bd0a3c0b359e585e879a8066b453980328df1288c7b769304cec4afe8a83376ad4e0552e779942a3aa68030b5f4a05c46bd3e3a790618232f544c1cc4026c1eab64d289f56944a490eaea9ce822dd3a2c5156d1b951a032c68647a5f772714b5f26e4a12b0761a8e2b83b6dd9d0fa23aeb96da98b547468dc6abbc225de0bf3a014a81a4cda725b6d833b91409bc71f36311ef2f9532c95024ca3d44c62e3eee19e035733c12976729ed3bd5acccc48f9a59c7d0ef607dc4a8b71a756373aa60b9da1986042eec11da6196b91e818c46474e79cd4461e929d913832e9fcbe7785e75e6af0d9e1d0e44b2e0349fed7aa42060be87a6d4219468068e22426a447318e4c259b2f835b3b0886a66e5ae9c47b3289294b91b1dc886e39313c03b4b34a8e3868dae542e2edb85f480dad3930a666e6c4548eef0dcd8b23a9be4fd4ee25353482f28bc23912c4e723d384158e48ac8e4aa78e9af38d00e360c3b2b945dae6a72c237a92fdc0251f22dc33526b3449e572d4f1c3c0a157aba497c4c148ecce9a5146f32a56884c98ba0a89094b2a6133b985efeb839654cd87f90723128565ae704edbdb3dee112f317f5ca0c4eb922086a1c227ee67629c2cd66ff6f1c7fed57c982e9c6551897670c0c25f437c1255dd084ba7249bb0863732c419c1f8b712116961686f49ec494951873895c17892254d7dcb94a4aa8287b932fefe20cf2e84f13785883952afa5ec8b05f06ada1e8b8b34e52962a1f3e69322e89cd743539f760cae5879f5f949e919d1d145a4792918c8421181966704e39a638d97f8b5bfe904c917fac7ed89501df72fb6e64fa3d34caca3a6274f58a2001ee4c34805f9ba65f4ee6c1c2ed77856d258597bef55a1ff4a16f8fe84f8fed5748b13b73c24fbf023aa84e695e1669af768433803992355b0ec588aa22e86d22d2ff0b1edf8e89d12b6fafac5c2c5dc3ca036109e6e267005b045cb9876b215ef15b3c4504f3bf41d75944e3aa3b5107fa1580a6c1c8f07e44f5290147bfce84a7af334ec4931c95ad5bc17674c74609c1f45968ade5b6d913828bf4f0f8dd90b80e727c659c99fbaa284e52276be15f69b0b4bb1ceb085105c2455b1b58babaae73305769b277aebcee685d69811ef66eead389dde13b50cf49c8882f3c84584ec5f099c86b6250287cdfb1607454af8e6a024f470c6324cad3e33af125d0ab1154a57031a0e76a7e73b161531d25c869552bf7420fc2b017a5568f25b182c63c479aba6ce2a4887a86c3a15ff429a4e84c76f3ca7b585b5de25e5e0f0bb252b77a29b547833785d14aa3872cb0c760d304522a8d40ef37433e0b718a4af716f2a6e868a509a2a729e2e8f385e9ea734c3f3051d42c260bc96ddec264ed4744e40478e564d9888af01537fe8fa85007c0fe3c600289d2c77c9f17f9972e65b546e871c4a3068140778d76d5ce29f18d98787b598818013e848fb620ca05a705cb9724d01ae9abf2362842af25e9d0ae3ba8961bfd1194b45b0342e458d321ef47013ee9ddefd6a0f0632b32901bbaa77c09bd5447287ee224e4c8548c6012771a2d2c9c68aed5dab75551e9f107f264622583f715a8c40e8cf6e3e80869f664415d2ae880aa7b814a181deddd69e7a0a15600fd40de4eed05b30348ccbf5d24a56b58d5cae23efa7a2b6865d16a4a98d122df3252abac01874c1b5805ee827b2c5e547fb20cdd51554dd87f64a2bca406bfe5d255f77cdc6e0b0da7b89ae39992108a6a448b256206c0f90e5ff215465b779a90b2572201c53e6e4a02b145a1344a7268cd33782601f1f9dbbcc2554d18565851c4242ad6ce2390e6c9c1518d4cfa57c2b1b835811c88ba783401510363cf024d4c3f5495a7c78006333aa23c884eb6f8a3ec2e7c1f3a060e7e8b63e5bca075c7ba10800240d64323347585ab9da38ea8327d2aebda657e6e9c1edd887a6bcff83fe3e384322536bd5158e45be8028443907956d14c339776162c57cd2b97fe0a5640cd34978e0a96463147649a150ca2db4071b62b61edd543671aff0d5d76f50aa7f0b4887f0438dfee4c81e338ced51e00b30751a0df83938d553a1924e4cd86edbaf4213664195555fca90ab171a52dde3a51a602deb3e417c5941a17bbf7f229484f9d6a3733e55ec8a9deb379bf0d5b0097aa94b4a7d0f66bac8a242b9c293afd9592d6f0389ef8a97678053d232d290a65a77d12f01d87e3a6bfd6adadc925431daa464e6d12e5b6aa991836e2ccb7a5a3e9ed7eb91ebb9bbab4703ee08dfb239a199618cfc0c1caf54020cc44338186f356e77cee885b94eec464f98b4680ae5c4acc185059086fdf420b9f1cdc187b649d011e68dd896282c64f6181faceea21f660986f6b04562ae46ccbb44747a939745aafa91a60bd9b1230a2f80e5e091c19149a18c31a945554cc259495ec32bd11846f11931020334ab8a0c74b8994e86095599c8620bc31740cd8f558a0933c57ac96fcd75d59b17444774e943d54bd03a5c71d484f2696029294165d3db2659296ba28b8b829f16893d1e9b7d47f97a3e26e2cc532b00892c307173618301b32597a910a15551c1744bca88bc316d09c6f0a60c4ba8846f97b92dc022fa6822e7a4fd67a28d2c1200e7015e00047dc2ecde80040e60596a22aa4ff263b0a4bb8df2883601e58f4cf05e583633fb729bc304f01a5e257d0500e4676b19a717261ae2e2a125dd77210f877986ca20ddbaa91e1eeec31ba390de7ac80465155631f98cacc123804f1c6d565881b4e5af140e54d720408f8346627de160b1b66361a42c77a1396b8621c940e726487383754705230cd057e92346b0b4f98e6e2ac35f850cc4df329a170528d5e48cba447f1f0daf4b79d374c65d119198f449a0ee6886b023dbbe85b8e761bdc768b56fda850380224a363869b211a0a7273c69c431a0afce2c3fcb00b26a1fc45d1adeccede2893df880509895dd85a063f4c2c7fc4c7104359810c1e94c85a15c9afd8cbd3178f0e3a24b9c9a6913c9c0d836dc328571908f56b3b4ff072e395309d837db5595bbcb4679e87ac415a807c34e7397a42c7222842a3e10d3db8659cb63a03f1cac1e0a103aea5a264586a1308889621ce58ec614136ab9bba1f2cf1c0ca942b501d4276b464a3fd4988c9af30f8046ead82c144a6ae3450543213771058c972192daba0e43fba0d249e6526b89789419d865284aa52a8b0f030bb23d7f8048c598e8defa23846f594e40c0893fb5dfe6ac5e5aa964e5ee66ca36129ef5b71759c4ef7162b0634e2dcd851f9e602c4ca90e2db1a8d3b302b7df669429c31602cda652796ed5a72d71aeb7f4d8f7921c30fafe0dc34d8c724d4560941878d85364d4e73cf80c85902b0bc3699b3738f74cb7aa4c5c1d560a1fa646f42b5502808b0407a8f245835f96c8e239921983415f84d28231c1748909a4aa30c66819015148580e6cb542ce12e077274f6d9553cdb3fb6361b12be3356d43192520ee1004a26312a9efa3b1d1a282a54b252ce2f089e608db2fce3a0c676b0a32bf1bcd20885bba1c0593fa56063772e167c05f42d2746d8a93c79395697316eafbf633b112107e088dd52f464ab35020976c6acb646cb38f9ba41207536452860fb1eb2682f2559cb907dd5d30c274d0351cf02b986d858c2dd14e5bcb105d70e7bdcadc18046218e6c2e2a890cdc9c9f55f5073cbc58b85db6175971073e0d62a50bd73cfb060c461fdc9bbb8d1993dfd729d0681b99826ad033dd554df42628b327d1a4926ef3644759104b464dd86ea124c2a7cf0d1e326abadb46930df00b0c9e37141c77dbd612f0472c803dd77e1bf3d4e623668b88b0d28a37d00e2ef072b2ed81841655fe027c52bc4230c6a0a39c965d949d5ad410d624eca3aa9deaccd35dfb7e598c79095bf78a9672ce9887a91ac9e4e896d013063cd0de014ab03ec937c5525a64136f8d79b5049184f02ba834e01d52660ee45930f2efaed47831266631ec56c1b6a89ff3739c64b972b36fb35515060a0adcbfd7385b67016dc64813e8a73f0d96b12b17af6ed3d839a04b9f5409d1737dba66fbc8184650478c0a2f6fffe9a52519f442d562d95fbddf444f0427fbb93d401191ec48af688ec54a45303147cc6cc9e8c6b37f77e202f41f9017a206796241399adf82957dddceae870ca5e82e77df459c849b3b902dc91794e9305c93a8a6b71778e16b39715c6273a0b442d669f078ee658407d2c4123be2e49dc21da95f746d9bb812eb4034d2072200cec80493bc2a8fd303ff4d01130045fa01be55f7c4283679bc080daebe04c499ab1c68d846923931c463690afd52e6590f94c5d64dbffce9eda7e2c98a0ff88e4b3e3984785cd1680253286cdbe350f0180449917aee78f44c12d11482e9294af4641357569f944df853f56d53de77eb7bc32f100188e400a047a196086d04313a11d70c9b41e558263f357cf67e75308deeaacfb5e5f81938ad4749fea865a1401c14e682566a9994601c2949f8402281775a879735b14447448f58ad36fc1b303b5391f294b4da8c6e37b9ce58b43aabac07b184517268a334c96d1216ebf398389ba6f70e32bd5951aa3d208e03d3f133fbd6acbc18ced821f0405b6985bf1d3f238a388080f7a54a7d297f8f20d026b34bd3c4c54df81c627fb6472e00f6b6c50da82413ca07c5a1d2f71850d9f26204251b8bca12ac533b69eb5917f6864b6c6b6430272f597539b4d67d0e73269170f7c9e3fb1c048ace4b1c178b7a1ce38ce334a2e6b9dcdd75e39c18d3a2d089ee7d518adb10316cf59f24c81ca3a15b53d226dc3e3a1ecc3949d123fa20e3116e8c32029d7ee12aa1c190cbb4566bb3ad471c7da9a21b6d94481981dd26441c8168ef4a5a5764e476058665a80c54796b6a5bb58e3980cbde8ad1be38fc770ff4111ef3178cc9b6b1d6767c97e2b59f278b21ed2c93c377796794251fb28ac8153dc774155ae57c867bd5a373e62fe6a07f648bc183c73934345196e6e694822b188926fd1ac1f3fad588fecdbd7975468b26365841a8f19296d5c01f1d394039516c177076fecc1ac908e50bae63b2653155cc4de7a0d18471cbf56cf209bd3bbe07442dcc0893b8a16177df4a9418c8c2369b29bb541b230d0a2964cee53f0c1f98fb85d5073e6e2d70219153e6771715ed70213f5dfcf10149c508002badf981eb32587e13485d685d3396ab693c9c1299edb77a18752ca80d0aeda98dbebd72db351b6fde9dc1ddb2c80d5bcbbdc8edf5f27c8e63f1cdd06e1e0c3cea6dcc2d62f688e88e83d0fa0b12fcaad7569159d8243b0182aa530bb5746148f8438a384fe86f7821012b829e81f7e9c28a1237ecfd09992f17c73c642eedc9a40e68f29b079bb28be3703433f31c01ba7fe9daf6aec1763074fb098475ce0ffcfd7e61466fbbf8fa4abf9578025b0555e95ea5053b4817872d94c61f06226ad4c585a846b0e5d8c8591e0a291518a4ba1919e00452d14585d2d3a6233c1d415cdd912e084dc932e195e9425da9e404fccc84c86b16fb29e10e4176893628a23d809ed0ec5134f3f288ac2d31b64c99f08e6e1fab20550a86384fe7c9c48c672429efe313d20a0c5c455e80fa06af65cd14c4c8f424ecb5bb9298871fcbb8eeb25b8ba5041023e7abdb27544ea5040b800d024a0f74ea35955b1b1c1a4bdb0a81b4d8d85b3723dc3a1f8898cb531f05960087348ee1e683da0b59da486a9233e9b0bf7f9364292ee04e896efb556ffa7d2eb307271ca2650081910283c7f99fc0f4506f5df1d9a0f279a9dde6651548aeb0b6ddeb076c0600fa54e2689b83723ed0da0cb4367dbdc33844e04cae081887d84849e94d351458f240b61a247f6912f2c383fdf3c56b2bc884e0594c02fafcc1a0188919a0fa31d48d8d0a4f64542585172c45b5e2550e88047599d72f1e7af7a1f4c54a0dd0717d8e34b45f7e34824b83388e9971878c9bd571c61c48722e22fe4ca03ea08838275f88a2f215ac224c9eed0c671b61486dc5994c82c39599cfbda3c16a90a010308e7dcaefec3291fc3425449938ca65b4e919155a7ea9073662d2965a6c6978396911e4f1f8b5c0a8ba9f8354c6060f63a771056f75fd5779e85eee84813b5d3f106073453ddfd35789e2d6a7231aa54ed8bb3c4fe9ce830b1539bdc1905645ed6fea02aa839c032cf475e7ddad8165c923bfcb8e7550621b285a4ae48a6b84924a3224d64f742dc6ec9a244e00b512c9882f865b221c317a46622144747a090304f2f54cadede1583925bf856b5d6beade9993fd7f96adbf5e6964ac017011822dd7ec8c786a29a34addaef122261b08362f4b013cb67e5c48eaccae921fb5407969e4c9590a9c2a73ec60df74b17d6913e1e4f2390e9f88740cc7008fe54782d7b37603187e4cb9813b982d9d0d76b0ad74851d2ee23fabb0f9b318b35770746172b8e19d1fdf08f181a7401188ef9e6c13a8a784b7ad04adc1ee01a007b5c0048bfb6381b0315ae0619f54f353a17175f951746fb579bd507a1a8012f45da2cad3ae2073e5a1bac20306f7b15aee6fdb2afbfce24fba26548015322679eaade084189eda9d43535bb1c33d584feddb14fa09a2e6f94f6d4d58dcdb1154fb3cb63c8e118046646830f13dbcdba2b6a22dee3846b51f6331f25663071423f08a2825916acf2e8715fc853bfd2c83fea2d9cc58a822a0c131d5eef24199b7a9b68679be24bb6f76aadd9cd1a407701166a4a26a17541e6dfa07a56a5ff453b53bd4a1e053555bcf8e1f6356b587e2fdf1467676953c19abb66674f9ee27b5a06aebd6dbaafd646291ce814a55aa667556d4e33272527cd5d611ed47b102ab7d39afaaddc72a7d4b73a77783c8d4eef18e94c93371c11ffa4e1ef8fb9c206f72435fce05799d17e8457ef8db7c20eff3035fc9370c3ea0437d851f6c87205dfff31ffb2e753f3c15ddce6ce4c32288e07204ad5ea570d6e7bb7ae2451ce21671a8f058a8d8e58f2c3180d5908f5b0f1f08476aff51fe207aff57346c4a3d17a4144d28fc31b5f57f28b405fdbfe6d759ce812904676b8267566e82816280d1431dac0eaf0310e15056ce0101af49be179799a68f5f1eee5a833379c9d41731e60b57cc1785700ec7173599fc9d24dee6faeb1a6578862a70dd9a67843e9ffcd62496ffa690548902f45bcddb9c6afbe90bc01487ce2e009749109e5514228b084c1631a80c61e8cc0210d9c4a032c4e0b30bc3671083ca2886cd2a00934504269be2b6dc2862cf5d412b5c04c3b41dc5ba0288760710e70430ee0a34c205488c0bb02807e0686700116ec0a21cc0e2dd01c73b008b72048e750510e30224c60d50c41970b403803837401137e0f1cec023dc0045ba81c63a0089710112e70410ed0c38c201b8dd4c9725b62421b714db42d7f64ab60bd86abf468ab52cd5a247ed58fd1000fd6910c4f7a0385f82e33f0682f91c14f95310f4d78010df8363bf0982ff1e10e54b60ecc781309f83227d088eff1a0ce67b70e42741d05f03467d098cfd2800fe7b509c2fc1f11f03c17c0e8afc2908fad34c97900a8ffa81e83cd23b31694060ea5ffa19a0180bccb7d4f8873480aaa9379e69e9c60aed2e89bd0fb28665c81a4774f9116b1e8a69e60f150f8e08f7e1cafe24b5ee495ba4b7ed932678b076c2198dfd184ff08f2fd926b69e45324fb90b346e974aaf6fcb98ef0502ffd0a5b1b826074f3962c3d90fd6b0316e919b557c4be0f266d8ee7605d4b907d9eda393e6fd8a308a2bc1f759ffd2d113095a2ea6f72d1343b529bab2985537809792ec0176cdda980b0c15cfe224ccdafec85ff6612474e57aeb06ed64cd43b598659ca0983b51c9ba2929611abffd8c3c1a8d8e382ca36bb918a7b7c01732d117406fe529c362f4dc2bf4fca87134752e2df062718927df1d4062c46bea145b200ef7e19a40176e7f5c7909bda9e8fe6f7d581d85de2a2490e50d6a5a3bd6000adf0b6ff949a48b20460c147e4b7ccbaf486c66d2ae16f692ee9a49c7b9bc705fe681cbd948fc44e53fa3faa450ac9db75ae7b779ce44001cf20c8ef09f96f8f74e766cb0e801a02a7255b35e614579e1b093f283df20346728de2646ace53816f0bc4e1af54ca3a9c83d327edacc5dc0433301fb28a01df4f5fb6c8acf662eae97e0a7449e4a501d9556b29fac8074d94c594b8fd8857d594d214e2ec719212767e7b2c8cc955c789aedfce7be9b1eccf534da9a6911857a37a5eeca8b39af5c72038242134590e945ce049d1969468d71e4a60d49e7956395d0dfd9a51a13351bea617dd027a1db16b761786ecb608b143d7c406e59d169f300799d199968416c83a808ac945bd9b6cae568f191da3593a40aa12eceeb1e009dfa864ed77224f148ed8a248540148b05b1f519c398d320d8f8b367d5b3d1149a808924a81f310d63163a33e71bc35a7c758b33f9b961ee7a3811673edb1493b8cffbc9ff7b0942f698a6ebfdfe3c4ec5193535e0c52df01a07bc73e58bd26a46456d59b136cf7924f930b67c57f4aa1decf1e71cbe7033953a714ca8b90b9c99dfb57c9504cbfa6e8c4ef8f9efd20b70744fc84845f67276a8f163a4f04aeaffcaf8c5bd544fd1c1d95702658e2c1acc180743d1ae1ef07d74f0066f4d246afd1cbf3a60517e6f48dbbd914dfeca823a08a6f0c498cd31163b6bd55c73929937c3493241cbb1ed6116637b37bec45166f60e6564f7b3e8996661a521f1db65da987bc6c13c8c5f986ae38e99389d56a4b3677f3434307ff16db310889faabf56f88bf519c4343c7ff4e0eea274eeff6e6d62772457cf713ed04755999503598753493ec75c3d731b78edc0f90ecb3f1476af5621f86af6760e5bef109ee1d06a26ec0039ac347b3a39d8a2743c822aebd60e8ac7f43f08aaf100ff01d7d9beabb53b8b3f622366f02a5adaf9a363d6bfad0f6f94e1c1a2e1120e81b87bff9f32ed5fe4e9abf575cf6ce172644deb83e5e48844ac42f0aa37c14c22da2c18cf4119339c58b84e6cda7510f6d78f2114980140d840e40da9614f7731d8e03f548dc2f064e3bb6ed8692339a6947aa8e09ab7f0bc4e5fdaf9c86c4b94e4f3973c0f55cdd8c07370868d77fcdf07bfdadc3f2d81353d267f74f9799b30a6b5d91e54e93183eec249e470ba9fe3f8a7f46a1ddf00249fe3f856a342cd36209bcc0b1c8c4327be84c350398678872e3c87607c30ba418a9510d5376cf4c0102780dcf7a3014d9fadb4ce719f3ed5b74e3ff2868278a6319087b29ddec3e2a18983ef12ab829e15f03558c12c1c081a70de3864819167c5a832db05e306c48a80d0f82b6aaf2ff528be796534e9b34dc69e652c5a68dc4e3568c5e80c571dc8acaf1f96cbc2d2fb3f21c780201175f518b657f2ef4199ba9e31b918ffb23246a06da46cfbef2f1d6d543250177bc41c0e38ab9d9ab0ba28ef0b1efce4cb39a8872ed43eac9d40dcc8bd4b7e49ff0041abdd822ba327a2b4bc7238cc1f4fe1bafd3b610f32bcff95d455bb1a7b168741708ff4929316c0a2b0c5eac871333f198d69715a3fbfb684c073d70a4cb9a68f3b357832943ff6ba450c409b44b3c51d1698d755bb1486232430ec20da860c3c40b9b776b90c59846bc2be4fa7a430ad0de4a4eb1a2f2c25f4799a334e73432e0aad536a856c580db1a46e3f95c2ba401b377090b055adeb1986911e1fe13b42aae7612d7fe642af49357d3afe5106135ef9e075a4ad22178ef85b2d37a30ddd3516e3f976626bfae44ff8df1c5c14287146baa868ca1a2060e59c684f9c9a675db0d0e73786b46e529d95a3d8b52214db521bc3cd9ea2a0160ba6624e942b16518b798c5d28c76504b6d90a0a65951cf079302a3b384531079ce8abae73a1f4c98f38f83cd1511bdbc51de886d5d14561e40ad0f3b70732d880b99053c2effdb8b5a0f3e0637e6d7df1b8e928c46e64e77d3b1e5df37d969c4ebdbb2e1727e0bff4d11def27927b4777f384dd0f31c3a3bb784ed72f8a7d3cbaab63f7734339f547376fdf20005994c0f9d1ece905907e382fd560ab5ad90990ee9003ebf589616720d6878b0b9add8e395068fbcd7bf9ef45d18588a0ebd1e532c4f711edf156957302e97b2f572a90ee4e1e48ebb801071655354974f2115ea2d97b6f76f056eebc50b63233884b15f2382eb6c33b0cd2b7c417177b6b6c7983b412d506e8b8b57a099814e3a164a1a3b01580b5a77160925f64aa3c9d2a8401433f8d9082f99879621e8ece5ac2f39b0061f89a73d6de09f4ad9b1c48cedf0092ba1d77805f74a7e81c04a4faa614a121a5e929e5479ac4869cfdc282298a80731c5940fbd715995888048e84a7f760fb29de96a1d89dd12a84978325ca3d01f8e22024485578710ba5c6d0640142801d6dbb34f681fdda23a6e3c84274b4d4c6781465318580a5957b2ee831e8c8f74a9041d63453b500c7e0b2bbdf073ee37840c35ce17b48e98e573d4e2e069674ae3b4c24d292d131a94acc55e089ca8f028d0419814023d4dd94b170f9c8da37425dc18c8484e0f1f795da5721324e80b48909a4f92db245aed022bb475c99257aeebc5b6fbda3b8a4d494e109e6a788afe361e89d7055ca54a8f62c72f86fdca88ab926e4810bcaa0c032ab2c1f09bda19d3298dfcade17171ab612e9b3ec5bb1ae9316de4c6ca2b62c7cfe5543e0119000cbcc53b7d73d28a4b7df1652aad4e176d109776c86b67f166b05d296af7b01e5dce58fa13227075fd798562b5e439d176348f21c85622cda5cde5a817f54be13912f770d0ad5dc4ca24f00c24609815867a21a653327652e789db0e77fad7e9ed3d0018ee0f4439ff909a98d76e89d0b9d0443319e99fa697c2b1ba3cd74d0036a6eedbf68942e1d0e0188ccbe1ec34670fe84327151899b9d0a731132db1b1de52a8aa54b9a3e09745cb134cbcbac7527d4ec7be5c770fba155d71a5765fe962db0d0ddc65ae5a3ac378581028081b1a8eecc4e23b8c947a4338d4732f672145ab3e6b3ad0d476ebb4ab705d6921485ffffc6383d5d6641dacd35a8f80b0c67f0686ef0e65abbd1d4012efad39c67adbb5835096a300338bbfa89654704191abb1e74c12e3eff18e66ec56f456bdf8ca590223065476b536bf86045014065306c4cffb2da2158da5c73ba6104774ce3262594efe44cc72b8ed0a743ba947414bcd37b03d963747c7ccddab838b8e5a9ba21780982b59ed5241d0c4e5096a3e18d2a4b8debbc324542ec37a290ed87e48f0dd8befe76cf224709aeefc9c3599d84ac8ba0e04b9c3df7b13ad1de4150f3e5c99b270d44092ab3ad16565e7eda4c62d88b89959fa1b641d60716a48611776a8390e2814f63db8594cf74523a8601946d5032ed5b93e9fa8eafff55a238f19ea5f0468b4da7a933b0ca90d39f53841ca294db052b964ca72f07da32462e930f471be72de86ca5944d919027c252b0dd593d598ccfa073d95b5672c03ba5a49d10c2d647c05d79feb678454467cf0a14b13b10380dba951fdce42a5fe98b871a377e8115bb303cdfc5e052c90487839e6fde8bd1dcc1c1c66d3f58da47225561b197751933ccdf8739b18ad5dc85ee58226447e810a4038bfc70dac03d46bbb4cd395d36dd7034c1a645080410190e6f9b97f735100280647e4f6705c702537b940a5f45d87932ca477c38d98d8ab74c042be870f501c85021c16a5e8b08a42709f80dcdfb39671b3172fe4278c36bc21ff3ba2d169289f8bfaa6bdf2ad9e7a052952927d4c26bf98d9392185f5d4dacb6c5afa3579509536ebfe2c4c74928c56858a9d2787cac95ecc85c0cc818cd85d6898d8249defcbc1a01dccf77d68f9d868723becdbdc5ac453dc31012a3761a8d696b1ee0c66d0c9f11f85ad56834d8458dbf46599cb3b3d5243c7956950b437c784b6b45991f61cad278bc5105a3e8f195e358a4e4b1ad5e666ba074ce584c9fce56bb287c90b309dc0a5db019079d5f1fc8d9befb4b340e12b5631ecac595c182a3357d8901d5e7390dad3ea31facb3a85b01e7052e6c9a20e4d143059a82520edd7a92d90681179b87c1a28268ae829b0bf196b73d083746ced3dd1a38ae57b8860bd9321082c9129091301cfb400f6f8ce9c37cf2cf3d081df73464476c9f93189690b694263772d9c88e7292379c6b2ffea8178eb1ec27c3f61e186de2cf8fc090072591b511eef9ca6a960b9c6dc3f76dd81187520acdef0441efab3b8c0cb6128d1fa41d21421de139c7716620d3be5c4eac60dcdb68f47d27aeedb4811d78a69b4035b211bfa902886c31491c7f097d13fbca5640ca186e67eceed94f6caaed082de0160e671bd08b0fe401b0024eb17aab9dd7b1dfecf6b8834c0a599bb6481f37a5ec51a7008628475035b472dfd53f7811266cd53e0f217ff3dcb478486e813426a66170802dc1244bfe9c71aea79877c727c231dae331998edbb564ead716f3197acd6820364e29c5166249408af3b89029f844739b91b14892bf2d8eec497f1942f8272427c96764dda353c7e9809760aa6bb19f2a3f4162416c5f3dd5493b168e1af38837a61232983c66de9b35f8d423652585bdd7a5e41283741d9380c9af14a23d98049cd0fc3d5c78e0f40706fbe182d3c9715a4aeb93a6db07a4f3517da0e2812c790f5ab05d4a3aa7bc70d6b05137c07d1864f0e0844f2f2fbc7fdfc7e57bd8a6213229c4c639d30057effd42f23688f3162547ab67325d7c7220c685dfae5dc0fc6f5cd0d28065d9fda767d1a6d73a7c8ef139927de3c5555fe74491c3fcfed870a2a907b3f48632f9c2a27416269f4f2b02f27dc34f9e48eb85b978377bc6e193ca7e38936ffe2e7a9b4862527805db84348225f4b30f5fc807368d45b80abbd902704bfe259d948a16bac31ae72cf976398be5da54bec521c0d3dcdfe8f8f7d36883a6eec2793ba70f9864bbad158258dde100ef3f4acc591bb5c97eca4678f68a3f06eb3b15cb308c1a443c126b6b832d010f05b0f058e5fb7459b5b64e9c09c923bc9cc6af81534c0414890c9a03f5a8e256b8046c223989f4d36d001f1caf6784fb18582367eca2dae054f5d9e5bdfb19692c3e4f53f5025f6abb7db5edb90ed56fad2fb653507970733c20f2438181a28cdd120e33efe1841d7de06da1c52f033028f356b84d4e6172351a5a914f0794d31054a1173cb7314f65066aca031fdb3601a0361acd8ccfdf4c0fb50eb1ffc507ddc05b39e0e1b524262b39fc7531fec2dd99fcf47f9beb8c31c91fbefb621a6842d1f502319bf843f8fdf38df9717f3bf829b55816ac5ff67582a2d1e6237eb11f2971e3cd202010e6386eb153e2887524e012cd551b6652eb9fe7334a15f8a1b573d4dd4a21c23d998c118cfcaa2c0248b03ad77264071a2f86daa0daaec874fd1c6714d0d15102a5052c005e0c90a9b8c1ca11ba694f8d6c347b64839de9acd9d96348a05ad8e614096b0b87332d043e6e7f0023865427eb192365554624ab725a8b92c2040892368f62986f285f22a96950309063e33fcf2ee06c6632730fc30850ef32c1fd0c30047a201c77b2e143991a57da254d9305d3875a540d2f2ab40ca5b9a81299aeb7489889a6e4a2f31d8717f4d7fd3fe7ac0935bf05400a09c344d74a7e637b98eb7fcf27247007007f2ee0d05acbdb2ef6c6cbeb7cb52e2eb19ae26932af086ff632f3beb85e8302cb28d6071b9e91b668caba87c227aa2eaf8ed47e1409d5dbe3814b6650bfd6056a4f6e4bf4f7f9e90025658993ef8dffa1e741b5958dffbb73364a967b14634c77823be46f10251e15f7b986f6dfc89e23b4a6d51ac560d700bb0af36a23a543f66001e2af45fe3e0fb96eaa1e27d48d87b750975945b8decffedfa3bbd97e1c22b81f2337c690e467f301c4198ab6a182388c3597ac2da257ffbf3779c58c169e80f16b2f86e8282af126777b1d65e0fea1827d8ede6e748394934431bd5e491cf1ed57caeaf8b1c6016c82ec772759cb131bbec42d967e84d79fdaef16dca4970b03bf3b848498e9e93473756b47d4e3aa05ccf666a474121ebb5bd1f3d0dea07d999775f36ccea836ca0f6ab28bcc6f22c98a895e21a404d302e76049bcdd95f4fe572c0a75f0114d85a7b0dcad0075f629aaafa0276a99155d6e87396abe2edf0f313946bacb0fd8f34b99bfbe00fc666a566b5d87ee3c62e2a3febe1225bd5793a3da72cc0f1ccc46c2a6241ba923af435a10670b814a310dfd8e6ac6cafe70c6121a16985ee16bf185bc61f60b6db3c1c2ef854c2b51510ea6e4ffd556bd50366478f1592f33453943103b84295f1f10b30506b79901a8ca9aae4d39779e2d72751d49b4f877ca11306a50edda6b0bbd3dff159793dbe1d69a080d2bb2759f64e7133d2e421e6ca8f732c061d6bd29e98bf4fcb1f08cc0fd79a36064323cc8c62e8b4c8f92f2e7aef2f79263f309f0a4869b05b6412ac375150250f259790ca74b3e9495e6e0601b4a02fdbe8a6fe09c2161765b0ad2f09a3dd89b07e812f430d38851d868c23ba71e160954b44f3573bb78ebcd85f967c4163296efea0524f3eb36d9e665ab5dea8b8a1da83732825ed301923c906b27903af02e26f4b2cec78c128071a636bfea08bd7fe5afdc79f92c59feb204698dd53c32b362bdb26d16e2dd37fc347de56b54f3952bf1b869f2c7dc0e286bf5a83ac988ee22066bcec23d4d376971bb83eb71d4a6777f74969b4bf4991ff03cfe1e916367bdce5f7610cb622b8111fd6aaf04b5f284f7eccb411b12973a20b2d7c40ade66ed926f632163b0c7d882ad210be68061030e8dff053601d66df243505764d49a1cb326105197883ab1c521478a7eab05879244ef317994290f7fc9f743078f1f091c711de91bc7f57495062fbfe9381c0051e2abad70a0396f13d5929fb9554cbdf7d40a1ac5b25353dfe332f99193f629a08a116f8636f77891f0bc83d4aa6bd09f35c6317ed4c014ce1934c8803443c1496d2f2c29772affa1e5d54812503375f10fdc930e6f6c358c950f99ee65650e73b2f128c78bea60b7796fb40a6ebb8aa3297ca13e7a5fd8c3de1a0dc39770f6f7969dfc58162ccc7a9ba2977ebd18b931eec0ea3250b5c5cd7e82ce667410d1d7adbb54aeb8af0e2b313c1e11f1374c9929f15a44a72206009b5144699e94c7786d0392d666eb68c592ea3168526f2d9f04637d03f9077b9fd0176c698dae3c2ee9a6e7e68eec033d50b6c1a3ce1c672272bd44903756c9b1016b5e8b65b0150eb401957ae39e3cca1fb5f0eaec4c5a7f1c0fc2c6eec9dc88752d35952c54e5b72c36bc964d9dd77c91e0e31f9fb8270510f399cc3da4f7c57cdd39b0545b4c92d941f550ca6f6275de87d03240822c7484f92f9a683d796f9936c21fb0525f02b653c2b322b05336ade09550d6d3cd20d18bec793c0285647356408b8db9edc100da39ac3926ac26621b06c23d12c360f96ab63a14a5d56af8a398a4742f802b5bb6bc606bad9461dcfe3a8a1af87cbac6eb2fc859ba78fb40442b1cb87dd161af312125a0f8bc4257d4f60727776361c20a7b1306d370c42491b489005da9d2f29d9697c17921bca47c8f703bfe5fd8c027056a53c68c9e537de31b06f2403f1e16d550b9f653f4113f8d59e4682e99e0b5dd67448e92ca12240dda7b6cf12299ccfe1c04714ffa2093ae7a384d4438853cf58641265887e3c46fe6c61876529d274f5ab41db058f3aa3975a03e8da30823843d51d0882cb18cbffd6e0cd0307a86c823e8eca502bc8aa47f2d20adfc4ab273aa4b7e9730959c03daf2f86bc7e8470d4ef16a6f8004109dd0d3029851ead34ac405c62bb3d276d23c9b965626d9277a5ce872167a0e7daac91c43c447fd33fc5e7ae651b394691f89e23eb3898c3e1cdc231946aff7e64b7b085e25909d129cfc069e7b3c7c85a55e84b6f9be649fd06a5dca6f53e00f85844c6800d5cd081c772738f2fe35762cfe09c0df84ad0f2c9fbb03df6fab84c1ec92f5332b5f299b77bbc93bc2ad4c9b09ad176da465876ef8ceb1116f2c04c97f218d28e88af8106cebc0ec9769c275b6f0f6c875ff249d0c25dbbef3b35cd758502f768881b43bbe8493a7e580034b29dc121601a58fd5b01e2a714f43c043cf7de4349c065f4e4020dbf0e278bcb67231f3444cfebed19c4db06a80c22ae33a88924e4ff82a3885ddf638a9b095f1c9cd9d52f1dc19a31b269be9aee4a0fc1906b960e4ba1fefdc8e5f50b07e1a89b9d393ff1cc94d0765c37032701275851842fed420434a5405a7bcf90c70f4f61b4ef959d7ee048929ef761ff15cea985c04f798e646482c26f1ec450a0dbc594076c5bfca0a906ba7d20082458e5141b455043254e95065741691b71d0c44da18ac85143ae7c431d9901bb72c09e544364b05027c59cd6c331d5773883989451c415449e64a968292c6be10861bd40a26c4c6d6ff520f6e301e668623973bcb1fa5cbce34b089a54bc9d4d3e7c70c8b893ad500fbb95f7c5fd828a1712956f5000ef9436aad884d54d2bcee1a29cf0201f449c61e3679912fcd89d530c476363dec7b7933d23ac8a3d650537ef6b25258a2d0204e1a4f824dfc6a978b5cc577801971e8d81cd1131807391c9cc04bb8f33512117f77b525ed5b997577343010de5c90e1d152a0635ae14e880a610b41db5c7e2727004c8f4d89c8e785edcc920484ebfe90f5371747c0120ec1bbfbd855b7ede265e965b38114c31575fead252b15cd8d5b5067890df147145d1074d9ff1d5c9dff5ac0f7596a91a12f3e2a0cc13d564446f27c60947e043ec0432ec20e7d00d5eb2e5a49d5bb296063b9248fdcebb93ab5f707326b08ab940739280e414dc24398e00d888fd7b93daefebd5f618b1b26af2880fb2803103b150f8c0137e81dbc809ad3b681198f90af4649354280d87a96fa4ba42947d674e05972ae1414c9881e77da28a3104f0d299b8dfa4114ba7b1794321333873abb7d6b56cc4d0ba6afdc79d8985713e7d264546f2fb85433c21256454f69d40de9e5132412467bb72a00616ddf92e519d9ea41ad5855687e903584c3ced55cb13fd31429038b352c7764c402c8bb0d4222641d9b0abab4b570bde2fc02f68301859ea97e1a3c0fe54b25e03ec7f5391214f026bda498b772597ffa0d903056b2bfa17d7a66cc170fb429b2af89ee077e3272168860defc4725668dd15c1672a334d404581aabc61d81e9f04a6b5f5a1339d5e19c35fd08f55eaeaeeb1b7e69151194fde26291ba94dfe7b218cdf34ff1d3652e3096f363fb17688ab42b6bfa0ed2d112eb7852c544bc362dfe937222ee66c264e1f8f6f830e4eab50f8a204e891352f6c8d7f14f212755ac10c0bdd66349357f5458f1b0e58cfd04bf502b4643c909c71ddec0c8fd98ce291c873afc02354973bdd7b95d12482098ea456d11be25eb19a12722ec8c41b3bda48eaa1654ca5e41067fc227a6ba2401dab0a61d8d496fb384e9a871e4e096b43804680e243cd5544b54de5d3546e841a42e0bf4605cdbd3729acf034f1a3ec521b8939f35fee6b392e70ed85b8016ac3877dca899eb86e660c5c4f810e8d4baf2d03e8af521bc3d65de3119263819ad5b3e91c1e7e19e8220ef8e402551287fe1b7cb1b3216e8c3124218a21b15825b3135804c4c002296485fba02b67d9c983d0bba7a1430eaf1d0a4e145034556bf631d9f8b33bb68b4b8f1f16efc49f5c2892b9f7ccfec19daf189629acaba6e7ae587ddc1ea0c616be0326b37f0122c08ffbd5b325e9e06d123e666958a5ec3731e33bb2a1fc3e3134107cfa9fc731498e5b9d141b1cc489d125cdca23a9b534b261cf05eb4bd33459c280547b82e95c22ed863bebaf55657e8f25c9539b9ef926eb72501446f8ad9c0b09882669fdfeb90f0f7c6422aebcf2094c275917245bce559333283d158bb8e3c319ee627999cbddf7dd74ba381995930a8c87b83c79a80c1e6690744c93e98f83ec89b2ce97724a2f0811269e722c9bf1317cb6fbe66bdff3521329609e91322091412cde4ce105b2a07879afd596d5c2057cc4c6e0249179888f2e9a53b7642347d3f664ac6d7c4324209fd1cbd309f9c17ed3c873c792934d64769f5fc6528da42bbfb95dd56931f6a12f96f36a8118fa61ee247c13716080d6374432c34712b482a5c4a6b45eef29905c702e5118815973e5fe1ce90a7865d15a6fa1a428227f0572bda5dd205ba50ee8244f0aa7f685947f41cc3325992687625c7c01367e9e898e9b2fbc3ca67dd8a8816e2131ad89e403688991ade28807f8a249fddbdce5f3b4ce6eb0253d9c1c3e4f19809e990837c4ce95a613273886005bd5f4ecacaea28091f7d6caf19ee800516105ebad4303b5b5578fb25cac49361cc6ce5bc439594a92e2c0bcce13b327ddb25a99f035271b5201ffb2ffd85fa6b1a3cdb281445607c5c3e720e39b88f77298e38930bea07a4254cf65efdcfd75720c89c3866628d5c0d68570d748df4f10498e986c891b7154b8160479d027e552d718bc1aaa3a5a1c9f5bf4d3e52b1fcbb1cd935f2295c794e8dcba00ecdd103a77cff521c01831d21f1057a91b80ff07d75a02445968690084ec887e6c892c242385c8f41a577c1334ae6f45d6a7baf8eff43cc9bb8debd4280fd02fbf3095768e8bdcc8d5f00c312d121095ea7a33178018cdfcf81caf81313d6c8f009180a9f19db8fec69bf34efdbcb26e1a109c7d4759084b4b1607a4a2480a868fbed8f5122b8102d63679aa5a0d46ec5e726ebe1f741a038460a575170b3e35b5db3414695ee89ca8ab3be4ac1c9b8cd9972d444252af1fbac79cf4354166df10a3b1454bfb7cdb8c22a467eb3a2ed090673d3e5fba3b6319968864ea5a747f16e9d21b5873916642f923afd49dafa94ce4d82afe20d1194eaba1a8a69fcebccf6b49b8c272f5014ddd8aa3e7ca7eef7e5feeb6df58fac930766f4bbeed6806e88eda3181b4170906e8959763264b05579e1d51f30165fd80b07d9da0f58f6d069cad6a2d132978e6c716413a458759dc6261c3f448cb68839db05bcd853f0c86b859898316fba540d021e077e4dc417f6746bf60d3481b59f783b7a45fede3d023bc201fdaf9c7508870e09d26b276d87af147f6a9086bdb14b313bf4f06635eb323bf224595fca5d346af71ef023f6907e793998487843f0e9ac1664d7ea2994b057bb9387e181c253d3e9ce7e809c3f01291c463cebe5ffe7a101df390ed2261c84d346197f43927cad1a0f1be0c07ad307aab4fe7ca70ea4968557901b6f5ad71540e88199b7c16a6433ec8305411ab8601988f0eb6b54f382172d261cb74189725635c69f30206d4443bd9994d27f026afa1ae756c783cf9bfed7a4a85be356d7e3811667f8cf6f43fb36bb1aed3202464240d28751c9513181098451f2ee1615d5f32bd32b621218ec8ce9c38b5a5ef9ddac2fa759ce65c73642e2266494bc931fed609d4cceec7fccb11e05f06d43d01fd2be00516ad7002f1eae34bf661df5e0c49f61ca238b421825069156802251b5646143a32b2df0d311a1da15403e02cfed98a5de41be88247db08244f14a6da3a3b0e15507f0f3156f7cc0381b515b78155d8bd5909d2d51cc477f4fb2d4aebdcca11d366f4bab43f194c3f167251bf5eea88be647ec4c22f9b109320106c3ecebf4cdca332bb7f2b1fdfc0cdef569eda9a2b35af65e287aa9011be13b4296858685a153e532dae69d09462ea932e6d03747857dd39c2af69d400bfb63fdf37b98be52d938235cf1cd97ab497cbc70d2a535624276ac0da5ab667a73769cbf69a0f189464227fcbfd2b2f4d560d90aed635218b051ea5e004354de10bf33c1906fbb676d9aabffd430b52a0dc1f5f16d4c8e2046871fda66df06c467cbf65138e5714de32e166e9ab936c2896c5fcecd22f0d610ddee27a82b75c9d4694a3f513850acd538fc8dbc0375be7ed5e8018d4127a9aee3e3251c70c25cc36da19b9b9da73b7168036f90a9f87ead71a8d85469f6e2f1eeb63429ae72a8b3007855c4ef050847a8a02df3b9d62a9a6ad2072344ce8073df5e664ee6d55d596e58753ba4a9e62f94ea38308b03841983e029d87b75a6474db55ce35c7166071cb0e52760de0010b430983496943817510a19b9a2b64d0572a73c68a43b487e853d9f3e1bdc19a8c239248ce4c92fecc2d668bc0008a7a212ec87a59eedbe40ff032554fb9d60ce1aeb11b6e4378b25d18b325c8a806bff3cb82edc2cc2b64016849f14b931fd1364b7d0339ea5a9803f249e80012940488482c05368023657abee1c8b4f317553ae513d49949e57dbf5625099e426507cbe114589e10c27241c64a9d2f9e4025b345b846518773f17ba7cc78374c711b0559e0a77dd67d3009460e1ef66758d8495ba96de21e6b756e3777e41ff967e161dac8f972c1f1c01196988a82e6db4f8c7b509d1225b3f6e0fd800f1a2b5a30202a8d0466d811ea05c66f6957b661da91e9a362ea7c4a46c395461b2a543f23a577f31f90ebaae20df2c82941b3e23395861670c52965d441b68483099fe024d534f0aca0248834bbc241bcfa55745c86bc3e8b9e5b95d189b4bbc2f8ee085ed9cd00b0409b95f53c428662f910469840792f7e90138efd17af94661b27a3bb6b258ec8c2586db360bb8c00ed972f7e974b5f9cd9647f894168dcf82df4c16633866d01678b5c04e2816e1ded96ea8e3592fede358ae5b1e94c2157cad25a60baa21ad35766119e01405114b4eba3ddf53425ad96bde7f9930c24830315c51b1b633244a7cc619bb661d2812e843a697d08487176fcabbe17f3fd927956ae0100519f3105114778da853e42b27975515d35b404f8471cca1980d33aa68f07f58b3dc1793a30a686c3ca6e9effbed3bd0956dde31aec2c6ace69848c54acfda34b66ea31d6841bd27f69307542605f9c2edd26f0cc6a3d60dce440d4d84f627473b7ea621117a37a6eb7f0ba2df461f81362a1f2e68a039e955d0c0adabe0e5edb47d4267c228e2155c95d5ba7f47334b92e13aca3c80130062caedb2fac15ef24eb230ed1c5fc47d85b8486ff404f86fcaf87fce6f8486f5952dce6eedb493668b89f0bf9a64137aa6c8eb4d1b36499d1b3f9f255d21982309e892446533f6f44ca2b28596de0f18060a1b71a3870e7f63356b5331016397f93024a6ac2773b1d2cde45607ac7ffa7fb387f7cd7b19521c279e922cff339b9efc7158a19415983d385199e0e06ce68b31be04c51ba09068268d38cd45797ad221815717ab309748f687cab274076aa075faf796ea4aa6f751237697488505860b217f21ea470b7f4e3c3e66fbd0a3878bc544a731217f230eef4b8525ddb1fbcbdc89ebbc1bfbef01845f07e555cbefc7be013efcafe9d0301b6fb4815767494361afb7fb19e687beb6d867623b906a926ef65ce5fd937a7d269a2971eb7b9f812aa6aa702b7693eb37fb8d2d310bad6f7b00927656d8e19b8adbcb75d1d65ffd61dc14b62d7ef96c0cd60df8c1c025f26d8f3faf7f468f03411476cadb640b816bfdb825bdc314b83231c66f5f153e4df363607c7ead510b88c9e314ec3cc29d1ba6fb52d485005617acac7bee2348df1cd98a30fc0ae08123d55073aff7b42da0e370886353e3929aab82b60dacbe388630a0bc7b2d01d950364c7ce21b0c54cefb13a6ca41d8a304d6cf6973dd78e99e69f86e45ce477f9560e710c8189648fb990b65f396a98968d1fcd196a2519421a3f0a6cdc06d071c36300c572b17d26c411c2d3541daac9c89bfeee3dbccf6fa5f63387c0c4d2fe4a1dc66b3bc39edfaa311a1408a2d964505e136096bc1ce023bf5eefe890918fa543e8719fe3108f8e2cb315c9d16c1357a8e3ef77611fe03f0a5a77b08be7def434d465c62ff8102aca03ef3c1589f294c64e221a8474642ba5599fe23c6f04cf5c535438357c4f456b0e58d3bcebdc0156396977b46a14d465fb3f7dd14ba3ab6b7023e281f76e37e81a68197ea327c45acde0a58d678c7b917b8b21806f91c8d45b3c92be2606e9e85b570d415a9fe057b045fd776a8e38d5762a7abe7dbbd01acb36fac87027e3d85200d3350b46439fcffffffffffffff8fa291465b6bff132a9394d286b78e7a8c22534a29a594c43880bf1920e0405d121ad9bc5f0702860c880d790dfc9d678ed35146f74249d77f902d7bdd2fe9427164529f93cdfce408178a3b22683d55ea37f7160afae4e4a5a89aa03a2d944f07bdcc1b3c483f0b8520729e3732baed86b050101ed7f46ce80a452dfd2422040d429aac503e11b473fe52150aa2bd3a88891f9e7d2a94d483ee91d9b4f57ea6504cc9bde1d6330493914231cd52b3c6a8db79138582d6d45752b7747a1d0a850d3fdfd1c388fcf9130a4a764c6524d95d8913caf94a998c76a63049130a571963cefc0df39b09a54f8d53dbf652255d4239fbb342c8fc5323554249d394a78d3209057de51e420ef3ffea20a134e35d7f71324728dcaf5c662c19a1a4caf68326d724c45e8442fcdb70be957d114284428cea26e3e7bc0eea104a75df698367350d53080513e196a97fde1f0b423188c888a8265d69192014e5454796dd6569fe0745f16c1e745ae913415ff8a02443e958be9f5e94ce4f24e6ca8b52a69f13fd7de22cb38bc2a9f7129e66194c5d94479ba5e96a8daf792ecaae1e46e64a86ffe0a2f4494cb627edb7289ea7d5ee9bdd16c5b8351b54e71cfca45e8ba27a48d0ff1f5399ca695196a43a9885aab096905914d65e4efba6bbf908914521464bf1f0f2d11e21b128c6cd6dcaed3bb7dc0416a5d7e4d947a6e77f92bca29cb76263725cff88c415e58fe9db96d9ad4524ad286b7b6edc24c2be42c28aa25cc6789d34e709175945315c09f52e55ae2d5245417d8de8f52ca53fdea9289eb8d9ee769676bd5151301331bbcedbe0779fa2983339db5c9d8bc66d8a4208a36d2ae352942fe2f6780e152f5a5214e53e577dfef6953a8ae28724e3799d74f05414c570e243946a658e6787a2b893e4b8a60ff13ad8a028c7686e9dbb7984fcfa1325112a23987477c95e7ba254da91aeb4baee6edd8992c62c41fec5b44d6b4e14443f5ddf537cd5b489f2260f63af339e45ae89d24d9070fe9b59e29e89b24ab6bfd5cda56482898252a59d3cd97c8992ffda6e9e9e0f62664b949412ab1625c4c35dae4439ea6fff98bccfa64c8982ee1c25845c7399274fa2f02efae4780c1f84c892284977dd0812942251f81f91647b26bd9a048962e91349a85ec7c7d1230a7225314749a3f4751c5188569353fcfe99ba8d28693cabd713c133db6544f94fc5679a984a4974175118152d4dd9a9842e5711c5526b22f2a6f547b78928b8c78839498e5d62848842d29975f77a4bd11ca29024c4609323e8b4dc1005cfa4a3edca64aaa410a5f8482255cdcb754d8882a8aedb0d254a793c8872a684c95d23d73e5541145e6436c9be0e72a506a21c43b22a5deaee43500151d88f1e6fbfa326b7f40f856c711dbe4c7e28c99010332797249aef43e92fee3fbc49ecf7f950b45022b3b37c06dbf7503c8992d443d9f3c3d347e7a75a330fe51aebcd169b437233f150128f416428a937eacb3b146d242795234e7628a5c60fa23c687af7eb50c8dd41451c3d5a3f4487b29a6413c92f94cce7504ce6259479f6a4472387e29dd21997639b668f43e942f66f2f9236170e653d9dbe53eebf2b7e4331957a9a7c4a6f5eea8672cbe4d16573616b1bca23d5376eab6e320bd950d8d0b95437c8583dada198547d8986a81bb5a38682caa7af9022d5634c1a0ac965e2a8a56cea1d0de5d169555c749a98e40ce5d21edb442afd096233943a454777fc3f8cf2ca50322159847e323d224339b5ed0675a2e6ce3486d266a42b95a3c734248672df4713fff9b9a30943216fde879c2acd360e8642b4acca7e31b937f942d1943e8f9921c88fea854294a02f37aaeac6a075a1f096923c87b8794c890b05d9e96e17396aaf5b28c85f4b941caa66222d94b384947b99c45fe464a124640ed59f2644da8d85c2e9d2a079fb5477be42513fe9c4f8d3a4d36b85927f7cc7a6ed741aad42b96ae4a417d341bda44279372849952b266a9c42f9649789ab4e0aa51072d72bb2fb2fab0044a11c125523686a4f711d2894d477cecdff69e9d49f508e1c25071152e8a8e13aa124ebb37ae786d45065138af6971a4b5643ceb90a00130ab1f3e9e95235a2592fa17c6d7a2c335dc5672514b429cf9cc497967651042df81823004928880d2a12773fe65515002414e2285d511561439a7984c2cc46ec92fa25934ce918648481811b37d804c00845131a1eb2c76ac8a15384f28caecc5db39ff66222144bc7fbf87d8b6dcd690086503c6521ada36c901a7d1c6c627c184fced3001d10002194b445884df1e147ce1f8452ffbc5daa8fccf822104a9e4508ad9f91b35d15801f94ad7450db9b733b3c0f800f9ae43131564b33e6dc6c1d00e84559d4e8ed874c3109265e946e5cf399ac733f8dd94521a60c5ff91e744b4600a08b72f0a839c690a9f4e40c805c14db2b52bc65884fd3b800005c944456c30925c257e4d12d0a26afea2f42e60711930500d8a2102d5f423e8d9e847c00508b924e42e41835a99262ad0100b4286aadd9e9a063cee6a7599433878cd9930e6d1d4eb228bb8e7e588e5a04a9894539aca4f3553f1d576458944ac41a7559959b45fb8a42bed620448e614572d41585a097b549882c9d7bb6a2283adb4c72d019928fac28e6081f4c2919724de95651ca30c9faaba38a420e3ac63f21bb74965c2acaeb26941c57b9d41d15a54c125642aa54339153944297e4fb20976a5799a228631f23ef93fbe710f601004a5152b30d93d7713e629d01005294db43f4ff49da46f7348a624e916387944d22e9491425a543e3797524312f85a234ee93b7e4e34ed30f8ac2e589da8814e29ffd270aa7579722a9fd2d7b4f14b34c765cc8245a13d489d2682a0f21ec675229e244216a4e777a7c9234fd6ca29057bce4339f897f8f260a231e6cae646a88b99389f2b7a65b868ce7af1d4c1494650c3a6911e5569d4b94b55ab4759ac6b0316e8972bf979daf5d8beb5e8942ec4d931a43345b764a143d42bc3a3bd91b5d9f4469258c98480e6146442451d231c4c9c798f63f4e2251deb80c5a26b66c740412650deda6b921496c8e3ca2281aba9d4b9c859fda11052d4187730d9549f6dd8852082db93db9e931d28c28875c22c8104df529752fa2a0edf1cdb3a976ba5b11659f6cb321d375166927a270673a278f89860a1b11e5d92046c9b85f429bf8102531b1459610362a1a36443162a42d3993ea2d7221caa1c7c37e26e9148b0951ea0a1d7dc25ca6f27410459f33ddb153994e7e0aa210433e624c7b424c2703512c21f3a4b536f7a449401445fb5724c70491f5fc4339ef4ce60d3a829075faa1108314351144e3a89a7d289f8c5de889ecbc5ff2a1f8de16af413526cfe31e4a1fb3c3c8cd1e44fca88792ccbbdb502f1a5a84792849554d32f1623da8110f850b959c527773848a77289a9b87adac939822daa170269e1bbf4dc89fae0e859329742ed9e950d2b4eab839a9ba7b0ea56b4df191efac734f0ec52cbf8ca6fc56c4290e4557cf61c63f6496cae0504ede494cc9dc0e7af486a29f6f8424b6b261e28662082fd9ccd35a69ba0d45d90ccd7eebea79d950768da1d69739eeb98642dcb0fc1931abde152b00a006b433ee4b5ea7ed1893063e9f083ae8586b0a0d96f84eca2d3f43c1f37ec8c61af75d9119cae5b2676e1a2a43515c2449d6fc38d33964287727a99a39c7d039396328c80dbabefff3af868f188aeba7afe9be94ba69c25014bd41bc35ed602878c6ebb57b1dcd9a7fa1d8b5715d2354d49bda0ba5caa46cb4a84ffa2877a124c4bc88cef82fa35c2e1425e7cc247d9e2693bb857288105b2f4b2d9423a595f0c8272cbd2c14affd8438a1b533a9160b05d7976fd90892dcd22b1473cafe1c9949259bd20a050f2159b37e69aa8c5528db69f897c89aa43d542868ce1a3a4425c8df3f8582e8b8f73796e9b27b2914a37eb44f27c4e5f52814620e3aeb7e7412a63e50289c8c614fcd75a2b8e70965d39f5ae3482dbfd99d50d0a55ad4cd64db2e35a19c3965e6fe89f01f674221965209f2b95e4249ec4aae7f9dd2d6b5128a6e1f74f07f390905fd267674a2bdea080905cfb21b114d6613b91ea198bd83087a334a74ea8c508cdf3526534587f817a194bac14d8ccf9a0e3222947ddbac3b461239410505000c41179d649ad3ebde01400889a569699dbfa73d01200865d568cacc44c35c9a8050564932945b8614ba04801f944668892983ea8daf01e083d4e4e924b39a6ec9b517c5d63f9174da8c172521afa5e24ba8acf9bb38c879499f3fc2f526ba28a729d9a97f54931042b928760afdcd9fc9e4b90817e50a9dfb09daab3744b728879c944e14c92a51746c51fab8d8f38e9c5f33a716c5f027438f948f49c40d2d4a4a24acaac8a03bab6616e58f8ba8318c599bccc8a2a44d354c0e322d4dc66351aa0f31e428725e45c661518e923da693dbcaeffc15a5cff2ee7c1db11f27ae28da0413e311d48a820c2773a773503b32c28af2894c3207f5483263641585209499aec8a47a735514db244facdc527db731e8949d3aa36975cfa312a5baad1f7f4d656e624a1424e8fbe9c753671e571f784ca2b42e29d46c0425d93e922879dc095a57362e138f44d9f48787d3992de36e90e0b2c54bc66636bb444826b3f64a25c83d1e51b05599a0944ac8fee58d1b1ff6030f4794745fee446850b9397a34a2b039f36e2e1146b4225e6775776fde5e2b169ecfacc67b633c1651504a4b6dc7a4938c892ba2941b638cc660bd1f738f44a06da29d3c10513a4faa1e73c63319443c0e51ec39a1f7b344ef106d92050f4394f7466b87cea9de934964906106098847218a9236ae9e6f758c6c0851fc8c39a70f7132886292bd95c9df9eee670f41142fad2bb467f5084471a25976fb9e69d279470e10741e3c00518afbdca7f1a36a4d1e7f289cba53c2378875f0f04329edc48c38d55bdea58584471f4a22f464b13b9deb27c6cd38c307e1c18772d6d335f9ed7bdc42bfc3779c600fa5ec18d6d767a592460660e0c3f450bab3d39359961eb36ef0c843316ebaefcf4e9276fa1a3cf0503cfdca181feb4a29993b14746e6cd8f0bfb23187c1c30ee55ca19e4294ccc1265d088f3a1475b7c2db2abeca346ec619ee418772c81b9a7c45b682c71cca714a07cf41688b333973c1430ec50b19736778f4dc3e3de250f03c6f9af4a4f2fd68c3a138b1b46fcebb9bc71b0cbd911bb3e2fc30c383871b4e232b71c4b7df8cf0684379d3da576b5f29a1997d18fac1830d25fd169db38d64d6ee9e213cd6c0a919fd22223d07070f352424e9b0b94774d2504c1a2d443ead2bf93b1aca39eed87a963ca6bff63843b9523df39929f3cdcac30c05e5f127dc0891733e65190ab235247d27dccfc64e257890a1182fb935d2be09ef770cc575715d8d6e256c53622864511a92e6da1cd37cc2500c19f25d6224995a3c227880e10b5ee8820e057870e177e408e383011e5bf8688087165016b0a0e30a56a0c214a4e01105287ce428c3020af078428e322c80000f2734a13d9860008f256c20011e4a48c2c7023c9070841c6558c0cd38c30205f0304211883004057808e1001e410002013c7e50000f1ff442003478512e9160a661e3d9ebb041b971a369eca2d425529a2e99532721a3a18b525b8dfee839d5db8d462e4a1325c95c42c56532d1c045c1329a8f77de557d931f41e316c5cca29448df1d69926a081ab628a5fca7312123e9d3e31568d4a22829df6abcab4a46a745e9c3849953a1c14eafec028d59146cc62db3632308fdb680862c4a326ad0d41a4f6f258c45318b8a90bdd755327f028be20999b7238cb48e1176da2b8a3187b00d6d7b9fa7e48a920c21acabfe74850ab5a2989406114783ec90d76645218a4ef62b762245d02aca7de7fb5944ecc6f1c618010330d036051aaa284917191dcf9ed57845f0614b071aa928bfa7ce3edb1ecbb38f8ae28bd8f01e42e7fbfda728dc86f64cf5e32baf99a278f39e5ed47244f21233106894a2bce2b9cc53758e8536f8316ef01f2065a4288ad6a444ebc8d0deb7031d781ae4d8e1307811dcb8a1e3c60d2b1aa3288691ad76911a0eb6da050d5114b3a4d3b69031e6dd1818bca0118ab2488c1393de5ba74ca9008040031425cd0c5a56bd221f93bd714304f989b244d11ad1344f1437ce3afe3dbf736570b05d106874a2f026dae945e3623ba6821c657819667c7c1001088106274a3a9c79aeb504f59ee36023c19f3f63903106061880811ba41c66bc0a42b07a64d400052f030fe30218b8716355708130cea31b37ccf88f327480010a1a9b2867281994880c76f2ec8d020d4d1425c6149f6a948a0cbd146864a2aced575e2daf723a01030d4c947777c49fd8fb9bacb96e028d4b9453eec80cda7613a2842b40c312e52851d546e48a8345d0820f1cd0a844499b6e779325fbf51e0d4a1473374c484a6dc9fcc5f031920128d8800f684c027d9a7495c8be39c76184d1081a9228fee8b439a3752795395ba0118992c97c91912d2647476940025d79f24d86a8196fdc28376eb0a982c6230a3da7a4cfc911978fca545beba0d188426f9f0e13d58477c4e81d6784600c0d2823d40e75fb506e0a230c3f3986068ec6228a31134656c5a96b2cd15044493fc2299d74b20791dd008d4414d33e68c850a25c4b0911e5d1a64d8974a92106a54394c37aa9885a891d45c910e5acf183ca0e25ad32a910a58f31db5ba5934e91448852fe4451997879194b8328978c9f8713224d9b5e1025cf61e2574d92e8d207a2106fcf7488f16ab3e70151f2d6fca13c95f5d8ff87722911418f4c10d3f3fba1dcaf196d55564383501f8af3e979b2cec9b50df1a12862573ba2e6a0c163f65074cd9b6819936d121b3d147fc4e8cc4bcf125f9387f28aac3ce1aac143393fefe222c86534cd1dca41a7dbd1d2ee7962c60e855132f8e8538db78dd7a190e7e3bb67d21966a243795ec6be4527592334877c448e614274e450d294891df7ddd77b1c4a1d59d428251be397040e859cdb94b40d2ae94bf286f2075b955cb6222147dc501075d93a95244ef26d28c49c4408aa3c74c99d0dc53e99c46476ef28925e4393e36e8390e1c664ae9d40430dbdb9db6d4c8c5bf7741325ce4ea6cd38d8b69f40230d4519b53ef9e22108d1d240432179c89b7589507b9b1ab8096c41e30c25352195e7f5ece95a3394d448704da76627865d865287681d2d4287af860c254f8f49a5df3c8692fc10c6726d1643f1224db4b114133985a1fcc1c3da6c3745fa6028b66f04a5c64ec9f77ca19cb4464ff7b9eeabf54249ba46ca98314c12e676a19837f9c589b431f2ca85b2c46426e963b3ead22d944ac611ffde2b29272d1452b88e767ae878fa2c145286dcd12c5ee468b0504e3ac993113f49f5902b14e7d44695cfa6154a732269c9e156a1b45a1ecf53753da65428694e11e17408feb64da11c9a767dc3d47d5e2985529c1a0f9f3ae62cfa2894373b7e995bbf8e0e14caa2113dbde7b835fd09e54e2173481394c6899d50f218e95d63333fe7b009259deb2bf1ebe136890925bd49f89d3e91cf444b28ec66c6bdc5d94594508cf51d7e449824a1b827640e9a37c46d774828070d91dbbe7e42968e50eed5104e091d4467d40865932e22dbdbe9422c4241b799f58de8987d468482bc1ebd96a2554e3484823aff3c1d3378ac5c0805bf4e19bdce39ef5637f0303e2c0c472b018d2094d53224fbb8e16256c4c176728461ce28e3a3066ec60b6800a1e4c1363d64dc78698b83ad75dce03f60f667ccc0cb20634140e30745cd11e3296d6a1268f8a098f4e612496df01033f2452f0a22b79ef567ce9eee6b4006066edc30d6bc28a7491f75596232666917c5fdacdb99f44b9a4771b07d8c913ec4d0e135f8206307228ba0051f17f8421745d392b64d990c4aaf3a3e1e0666bc18650ff8221725b1cdf1435e5bb3c62f7051b8503da653a4d54ffa5b1472f2a8d71dacbdf48383ed0b5b147248b9495248f0066e8619ae627c510b9cc117b42889cdfafa31dda6d0fc2cca998388ba267323ef238b92eefe0fea746d6af4c4a2982be3ceb87760510e9f14da622f4298bca2b4657f22f88ac8a0195794c63c9c3cd318bc635a51b8d49ccc5de38c4c082b0aea23e7eef15c45497a34b55f09aaa29c25e8eda4be44ce19a5a2243593cce5db263c635051144d5a6394946a1fe41b37fa696072f8e214c58f1f36621a193a21ff85298aaa922a447930b5b876105f94a264f22f3f846ff49511e2fe8214a5f1f8fb2186492a454d78638c2005234001086edcd0447c318a924e6fea630c2a928819812f44510ca139e93cbf4cf8ea8b5014d2ff6e08113775fa078a52fa689886741dbc933e5190113523ad68ee60254f94b44ed3bf27e1a274be13a5349113277d4e14266fd6d8749922f4681385310f5312454813c5f4c13b6b12ffa263271305a11a42b24cd151754c944d7dea4e1ac45c89fd12c50fa9b645848d92b4250a9f37e80e93feebc4ae44c12af34c4d5a4f2ea244317d669edb68a6527712e5b56d4f2209cfcb5c12c5cd6d1171638698b646a2201f63e37b3e88d22151facb10a525460c4fe5238a1a35b7a889fd08417344e9fa55b326513511528d28ccb927b5dd392a84d0178c289a8e08ba5f115348c906be584441880819735fe7aa5c1185efac994cc4c9994c948852fe5ac4cd8e3d7a7ea2852f10511027bbbc326fd239280b5f1ca258923ecbfe6a0ec2b401191720c3587f618882bd87cec9df4ba7ce112b7c5188a28824644b760d3176f40b4214e3c6d39aace39c6707812f0651fa4df7b672b7d37955117c2188729c546aac4d676f147d1188d25da866c89d840eda64c2178040999cb471bb4f4906321d7cf187c26b7cd3da99c9733ab6852ffc909c8aaa9ba669a6596453b1c92189a0f639fc410652abf3c1177d2847ac8fb176ed2d132ef3c2177c38a278cea226097dd8ede18b3d947cb32544efdc29e2ae8762adc838a12276f2087f91875298f845705fd5103c78289c87489711212491f90e25fd0cc93c7a2230c83cf8c20e654d4a3f5364da9c4c5387e28a578715091dca693f2911433f44b36f0e25f531dbc735f549e4911ccaa2fe6a25c750199b8b43317a87f7d19d7e0187625ef5f7fac9633aba5fbca1904aa8b50fcacd2ee48682df9d28f1139229db501223cfdf3e558c78d850ca0df16091ff363c6b28ef8a9d5ba450a1266a28be8d9a1c6f0d57ea34947b366fb776e528311aca695b3273d0ae61ee198aa12372b935e6216986e2c6ba4c317979ea4c65288710825022c549cb2a91a1d89a635f1a4db89cc6506edf9841e652ebc917437174c9fca4ac229650180ae1e4a8a9beab3ced83a168ed99ef5492af6aff85d201478942dc68aae64f8996ec265192d9b0e1dee29eb59344318c8c3cb66fb951bb48143cedb3ac78f492a983443966952092aabe98d223ca2141d527c8d2598d230aaaf98492371949e68d2876ae1bd5db6fed218c28849093f23af5220af2c3aa33efc9ecdb14519231fb33b7dd3bb2968882f8691625fab4e96a88286dfaa8caecd3264b3b44215d8d668862f0cc25c2e6fc629d15a29064e8eca9463d8d10210a4ae969b7999d92d1204aa346e8fca39a1f1e41945d3f6b94a4bab93d10a5ed74133425802868afcf9e49095d42fd87528ace2eab191132ec87d2e72fef4c6fe32ef7a19c57a3f857c631fdf2a178a165a46ff544c9eea1a4d16525088f6b9beaa1209b2177e68d279d3c145367fad62e75b9e3a1184ffc84a6d3db6adea1d46da23984ed505a3dd9e8194ec660d6a1b43e4195dee70d91a34351476fa94c71bb75151873288f92b924289f9c845560c8a1a49ba212932ce160231f00230ec5be94d849bbc5e4b0c2a1186623bd441d65fb8ba0051f3500e377fd584230dc50f2dcec12d5c132c724186d285f96ce11dd9388616630d8501023fd7ef24eae936d0da5d5f0399a85e804d1e85043b1d33ee82464d0fe254d4341638ae4796dc5c1568786729c6cd09b9f8459781a00e30c2569b9bb498beca6d8e0606b3394639ed5e8e13e7688058c3294449460bea1aa76424b86b27d183d71e3c3c1d66328665cd6d8ed69d8d38aa13077d6563a6fd449370046180a7f1bd386fd2d89418e030c85d39d3b32c4bc923be1601363bf505229d9d424cb8eaf999278a17c42c74efd24d97a4238d874046074a138b3a13b49278c3ab98c0ba557b3dea0f91233c471b0d9e0717c1c1d39c4e000185b2866d024f46a380db6270e36f4ab87f739ccd8915a28cfa7d5899cbd41a70c8c2c6037779e3dc628231c040c2cf41252a6acbb6c6866e7c758727d3cd48b068c2b1463d2750b31ea43ca676588d1811b5c40870e27c18d1b37160c2b9484c8cf1362a21c297d18d20e68400362e8f01adcb8f1716054a1ec7dfa412fc31d00830a650fd9b0a6e6c3961ae1609b424188ef4a75d79845ef38d85a0a05e1a7ea46b388243c8a832d0a05bdf0b1b4d3e0605b3d28942e5d33acb5cc5ba670b0150cc6c8814387e3e01080f184626edba0dea7b2abb338d81a056038a1a084c8b10f5274e4b09a50cc378faa5adea732ce84e2e76f7799bc1f0f191c6c89088c25144c9bdf077db9d1afc1c14652008612ca319ddc341223e95092008c2414f7e735e9293122bba6e363870e3270d8e0f74c000612c420838c0f1898f1f141038c23945f63cec939dbf11e250317008611d018396a30c6191f63e480c11b0280518423ef0971b5a9d2e630fec30c323eca7031cc140c22149ea9b525c1d6e265d4328d6cbf77878e0f1dbe648718098c2114d44b7de4ed2c8c30727ce0c60d32729091e3a30c1d0a86108a9e63b43b177d4f3907a11442ae959e0c9a84c400a174ea3ee97d6cbfbd930e307e50740f4173697d5e339f0260f8a0a46fff25bbc567f5ab462f8a631bb6e544c8f7ad7951defa1e1951e2c6c46c6beca29cdab126b96f38d8bacfe834410d5d944f739f7b99dae85ae6a2206482cc1f94fc249dc24512ee6b826dc64b186bd74cf27b82561d19dea2a4772386ba6e4aedcf16e5fbc929b97e83cc335a29d4a84531c99bd1bf9ebf4fbefeb032ca70a445e183fe6c9616ead34ec68e66000a3640060a7216c5d0a14f4810593c3b599473c7d1289f3132ed148b5209fda77c821c16d64b6e13536fb5cfd154dc8cd21af457146563833411cdb3df873f4c7b50c315659d18f7474fa8c86fb5a2904bdcda85de6b780c7a5614f3c7141f9bd356b7ed55146deeae5783f8d507355451d4fb379b20d7484549c8d1b67ddf8d927754943bc834b91a2e83feed14e5b27619b5487e1d444c514eff3924b148f1312795a218a3648b9c7f94285993a2705f5b35135ebf64388ae28ebc92a075bb44ac2807192af830316a88a26cd7b1ce4cae5489eac3b64628ca91fddb45739eb8d71c92b1a38c1b9851031485cd68bacd425f6fdc3f51d2a2973b71bb2296e98962d069c2ee7ac86a599dd03e5446c6c4d6aeba3c53e3e3d662aecb674e94e763a695f4185a74db41461827b871a385506313a53e3b09a934892c93d24451eb42df699791db6a260aa62ceb74c820c74e0813845c775de512be54ab52d4672891f24be8567f89e277fa52133f882ffd93e0c68d1b376edc306337e30ce736d4b04449c4fcbaa95e3aa45d8982c6c60fbb36dda0279428adc6480eb6251e7c9b446993248c8d68c8655a686f47701b51a52a2289e28fc91955b5903c7a242a13093f4a63854471e627e80f33a5348b376ef4c723ca39e69fcb55ff4f19bf71e3c60dabe188e2af76664e4a8e19207d46b211055d2f1bdfe274d0ba33a2f0914be4d9f2905ca38b28a73ba5ee46ef534b5a11e52dd9c1cf3f7c049325a2fb38ab9818934a82a65dc64d3e5263b08c904444d994a69790acfc3cb2a48c1a8728a710edeb1f9f94109d3e3bc310258d1ce26c10e929245a8882a88ca4b5c34d88a206e51dd9fab294418d41144d8ed434ebc9c6368231a8218882780f5d16d974fe7d61d40844493683595a9a3cd90d0e36317298b1a37798b14307208ad5eb6ea3f63a1fe38f4dc19b1b7808ea76e41823c718677cbcb9818b7126c68b51e30f857f51c24ff7fffaa8fd503a511a2e6b73ef355aa30f25bbff52932285a979f850ec53636234b3974efa0f33c8f818c38c1f83cd8c1fc38cdf31468e16418d3d947e92f64f901ca39d5c0f85d119114643e91891e5a19863776e52a1364575d267e0cbb8010a6850030f654d519a5389ca1ceebf43e1f5d2cf523376287506e517ca9435eab095678b6b75457a9790f393bd89d26091204718659ca0061d4a994a694df22b63c8c13914f44c8db0fb9cd4f42e8772501dff6f4432a129ab1187c264cd36f59c21f3936aa80187c2c5fe4dbadbdb18be37942d34c7f7a7522b22ef7012dcb871e3063bbc861b4a9a914ca37e2c213ce88c30de8c32b86bb4a1e422c3ebec06b1af168e1d4e82c4b1c3cbc81becb8acc186a207f19c316ddc77f0aca16cc273d449b2cfd7000a40c0801b379250430de5b213e993d6090e36b41b98a06fd035d2509011531292c6c96e0e1d39da74849163c71912a8818682160d1562268b64d79ca1a4e34da7a9fc75bd5ec30c05ef20cf7cbeffb881193bfa0835ca5052f2ad2cbc3d73b4b706194a6a3b36ccf5e73dd431942468cd9ccb44beb892180a23bdc4a8a66de4cc87a1783631247e3a3d4936180aa9b4e93c6927e208d5174a22f767bb191d52e7c90bc5ea94af21246d753de942b96773c4dde6e41f23c7c7ef70332e9c626231ab65dba9995df76e42e8515e224db78592be5ed3bd7ed56163b4504855f63124496ac4aa35b250aa91da71732449759d076a60a1bca1211a36daa7c5aa7185a2e9c61c6b565f64486a58a1a44ac9c839a70fd93fd6a842b1834c2d2139d2c94985420e59d3c41c2ee5eaad31856290114abcd3c7560949a118c642891bcd3c8b5c14d0ac1411c9319262c6b6cf3fe9a6919a2f5d04857264cbcaf2f6d03e21fc61359e500a25e96c5b63529fc7012794848b9aef4da73d66b8b60965ef134ac42715aa2b5bbd14dcb8a12268c1c70a6a30a12c5eba459e84a0317838d81019627cd02007e2d54b41aad658827133af566da36e15177fa3d4ccfc84b41007ccf81a9001024d251424e7b09ae2740c9e721298adf7f4ba967babb9ccaf35f9f91e31c5db1a48b056c6bd4b2c43d4b4bc1a4728c8549157fd4ce4846a18a1a8e349ee6b4eb21f32358a504acf5dea6cf47c7f480d229435476795d9acd1265b6308a59e8becbbf9db563d6f463200051bd8714619630ca186104a5b3a5c94c4529e6dbd71c30a358250fa109ec9b324ada106108a9a25d88ae684f45faaf183e29e59bee7b49ff6f71a3e6024484832def5754a4f654ea15af2e75c2f0aeaf3a452d17e5ba6570f5e1433998c993ae678eca2e4e94a3c73cb7dbeeca18b42d89f0d5ddf31eaf5b928a6755b9abc4bd968e2a2f0dec94cc4372522c93c6e51c825b3795ba61695872d4a17db49e78dd1744f502d8ad9b499dc187a27ba79d0a2903b7db47e4e6a2682b328c7b8a93a5ac64316c5129518744e62d3ee8c834d03262834634719e8118ba25aa65bcdb223d38d1eb028b8c8cdfb86a422aeff8ad2fc4e9675cbd224e38a8270351d93f02841f5a715c55cd39d53b7e21b425694d3660cdf6925fccdb68a62f4ecef5fd3f6f754a15acdc8bcda5b4decb0cda16453944c0c9d512a8a237f3edd8ddcc5f88f1d2e062aca1fc2552693f30b974f51d0a0facaca4e096d7b0ef4627ca03745e94bfcde47c3b4c6521939c2381f619071b414e5a4841259b4b6e6274b071965dc063c4851b8525ef521b8be5519c61962ec7031d024e0318ae2c6bcb44cf7fba40d7298b1c3c5f890810e5114367b12975977cb4e0e03cf814251507a24994c72d3510332ccf890810e1763ad021ea02858aa9efb8a4bdc5fc320e3f427cae3e3b31fb1445c8f0e33bc0c1d1f629ca1e3a30664989136010f4f14f3327756e8b2cc13e160a3410e749e06ab1e9d287bfcb57d1dad253eff781ce4f8f8e3619431468ee244c1736dc4d3bda25a2c838c1d1f3a72f07a6ca2581de52e845ef7883a0e362b1e9a2867f0be947ded8cf58047268a2763b37e92a73dae9df141c68e32cef8b0818bf1719e06773876b818a9010f4c94acb48653a91e43657489423e5d225d84241bd9db120561648871c6c787c70096803976d020073ae3e3c363004ac01c3bc2c0a1c33f3e3c06908482ae9a53e2ec454279255cb688878d1de61ea13c7f1ae2a63b9c854628af65fef8df8ed253251a4011caa7d37a50aa2ef7b91d63e440234428094d35217e7c0dfa7133ca08e339b0430c0ddcb871c619c010caf1bdd1437e071135bb17d8090c4008e51051647a7668de641a40108a4956a9929b6d84abd7830100a118c42911b17f8f99436b18c00f0a3aea676aa2c235e703f041795ed449f59cb9718354126cf4a260ca6ecef3e414710d2fca71436ab9cbbe2eedef025999b7d9108f58914562ce633aa6523e2672aa8b92f6b43e89b9a9736f07c1462ede16f5d821f3670317457bbf91224265144fdfa25ceabef3483e731d111bb6289d6bc81af2c3b528bf28f52a9fb2da3da44539428e41ff689c91c19e4549e459a9a6902c38f12a39194bcd967d3f4f179edfddc7a2206313ff7ad347f2070bac55c326490856dda29a7dd599d3e4b7e5f58aa2e82d1dbf415e5794d3277da5cfdffe64d58a62b8bdd79434273eb7c18a923ea1f367eee90419c2c1b62620ab28de26254b24f91a32531d3654511e95173a7b96a927dd8c72e3869bf1396ca4a2a041ed7ffa28e2c0062a8ae7954157adcecddf3bb0718a92aa308ffe9e33334856042df870810d53143cb824f5555e186ea314e50c3166a527b6549299031ba42857292d19a4ba84207f180f6c8c42023644518ca7c33d93efe61199509434aa8cccbed1b3630745d1bdcdf6bf3c4408d58d1b363e51f0d4ebf9476f9a4ccb13059d4a9b07736d17df74a25c25b4652e9dbda3c670a23033b32ec2fe2478bc899248a3a2eb1f925c6b6bb0a189729b3e61263747abf564a2a0db5df636e736305196142a4bdd3443f9c8c625ca9d31a57d692a860d4b147572daca8f399deda6124497d13037ed32994a98108df9b25a0448051b9428e89465b2749f08715a1b93286c34ed6ab223937db6268972f7968918c4fe3ba6de46240a9efd29d93b7f0935711c1b9028df88ff9c86390c18d878841a336e12264d96189a319f4c45266dda7044b1654d890abf3111fe097488a0051f67d86804d963e4f89dc408ef73c35663d85804a71abe75b3e7e9bf82326c284209c146b442bbc5bcb433668def5a5331cfe98e1c66ace0c60db443c747193ad84622ca3169181d23ddec800d4414bba3bd56a67fd8d01fa254adb9df944750791a4314cef6e3455ddfb6c442146b5d43af9fd076d1d9204469743b7d14990fa228f69f848831172559411482e9b591a9afce620b4471fda3beb6a45860031085afbfad4f251ba475fe50acfe2dcb4e32827eea87e2a8d5c731a5bf548d36fa806ab97c76490e35170fd3bd7f5a9ff4b7531b7c2898ba199f543246d31eca1f3406a19426e17bd24379b54da588f8e9b29487a2dfe556e97cc243499ffe3c33327d9350ba435124c754e6213b94b2a44d4ceadf4d64ea50ca2029638cae7732a74349a28c0c51b3d79a640e8520afaf17ae49959ce450f218d79063189d3f98e25048aea6f6fbd4b765090ee5a471c5f337c48f557a433157c8c9a0426ad6c90da54e97b31383fd87531b4a923f7ed6e9d95050976132bbe775b286e247ca89a5c1734c1d359424c693fcd1fffe938692e4529f3ce96828c6a68e216fce508cbf19db942c2125c40cc5f48dbf8aa4c4c848194aa7f42bf2d84f86d26ef2fb3e8d1aa97ef2af6e4aae6f5f0c454de23185cd8bc6f5c350561b8d1c6a7d30144f66fa3c7db15ee37fa17459623e043f3bfdbd174ad5b95456a88ceeeabb503af1cdabf974f43e71a16cb66be2219278e76ca1103779ca963e31bbd142f15fd344b29f1039938572feb90f3e313484040b85684a6d8eaa5f15ea2b14fc7c94c89125a6bdad50748b98f3c668fdc9a22a9484063f93242a14ec338ab4deefa4291424c98b4e6b151ba25228c695d2269397a8245114ca9a32649ab6041d43a1d42777acd5f484822e0d69dd4a4e2847102aaf26a4dee869822b66fe49c4c38462adc4f55c91d81f5942f133c88e398eb7e9ad84926a10a6d3c64baa9d84a266e9a07136124a273f56ed9d29653e4231c4d325830c6fa1e36384420e91e1ed644efaee2942c1a48fcc1aefd7a31e1b44287a0a75d6616eede3b1318492fa6b2f5d1edc4a3c36845008716cbe34b7f7bb63230805d9597631134b53776c00a15cf3714ce2293de9b7f183e28c7c0c15365ab3c8860f0a21c9d1355972be0bf5a290526cc5537a90cde145217c86ed983fc88bcf2e8a69bed33d87aaa84717a5d9f31cbf457e622717a5dfd19f397c6cb40e2eca415b49f4941e2727b728cefa678ebe23944b6c519ad39c6d62728951d7a264da77369e6388d0d2a2207e7fe67329bf93cea2a44587d41cb9c74e954531c79958ddfea5fe34b1284990ba21e44f4b3a0d2c8ae2b69e733015c24cf38a62e795cfaaa236ad685c510c565a52d4868ed933ad285e979549f913933bc38a926e9746115ad62466565156ef594d367a5c4eaa28a9dad9da0ad36ba6549422994e21d39c103a8f8a8296d918b2b587d7fe14e51ed5ee1c3aa62884959198c1a40799538ac28818d5da27c9891d52944bd7fd84c9f4fb31a3288bd470213e96d069174541e63af977d119f10e4551dbaf6744b0c9490c8af224df203b67ff4441c4d01b4caec61459f74421079778f19b9d46ea9d2867f1b8ffb472a2b0b51f3e4bda30656ea27c5273eacc29438ea226caa7c3aa350699367c9e897256a7195519a4f8e59828db7d9be5964912cb2f510eb1fa0fc233348e2c51ee0c9b6d46c94bce54a22422be9539bc99ef9428c811574a7d782b514fa2bca6e404d3c1c46e2d898228a52629b16dcf3a12e5ce1e44846721515262c6f3d959c4c83ea21c3599b9888cac591df18cdf4f127336a21cd13608d53967741851da0c13ff1a248b28b985d8b57b441145dfdc31cc89f7981e494421c66528517b5f7d424449e74d9bf483baf6d2218a9f315b726688f28728d1d1e48b6f5f88d24d0ca13fe8a4639a1005552addaa44dbac8328c85155bff3b4000451aa1279c4ede4984fa6051008f76f7458897903a2ecb349684ebdaf12f71f4a1f47be46aaed87921ca9a543d88510caee43398b96bae708355d361f0ab376972d39a8d35fefa194e933ef32b7b975ad87a2a57a0e1df3674f56e7a1e8a131c474b3d049c2433964e84bf449b6dedfa12c49d34687113948901d8a1ab2e710761a64d079545c6791b71ea643493c6c9ec87c0ec578523c7b27a96d133914e66c92e4cb7023e238143373a80f7b0dbb180ee5282284b87562fedf379434b584b81ebaad5a379494c4d421ddfdeeaf6d28cbe994eb8ad7f2950d45f5ddfc9c233ea8750d45eb0e7a728ac8e6a71a0a42471e9db5835507390d25df1ecbd53c51828ed150527b9925739a6dccf119ca3a427665344f114b66288e9ed470a22f43a9f37e76cd9934b9870cc52c9ef59356374ff23114add4c414d7570cc56092931aa54dcfda1b86b2b8686ff5db87cc0b86a27b682df521c670f27ea1a4be5c4b7990e61a5d2f14a4e43dab2a19dfc4ed42413753791ef14164b65c28c670532152b55b2875a586d8793b76435a287666492cf5541f3b59284e9a5c663a5277da60a1a0764c83461699a47e85f28826295a55432cb74249a9cdbf9b2ec4da5d85527d105b7591e52e150ab92475a5aa4554a75098afceed1f74c65c2914a497e64a9a11f96d140adff6318cbecd310f85928b689f0eeb138a318e6a0cffb1f4aa4e286ff7a94d28af86183f9392fab154261425885cd5f685cda74b289f94309f4dac7bc75409e5df0d7f7d3ae8d84b9350f04f5b27424a91509c5c79d69365d7fd2394bb6d744eae572a324221c9c63819bb36bea80825513fcac3e9c8e1394428a4d17bf3de0eff104a9e7225637b089d08a1a83b5afa626b231d84624c337b9d2c54a80c10cabba722acca4e967f50facf73950dc9b302f041b1d64a6e9496dbd07b51929ae57d5b375e1866924e1f91eca22cc14faae5a9ab475217a58ff6a54a67bed272e6a2102426a5fc440e41b2898be2675fe409da47559bb728460a5522bb76f3a9698ba27f90ac49dc4a8498b5289ebaf1cbb85322b7a44561c4ecc8796eb9da7216a591969abd3cc6dd932ccaa2133f6b4e8cb57e2ccab3a2a2f3b383e91c5894c3a7897edb399a645e514cbda39d3cc615a5fc1c474b645f499a56942769d0693122b9e4ac28c807352582f67b9faca2541ec4240f3a491e11551442febc1f4f1e47291505c9f72f13c27a30191505a5b3c990c332778e4f51d272ffa03a6c8ae2d577a5c9a4a173b414053bd5b6dd495108b226691553e6778ea2d41ef964c730b5f689c2cd4908cb541a56a9dfe1448786a02845fa949e4e423a61e1274a9a31b3734e5c19254f14cf468fd0becb70a6eb4441a59bbcc9cb1ff31c278aa2b3a653c25e53696e13c590a0f7f5e6fb4f3f4d947498095964381325552abf2b25cbaa8c89d289c8f7e3112fa479894218712e2ac206994b4b9443d4d2259e44d9959528cfc9541a82369309068a73a8f4abc402222171301c0e0543813020fcc6b810004315080018401c8a4462a1244ff5f51a1400033f3024342a221e1e18121212120e0e120a0c088481c13018100883c1c05018101409346722b2130373dcd0b174df797080fd0de418fa02024554a8405747d40f504aa360574fa71f892815d14dd56a62ae90d69f9d26444e2455d41e08294913e3f4a23866cd924ae6198774753ebdebc73313832570d5a80721849a29820be9d3e7f937049495eebe04be7d6d24ff7c85ed8f96d96065f45637a8a83afbd212e7954870a23b7ecb23ca012c778c6435c927f12583cee8380db16e8f4302431567ed0082d3940b1c5a744bf7801a12b30f89548617a9f61aceded510b79ad05fbcc8f4553591d53833fbeca63147258058a2016775b7cdb041036b8c6bdf630937211e28a5c1000f66e1006bacedf85c331f0c383b008b6ffef65178cd69bec1bf68f36f815a1fad0a361cf9cf4e6b4b3317df246e74303169546f5a343cb99a84965ad05cec7926e93b0ead3e9cb41c0da66bfa1c309b7e1fbe8b3e152b861d04f4d0e87a90e3036798d9f795a26b8276de6b8447976c1acf5fbfc9ecdf5b9b32f302f52ab788b22bb3d447ec2b85fd3e804530c27fb39019d822067ebc61b34862dbd4c2005bd93c2b18601ab4e9b3976510ad123037d1ccadd400e45e5850a6ad899de95e46c4462481d84f712b2484792401fb1ad77bfaad95ff3dd845942a457e4b88c965ac44efd023b57291afdf57e5c213d21a842b3b5bcb7f050db96af3aa286c747083a3de841313e66ea124ac6c69f55303095646e44ee10d8e13661cdaab95fd4e3f037defa05e782d61bf53857054541eb8e97e561092db044b79c033e0c7303de10312115c3c19756ab8059ab4594985396fdc1597beed39a147529c554d91586ae5dae47a0064b2d47409ca270580f4531d0b9ebb76d9432d1c495989b9f5b35322a657a19ee9e6b0258778efe1419fa110d7feccf40def6701872acd75a8b77653595397ad049d32ba55b355dfce18e9a617ba8a45670916ffd4080407c6e74412e0c0a74c6111417bdf52fe3c2bbf9d80ef959d18fa455f00e5483b1416faed61a1aeb02f067ae00417df4b47209f979857119dc068b5512c6997129ff91f58b34e3d84196003da9c74b85a3224641089d3f10d94503bda44381e7f302506a5e0c4394bf37d95e08f7673bdf457b845e8f5906468015c34bc4ca849fbf5d022bbe90d271069a564309e8b169cee948e319ad824d3a7e84363f6f1310f4aab2d64c22019155432745af2b06fde4b0a9dcb93068b0429d484a0af04c8944fa7218aae4eddb344273291342bf7ae8f78e3094f794f0334996583a10994427a6183a66553b96f987926b148f0050c9a4fc87db08b27bbe4614f7d466666e261a868f39289c4a061f4b1673f00d9a420f43e4771d3331c72f7c7b9fc1c0eb8c3102893435a3a89a24c599c73f3038b471f906b6ca16b2ff5d83b1dd6158261ff222febc8a6b385aeba6cb4cd7da123324e3c316b70a45738c9f9371b1b36623e7df9a86b7491857e3b8fd6da361538684cd3cefaf85ab32ca17e694fd7d8c7c4a5017a3226878098ca752f8acd51982e630d1453d250b288c1b712a54f67a248a2662d9ce1949b03350f8eb15be5ffea04b6f082bbbdf7e1c77af0196ff590b14921435df46109b5e51bf242eefa471f0f691b26b969268a2c52d285d75570761961a9f19a1779dda90eb10188c8e6e40d8819f12a14f27ebf6b663f0d61e1792fa1a9412c0164f20b624428b946556dc7e49eb8d7d10e9d526917201bcbf22901ad97b5c9b3d81ac33acc4d832804924fd7a48952f059def734eeedd89a05d3ea75273a9897983e3d37392d12f07d308edd0c8b81d95bb5e9bf3553be59a1eea035be74ee22b0ccc6c94b5bd224955f6e9ac2e2a1defaf4c3194c3ead642552e9e77aeb5398339bec28deb04d7c8a53e9a9959b6617a3df069eb40a3ce725af8c2cd948a0d2ac973ef9038e7ae8b3db17d189240f2b7f21b5cbd55d32c51e424e35f49ab0db421868a3877d9d8bcee0d2e9d33e05d7e098ab69cda5b27b5b5d0c2a78258422fc1d76ea67c386656333acaf698dd411c34fe13cfc5aefbbee3d4c450728c7f9a7d92d0293b5d3f0a25322140e92fdb59a7ce3eca0d6d8db16ddce90064718ab2e7c622994a1415b63d54de68ae8fdb8ba0cd22148bbd44a5f427fc1da606df86be1a46a14104534a264d8610c6c262803842c9c818c04a0cedabe67732e03b0143ccbba61935a203a9aaf1d729b9ac54c26b5f4d3dac0b73743b6528cd90cc201f51fe0743a7930b49348b3128f6ce1e999f7bb6a077ad73b8cca1cdf145a906e7990fa9800a436e8ab78f174cc3819ecb6265e92f686406e0bd9665d6a0a38ef96a0fad404029d1e4badc19278a565dc2213b7f207cf380443146ca8149eed661d27a32689493f3ec2af4912c82aa62e05d64b5f99ce0dcd5ba8cfc1e8763a701d0bca5b06a12d5d96afd206112a2425d9f9ace83e25650c1e3525c9fd76d254164da1e8c0b1c6f083fa3e19ab869f434b3082364285592c58104eb1a989cf9d9d56bade0c43e3476a9c4915781571c33a81014c60dd484cce2c584784a915399782458a6c9b93b968617439ae589e7c9472f42af9d28b5d472e5a6ca18d6f11f915f5300775e9c6c7947f021d510007975b727c882fc903a2cd98ab9a16d87235cff73494097dde8110a3b24fa92faf0db70ca87c297c85ecb837952b45ba29d8b60a7b2faea6e1a9f4968aa485fd4510c68ea3d1b5ee2442d7aa2b963b66850111724bff8b6b883d54228f6977b4aebfcb164a6a6c2f8451f7e6193462428e9b3f63e7df2f9945201992689c0e903960594f0a28c24a0adbdc2000adb6c7f179d752f6131e37f902b5599d3d293c041b0bee0e93678433ca99d85595a5c49521b2488019cc01439c8773d7114d44c3f55a90b723581c9ea3c7517209cf7d8b8638fa7b69daf88c49d84a221cc31e2ffe2f1147d7f984dd5c2d60995560a2ca8a48745dfd59830b01db41b49c152402617a2c938f4302a124d015108541ce0bedfb1de81462e019a91a8ae48090d724a8a1a3c6ff7b50c1e4289e0513287ba2002772a2bfbba962b17898ca364225f10968821738b493c1a10271ab926a3196cc18013b3070d1af3196de3ea616db3876abc4510c02ac407bbe212e578f84c9f3e03ff9a8fa12c3613548bf464438a5ba16fa46977ba089c660c86d27a94d17984dc2b4284d56e42328227622d6d083dc0e138d40c8fd165bdc2508100e9e3a30c8a517a56b78fb9a4c7c08e9eddb85c02e535c1e640a04615c7abe425af8271b4d63849e15abb460d2715d5e2703db536de8b2929e8387063827534ac6472f87c695ae52af0387d02deb71278448d9e43995cf61b4ecccf88b3e8f7c75cd00e2e4dcddc3d833562be683c4822cfbf2146b6e9129455e7de7d734bcf70556a823d33c76df32af0131988a95ff732e4700baaacb19b71fac4ed2758510c492398e502148998f01c6fd814c44ba50ad52a8e1fae3f8195a1cdbcf136aaa7a6c8d6431ec325f140a8559504232053bb2eeb2816387a598a7c8dd6358e42c42b0d0894a55998b2214eda280f24349e95cd7624fbe131e46aab43b0f901f7c3f99acc4d2c3c9fb1bd345c6f7affd3021ff876b0357efa74a0788c0fb173f1ac19346a8fb28d4fcc394cbd8e8d8a89248c94b3d6e02c0c1d374ba78b15279701873f6a62ddae8d1a507d8fb68acdd96b11ade930cc31927ad660c792d4b9401b5d0fc6c4a192ce2e4fa56714a671950c34154f4af314a353aca18d81463407b3a23f9123dd1d7a405b70137feb99008723cb30e40040068727f61629bdf1692aeebd12afe2067babfa9be7777f8c4fb82de763e915828ff06162c1624e8e8717cd699c317fe8505c156134f859d2e9570ae0eba360643442ecd5d4f1d53b5c631179f74497ce888076c89e5045d045172ecd63b120ad4cd8579d29391931e87c0a106f1890255b84479f80be25c6528b385650fa68f9d1ba77734255008ba630a98c41b1c91ac149e1e5c90e707641ca4cb202d8f6253ff25ff9a48e99284eae13cd030c1a5fc723b64a028d298f2fa7509eefbeea35e37948125a9d0f45c424786cbb7c1061f4dffed8e3d9485710ab6a0b2dac248fe5f211033bf48d2ae22e5f9b93e542a91b7a3930850be226aea1c19e97b564680d5967b1d0cae261a1c2d6e8e4972cadc2cc66169b6e50c1e992ec9d5517187dbddea617d74088020bae5b6edc4004a27896aa694e609ff5757c28812b8892be6aba861d506e25fee0e356a65466f159c3758185f18957630eae2804854354247b58c2d9ba284eb6767d4987737a67923a3436135ba450db11de518bc593608e977f11d0818a80ce70c04c0094022b3226fb8a130925c03e2ab6eaee31f86d8e39e0b91d0d3cbc989c94eef3f5effbcec203747c7327c85608249ded6635951ee2d211ef433070c302b2205215f9f46a9b28159cb3f5e5665b0ec05920bc9ff07ab29ee1d541a7407b68aae0d84c36b24495067a00ca83c15b32a62a07597028b0946616fd191b399a1345603393b858fe2bdf78c75130b6cfbb62a1827e8cae3f7e914f447c848a2b7e915d3546dc60fc08580c14a5d1ec817ea542f4d4a3cdf00ef8222b2f77f5d555a111829247f4851eaa659232c1cd7bfacccff43b1b0857bc6650ca000b297d2887ab5af6e9c38c438ac9d93ac3815a97242295674647ad361a44816d63b21545f82efd634e2ce85a744890b278c50345b846bd8ceed14c633dce569081e3a3d912e972f1398e9feacd5ad8f551a21bf4b1389f7836ea1493092847307e29a40960f4f9e3158565cf573a2fe86bd9397a13485346d2a46a0b7eea6425d483f8d4cc788a738ff6f6c71a887808a31326e72e17973072d3392c05729707946ef07324bf1b98a728ed257d9a4830d89d1396dc0b321a8c2f64a3633c59c601f92ad5fac8b491d756d1dce69fd6291c2521dba1bd5f03fc4b992da1e1a97e7df7a90ed53544338b74d86bb5c343dcd12023feb81965345c8e6e4890cc1ac2054a90a28a6d31728b74d414b90e57e18418ad04537307c05900ac56c0e15c02a88140474b5e4442c609f5d1b3b1283c8c018150d89673b5fb58057031600b91dd628de3a214cb9449d1b4f3544090be5487a20398b265f30949526572ba175961a2926b154342a2b4a3a88b7aa633609794f591ef5c7f5cc36586a1e88f07f34bded33e280ee08a1882bb85199048935c958dfc9d55a0560bde5bac17c071705d143ae341f0fc5eb5b68af7b6c1b0797ec7f5c1841f30f010074412cbf9c5bf9e27e5e78a5015d54b6b03655db8643d39d53da8381e66288922a097955a42a7e72de1e658e56d44a763cf84dd6098a0040febdb68c6a4878248d7f32a469decfe93d8f64c128a88b8639531f3a1c5853b0b8bd4236c2223702c38e74df8967ed41522be0ff9059b44a9767d54413d01cfe8c216f68a98530f22e62b083250f38b8c93fb8f89566225fdac61ba43265144ae9f78464ae5da50040ec1e061123f3610f3b19b58b0b1fd172993bc4d5494836b4c3c7a21ba8aac72372a11198669e86d013e317e43e367ef28ed85edfb0bc99cc259460e94fe3e7d586ca7906b9d2a871584efb8ddd2529842358726f0c3254e58ce06749f2ea33c7598c9e6cc50f932a9ac5da0aa4017ac52f1b94fcf527839c1535f6604bce69df232c370207d303340bf828c00bbe43b5aad3fa044fa2b460d34487ba4a89c5b75cb8f858b5f12e2a3ad4ddafc28f6d1228342104c000e3b07faa0dd15dfbd586e872226b4374095cc5c7638b1731b26729fb60715853de10a4157db10da113cd58436871a102ab272b7c44acc186d0417f6d436880c28f6b56d510dd9f1e2701fc5442f23815be4f61a0879d88455196dc86501c45cb8aac66dfb56393343cdf0e918024485796cae9126f43a8c3db3e2eb77a3fccf6e1664bf87e7f1ba207acb313555be08650f2b00fc9926344f16151e186a8fcd927fcbdf4524c1b7143743359dc107d181fb3f3a3dde229a450a03b0cdca0ba65a63e221609db61ec6b65d610d273a01542e7875dd377a28a56894be82781b921f40ef496d7f895e3555169cb4a9f57f7ade1ce9c0882010c1d29ad24f75aa964d4f716424b69d6fe5717194cce0b848c25d3a139c563c0ecfe9f00e121069ef85e789b67b8cf8304b5fde8a7fd113223ec9ba262222e511a91b91d26423ba265d0362cbb870c6a1ec7a03e3ea4d0ec28e8bd2882bdb7c8744a4483f301abcc556c601756e5cc17ed2dfc8fd98c0d5a392c176ee72a39457b271f64e36a2d18a01ff894cf2ce408b8a63c705524f6372fcfbf44bc7c2254bc15499341ffc71ac6a83e039f6d4337eb49fdca4258472032217a054e0b2cd34346c460b1f8a6ed3419bb2963ee9af0c24a5d9b716216fa3fd0f6971c0f27402c591e2787d4d1aaa3aaf5f8f5ea1d731b756fc3db238b9568720887426e4b7585e27d625a171398728d5d58348d4dfc44a2b8320c40fcbebe8bf701d141f61f2b549b1cf00834b0cbe7d3df24bbad68cb6aa9a02897e2ad4ba4a9933b7d83b372927342c9603c8e6b9826b9aef61abb0785d338c7fd0bcc5d81dfbf4ceea176b7334d829048b9af53905e38d8cb0d51dd7b293d794759c8777fe993002437216c9c40a53309d264a28878f14c07fd9e0475ebfd0ade82ef7b103bfdfdee105a5483f59df5fa341b050e250d3577d98eb1f2c3948bfc0d9a5ebc83930bd038865514c905427a88eb977ede6111bdcf7bf6db0368ed53208f10d727c9b38f5185ccc42898605f8879c1eeb00895df2a5cd0e4b00dfa8689448bca2a33d6aae0270eaed98378bd096d1b2421b833a6a31e6ca7a515ee3817acb88e63c03ae6e1bedcbeccbc0ebc0b8f4db16944046fc53f4479a918c9ab7787b0795ae5b3f89c37c26e88e1b20fa04ceee6a4607554c9734549c791d1041e7d6dc3cd2b02c9e8ddde1adc807fa88f0882b32ac4c3a639f5a44a3ce3f0e195c27b1996a0c0668016429ab87186a5c06110790cd18b97911401aeb5b638491d98e42753a42c13e720c8f9e13f89046280aeea2fcbb62d6eb0db2736cc5f270ddf5798c059652098af75f8163578adc2739b8f6a43bdef6c4a33f5d8560642fe461d736cdd0952127a69c7980a81cbf456f804607332dae5c232291c048d780fe7b9331c99fea21d1284467a5029022c0d5220c7a03238eaf6bb96aa9b689004076f238b69a9fb70fac57f17eda85146d743e652f7e2d739c4a4aa1912bafcd0b41f8f64693daf24de6834acd9d72062d43c1183b4de6086edfcf29b3a46046b5c1e262cf1ae54c8d3ffc495f1788e1d5acfcebb2cf6f99814031fabf1fd79cfaaf6eff653d5e68c974ef34bf7a314517fe6bcd2668c615bd6beea6114075f4630617c04d8fc4d754e6c1d1203bbb165288fca935fe6692ea2198e658857a646fa8ad351b2d9a92a219c240c2784def9f0d4fc34e1541739162ae36ea77d578568ebb1ba9c56d8cc044e30517fda79b2ece53a58f11aeabc74d2d353fc247fef375e18e4539ddefdea51bdcefde628be86f3e5a9aea6fbf2291c8dd727fa3ed989fb16f6a3bdabb3f65083530ea7b54773f7d3600b667751b72efe7f35dc02e5dd9337a828f0f2681f40c7539e9505524daebf5f1a26690791ad7f3b1b6825d528aea88dedcbced8ae4a8aa44c00fdc2260b056d9326514e44ddd960351aaf562640a3fdbc00da0d19d0e02d92b9826f292484a401b31713d79129ee14373264bd6e780fa0c0c9e516b9fe27636ca9f6341526c793ecd1425e70eadfe7e142f2847393989f0fcd2826567e026192703ff000603d1ad0de3f12b0072ac1cc0f2e074b4765599f741b68ede2049ba12700d6a3072dc20f410ea0f01c8378e02407a9071838f1ae855b810f00ce23076707175ed4726f28321f4668b33df1f6bf65df7f56bddd06ce37b404a81ace2191207b3c0ddbc2b407eb4e304fc2f8a7bfac68646f5bc75b447ce23d07e1a14e996e189c0eaf5b2e4a69aa5350a4e3431f652360e2df3d2b93667b700e16c82a02beaabb9310c1b343909e67c80fbe55a70bac33665b411ebb23c3d677c4cbcd8d7df79890134075186bce1b9e0653a0d2268d52afdf40f4fd841e2a5d39c78afbf6a37736228faa4569a67073eb1661564db68de77662ed6e739ecd5d62d4c6e74a6b40a4eb611a7584d14e898f8af752017de22162ebabea27de080519b001c4d3f23a74e36d3d984643495ac08a87adca17cc4760613c47be68d9cb3ed3babbbc1e98e10e7388acd72bf51f3fbf5649e045618375eb9fec12b8098fc1be417ea1b03f698541e1916b3a254fe3656fa876df136b40c427131768a9f52ea7796066cc2713ca94607e61a1b58c506491a0886680fef9eb8b4e90e9a1ff2cdb9682542399a51c4d2f968a1eba0cb4dfbc38f09180754e46eabb9f08902a4141ea53f5394185ee19186da5cf620006aba4b2234fa30266b6f34f57d545788521c85f441508672d15f3cf22c7555c66f6dec8a8d1e1a64e78fff6b7572c83087923c5c572a1ef4fa145aea575953a376b07b258ba347d59a867099f4f674c5dcb3eef4cec6eb803c0859319d355bec6551afe37896bc469a2caa69180b86dcb7355d20da735ee44b6644211cc10367c9034d20ed33545e8d42423430dbf00ca7297330ca6936be3271399bba7e4fd8070aa6af651f947237e4b30a6cc7759488e4a7fb902f41803034bf0f7415987d9835642a2627d9e38bb802be970d047464b10fceb9da5b965922f7a0a90687914d92a60df49648a48774ec3c5e8b0511224b2dd81055f95e33fcba2f113eaecb08732207b5e4a36c6928d8c803ada36c2a67d4e1dbed20aec892b5d9341223f25cf6b8ceb69a43a5204b071b2a1b28cfdd30390d8b54009ce6c76d13650728e38a627dce5d8af9944e81a1de67d26135ecf23bbea3cc7600342a922add4e3d3be04caae8eb643d074eb3b6ff380379ee281f73baa5bce832fb2907b4c6a0e2be5ea29fc84a104057b0d1bee008f3df5455e68c6c2df944989cba45e7e3e33d4b90e8c9d8dd913c78a934a0e96db5c17a4cb662d3d7692e9208c0c64d04916c2099324cd8a18076112e8de51796427a1a15c7be98b584c3621e82686b2a539e2460f00596b04a697e928520971fa0998574825bc62e0da5dc9618eb686e06449558f271ef18ca7bca1a104124deeb23e8a2977f970dccdce62ec63cab6457a84ec4ae4d290cc04fdc75a7eca753b12abe4fd3486e7ccc1ada05761373bdecd4e77966dad60bb9dda61f73bba9bcc54b85d24f7e2733ff4ce8778e9f96d0a558c91d414fcf4823d46e9a1985070a28c5fce9f331093932f4d5f00cc8f9971ee4c4c67da2a2bfb57e9d6f0951b7e00b584c72118fc5a456838acc16118c07a8cc963c30fe6b9169cf48dc46979889c8932ffb7da63cd352531c09543d0b71f89226c13ec142b22683e22ee62eb81a8096dbef2609c9c11509e3e1f9cad1b3ac395802ea410c11787e142b129c6b973c0d6309bdfcc4dd086ac65cf0b2a068f5b0fc223caa3f75e795078bb9e33eabb74edbc1a04fcfc0116960696cc3bfc786c24e4e1d6633901de14212ca4de5286f0bd3731ee54e85ee88ed1a5d125a4dbaf8b4097adabb2eebeae095d7e5d145dafbab3ba50bad07441742de81ed3d5ea12a8cbd3554077439752178faeb9aebfee94ba6e5dd8bad0ba52ba43e82a7589ea6aeb1ad4dda14ba88ba14b59574ff799aea72ea27cf72efa94c122c03b9defeb2e0c2f08bba9bd40f72af61ac97b4ddf3a09bcff7eb80c3e5629626da43337d73722304a0d534d299b191633147804f867b203dac5650d9703672606cc08a3224d744b5b169a1188d7a1e84b5fe05a608480bd000cc8c061b97ab42205d409b40ba010e00ab8990ec4a317b9497205f6049d0562040ac93c05f83b191e4c1a63250257020e034e12fc08a00a7019c02a7028247829b211a76b018c5081c38305c7280c8c4690121866808aa0a20532dfd823f65cef2c024b4bc1adb9516c7918c480e603382cbab794e31fe5b10ad66a93961bc56ff3a7f66531579aa665f20df81cde9d17993fced3e059f328bcbdfc465e7bc223c0872141ea07f2a7ea97f9c66e4005120d0c00d691402f40a5c60dc33080374992243f0c60bc0555360d5a7ab4d9127680fd00bbc496d4221776e23761ae6c23fd49f17c4b0230003300350092f2b7e08bf0339b6f9ef2cd9273a822e95da7d18c34cee8a5e99a54845b84f5b1bd99a67063586a0c6f6dba0689f70714976c22cb36df2ce024a825d1b7d24ad4f4d78459b61115053ee9e19c915310d5bb141a29c5ba1faf996f9e937ba8a874eb03912600a3b5053518befd2147e3db9f41831c268000a2dd05eb5bd72a619a01675f0da0d687bb709c85c45bbbb4a55688ab90b53e5c07143ef03d32be3831985c2cfbaa60ec79103541b0c06ebd698ee6623d1166eb2da1d1dc8a6f15cc2d0f86d62e44819220689c7921201138144729233446a00cb51703235540195178536e4bf44753b735b3e7b6ce85ec40d36d96471a7141d78b4dcf6d0d251ff1244ed5a4a6e27185e00d4d672e0411dc0c87480c6eb4618ba5fcd889dc726908f41873a297fcca9073915b6cf07853902eaa226af84e2e67db161f6a75392421c9af97177da5658a", - "0x3a65787472696e7369635f696e646578": "0x00000000", - "0x3a6772616e6470615f617574686f726974696573": "0x010888dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee0100000000000000d17c2d7823ebf260fd138f2d7e27d114c0145d968b5ff5006125f2414fadae690100000000000000", - "0x3f1467a096bcd71a5b6a0c8155e20810308ce9615de0775a82f8a94dc3d285a1": "0x01", - "0x3f1467a096bcd71a5b6a0c8155e208103f2edf3bdf381debe331ab7446addfdc": "0x000064a7b3b6e00d0000000000000000", - "0x3f1467a096bcd71a5b6a0c8155e208104e7b9012096b41c4eb3aaf947f6ea429": "0x0000", - "0x57f8dc2f5ab09467896f47300f0424384e7b9012096b41c4eb3aaf947f6ea429": "0x0000", - "0x57f8dc2f5ab09467896f47300f0424385e0621c4869aa60c02be9adcc98a0d1d": "0x08d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48", - "0x5c0d1176a568c1f92944340dbfed9e9c4e7b9012096b41c4eb3aaf947f6ea429": "0x0000", - "0x5c0d1176a568c1f92944340dbfed9e9c530ebca703c85910e7164cb7d1c9e47b": "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", - "0x5f9cc45b7a00c5899361e1c6099678dc4e7b9012096b41c4eb3aaf947f6ea429": "0x0400", - "0x5f9cc45b7a00c5899361e1c6099678dc8a2d09463effcc78a22d75b9cb87dffc": "0x0000000000000000", - "0x5f9cc45b7a00c5899361e1c6099678dcd47cb8f5328af743ddfb361e7180e7fcbb1bdbcacd6ac9340000000000000000": "0x00000000", - "0x658faa385070e074c85bf6b568cf055506d22dc781f44e506e51707fab5eea4d0300": "0xff7f", - "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430300": "0x01", - "0x658faa385070e074c85bf6b568cf05552fd68e6f37598f679d0698930b5bbb470300": "0x0000", - "0x658faa385070e074c85bf6b568cf05553168007c5d4f8e047393394f969878370300": "0x3c00", - "0x658faa385070e074c85bf6b568cf05554e7b9012096b41c4eb3aaf947f6ea429": "0x0000", - "0x658faa385070e074c85bf6b568cf05554efd2c1e9753037696296e2bfa4460950300": "0x0000000000000000", - "0x658faa385070e074c85bf6b568cf055557c875e4cff74148e4628f264b974c80": "0x0000000000000000", - "0x658faa385070e074c85bf6b568cf05555cd1c97edf92be296fb8ae73ee8611260300": "0x0004", - "0x658faa385070e074c85bf6b568cf05555f3bb7bcd0a076a48abf8c256d221721": "0x0100", - "0x658faa385070e074c85bf6b568cf055564b6168414916325e7cb4f3f47691e110300": "0x0000", - "0x658faa385070e074c85bf6b568cf055565dea649340381db767c1635ca2acb950300": "0x6400", - "0x658faa385070e074c85bf6b568cf05556dcf6d297802ab84a1c68cb9453399920300": "0x0000", - "0x658faa385070e074c85bf6b568cf05557641384bb339f3758acddfd7053d33170300": "0x6300", - "0x658faa385070e074c85bf6b568cf05557d15dd66fbf0cbda1d3a651b5e606df20300": "0x8096980000000000", - "0x658faa385070e074c85bf6b568cf0555919db2fe18203eba898cee471ef192400300": "0xe803", - "0x658faa385070e074c85bf6b568cf0555a1048e9d244171852dfe8db314dc68ca0300": "0x0000", - "0x658faa385070e074c85bf6b568cf0555b6522cfe03433e9e101a258ee2f580ab0300": "0x0010", - "0x658faa385070e074c85bf6b568cf0555b69925e91d8c0ca3e838d1cbca1e314a0300": "0x0001", - "0x658faa385070e074c85bf6b568cf0555c57fc7240b4e0c444a010d7fe83ec3ec0300": "0x8813", - "0x658faa385070e074c85bf6b568cf0555ed6f7eabb8e04489185225527c965b020300": "0x2000", - "0x658faa385070e074c85bf6b568cf0555fabe6b131d9fa6e6d6cacbe7586c3b8a0300": "0x0010", - "0x658faa385070e074c85bf6b568cf0555ffabb584688c82a9b01a0527f0afd3db0300": "0x0000", - "0xbd2a529379475088d3e29a918cd478724e7b9012096b41c4eb3aaf947f6ea429": "0x0000", - "0xc2261276cc9d1f8598ea4b6a74b15c2f4e7b9012096b41c4eb3aaf947f6ea429": "0x0100", - "0xc2261276cc9d1f8598ea4b6a74b15c2f57c875e4cff74148e4628f264b974c80": "0x0060c4b9c45c0000", - "0xf0c365c3cf59d671eb72da0e7a4113c44e7b9012096b41c4eb3aaf947f6ea429": "0x0000" - }, - "childrenDefault": {} - } - } -} \ No newline at end of file diff --git a/tests/mocks/__init__.py b/tests/mocks/__init__.py new file mode 100644 index 0000000000..a5edffa7bd --- /dev/null +++ b/tests/mocks/__init__.py @@ -0,0 +1,19 @@ +# The MIT License (MIT) +# Copyright © 2023 Opentensor Technologies + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +from .wallet_mock import MockWallet as MockWallet +from .keyfile_mock import MockKeyfile as MockKeyfile \ No newline at end of file diff --git a/tests/mocks/keyfile_mock.py b/tests/mocks/keyfile_mock.py new file mode 100644 index 0000000000..92b40d8bd3 --- /dev/null +++ b/tests/mocks/keyfile_mock.py @@ -0,0 +1,82 @@ +# The MIT License (MIT) + +# Copyright © 2021 Yuma Rao +# Copyright © 2022 Opentensor Foundation +# Copyright © 2023 Opentensor Technologies + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +from bittensor_wallet import serialized_keypair_to_keyfile_data, Keyfile +from bittensor_wallet import Keypair + +class MockKeyfile( Keyfile ): + """ Defines an interface to a mocked keyfile object (nothing is created on device) keypair is treated as non encrypted and the data is just the string version. + """ + def __init__( self, path: str ): + super().__init__( path ) + + self._mock_keypair = Keypair.create_from_mnemonic( mnemonic = 'arrive produce someone view end scout bargain coil slight festival excess struggle' ) + self._mock_data = serialized_keypair_to_keyfile_data( self._mock_keypair ) + + def __str__(self): + if not self.exists_on_device(): + return "Keyfile (empty, {})>".format( self.path ) + if self.is_encrypted(): + return "Keyfile (encrypted, {})>".format( self.path ) + else: + return "Keyfile (decrypted, {})>".format( self.path ) + + def __repr__(self): + return self.__str__() + + @property + def keypair( self ) -> 'Keypair': + return self._mock_keypair + + @property + def data( self ) -> bytes: + return bytes(self._mock_data) + + @property + def keyfile_data( self ) -> bytes: + return bytes( self._mock_data) + + def set_keypair ( self, keypair: 'Keypair', encrypt: bool = True, overwrite: bool = False, password:str = None): + self._mock_keypair = keypair + self._mock_data = serialized_keypair_to_keyfile_data( self._mock_keypair ) + + def get_keypair(self, password: str = None) -> 'Keypair': + return self._mock_keypair + + def make_dirs( self ): + return + + def exists_on_device( self ) -> bool: + return True + + def is_readable( self ) -> bool: + return True + + def is_writable( self ) -> bool: + return True + + def is_encrypted ( self ) -> bool: + return False + + def encrypt( self, password: str = None): + raise ValueError('Cannot encrypt a mock keyfile') + + def decrypt( self, password: str = None): + return diff --git a/tests/mocks/wallet_mock.py b/tests/mocks/wallet_mock.py new file mode 100644 index 0000000000..6e7a941ea9 --- /dev/null +++ b/tests/mocks/wallet_mock.py @@ -0,0 +1,79 @@ +# The MIT License (MIT) + +# Copyright © 2021 Yuma Rao +# Copyright © 2022 Opentensor Foundation +# Copyright © 2023 Opentensor Technologies + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import os +import bittensor +import bittensor_wallet + +from .keyfile_mock import MockKeyfile + +class MockWallet(bittensor_wallet.Wallet): + """ + Mocked Version of the bittensor wallet class, meant to be used for testing + """ + def __init__( + self, + **kwargs, + ): + r""" Init bittensor wallet object containing a hot and coldkey. + Args: + _mock (required=True, default=False): + If true creates a mock wallet with random keys. + """ + super().__init__(**kwargs) + # For mocking. + self._is_mock = True + self._mocked_coldkey_keyfile = None + self._mocked_hotkey_keyfile = None + + print("---- MOCKED WALLET INITIALIZED- ---") + + @property + def hotkey_file(self) -> 'bittensor_wallet.Keyfile': + if self._is_mock: + if self._mocked_hotkey_keyfile == None: + self._mocked_hotkey_keyfile = MockKeyfile(path='MockedHotkey') + return self._mocked_hotkey_keyfile + else: + wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) + hotkey_path = os.path.join(wallet_path, "hotkeys", self.hotkey_str) + return bittensor.keyfile( path = hotkey_path ) + + @property + def coldkey_file(self) -> 'bittensor_wallet.Keyfile': + if self._is_mock: + if self._mocked_coldkey_keyfile == None: + self._mocked_coldkey_keyfile = MockKeyfile(path='MockedColdkey') + return self._mocked_coldkey_keyfile + else: + wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) + coldkey_path = os.path.join(wallet_path, "coldkey") + return bittensor.keyfile( path = coldkey_path ) + + @property + def coldkeypub_file(self) -> 'bittensor_wallet.Keyfile': + if self._is_mock: + if self._mocked_coldkey_keyfile == None: + self._mocked_coldkey_keyfile = MockKeyfile(path='MockedColdkeyPub') + return self._mocked_coldkey_keyfile + else: + wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) + coldkeypub_path = os.path.join(wallet_path, "coldkeypub.txt") + return bittensor_wallet.Keyfile( path = coldkeypub_path ) \ No newline at end of file diff --git a/tests/unit_tests/bittensor_tests/test_axon.py b/tests/unit_tests/bittensor_tests/test_axon.py index 6b3d74cccd..679dee88b2 100644 --- a/tests/unit_tests/bittensor_tests/test_axon.py +++ b/tests/unit_tests/bittensor_tests/test_axon.py @@ -26,49 +26,11 @@ import bittensor from bittensor.utils.test_utils import get_random_unused_port -wallet = bittensor.wallet.mock() -axon = bittensor.axon( wallet = wallet, metagraph = None ) - -sender_wallet = bittensor.wallet.mock() +from tests.helpers import get_mock_wallet, get_mock_keypair def gen_nonce(): return f"{time.monotonic_ns()}" -def test_axon_start(): - mock_wallet = MagicMock( - spec=bittensor.Wallet, - coldkey=MagicMock(), - coldkeypub=MagicMock( - # mock ss58 address - ss58_address="5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" - ), - hotkey=MagicMock( - ss58_address="5CtstubuSoVLJGCXkiWRNKrrGg2DVBZ9qMs2qYTLsZR4q1Wg" - ), - ) - axon = bittensor.axon( wallet = mock_wallet, metagraph = None ) - axon.start() - assert axon.server._state.stage == grpc._server._ServerStage.STARTED - -def test_axon_stop(): - mock_wallet = MagicMock( - spec=bittensor.Wallet, - coldkey=MagicMock(), - coldkeypub=MagicMock( - # mock ss58 address - ss58_address="5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" - ), - hotkey=MagicMock( - ss58_address="5CtstubuSoVLJGCXkiWRNKrrGg2DVBZ9qMs2qYTLsZR4q1Wg" - ), - ) - axon = bittensor.axon( wallet = mock_wallet, metagraph = None ) - axon.start() - time.sleep( 1 ) - axon.stop() - time.sleep( 1 ) - assert axon.server._state.stage == grpc._server._ServerStage.STOPPED - def sign_v2(sender_wallet, receiver_wallet): nonce, receptor_uid = gen_nonce(), str(uuid.uuid1()) sender_hotkey = sender_wallet.hotkey.ss58_address @@ -80,9 +42,6 @@ def sign_v2(sender_wallet, receiver_wallet): def sign(sender_wallet, receiver_wallet, receiver_version): return sign_v2(sender_wallet, receiver_wallet) -def test_sign_v2(): - sign_v2(sender_wallet, wallet) - def is_port_in_use(port): import socket with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: @@ -92,54 +51,108 @@ def is_port_in_use(port): else: return False -def test_axon_is_destroyed(): - mock_wallet = MagicMock( - spec=bittensor.Wallet, - coldkey=MagicMock(), - coldkeypub=MagicMock( - # mock ss58 address - ss58_address="5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" - ), - hotkey=MagicMock( - ss58_address="5CtstubuSoVLJGCXkiWRNKrrGg2DVBZ9qMs2qYTLsZR4q1Wg" - ), - ) - - port = get_random_unused_port() - assert is_port_in_use( port ) == False - axon = bittensor.axon ( wallet = mock_wallet, metagraph = None, port = port ) - assert is_port_in_use( port ) == True - axon.start() - assert is_port_in_use( port ) == True - axon.stop() - assert is_port_in_use( port ) == False - axon.__del__() - assert is_port_in_use( port ) == False - - port = get_random_unused_port() - assert is_port_in_use( port ) == False - axon2 = bittensor.axon ( wallet = mock_wallet, metagraph = None, port = port ) - assert is_port_in_use( port ) == True - axon2.start() - assert is_port_in_use( port ) == True - axon2.__del__() - assert is_port_in_use( port ) == False - - port_3 = get_random_unused_port() - assert is_port_in_use( port_3 ) == False - axonA = bittensor.axon ( wallet = mock_wallet, metagraph = None, port = port_3 ) - assert is_port_in_use( port_3 ) == True - axonB = bittensor.axon ( wallet = mock_wallet, metagraph = None, port = port_3 ) - assert axonA.server != axonB.server - assert is_port_in_use( port_3 ) == True - axonA.start() - assert is_port_in_use( port_3 ) == True - axonB.start() - assert is_port_in_use( port_3 ) == True - axonA.__del__() - assert is_port_in_use( port ) == False - axonB.__del__() - assert is_port_in_use( port ) == False +class TestAxon(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.wallet = wallet = get_mock_wallet( + coldkey = get_mock_keypair(0, cls.__name__), + hotkey= get_mock_keypair(100 + 0, cls.__name__), + ) + + cls.axon = bittensor.axon( wallet = wallet, metagraph = None ) + + cls.sender_wallet = get_mock_wallet( + coldkey = get_mock_keypair(1, cls.__name__), + hotkey= get_mock_keypair(100 + 1, cls.__name__), + ) + + + def test_axon_start(self): + mock_wallet = MagicMock( + spec=bittensor.Wallet, + coldkey=MagicMock(), + coldkeypub=MagicMock( + # mock ss58 address + ss58_address="5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" + ), + hotkey=MagicMock( + ss58_address="5CtstubuSoVLJGCXkiWRNKrrGg2DVBZ9qMs2qYTLsZR4q1Wg" + ), + ) + axon = bittensor.axon( wallet = mock_wallet, metagraph = None ) + axon.start() + assert axon.server._state.stage == grpc._server._ServerStage.STARTED + + def test_axon_stop(self): + mock_wallet = MagicMock( + spec=bittensor.Wallet, + coldkey=MagicMock(), + coldkeypub=MagicMock( + # mock ss58 address + ss58_address="5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" + ), + hotkey=MagicMock( + ss58_address="5CtstubuSoVLJGCXkiWRNKrrGg2DVBZ9qMs2qYTLsZR4q1Wg" + ), + ) + axon = bittensor.axon( wallet = mock_wallet, metagraph = None ) + axon.start() + time.sleep( 1 ) + axon.stop() + time.sleep( 1 ) + assert axon.server._state.stage == grpc._server._ServerStage.STOPPED + + def test_sign_v2(self): + sign_v2(self.sender_wallet, self.wallet) + + def test_axon_is_destroyed(self): + mock_wallet = MagicMock( + spec=bittensor.Wallet, + coldkey=MagicMock(), + coldkeypub=MagicMock( + # mock ss58 address + ss58_address="5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" + ), + hotkey=MagicMock( + ss58_address="5CtstubuSoVLJGCXkiWRNKrrGg2DVBZ9qMs2qYTLsZR4q1Wg" + ), + ) + + port = get_random_unused_port() + assert is_port_in_use( port ) == False + axon = bittensor.axon ( wallet = mock_wallet, metagraph = None, port = port ) + assert is_port_in_use( port ) == True + axon.start() + assert is_port_in_use( port ) == True + axon.stop() + assert is_port_in_use( port ) == False + axon.__del__() + assert is_port_in_use( port ) == False + + port = get_random_unused_port() + assert is_port_in_use( port ) == False + axon2 = bittensor.axon ( wallet = mock_wallet, metagraph = None, port = port ) + assert is_port_in_use( port ) == True + axon2.start() + assert is_port_in_use( port ) == True + axon2.__del__() + assert is_port_in_use( port ) == False + + port_3 = get_random_unused_port() + assert is_port_in_use( port_3 ) == False + axonA = bittensor.axon ( wallet = mock_wallet, metagraph = None, port = port_3 ) + assert is_port_in_use( port_3 ) == True + axonB = bittensor.axon ( wallet = mock_wallet, metagraph = None, port = port_3 ) + assert axonA.server != axonB.server + assert is_port_in_use( port_3 ) == True + axonA.start() + assert is_port_in_use( port_3 ) == True + axonB.start() + assert is_port_in_use( port_3 ) == True + axonA.__del__() + assert is_port_in_use( port ) == False + axonB.__del__() + assert is_port_in_use( port ) == False # test external axon args class TestExternalAxon(unittest.TestCase): diff --git a/tests/unit_tests/bittensor_tests/test_config.py b/tests/unit_tests/bittensor_tests/test_config.py index c4bae04942..0fd99d64e5 100644 --- a/tests/unit_tests/bittensor_tests/test_config.py +++ b/tests/unit_tests/bittensor_tests/test_config.py @@ -23,33 +23,6 @@ import bittensor - -def test_loaded_config(): - with pytest.raises(NotImplementedError): - bittensor.Config(loaded_config=True) - -def test_strict(): - parser = argparse.ArgumentParser() - - # Positional/mandatory arguments don't play nice with multiprocessing. - # When the CLI is used, the argument is just the 0th element or the filepath. - # However with multiprocessing this function call actually comes from a subprocess, and so there - # is no positional argument and this raises an exception when we try to parse the args later. - # parser.add_argument("arg", help="Dummy Args") - parser.add_argument("--cov", help="Dummy Args") - parser.add_argument("--cov-append", action='store_true', help="Dummy Args") - parser.add_argument("--cov-config", help="Dummy Args") - #bittensor.Dendrite.add_args( parser ) - bittensor.logging.add_args( parser ) - bittensor.wallet.add_args( parser ) - bittensor.subtensor.add_args( parser ) - #bittensor.metagraph.add_args( parser ) - bittensor.dataset.add_args( parser ) - bittensor.axon.add_args( parser ) - #bittensor.wandb.add_args( parser ) - bittensor.config( parser, strict=False) - bittensor.config( parser, strict=True) - def test_prefix(): # Test the use of prefixes to instantiate all of the bittensor objects. parser = argparse.ArgumentParser() @@ -90,8 +63,9 @@ def test_prefix(): #bittensor.wandb.add_args( parser ) #bittensor.wandb.add_args( parser, prefix = 'second' ) - config_non_strict = bittensor.config( parser, strict=False) - config_strict = bittensor.config( parser, strict=True) + # Test with argv=[] + config_non_strict = bittensor.config( parser, strict=False, args=[] ) + config_strict = bittensor.config( parser, strict=True, args=[] ) #bittensor.dendrite( config_strict ).__del__() #bittensor.dendrite( config_non_strict ).__del__() @@ -126,25 +100,7 @@ def test_prefix(): #bittensor.wandb( config_strict.second ) #bittensor.wandb( config_non_strict.second ) - -def construct_config(): - defaults = bittensor.Config() - bittensor.subtensor.add_defaults( defaults ) - #bittensor.dendrite.add_defaults( defaults ) - bittensor.axon.add_defaults( defaults ) - bittensor.wallet.add_defaults( defaults ) - bittensor.dataset.add_defaults( defaults ) - bittensor.logging.add_defaults( defaults ) - #bittensor.wandb.add_defaults( defaults ) - - return defaults - -def test_to_defaults(): - config = construct_config() - config.to_defaults() - if __name__ == "__main__": # test_loaded_config() # test_strict() - # test_to_defaults() test_prefix() \ No newline at end of file diff --git a/tests/unit_tests/bittensor_tests/test_keypair.py b/tests/unit_tests/bittensor_tests/test_keypair.py deleted file mode 100644 index cd02a89f3c..0000000000 --- a/tests/unit_tests/bittensor_tests/test_keypair.py +++ /dev/null @@ -1,213 +0,0 @@ -# Python Substrate Interface Library -# -# Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest - -from scalecodec import ScaleBytes -from substrateinterface import Keypair, KeypairType -from substrateinterface.constants import DEV_PHRASE -from substrateinterface.exceptions import ConfigurationError -from bip39 import bip39_validate - - -class KeyPairTestCase(unittest.TestCase): - - def test_generate_mnemonic(self): - mnemonic = Keypair.generate_mnemonic() - self.assertTrue(bip39_validate(mnemonic)) - - def test_invalid_mnemic(self): - mnemonic = "This is an invalid mnemonic" - self.assertFalse(bip39_validate(mnemonic)) - - def test_create_sr25519_keypair(self): - mnemonic = "old leopard transfer rib spatial phone calm indicate online fire caution review" - keypair = Keypair.create_from_mnemonic(mnemonic, ss58_format=0) - - self.assertEqual(keypair.ss58_address, "16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2") - - def test_only_provide_ss58_address(self): - - keypair = Keypair(ss58_address='16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2') - self.assertEqual("0x" + keypair.public_key.hex(), '0xe4359ad3e2716c539a1d663ebd0a51bdc5c98a12e663bb4c4402db47828c9446') - - def test_only_provide_public_key(self): - - keypair = Keypair( - public_key='0xe4359ad3e2716c539a1d663ebd0a51bdc5c98a12e663bb4c4402db47828c9446', - ss58_format=0 - ) - self.assertEqual(keypair.ss58_address, '16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2') - - def test_provide_no_ss58_address_and_public_key(self): - self.assertRaises(ValueError, Keypair) - - def test_incorrect_private_key_length_sr25519(self): - self.assertRaises( - ValueError, Keypair, private_key='0x23', ss58_address='16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2' - ) - - def test_incorrect_public_key(self): - self.assertRaises(ValueError, Keypair, public_key='0x23') - - def test_sign_and_verify(self): - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = keypair.sign("Test123") - self.assertTrue(keypair.verify("Test123", signature)) - - def test_sign_and_verify_hex_data(self): - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = keypair.sign("0x1234") - self.assertTrue(keypair.verify("0x1234", signature)) - - def test_sign_and_verify_scale_bytes(self): - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - - data = ScaleBytes('0x1234') - - signature = keypair.sign(data) - self.assertTrue(keypair.verify(data, signature)) - - def test_sign_missing_private_key(self): - keypair = Keypair(ss58_address="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY") - self.assertRaises(ConfigurationError, keypair.sign, "0x1234") - - def test_sign_unsupported_crypto_type(self): - keypair = Keypair.create_from_private_key( - ss58_address='16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2', - private_key='0x1f1995bdf3a17b60626a26cfe6f564b337d46056b7a1281b64c649d592ccda0a9cffd34d9fb01cae1fba61aeed184c817442a2186d5172416729a4b54dd4b84e', - crypto_type=3 - ) - self.assertRaises(ConfigurationError, keypair.sign, "0x1234") - - def test_verify_unsupported_crypto_type(self): - keypair = Keypair.create_from_private_key( - ss58_address='16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2', - private_key='0x1f1995bdf3a17b60626a26cfe6f564b337d46056b7a1281b64c649d592ccda0a9cffd34d9fb01cae1fba61aeed184c817442a2186d5172416729a4b54dd4b84e', - crypto_type=3 - ) - self.assertRaises(ConfigurationError, keypair.verify, "0x1234", '0x1234') - - def test_sign_and_verify_incorrect_signature(self): - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = "0x4c291bfb0bb9c1274e86d4b666d13b2ac99a0bacc04a4846fb8ea50bda114677f83c1f164af58fc184451e5140cc8160c4de626163b11451d3bbb208a1889f8a" - self.assertFalse(keypair.verify("Test123", signature)) - - def test_sign_and_verify_invalid_signature(self): - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = "Test" - self.assertRaises(TypeError, keypair.verify, "Test123", signature) - - def test_sign_and_verify_invalid_message(self): - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = keypair.sign("Test123") - self.assertFalse(keypair.verify("OtherMessage", signature)) - - def test_create_ed25519_keypair(self): - mnemonic = "old leopard transfer rib spatial phone calm indicate online fire caution review" - keypair = Keypair.create_from_mnemonic(mnemonic, ss58_format=0, crypto_type=KeypairType.ED25519) - - self.assertEqual(keypair.ss58_address, "16dYRUXznyhvWHS1ktUENGfNAEjCawyDzHRtN9AdFnJRc38h") - - def test_sign_and_verify_ed25519(self): - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic, crypto_type=KeypairType.ED25519) - signature = keypair.sign("Test123") - - self.assertTrue(keypair.verify("Test123", signature)) - - def test_sign_and_verify_invalid_signature_ed25519(self): - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic, crypto_type=KeypairType.ED25519) - signature = "0x4c291bfb0bb9c1274e86d4b666d13b2ac99a0bacc04a4846fb8ea50bda114677f83c1f164af58fc184451e5140cc8160c4de626163b11451d3bbb208a1889f8a" - self.assertFalse(keypair.verify("Test123", signature)) - - def test_unsupport_crypto_type(self): - self.assertRaises( - ValueError, Keypair.create_from_seed, - seed_hex='0xda3cf5b1e9144931?a0f0db65664aab662673b099415a7f8121b7245fb0be4143', - crypto_type=2 - ) - - def test_create_keypair_from_private_key(self): - keypair = Keypair.create_from_private_key( - ss58_address='16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2', - private_key='0x1f1995bdf3a17b60626a26cfe6f564b337d46056b7a1281b64c649d592ccda0a9cffd34d9fb01cae1fba61aeed184c817442a2186d5172416729a4b54dd4b84e' - ) - self.assertEqual("0x" + keypair.public_key.hex(), '0xe4359ad3e2716c539a1d663ebd0a51bdc5c98a12e663bb4c4402db47828c9446') - - def test_hdkd_hard_path(self): - mnemonic = 'old leopard transfer rib spatial phone calm indicate online fire caution review' - derivation_address = '5FEiH8iuDUw271xbqWTWuB6WrDjv5dnCeDX1CyHubAniXDNN' - derivation_path = '//Alice' - - derived_keypair = Keypair.create_from_uri(mnemonic + derivation_path) - - self.assertEqual(derivation_address, derived_keypair.ss58_address) - - def test_hdkd_soft_path(self): - mnemonic = 'old leopard transfer rib spatial phone calm indicate online fire caution review' - derivation_address = '5GNXbA46ma5dg19GXdiKi5JH3mnkZ8Yea3bBtZAvj7t99P9i' - derivation_path = '/Alice' - - derived_keypair = Keypair.create_from_uri(mnemonic + derivation_path) - - self.assertEqual(derivation_address, derived_keypair.ss58_address) - - def test_hdkd_default_to_dev_mnemonic(self): - derivation_address = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' - derivation_path = '//Alice' - - derived_keypair = Keypair.create_from_uri(derivation_path) - - self.assertEqual(derivation_address, derived_keypair.ss58_address) - - def test_hdkd_nested_hard_soft_path(self): - derivation_address = '5CJGwWiKXSE16WJaxBdPZhWqUYkotgenLUALv7ZvqQ4TXeqf' - derivation_path = '//Bob/test' - - derived_keypair = Keypair.create_from_uri(derivation_path) - - self.assertEqual(derivation_address, derived_keypair.ss58_address) - - def test_hdkd_nested_soft_hard_path(self): - derivation_address = '5Cwc8tShrshDJUp1P1M21dKUTcYQpV9GcfSa4hUBNmMdV3Cx' - derivation_path = '/Bob//test' - - derived_keypair = Keypair.create_from_uri(derivation_path) - - self.assertEqual(derivation_address, derived_keypair.ss58_address) - - def test_hdkd_path_gt_32_bytes(self): - derivation_address = '5GR5pfZeNs1uQiSWVxZaQiZou3wdZiX894eqgvfNfHbEh7W2' - derivation_path = '//PathNameLongerThan32BytesWhichShouldBeHashed' - - derived_keypair = Keypair.create_from_uri(derivation_path) - - self.assertEqual(derivation_address, derived_keypair.ss58_address) - - def test_hdkd_unsupported_password(self): - self.assertRaises(NotImplementedError, Keypair.create_from_uri, DEV_PHRASE + '///test') - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/unit_tests/bittensor_tests/test_metagraph.py b/tests/unit_tests/bittensor_tests/test_metagraph.py index 7b810c922a..5324dc6502 100644 --- a/tests/unit_tests/bittensor_tests/test_metagraph.py +++ b/tests/unit_tests/bittensor_tests/test_metagraph.py @@ -16,14 +16,25 @@ # DEALINGS IN THE SOFTWARE. import bittensor +import unittest +_subtensor_mock = bittensor.subtensor( network = 'mock', _mock = True ) + +class TestMetagraph(unittest.TestCase): + def setUp(self) -> None: + global _subtensor_mock + _subtensor_mock.reset() -def test_metagraph(): - metagraph = bittensor.metagraph( netuid = 999, network = "mock" ) + _subtensor_mock.create_subnet( + netuid = 999 + ) - assert metagraph.network == "mock" - assert metagraph.netuid == 999 - assert metagraph.n == 0 - assert len(metagraph.hotkeys) == 0 - assert len(metagraph.coldkeys) == 0 - assert len(metagraph.uids) == 0 \ No newline at end of file + def test_metagraph(self): + global _subtensor_mock + metagraph = _subtensor_mock.metagraph( netuid = 999 ) + + assert metagraph.netuid == 999 + assert metagraph.n == 0 + assert len(metagraph.hotkeys) == 0 + assert len(metagraph.coldkeys) == 0 + assert len(metagraph.uids) == 0 \ No newline at end of file diff --git a/tests/unit_tests/bittensor_tests/test_subtensor.py b/tests/unit_tests/bittensor_tests/test_subtensor.py index 8d5567c459..eb1acde067 100644 --- a/tests/unit_tests/bittensor_tests/test_subtensor.py +++ b/tests/unit_tests/bittensor_tests/test_subtensor.py @@ -181,22 +181,16 @@ def test_stake_multiple(self): is_null = False, ) - mock_compose_call = MagicMock( + mock_do_stake = MagicMock( side_effect=ExitEarly ) mock_subtensor = MagicMock( spec=bittensor.Subtensor, - network="mock", + network="mock_net", get_balance=MagicMock(return_value=bittensor.Balance.from_tao(mock_amount.tao + 20.0)), # enough balance to stake get_neuron_for_pubkey_and_subnet=MagicMock(return_value=mock_neuron), - substrate=MagicMock( - __enter__=MagicMock( - return_value=MagicMock( - compose_call=mock_compose_call, - ), - ), - ), + _do_stake=mock_do_stake ) with pytest.raises(ExitEarly): @@ -207,12 +201,10 @@ def test_stake_multiple(self): amounts=mock_amounts, ) - mock_compose_call.assert_called_once() + mock_do_stake.assert_called_once() # args, kwargs - _, kwargs = mock_compose_call.call_args - self.assertEqual(kwargs['call_module'], 'SubtensorModule') - self.assertEqual(kwargs['call_function'], 'add_stake') - self.assertAlmostEqual(kwargs['call_params']['ammount_staked'], mock_amount.rao, delta=1.0 * 1e9) # delta of 1.0 TAO + _, kwargs = mock_do_stake.call_args + self.assertAlmostEqual(kwargs['ammount'], mock_amount.rao, delta=1.0 * 1e9) # delta of 1.0 TAO if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/unit_tests/bittensor_tests/test_wallet.py b/tests/unit_tests/bittensor_tests/test_wallet.py deleted file mode 100644 index f090131bcc..0000000000 --- a/tests/unit_tests/bittensor_tests/test_wallet.py +++ /dev/null @@ -1,232 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2022 Yuma Rao - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -import unittest -from unittest.mock import patch, MagicMock -import pytest -import bittensor - -class TestWallet(unittest.TestCase): - def setUp(self): - self.mock_wallet = bittensor.wallet( _mock = True ) - - def test_regen_coldkeypub_from_ss58_addr(self): - ss58_address = "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" - with patch.object(self.mock_wallet, 'set_coldkeypub') as mock_set_coldkeypub: - self.mock_wallet.regenerate_coldkeypub( ss58_address=ss58_address ) - - mock_set_coldkeypub.assert_called_once() - keypair: bittensor.Keypair = mock_set_coldkeypub.call_args_list[0][0][0] - self.assertEqual(keypair.ss58_address, ss58_address) - - ss58_address_bad = "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zx" # 1 character short - with pytest.raises(ValueError): - self.mock_wallet.regenerate_coldkeypub(ss58_address=ss58_address_bad) - - def test_regen_coldkeypub_from_hex_pubkey_str(self): - pubkey_str = "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f" - with patch.object(self.mock_wallet, 'set_coldkeypub') as mock_set_coldkeypub: - self.mock_wallet.regenerate_coldkeypub(public_key=pubkey_str) - - mock_set_coldkeypub.assert_called_once() - keypair: bittensor.Keypair = mock_set_coldkeypub.call_args_list[0][0][0] - self.assertEqual('0x' + keypair.public_key.hex(), pubkey_str) - - pubkey_str_bad = "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512" # 1 character short - with pytest.raises(ValueError): - self.mock_wallet.regenerate_coldkeypub(ss58_address=pubkey_str_bad) - - def test_regen_coldkeypub_from_hex_pubkey_bytes(self): - pubkey_str = "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f" - pubkey_bytes = bytes.fromhex(pubkey_str[2:]) # Remove 0x from beginning - with patch.object(self.mock_wallet, 'set_coldkeypub') as mock_set_coldkeypub: - self.mock_wallet.regenerate_coldkeypub(public_key=pubkey_bytes) - - mock_set_coldkeypub.assert_called_once() - keypair: bittensor.Keypair = mock_set_coldkeypub.call_args_list[0][0][0] - self.assertEqual(keypair.public_key, pubkey_bytes) - - def test_regen_coldkeypub_no_pubkey(self): - with pytest.raises(ValueError): - # Must provide either public_key or ss58_address - self.mock_wallet.regenerate_coldkeypub(ss58_address=None, public_key=None) - - def test_regen_coldkey_from_hex_seed_str(self): - ss58_addr = "5D5cwd8DX6ij7nouVcoxDuWtJfiR1BnzCkiBVTt7DU8ft5Ta" - seed_str = "0x659c024d5be809000d0d93fe378cfde020846150b01c49a201fc2a02041f7636" - with patch.object(self.mock_wallet, 'set_coldkey') as mock_set_coldkey: - self.mock_wallet.regenerate_coldkey(seed=seed_str) - - mock_set_coldkey.assert_called_once() - keypair: bittensor.Keypair = mock_set_coldkey.call_args_list[0][0][0] - self.assertRegex(keypair.seed_hex if isinstance(keypair.seed_hex, str) else keypair.seed_hex.hex(), rf'(0x|){seed_str[2:]}') - self.assertEqual(keypair.ss58_address, ss58_addr) # Check that the ss58 address is correct - - seed_str_bad = "0x659c024d5be809000d0d93fe378cfde020846150b01c49a201fc2a02041f763" # 1 character short - with pytest.raises(ValueError): - self.mock_wallet.regenerate_coldkey(seed=seed_str_bad) - - def test_regen_hotkey_from_hex_seed_str(self): - ss58_addr = "5D5cwd8DX6ij7nouVcoxDuWtJfiR1BnzCkiBVTt7DU8ft5Ta" - seed_str = "0x659c024d5be809000d0d93fe378cfde020846150b01c49a201fc2a02041f7636" - with patch.object(self.mock_wallet, 'set_hotkey') as mock_set_hotkey: - self.mock_wallet.regenerate_hotkey(seed=seed_str) - - mock_set_hotkey.assert_called_once() - keypair: bittensor.Keypair = mock_set_hotkey.call_args_list[0][0][0] - self.assertRegex(keypair.seed_hex if isinstance(keypair.seed_hex, str) else keypair.seed_hex.hex(), rf'(0x|){seed_str[2:]}') - self.assertEqual(keypair.ss58_address, ss58_addr) # Check that the ss58 address is correct - - seed_str_bad = "0x659c024d5be809000d0d93fe378cfde020846150b01c49a201fc2a02041f763" # 1 character short - with pytest.raises(ValueError): - self.mock_wallet.regenerate_hotkey(seed=seed_str_bad) - -class TestWalletReregister(unittest.TestCase): - def test_wallet_reregister_use_cuda_flag_none(self): - config = bittensor.Config() - config.wallet = bittensor.Config() - config.wallet.reregister = True - - config.subtensor = bittensor.Config() - config.subtensor.register = bittensor.Config() - config.subtensor.register.cuda = bittensor.Config() - config.subtensor.register.cuda.use_cuda = None # don't set the argument, but do specify the flag - # No need to specify the other config options as they are default to None - - mock_wallet = bittensor.wallet.mock() - mock_wallet.is_registered = MagicMock(return_value=False) - mock_wallet.config = config - - class MockException(Exception): - pass - - def exit_early(*args, **kwargs): - raise MockException('exit_early') - - with patch('bittensor.Subtensor.register', side_effect=exit_early) as mock_register: - # Should be able to set without argument - with pytest.raises(MockException): - mock_wallet.reregister( netuid = -1 ) - - call_args = mock_register.call_args - _, kwargs = call_args - - mock_register.assert_called_once() - self.assertEqual(kwargs['cuda'], None) # should be None when no argument, but flag set - - def test_wallet_reregister_use_cuda_flag_true(self): - config = bittensor.Config() - config.wallet = bittensor.Config() - config.wallet.reregister = True - - config.subtensor = bittensor.Config() - config.subtensor.register = bittensor.Config() - config.subtensor.register.cuda = bittensor.Config() - config.subtensor.register.cuda.use_cuda = True - config.subtensor.register.cuda.dev_id = 0 - # No need to specify the other config options as they are default to None - - mock_wallet = bittensor.wallet.mock() - mock_wallet.is_registered = MagicMock(return_value=False) - mock_wallet.config = config - - class MockException(Exception): - pass - - def exit_early(*args, **kwargs): - raise MockException('exit_early') - - with patch('bittensor.Subtensor.register', side_effect=exit_early) as mock_register: - # Should be able to set without argument - with pytest.raises(MockException): - mock_wallet.reregister( netuid = -1 ) - - call_args = mock_register.call_args - _, kwargs = call_args - - mock_register.assert_called_once() - self.assertEqual(kwargs['cuda'], True) # should be default when no argument - - def test_wallet_reregister_use_cuda_flag_false(self): - config = bittensor.Config() - config.wallet = bittensor.Config() - config.wallet.reregister = True - - config.subtensor = bittensor.Config() - config.subtensor.register = bittensor.Config() - config.subtensor.register.cuda = bittensor.Config() - config.subtensor.register.cuda.use_cuda = False - config.subtensor.register.cuda.dev_id = 0 - # No need to specify the other config options as they are default to None - - mock_wallet = bittensor.wallet.mock() - mock_wallet.is_registered = MagicMock(return_value=False) - mock_wallet.config = config - - class MockException(Exception): - pass - - def exit_early(*args, **kwargs): - raise MockException('exit_early') - - with patch('bittensor.Subtensor.register', side_effect=exit_early) as mock_register: - # Should be able to set without argument - with pytest.raises(MockException): - mock_wallet.reregister( netuid = -1 ) - - call_args = mock_register.call_args - _, kwargs = call_args - - mock_register.assert_called_once() - self.assertEqual(kwargs['cuda'], False) # should be default when no argument - - def test_wallet_reregister_use_cuda_flag_not_specified_false(self): - config = bittensor.Config() - config.wallet = bittensor.Config() - config.wallet.reregister = True - - config.subtensor = bittensor.Config() - config.subtensor.register = bittensor.Config() - config.subtensor.register.cuda = bittensor.Config() - #config.subtensor.register.cuda.use_cuda # don't specify the flag - config.subtensor.register.cuda.dev_id = 0 - # No need to specify the other config options as they are default to None - - mock_wallet = bittensor.wallet.mock() - mock_wallet.is_registered = MagicMock(return_value=False) - mock_wallet.config = config - - class MockException(Exception): - pass - - def exit_early(*args, **kwargs): - raise MockException('exit_early') - - with patch('bittensor.Subtensor.register', side_effect=exit_early) as mock_register: - # Should be able to set without argument - with pytest.raises(MockException): - mock_wallet.reregister( netuid = -1 ) - - call_args = mock_register.call_args - _, kwargs = call_args - - mock_register.assert_called_once() - self.assertEqual(kwargs['cuda'], False) # should be False when no flag was set - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/unit_tests/bittensor_tests/utils/test_utils.py b/tests/unit_tests/bittensor_tests/utils/test_utils.py index ebf140829f..bd396690f4 100644 --- a/tests/unit_tests/bittensor_tests/utils/test_utils.py +++ b/tests/unit_tests/bittensor_tests/utils/test_utils.py @@ -24,6 +24,10 @@ import bittensor from bittensor.utils.registration import _CUDASolver, _SolverBase +from bittensor._subtensor.subtensor_mock import MockSubtensor + +from tests.mocks.wallet_mock import MockWallet +from tests.helpers import get_mock_wallet as generate_wallet, get_mock_keypair @fixture(scope="function") @@ -66,20 +70,6 @@ def select_port(): port = random.randrange(1000, 65536, 5) return port -def generate_wallet(coldkey : 'Keypair' = None, hotkey: 'Keypair' = None): - wallet = bittensor.wallet(_mock=True) - - if not coldkey: - coldkey = Keypair.create_from_mnemonic(Keypair.generate_mnemonic()) - if not hotkey: - hotkey = Keypair.create_from_mnemonic(Keypair.generate_mnemonic()) - - wallet.set_coldkey(coldkey, encrypt=False, overwrite=True) - wallet.set_coldkeypub(coldkey, encrypt=False, overwrite=True) - wallet.set_hotkey(hotkey, encrypt=False, overwrite=True) - - return wallet - def setup_subtensor( port:int ): chain_endpoint = "localhost:{}".format(port) subtensor = bittensor.subtensor( @@ -134,10 +124,10 @@ def test_solve_for_difficulty_fast(self): subtensor.get_current_block = MagicMock( return_value=1 ) subtensor.difficulty = MagicMock( return_value=1 ) subtensor.substrate = MagicMock() - subtensor.substrate.get_block_hash = MagicMock( return_value=block_hash ) + subtensor.get_block_hash = MagicMock( return_value=block_hash ) + subtensor.is_hotkey_registered = MagicMock( return_value=False ) wallet = MagicMock( hotkey = Keypair.create_from_mnemonic(Keypair.generate_mnemonic()), - is_registered = MagicMock( return_value=False ) ) num_proc: int = 1 limit = int(math.pow(2,256))- 1 @@ -151,6 +141,7 @@ def test_solve_for_difficulty_fast(self): solution = bittensor.utils.registration._solve_for_difficulty_fast( subtensor, wallet, netuid = -1, num_processes=num_proc ) seal = solution.seal assert bittensor.utils.registration._seal_meets_difficulty(seal, 10, limit) + def test_solve_for_difficulty_fast_registered_already(self): # tests if the registration stops after the first block of nonces for _ in range(10): @@ -163,10 +154,10 @@ def test_solve_for_difficulty_fast_registered_already(self): subtensor.get_current_block = MagicMock( return_value=1 ) subtensor.difficulty = MagicMock( return_value=int(1e10)) # set high to make solving take a long time subtensor.substrate = MagicMock() - subtensor.substrate.get_block_hash = MagicMock( return_value=block_hash ) + subtensor.get_block_hash = MagicMock( return_value=block_hash ) + subtensor.is_hotkey_registered = MagicMock( side_effect=is_registered_return_values ) wallet = MagicMock( hotkey = Keypair.create_from_mnemonic(Keypair.generate_mnemonic()), - is_registered = MagicMock( side_effect=is_registered_return_values ) ) # all arugments should return None to indicate an early return @@ -174,7 +165,7 @@ def test_solve_for_difficulty_fast_registered_already(self): assert solution is None # called every time until True - assert wallet.is_registered.call_count == workblocks_before_is_registered + 1 + assert subtensor.is_hotkey_registered.call_count == workblocks_before_is_registered + 1 def test_solve_for_difficulty_fast_missing_hash(self): block_hash = '0xba7ea4eb0b16dee271dbef5911838c3f359fcf598c74da65a54b919b68b67279' @@ -182,10 +173,10 @@ def test_solve_for_difficulty_fast_missing_hash(self): subtensor.get_current_block = MagicMock( return_value=1 ) subtensor.difficulty = MagicMock( return_value=1 ) subtensor.substrate = MagicMock() - subtensor.substrate.get_block_hash = MagicMock( side_effect= [None, None] + [block_hash]*20) + subtensor.get_block_hash = MagicMock( side_effect= [None, None] + [block_hash]*20) + subtensor.is_hotkey_registered = MagicMock( return_value=False ) wallet = MagicMock( hotkey = Keypair.create_from_mnemonic(Keypair.generate_mnemonic()), - is_registered = MagicMock( return_value=False ) ) num_proc: int = 1 limit = int(math.pow(2,256))- 1 @@ -343,12 +334,11 @@ def test_check_for_newest_block_and_update_new_block(self): current_diff: int = 0 mock_substrate = MagicMock( + ) + subtensor = MagicMock( get_block_hash=MagicMock( return_value=mock_block_hash ), - - ) - subtensor = MagicMock( substrate=mock_substrate, difficulty=MagicMock(return_value=current_diff + 1), # new diff ) @@ -408,9 +398,7 @@ def test_get_block_with_retry_network_error_exit(self): mock_subtensor = MagicMock( get_current_block=MagicMock(return_value=1), difficulty=MagicMock(return_value=1), - substrate=MagicMock( - get_block_hash=MagicMock(side_effect=self.MockException('network error')) - ) + get_block_hash=MagicMock(side_effect=self.MockException('network error')) ) with pytest.raises(self.MockException): # this should raise an exception because the network error is retried only 3 times @@ -515,19 +503,22 @@ def test_pow_not_stale_diff_block_number_too_old(self): assert mock_solution.is_stale(mock_subtensor) class TestPOWCalled(unittest.TestCase): + def setUp(self) -> None: + # Setup mock subnet + self._subtensor = bittensor.subtensor(_mock=True) + + self._subtensor.create_subnet( + netuid = 99 + ) + def test_pow_called_for_cuda(self): class MockException(Exception): pass - mock_compose_call = MagicMock(side_effect=MockException) + mock_pow_register_call = MagicMock(side_effect=MockException) mock_subtensor = bittensor.subtensor(_mock=True) mock_subtensor.get_neuron_for_pubkey_and_subnet=MagicMock(is_null=True) - mock_subtensor.substrate = MagicMock( - __enter__= MagicMock(return_value=MagicMock( - compose_call=mock_compose_call - )), - __exit__ = MagicMock(return_value=None), - ) + mock_subtensor._do_pow_register = mock_pow_register_call mock_wallet = SimpleNamespace( hotkey=bittensor.Keypair.create_from_seed( @@ -556,7 +547,7 @@ class MockException(Exception): ) as mock_create_pow: # Should exit early with pytest.raises(MockException): - mock_subtensor.register(mock_wallet, netuid=-1, cuda=True, prompt=False) + mock_subtensor.register(mock_wallet, netuid=99, cuda=True, prompt=False) mock_pow_is_stale.assert_called_once() mock_create_pow.assert_called_once() @@ -566,11 +557,10 @@ class MockException(Exception): _, kwargs = call0 assert kwargs['subtensor'] == mock_subtensor - mock_compose_call.assert_called_once() - call1 = mock_compose_call.call_args - assert call1[1]['call_function'] == 'register' - call_params = call1[1]['call_params'] - assert call_params['nonce'] == mock_result.nonce + mock_pow_register_call.assert_called_once() + _, kwargs = mock_pow_register_call.call_args + kwargs['pow_result'].nonce == mock_result.nonce + class TestCUDASolverRun(unittest.TestCase): def test_multi_cuda_run_updates_nonce_start(self): @@ -652,5 +642,217 @@ def test_get_explorer_url_for_network_by_network_and_block_hash(self, network: s self.assertEqual(bittensor.utils.get_explorer_url_for_network(network, block_hash, self.network_map), expected) +class TestWalletReregister(unittest.TestCase): + _mock_subtensor: MockSubtensor + + def setUp(self): + self.subtensor = bittensor.subtensor( network = 'mock' ) # own instance per test + + @classmethod + def setUpClass(cls) -> None: + # Keeps the same mock network for all tests. This stops the network from being re-setup for each test. + cls._mock_subtensor = bittensor.subtensor( network = 'mock' ) + + cls._do_setup_subnet() + + @classmethod + def _do_setup_subnet(cls): + # reset the mock subtensor + cls._mock_subtensor.reset() + # Setup the mock subnet 3 + cls._mock_subtensor.create_subnet( + netuid = 3 + ) + + def test_wallet_reregister_reregister_false(self): + mock_wallet = generate_wallet( + hotkey = get_mock_keypair( + 100, self.id() + ) + ) + + class MockException(Exception): + pass + + with patch('bittensor._subtensor.extrinsics.registration.register_extrinsic', side_effect=MockException) as mock_register: + with pytest.raises(SystemExit): # should exit because it's not registered + bittensor.utils.reregister( + wallet = mock_wallet, + subtensor = self._mock_subtensor, + netuid = 3, + reregister = False, + ) + + mock_register.assert_not_called() # should not call register + + def test_wallet_reregister_reregister_false_and_registered_already(self): + mock_wallet = generate_wallet( + hotkey = get_mock_keypair( + 100, self.id() + ) + ) + + class MockException(Exception): + pass + + self._mock_subtensor.force_register_neuron( + netuid = 3, + hotkey = mock_wallet.hotkey.ss58_address, + coldkey = mock_wallet.coldkeypub.ss58_address, + ) + self.assertTrue(self._mock_subtensor.is_hotkey_registered_on_subnet( + netuid = 3, + hotkey_ss58 = mock_wallet.hotkey.ss58_address, + )) + + with patch('bittensor._subtensor.subtensor_impl.register_extrinsic', side_effect=MockException) as mock_register: + bittensor.utils.reregister( + wallet = mock_wallet, + subtensor = self._mock_subtensor, + netuid = 3, + reregister = False, + ) # Should not exit because it's registered + + mock_register.assert_not_called() # should not call register + + def test_wallet_reregister_reregister_true_and_registered_already(self): + mock_wallet = generate_wallet( + hotkey = get_mock_keypair( + 100, self.id() + ) + ) + + class MockException(Exception): + pass + + self._mock_subtensor.force_register_neuron( + netuid = 3, + hotkey = mock_wallet.hotkey.ss58_address, + coldkey = mock_wallet.coldkeypub.ss58_address, + ) + self.assertTrue(self._mock_subtensor.is_hotkey_registered_on_subnet( + netuid = 3, + hotkey_ss58 = mock_wallet.hotkey.ss58_address, + )) + + with patch('bittensor._subtensor.subtensor_impl.register_extrinsic', side_effect=MockException) as mock_register: + bittensor.utils.reregister( + wallet = mock_wallet, + subtensor = self._mock_subtensor, + netuid = 3, + reregister = True, + ) # Should not exit because it's registered + + mock_register.assert_not_called() # should not call register + + + def test_wallet_reregister_no_params(self): + mock_wallet = generate_wallet( + hotkey = get_mock_keypair( + 100, self.id() + ) + ) + + class MockException(Exception): + pass + + with patch('bittensor._subtensor.subtensor_impl.register_extrinsic', side_effect=MockException) as mock_register: + # Should be able to set without argument + with pytest.raises(MockException): + bittensor.utils.reregister( + wallet = mock_wallet, + subtensor = self._mock_subtensor, + netuid = 3, + reregister = True, + # didn't pass any register params + ) + + mock_register.assert_called_once() # should call register once + + def test_wallet_reregister_use_cuda_flag_true(self): + mock_wallet = generate_wallet( + hotkey = get_mock_keypair( + 100, self.id() + ) + ) + + class MockException(Exception): + pass + + with patch('bittensor._subtensor.subtensor_impl.register_extrinsic', side_effect=MockException) as mock_register: + # Should be able to set without argument + with pytest.raises(MockException): + bittensor.utils.reregister( + wallet = mock_wallet, + subtensor = self._mock_subtensor, + netuid = 3, + dev_id = 0, + cuda = True, + reregister = True, + ) + + call_args = mock_register.call_args + _, kwargs = call_args + + mock_register.assert_called_once() + self.assertIn('cuda', kwargs) + self.assertEqual(kwargs['cuda'], True) + + def test_wallet_reregister_use_cuda_flag_false(self): + mock_wallet = generate_wallet( + hotkey = get_mock_keypair( + 100, self.id() + ) + ) + + class MockException(Exception): + pass + + with patch('bittensor._subtensor.subtensor_impl.register_extrinsic', side_effect=MockException) as mock_register: + # Should be able to set without argument + with pytest.raises(MockException): + bittensor.utils.reregister( + wallet = mock_wallet, + subtensor = self._mock_subtensor, + netuid = 3, + dev_id = 0, + cuda = False, + reregister = True, + ) + + call_args = mock_register.call_args + _, kwargs = call_args + + mock_register.assert_called_once() + self.assertEqual(kwargs['cuda'], False) + + def test_wallet_reregister_cuda_arg_not_specified_should_be_false(self): + mock_wallet = generate_wallet( + hotkey = get_mock_keypair( + 100, self.id() + ) + ) + + class MockException(Exception): + pass + + with patch('bittensor._subtensor.subtensor_impl.register_extrinsic', side_effect=MockException) as mock_register: + # Should be able to set without argument + with pytest.raises(MockException): + bittensor.utils.reregister( + wallet = mock_wallet, + subtensor = self._mock_subtensor, + netuid = 3, + dev_id = 0, + reregister = True, + ) + + call_args = mock_register.call_args + _, kwargs = call_args + + mock_register.assert_called_once() + self.assertEqual(kwargs['cuda'], False) # should be False by default + + if __name__ == "__main__": unittest.main()