diff --git a/HISTORY.rst b/HISTORY.rst index 30d770be3e..b85f312a17 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -36,3 +36,11 @@ Release History - Adds cli functionality to add connections - Multiple additional minor fixes and changes + +0.1.5 (2019-09-26) +------------------- + +- Adds scaffolding command to the CLI tool +- Extended docs +- Increased test coverage +- Multiple additional minor fixes and changes diff --git a/Pipfile b/Pipfile index 25e641bb8d..4c2cd0067a 100644 --- a/Pipfile +++ b/Pipfile @@ -9,28 +9,30 @@ verify_ssl = true name = "test-pypi" [dev-packages] -flake8 = "*" -pytest = "*" tox = "==3.7.0" tox-pipenv = "==1.9.0" -pytest-cov = "*" +flake8 = "*" flake8-docstrings = "*" -pygments = "*" -docker = "*" +pydocstyle = "==3.0.0" +pytest = "*" +pytest-cov = "*" mypy = "*" +mkdocs = "*" +mkdocs-material = "*" +pymdown-extensions = "*" +pygments = "*" [packages] cryptography = "*" base58 = "*" docker = "*" click = "*" -pyyaml = "*" +pyyaml = ">=4.2b1" click-log = "*" oef = {index = "test-pypi",version = "==0.6.10"} colorlog = "*" jsonschema = "*" protobuf = "*" -mkdocs = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 9256558791..392c3be869 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "86dd2bb2e1e88ffb75b7d5d8dfff05425901a033e107f7b9a63e52c6db201fc8" + "sha256": "51ca0cf2176220c5c57ccd45b26dc3decfc059f40f0889eda89ea164a71fa714" }, "pipfile-spec": 6, "requires": { @@ -159,13 +159,6 @@ ], "version": "==2.8" }, - "jinja2": { - "hashes": [ - "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", - "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" - ], - "version": "==2.10.1" - }, "jsonschema": { "hashes": [ "sha256:5f9c0a719ca2ce14c5de2fd350a64fd2d13e8539db29836a86adc990bb1a068f", @@ -174,61 +167,6 @@ "index": "pypi", "version": "==3.0.2" }, - "livereload": { - "hashes": [ - "sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", - "sha256:89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66" - ], - "version": "==2.6.1" - }, - "markdown": { - "hashes": [ - "sha256:2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a", - "sha256:56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c" - ], - "version": "==3.1.1" - }, - "markupsafe": { - "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" - ], - "version": "==1.1.1" - }, - "mkdocs": { - "hashes": [ - "sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939", - "sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a" - ], - "index": "pypi", - "version": "==1.0.4" - }, "oef": { "hashes": [ "sha256:bbe0d85b1fa104868f5971c3c56deefe1f5104c647fa795a84e68a8034e01480", @@ -239,26 +177,27 @@ }, "protobuf": { "hashes": [ - "sha256:00a1b0b352dc7c809749526d1688a64b62ea400c5b05416f93cfb1b11a036295", - "sha256:01acbca2d2c8c3f7f235f1842440adbe01bbc379fa1cbdd80753801432b3fae9", - "sha256:0a795bca65987b62d6b8a2d934aa317fd1a4d06a6dd4df36312f5b0ade44a8d9", - "sha256:0ec035114213b6d6e7713987a759d762dd94e9f82284515b3b7331f34bfaec7f", - "sha256:31b18e1434b4907cb0113e7a372cd4d92c047ce7ba0fa7ea66a404d6388ed2c1", - "sha256:32a3abf79b0bef073c70656e86d5bd68a28a1fbb138429912c4fc07b9d426b07", - "sha256:55f85b7808766e5e3f526818f5e2aeb5ba2edcc45bcccede46a3ccc19b569cb0", - "sha256:64ab9bc971989cbdd648c102a96253fdf0202b0c38f15bd34759a8707bdd5f64", - "sha256:64cf847e843a465b6c1ba90fb6c7f7844d54dbe9eb731e86a60981d03f5b2e6e", - "sha256:917c8662b585470e8fd42f052661fc66d59fccaae450a60044307dcbf82a3335", - "sha256:a657e3c27870f2b16d4bd415b00d46a846a827758d1414600872b1dc57202178", - "sha256:afed9003d7f2be2c3df20f64220c30faec441073731511728a2cb4cab4cd46a6", - "sha256:bf8e05d638b585d1752c5a84247134a0350d3a8b73d3632489a014a9f6f1e758", - "sha256:d831b047bd69becaf64019a47179eb22118a50dd008340655266a906c69c6417", - "sha256:de2760583ed28749ff885789c1cbc6c9c06d6de92fc825740ab99deb2f25ea4d", - "sha256:eabc4cf1bc19689af8022ba52fd668564a8d96e0d08f3b4732d26a64255216a4", - "sha256:fcff6086c86fb1628d94ea455c7b9de898afc50378042927a59df8065a79a549" + "sha256:24f1f34ab99067d05188f932b45ae8be49edf6ea905a93b47121bedf53899227", + "sha256:26c0d756c7ad6823fccbc3b5f84c619b9cc7ac281496fe0a9d78e32023c45034", + "sha256:3200046e4d4f6c42ed66257dbe15e2e5dc76072c280e9b3d69dc8f3a4fa3fbbc", + "sha256:368f1bae6dd22d04fd2254d30cd301863408a96ff604422e3ddd8ab601f095a4", + "sha256:3902fa1920b4ef9f710797496b309efc5ccd0faeba44dc82ed6a711a244764a0", + "sha256:3a7a8925ba6481b9241cdb5d69cd0b0700f23efed6bb691dc9543faa4aa25d6f", + "sha256:4bc33d49f43c6e9916fb56b7377cb4478cbf25824b4d2bedfb8a4e3df31c12ca", + "sha256:568b434a36e31ed30d60d600b2227666ce150b8b5275948f50411481a4575d6d", + "sha256:5c393cd665d03ce6b29561edd6b0cc4bcb3fb8e2a7843e8f223d693f07f61b40", + "sha256:80072e9ba36c73cf89c01f669c7b123733fc2de1780b428082a850f53cc7865f", + "sha256:843f498e98ad1469ad54ecb4a7ccf48605a1c5d2bd26ae799c7a2cddab4a37ec", + "sha256:aa45443035651cbfae74c8deb53358ba660d8e7a5fbab3fc4beb33fb3e3ca4be", + "sha256:aaab817d9d038dd5f56a6fb2b2e8ae68caf1fd28cc6a963c755fa73268495c13", + "sha256:e1d6aef62835617e174ff8d01f662cc0e69bf0c9a64e89da8fc14f987aef46b5", + "sha256:e6f68b9979dc8f75299293d682f67fecb72d78f98652da2eeb85c85edef1ca94", + "sha256:e7366cabddff3441d583fdc0176ab42eba4ee7090ef857d50c4dd59ad124003a", + "sha256:f0144ad97cd28bfdda0567b9278d25061ada5ad2b545b538cd3577697b32bda3", + "sha256:f655338491481f482042f19016647e50365ab41b75b486e0df56e0dcc425abf4" ], "index": "pypi", - "version": "==3.9.1" + "version": "==3.9.2" }, "pycparser": { "hashes": [ @@ -305,24 +244,12 @@ ], "version": "==1.12.0" }, - "tornado": { - "hashes": [ - "sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", - "sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", - "sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", - "sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", - "sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", - "sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", - "sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5" - ], - "version": "==6.0.3" - }, "urllib3": { "hashes": [ - "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", - "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232" + "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", + "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" ], - "version": "==1.25.3" + "version": "==1.25.6" }, "websocket-client": { "hashes": [ @@ -354,12 +281,13 @@ ], "version": "==2019.9.11" }, - "chardet": { + "click": { "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" ], - "version": "==3.0.4" + "index": "pypi", + "version": "==7.0" }, "coverage": { "hashes": [ @@ -398,14 +326,6 @@ ], "version": "==4.5.4" }, - "docker": { - "hashes": [ - "sha256:acf51b5e3e0d056925c3b780067a6f753c915fffaa46c5f2d79eb0fc1cbe6a01", - "sha256:cc5b2e94af6a2b1e1ed9d7dcbdc77eff56c36081757baf9ada6e878ea0213164" - ], - "index": "pypi", - "version": "==4.0.2" - }, "entrypoints": { "hashes": [ "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", @@ -437,12 +357,11 @@ "index": "pypi", "version": "==1.4.0" }, - "idna": { + "htmlmin": { "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + "sha256:50c1ef4630374a5d723900096a961cff426dff46b48f34d194a81bbe14eca178" ], - "version": "==2.8" + "version": "==0.1.12" }, "importlib-metadata": { "hashes": [ @@ -452,6 +371,66 @@ "markers": "python_version < '3.8'", "version": "==0.23" }, + "jinja2": { + "hashes": [ + "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", + "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" + ], + "version": "==2.10.1" + }, + "jsmin": { + "hashes": [ + "sha256:b6df99b2cd1c75d9d342e4335b535789b8da9107ec748212706ef7bbe5c2553b" + ], + "version": "==2.2.2" + }, + "livereload": { + "hashes": [ + "sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", + "sha256:89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66" + ], + "version": "==2.6.1" + }, + "markdown": { + "hashes": [ + "sha256:2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a", + "sha256:56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c" + ], + "version": "==3.1.1" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + ], + "version": "==1.1.1" + }, "mccabe": { "hashes": [ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", @@ -459,6 +438,29 @@ ], "version": "==0.6.1" }, + "mkdocs": { + "hashes": [ + "sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939", + "sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a" + ], + "index": "pypi", + "version": "==1.0.4" + }, + "mkdocs-material": { + "hashes": [ + "sha256:75807513b2fa904219124f05802ef4e674a7312aa018446ed58054a48a76e66a", + "sha256:d3e641f634227ce113ebdde1df0a0e0b1eba1d3f1da35344d68095b4270407b2" + ], + "index": "pypi", + "version": "==4.4.2" + }, + "mkdocs-minify-plugin": { + "hashes": [ + "sha256:3000a5069dd0f42f56a8aaf7fd5ea1222c67487949617e39585d6b6434b074b6", + "sha256:d54fdd5be6843dd29fd7af2f7fdd20a9eb4db46f1f6bed914e03b2f58d2d488e" + ], + "version": "==0.2.1" + }, "more-itertools": { "hashes": [ "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", @@ -492,10 +494,17 @@ }, "packaging": { "hashes": [ - "sha256:a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9", - "sha256:c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe" + "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", + "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" ], - "version": "==19.1" + "version": "==19.2" + }, + "pep562": { + "hashes": [ + "sha256:58cb1cc9ee63d93e62b4905a50357618d526d289919814bea1f0da8f53b79395", + "sha256:d2a48b178ebf5f8dd31709cc26a19808ef794561fa2fe50ea01ea2bad4d667ef" + ], + "version": "==1.0" }, "pipenv": { "hashes": [ @@ -528,10 +537,14 @@ }, "pydocstyle": { "hashes": [ - "sha256:04c84e034ebb56eb6396c820442b8c4499ac5eb94a3bda88951ac3dc519b6058", - "sha256:66aff87ffe34b1e49bff2dd03a88ce6843be2f3346b0c9814410d34987fbab59" + "sha256:0575e62020cea5fd5c386e267e95a68c156d2f92f9c1347d47edda8c5301f5f2", + "sha256:2258f9b0df68b97bf3a6c29003edc5238ff8879f1efb6f1999988d934e432bd8", + "sha256:5741c85e408f9e0ddf873611085e819b809fca90b619f5fd7f34bd4959da3dd4", + "sha256:c7e32e9de6bd54c1de7cf88af00117ff478b24bd8819552e0d2b52a150c087fa", + "sha256:ed79d4ec5e92655eccc21eb0c6cf512e69512b4a97d215ace46d17e4990f2039" ], - "version": "==4.0.1" + "index": "pypi", + "version": "==3.0.0" }, "pyflakes": { "hashes": [ @@ -548,6 +561,14 @@ "index": "pypi", "version": "==2.4.2" }, + "pymdown-extensions": { + "hashes": [ + "sha256:24c1a0afbae101c4e2b2675ff4dd936470a90133f93398b9cbe0c855e2d2ec10", + "sha256:960486dea995f1759dfd517aa140b3d851cd7b44d4c48d276fd2c74fc4e1bce9" + ], + "index": "pypi", + "version": "==6.1" + }, "pyparsing": { "hashes": [ "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", @@ -557,11 +578,11 @@ }, "pytest": { "hashes": [ - "sha256:95d13143cc14174ca1a01ec68e84d76ba5d9d493ac02716fd9706c949a505210", - "sha256:b78fe2881323bd44fd9bd76e5317173d4316577e7b1cddebae9136a4495ec865" + "sha256:813b99704b22c7d377bbd756ebe56c35252bb710937b46f207100e843440b3c2", + "sha256:cc6620b96bc667a0c8d4fa592a8c9c94178a1bd6cc799dbb057dfd9286d31a31" ], "index": "pypi", - "version": "==5.1.2" + "version": "==5.1.3" }, "pytest-cov": { "hashes": [ @@ -571,12 +592,24 @@ "index": "pypi", "version": "==2.7.1" }, - "requests": { + "pyyaml": { "hashes": [ - "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", - "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", + "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", + "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", + "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", + "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", + "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", + "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", + "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", + "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", + "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", + "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", + "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", + "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" ], - "version": "==2.22.0" + "index": "pypi", + "version": "==5.1.2" }, "six": { "hashes": [ @@ -599,6 +632,18 @@ ], "version": "==0.10.0" }, + "tornado": { + "hashes": [ + "sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", + "sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", + "sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", + "sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", + "sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", + "sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", + "sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5" + ], + "version": "==6.0.3" + }, "tox": { "hashes": [ "sha256:04f8f1aa05de8e76d7a266ccd14e0d665d429977cd42123bc38efa9b59964e9e", @@ -643,13 +688,6 @@ ], "version": "==3.7.4" }, - "urllib3": { - "hashes": [ - "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", - "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232" - ], - "version": "==1.25.3" - }, "virtualenv": { "hashes": [ "sha256:680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30", @@ -671,13 +709,6 @@ ], "version": "==0.1.7" }, - "websocket-client": { - "hashes": [ - "sha256:1151d5fb3a62dc129164292e1227655e4bbc5dd5340a5165dfae61128ec50aa9", - "sha256:1fd5520878b68b84b5748bb30e592b10d0a91529d5383f74f4964e72b297fd3a" - ], - "version": "==0.56.0" - }, "zipp": { "hashes": [ "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", diff --git a/README.md b/README.md index f5421d917f..2d7b771318 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,23 @@ First, install the package from [pypi](https://pypi.org/project/aea/): pip install aea ` -Then, build your agent as described in the [AEA CLI readme](../master/aea/cli/README.md) +Then, build your agent as described in the [AEA CLI readme](../master/aea/cli/README.md) or in the [examples](../master/examples). -## Dependencies +## Install from Source + +## Cloning + +This repository contains submodules. Clone with recursive strategy: + + git clone git@github.com:fetchai/agents-aea.git --recursive && cd agents-aea + +### Dependencies All python specific dependencies are specified in the Pipfile (and installed via the commands specified in 'Preliminaries'). Or, you can have more control on the installed dependencies by leveraging the setuptools' extras mechanism (more details later). -## Preliminaries +### Preliminaries - Create and launch a virtual environment: @@ -27,9 +35,9 @@ Or, you can have more control on the installed dependencies by leveraging the se pip install .[all] -- To install only specific extra dependencies, e.g. `cli` and `oef-protocol`: +- To install only specific extra dependencies, e.g. `cli`: - pip install .[cli,oef-channel] + pip install .[cli] ## Contribute diff --git a/aea/__version__.py b/aea/__version__.py index 11b814de56..2b89208cc1 100644 --- a/aea/__version__.py +++ b/aea/__version__.py @@ -23,7 +23,7 @@ __title__ = 'aea' __description__ = 'Autonomous Economic Agent framework' __url__ = 'https://github.com/fetchai/agents-aea.git' -__version__ = '0.1.4' +__version__ = '0.1.5' __author__ = 'Fetch.AI Limited' __license__ = 'Apache 2.0' __copyright__ = '2019 Fetch.AI Limited' diff --git a/aea/aea.py b/aea/aea.py index d1ccfe6004..c146fe615d 100644 --- a/aea/aea.py +++ b/aea/aea.py @@ -24,8 +24,9 @@ from aea.agent import Agent from aea.mail.base import Envelope, MailBox -from aea.skills.base import AgentContext, Resources -from aea.skills.default.handler import DefaultHandler +from aea.registries.base import Resources +from aea.skills.base import AgentContext +from aea.skills.error.handler import ErrorHandler logger = logging.getLogger(__name__) @@ -36,7 +37,7 @@ class AEA(Agent): def __init__(self, name: str, mailbox: MailBox, private_key_pem_path: Optional[str] = None, - timeout: float = 0.0, # TODO we might want to set this to 0 for the aea and let the skills take care of slowing things down on a skill level + timeout: float = 0.0, debug: bool = False, max_reactions: int = 20, directory: str = '') -> None: @@ -113,31 +114,28 @@ def handle(self, envelope: Envelope) -> None: """ protocol = self.resources.protocol_registry.fetch(envelope.protocol_id) - # fetch the handler of the "default" protocol for error handling. TODO: change with the handler of "error" protocol. - default_handler = self.resources.handler_registry.fetch("default") - default_handler = cast(DefaultHandler, default_handler) + error_handler = self.resources.handler_registry.fetch_by_skill("default", "error") + assert error_handler is not None, "ErrorHandler not initialized" + error_handler = cast(ErrorHandler, error_handler) if protocol is None: - if default_handler is not None: - default_handler.send_unsupported_protocol(envelope) + error_handler.send_unsupported_protocol(envelope) return try: msg = protocol.serializer.decode(envelope.message) except Exception: - if default_handler is not None: - default_handler.send_decoding_error(envelope) + error_handler.send_decoding_error(envelope) return - if not protocol.check(msg): - if default_handler is not None: - default_handler.send_invalid_message(envelope) - return + if not protocol.check(msg): # pragma: no cover + error_handler.send_invalid_message(envelope) # pragma: no cover + return # pragma: no cover handlers = self.resources.handler_registry.fetch(protocol.id) if handlers is None: - if default_handler is not None: - default_handler.send_unsupported_skill(envelope, protocol) + if error_handler is not None: + error_handler.send_unsupported_skill(envelope, protocol) return # each handler independently acts on the message diff --git a/aea/agent.py b/aea/agent.py index 8da5791316..69519593b9 100644 --- a/aea/agent.py +++ b/aea/agent.py @@ -126,7 +126,7 @@ def agent_state(self) -> AgentState: elif self.mailbox.is_connected and not self.liveness.is_stopped: return AgentState.RUNNING else: - raise ValueError("Agent state not recognized.") + raise ValueError("Agent state not recognized.") # pragma: no cover def start(self) -> None: """ diff --git a/aea/cli/__main__.py b/aea/cli/__main__.py index d723242b2c..d6cfb371bc 100755 --- a/aea/cli/__main__.py +++ b/aea/cli/__main__.py @@ -31,13 +31,15 @@ from jsonschema import ValidationError import aea -from aea.cli.add import connection, add +from aea.cli.add import connection, add, skill from aea.cli.common import Context, pass_ctx, logger from aea.cli.remove import remove from aea.cli.run import run +from aea.cli.scaffold import scaffold from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, AgentConfig DEFAULT_CONNECTION = "oef" +DEFAULT_SKILL = "error" @click.group() @@ -64,16 +66,20 @@ def create(click_context, agent_name): # create a config file inside it config_file = open(os.path.join(agent_name, DEFAULT_AEA_CONFIG_FILE), "w") - agent_config = AgentConfig(agent_name=agent_name, aea_version=aea.__version__, authors="", version="v1", license="", url="", private_key_pem_path="") + agent_config = AgentConfig(agent_name=agent_name, aea_version=aea.__version__, authors="", version="v1", license="", url="", registry_path="../packages", private_key_pem_path="") agent_config.default_connection = DEFAULT_CONNECTION ctx.agent_loader.dump(agent_config, config_file) logger.info("Created config file {}".format(DEFAULT_AEA_CONFIG_FILE)) - logger.info("Adding default connection '{}' to the agent...".format(DEFAULT_CONNECTION)) + # next commands must be done from the agent's directory -> overwrite ctx.cwd ctx.agent_config = agent_config - # next command must be done from the agent's directory -> overwrite ctx.cwd ctx.cwd = agent_config.agent_name - click_context.invoke(connection, dirpath=os.path.join(aea.AEA_DIR, "channels", DEFAULT_CONNECTION)) + + logger.info("Adding default connection '{}' to the agent...".format(DEFAULT_CONNECTION)) + click_context.invoke(connection, connection_name=DEFAULT_CONNECTION) + + logger.info("Adding default skill '{}' to the agent...".format(DEFAULT_SKILL)) + click_context.invoke(skill, skill_name=DEFAULT_SKILL) except OSError: logger.error("Directory already exist. Aborting...") @@ -105,8 +111,9 @@ def delete(ctx: Context, agent_name): cli.add_command(add) +cli.add_command(scaffold) cli.add_command(remove) cli.add_command(run) if __name__ == '__main__': - cli() + cli() # pragma: no cover diff --git a/aea/cli/add.py b/aea/cli/add.py index 315c64ac81..930a1e6522 100644 --- a/aea/cli/add.py +++ b/aea/cli/add.py @@ -19,7 +19,6 @@ """Implementation of the 'aea add' subcommand.""" -import importlib.util import os import shutil from pathlib import Path @@ -29,9 +28,9 @@ from click import pass_context from jsonschema import ValidationError +from aea import AEA_DIR from aea.cli.common import Context, pass_ctx, logger, _try_to_load_agent_config -from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE -from aea.skills.base import DEFAULT_SKILL_CONFIG_FILE, DEFAULT_CONNECTION_CONFIG_FILE +from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, DEFAULT_CONNECTION_CONFIG_FILE, DEFAULT_SKILL_CONFIG_FILE, DEFAULT_PROTOCOL_CONFIG_FILE @click.group() @@ -42,46 +41,48 @@ def add(ctx: Context): @add.command() -@click.argument('dirpath', type=str, required=True) -@pass_ctx -def connection(ctx: Context, dirpath): +@click.argument('connection_name', type=str, required=True) +@pass_context +def connection(click_context, connection_name): """Add a connection to the configuration file.""" + ctx = cast(Context, click_context.obj) + agent_name = ctx.agent_config.agent_name + logger.debug("Adding connection {} to the agent {}...".format(connection_name, agent_name)) + + # check if we already have a connection with the same name + logger.debug("Connection already supported by the agent: {}".format(ctx.agent_config.connections)) + if connection_name in ctx.agent_config.connections: + logger.error("A connection with name '{}' already exists. Aborting...".format(connection_name)) + exit(-1) + # check that the provided path points to a proper connection directory -> look for connection.yaml file. - connection_configuration_filepath = Path(os.path.join(dirpath, DEFAULT_CONNECTION_CONFIG_FILE)) + # first check in aea dir + registry_path = ctx.agent_config.registry_path + connection_configuration_filepath = Path(os.path.join(registry_path, "connections", connection_name, DEFAULT_CONNECTION_CONFIG_FILE)) if not connection_configuration_filepath.exists(): - logger.error("Path '{}' does not exist.".format(connection_configuration_filepath)) - exit(-1) - return + # then check in registry + registry_path = AEA_DIR + connection_configuration_filepath = Path(os.path.join(registry_path, "connections", connection_name, DEFAULT_CONNECTION_CONFIG_FILE)) + if not connection_configuration_filepath.exists(): + logger.error("Cannot find connection: '{}'.".format(connection_name)) + exit(-1) # try to load the connection configuration file try: connection_configuration = ctx.connection_loader.load(open(str(connection_configuration_filepath))) + logger.info("Connection supports the following protocols: {}".format(connection_configuration.supported_protocols)) except ValidationError as e: logger.error("Connection configuration file not valid: {}".format(str(e))) exit(-1) - return - - # check if we already have a connection with the same name - logger.debug("Connection already supported by the agent: {}".format(ctx.agent_config.connections)) - connection_name = connection_configuration.name - if connection_name in ctx.agent_config.connections: - logger.error("A connection with name '{}' already exists. Aborting...".format(connection_name)) - exit(-1) - return - - agent_name = ctx.agent_config.agent_name - logger.debug("Adding connection {connection_name} to the agent {agent_name}..." - .format(agent_name=agent_name, connection_name=connection_name)) # copy the connection package into the agent's supported connections. - dirpath = str(Path(dirpath).absolute()) - src = dirpath + src = str(Path(os.path.join(registry_path, "connections", connection_name)).absolute()) dest = os.path.join(ctx.cwd, "connections", connection_name) logger.info("Copying connection modules. src={} dst={}".format(src, dest)) try: shutil.copytree(src, dest) except Exception as e: - logger.error(e) + logger.error(str(e)) exit(-1) # make the 'connections' folder a Python package. @@ -97,72 +98,86 @@ def connection(ctx: Context, dirpath): @add.command() @click.argument('protocol_name', type=str, required=True) -@pass_ctx -def protocol(ctx: Context, protocol_name): +@pass_context +def protocol(click_context, protocol_name): """Add a protocol to the agent.""" + ctx = cast(Context, click_context.obj) agent_name = cast(str, ctx.agent_config.agent_name) - logger.debug("Adding protocol {protocol_name} to the agent {agent_name}..." - .format(agent_name=agent_name, protocol_name=protocol_name)) - - # find the supported protocols and check if the candidate protocol is supported. - protocols_module_spec = importlib.util.find_spec("aea.protocols") - assert protocols_module_spec is not None, "Protocols module spec is None." - _protocols_submodules = protocols_module_spec.loader.contents() # type: ignore - _protocols_submodules = filter(lambda x: not x.startswith("__") and x != "base", _protocols_submodules) - aea_supported_protocol = set(_protocols_submodules) - logger.debug("Supported protocols: {}".format(aea_supported_protocol)) - if protocol_name not in aea_supported_protocol: - logger.error("Protocol '{}' not supported. Aborting...".format(protocol_name)) - return + logger.debug("Adding protocol {} to the agent {}...".format(protocol_name, agent_name)) # check if we already have a protocol with the same name logger.debug("Protocols already supported by the agent: {}".format(ctx.agent_config.protocols)) if protocol_name in ctx.agent_config.protocols: logger.error("A protocol with name '{}' already exists. Aborting...".format(protocol_name)) - return + exit(-1) + + # check that the provided path points to a proper protocol directory -> look for protocol.yaml file. + # first check in aea dir + registry_path = ctx.agent_config.registry_path + protocol_configuration_filepath = Path(os.path.join(registry_path, "protocols", protocol_name, DEFAULT_PROTOCOL_CONFIG_FILE)) + if not protocol_configuration_filepath.exists(): + # then check in registry + registry_path = AEA_DIR + protocol_configuration_filepath = Path(os.path.join(registry_path, "protocols", protocol_name, DEFAULT_PROTOCOL_CONFIG_FILE)) + if not protocol_configuration_filepath.exists(): + logger.error("Cannot find protocol: '{}'.".format(protocol_name)) + exit(-1) + + # # try to load the connection configuration file + # try: + # connection_configuration = ctx.connection_loader.load(open(str(connection_configuration_filepath))) + # logger.info("Connection supports the following protocols: {}".format(connection_configuration.supported_protocols)) + # except ValidationError as e: + # logger.error("Connection configuration file not valid: {}".format(str(e))) + # exit(-1) + # return - # copy the protocol package into the agent's supported protocols. - assert protocols_module_spec.submodule_search_locations is not None, "Submodule search locations is None." - protocols_dir = protocols_module_spec.submodule_search_locations[0] - src = os.path.join(protocols_dir, protocol_name) - dest = os.path.join("protocols", protocol_name) + # copy the connection package into the agent's supported connections. + src = str(Path(os.path.join(registry_path, "protocols", protocol_name)).absolute()) + dest = os.path.join(ctx.cwd, "protocols", protocol_name) logger.info("Copying protocol modules. src={} dst={}".format(src, dest)) - shutil.copytree(src, dest) + try: + shutil.copytree(src, dest) + except Exception as e: + logger.error(str(e)) + exit(-1) # make the 'protocols' folder a Python package. logger.debug("Creating {}".format(os.path.join(agent_name, "protocols", "__init__.py"))) - Path(os.path.join("protocols", "__init__.py")).touch(exist_ok=True) + Path(os.path.join(ctx.cwd, "protocols", "__init__.py")).touch(exist_ok=True) # add the protocol to the configurations. logger.debug("Registering the protocol into {}".format(DEFAULT_AEA_CONFIG_FILE)) ctx.agent_config.protocols.add(protocol_name) - ctx.agent_loader.dump(ctx.agent_config, open(DEFAULT_AEA_CONFIG_FILE, "w")) + ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w")) @add.command() @click.argument('skill_name', type=str, required=True) -@click.argument('dirpath', type=str, required=True) @pass_context -def skill(click_context, skill_name, dirpath): +def skill(click_context, skill_name): """Add a skill to the agent.""" ctx = cast(Context, click_context.obj) agent_name = ctx.agent_config.agent_name - logger.debug("Adding skill {skill_name} to the agent {agent_name}..." - .format(agent_name=agent_name, skill_name=skill_name)) + logger.debug("Adding skill {} to the agent {}...".format(skill_name, agent_name)) # check if we already have a skill with the same name logger.debug("Skills already supported by the agent: {}".format(ctx.agent_config.skills)) if skill_name in ctx.agent_config.skills: logger.error("A skill with name '{}' already exists. Aborting...".format(skill_name)) exit(-1) - return # check that the provided path points to a proper skill directory -> look for skill.yaml file. - skill_configuration_filepath = Path(os.path.join(dirpath, DEFAULT_SKILL_CONFIG_FILE)) + # first check in aea dir + registry_path = ctx.agent_config.registry_path + skill_configuration_filepath = Path(os.path.join(registry_path, "skills", skill_name, DEFAULT_SKILL_CONFIG_FILE)) if not skill_configuration_filepath.exists(): - logger.error("Path '{}' does not exist.".format(skill_configuration_filepath)) - exit(-1) - return + # then check in registry + registry_path = AEA_DIR + skill_configuration_filepath = Path(os.path.join(registry_path, "skills", skill_name, DEFAULT_SKILL_CONFIG_FILE)) + if not skill_configuration_filepath.exists(): + logger.error("Cannot find skill: '{}'.".format(skill_name)) + exit(-1) # try to load the skill configuration file try: @@ -170,21 +185,19 @@ def skill(click_context, skill_name, dirpath): except ValidationError as e: logger.error("Skill configuration file not valid: {}".format(str(e))) exit(-1) - return # copy the skill package into the agent's supported skills. - dirpath = str(Path(dirpath).absolute()) - src = dirpath - dest = os.path.join("skills", skill_name) + src = str(Path(os.path.join(registry_path, "skills", skill_name)).absolute()) + dest = os.path.join(ctx.cwd, "skills", skill_name) logger.info("Copying skill modules. src={} dst={}".format(src, dest)) try: shutil.copytree(src, dest) except Exception as e: - logger.error(e) + logger.error(str(e)) exit(-1) # make the 'skills' folder a Python package. - skills_init_module = os.path.join("skills", "__init__.py") + skills_init_module = os.path.join(ctx.cwd, "skills", "__init__.py") logger.debug("Creating {}".format(skills_init_module)) Path(skills_init_module).touch(exist_ok=True) @@ -196,4 +209,4 @@ def skill(click_context, skill_name, dirpath): # add the skill to the configurations. logger.debug("Registering the skill into {}".format(DEFAULT_AEA_CONFIG_FILE)) ctx.agent_config.skills.add(skill_name) - ctx.agent_loader.dump(ctx.agent_config, open(DEFAULT_AEA_CONFIG_FILE, "w")) + ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w")) diff --git a/aea/cli/common.py b/aea/cli/common.py index 8316d1f279..e61dd074fe 100644 --- a/aea/cli/common.py +++ b/aea/cli/common.py @@ -19,8 +19,11 @@ """Implementation of the common utils of the aea cli.""" +import importlib.util import logging +import os from pathlib import Path +import sys from typing import Dict import click @@ -76,5 +79,25 @@ def _try_to_load_agent_config(ctx: Context): "Aborting...".format(DEFAULT_AEA_CONFIG_FILE)) +def _try_to_load_protocols(ctx: Context): + try: + for protocol_name in ctx.agent_config.protocols: + logger.debug("Processing protocol {}".format(protocol_name)) + # protocol_config = ctx.protocol_loader.load(open(os.path.join(directory, DEFAULT_PROTOCOL_CONFIG_FILE))) + # if protocol_config is None: + # exit(-1) + + protocol_spec = importlib.util.spec_from_file_location(protocol_name, os.path.join(ctx.agent_config.registry_path, "protocols", protocol_name, "__init__.py")) + if protocol_spec is None: + logger.warning("Protocol not found in registry.") + continue + + protocol_module = importlib.util.module_from_spec(protocol_spec) + sys.modules[protocol_spec.name + "_protocol"] = protocol_module + except FileNotFoundError: + logger.error("Protocols not found in registry") + exit(-1) + + class AEAConfigException(Exception): """Exception about AEA configuration.""" diff --git a/aea/cli/remove.py b/aea/cli/remove.py index 8f97b696d3..1e126242ff 100644 --- a/aea/cli/remove.py +++ b/aea/cli/remove.py @@ -53,16 +53,13 @@ def connection(ctx: Context, connection_name): shutil.rmtree(connection_folder) except BaseException: logger.exception("An error occurred while deleting '{}'.".format(connection_folder)) - return - - ctx.agent_config.connections.remove(connection_name) + exit(-1) # removing the connection to the configurations. logger.debug("Removing the connection from {}".format(DEFAULT_AEA_CONFIG_FILE)) if connection_name in ctx.agent_config.connections: ctx.agent_config.connections.remove(connection_name) - - ctx.agent_loader.dump(ctx.agent_config, open(DEFAULT_AEA_CONFIG_FILE, "w")) + ctx.agent_loader.dump(ctx.agent_config, open(DEFAULT_AEA_CONFIG_FILE, "w")) @remove.command() @@ -75,7 +72,7 @@ def protocol(ctx: Context, protocol_name): .format(agent_name=agent_name, protocol_name=protocol_name)) if protocol_name not in ctx.agent_config.protocols: - logger.warning("Protocol '{}' not found.".format(protocol_name)) + logger.error("Protocol '{}' not found.".format(protocol_name)) exit(-1) protocol_folder = os.path.join("protocols", protocol_name) @@ -83,13 +80,13 @@ def protocol(ctx: Context, protocol_name): shutil.rmtree(protocol_folder) except BaseException: logger.exception("An error occurred.") - return + exit(-1) # removing the protocol to the configurations. logger.debug("Removing the protocol from {}".format(DEFAULT_AEA_CONFIG_FILE)) if protocol_name in ctx.agent_config.protocols: ctx.agent_config.protocols.remove(protocol_name) - ctx.agent_loader.dump(ctx.agent_config, open(DEFAULT_AEA_CONFIG_FILE, "w")) + ctx.agent_loader.dump(ctx.agent_config, open(DEFAULT_AEA_CONFIG_FILE, "w")) @remove.command() @@ -102,7 +99,7 @@ def skill(ctx: Context, skill_name): .format(agent_name=agent_name, skill_name=skill_name)) if skill_name not in ctx.agent_config.skills: - logger.warning("The skill '{}' is not supported.".format(skill_name)) + logger.error("The skill '{}' is not supported.".format(skill_name)) exit(-1) skill_folder = os.path.join("skills", skill_name) @@ -110,10 +107,10 @@ def skill(ctx: Context, skill_name): shutil.rmtree(skill_folder) except BaseException: logger.exception("An error occurred.") - return + exit(-1) # removing the protocol to the configurations. logger.debug("Removing the skill from {}".format(DEFAULT_AEA_CONFIG_FILE)) if skill_name in ctx.agent_config.skills: ctx.agent_config.skills.remove(skill_name) - ctx.agent_loader.dump(ctx.agent_config, open(DEFAULT_AEA_CONFIG_FILE, "w")) + ctx.agent_loader.dump(ctx.agent_config, open(DEFAULT_AEA_CONFIG_FILE, "w")) diff --git a/aea/cli/run.py b/aea/cli/run.py index 2aed65cbbf..34cabcba73 100644 --- a/aea/cli/run.py +++ b/aea/cli/run.py @@ -28,11 +28,12 @@ from typing import cast from aea.aea import AEA -from aea.cli.common import Context, pass_ctx, logger, _try_to_load_agent_config, AEAConfigException +from aea.cli.common import Context, pass_ctx, logger, _try_to_load_agent_config, _try_to_load_protocols, AEAConfigException +from aea.connections.base import Connection from aea.crypto.base import Crypto from aea.crypto.helpers import _try_validate_private_key_pem_path, _create_temporary_private_key_pem_path # from aea.helpers.base import locate -from aea.mail.base import MailBox, Connection +from aea.mail.base import MailBox def _setup_connection(connection_name: str, public_key: str, ctx: Context) -> Connection: @@ -89,6 +90,7 @@ def run(ctx: Context, connection_name: str): crypto = Crypto(private_key_pem_path=private_key_pem_path) public_key = crypto.public_key connection_name = ctx.agent_config.default_connection if connection_name is None else connection_name + _try_to_load_protocols(ctx) try: connection = _setup_connection(connection_name, public_key, ctx) except AEAConfigException as e: diff --git a/aea/cli/scaffold.py b/aea/cli/scaffold.py new file mode 100644 index 0000000000..1c4d57716c --- /dev/null +++ b/aea/cli/scaffold.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""Implementation of the 'aea add' subcommand.""" + +import os +import shutil +from pathlib import Path + +import click +from jsonschema import ValidationError + +from aea import AEA_DIR +from aea.cli.common import Context, pass_ctx, logger, _try_to_load_agent_config +from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE + + +@click.group() +@pass_ctx +def scaffold(ctx: Context): + """Scaffold a resource for the agent.""" + _try_to_load_agent_config(ctx) + + +@scaffold.command() +@click.argument('connection_name', type=str, required=True) +@pass_ctx +def connection(ctx: Context, connection_name: str) -> None: + """Add a connection scaffolding to the configuration file and agent.""" + # check if we already have a connection with the same name + logger.debug("Connections already supported by the agent: {}".format(ctx.agent_config.connections)) + if connection_name in ctx.agent_config.connections: + logger.error("A connection with name '{}' already exists. Aborting...".format(connection_name)) + exit(-1) + return + + try: + # create the 'connections' folder if it doesn't exist: + if not os.path.exists("connections"): + os.makedirs("connections") + + # create the connection folder + dest = Path(os.path.join("connections", connection_name)) + + # copy the skill package into the agent's supported skills. + src = Path(os.path.join(AEA_DIR, "connections", "scaffold")) + logger.info("Copying connection modules. src={} dst={}".format(src, dest)) + try: + shutil.copytree(src, dest) + except Exception as e: + logger.error(e) + exit(-1) + + # add the connection to the configurations. + logger.info("Registering the connection into {}".format(DEFAULT_AEA_CONFIG_FILE)) + ctx.agent_config.connections.add(connection_name) + ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w")) + + except OSError: + logger.error("Directory already exist. Aborting...") + exit(-1) + except ValidationError as e: + logger.error(str(e)) + shutil.rmtree(connection_name, ignore_errors=True) + exit(-1) + except Exception as e: + logger.exception(e) + shutil.rmtree(connection_name, ignore_errors=True) + exit(-1) + + +@scaffold.command() +@click.argument('protocol_name', type=str, required=True) +@pass_ctx +def protocol(ctx: Context, protocol_name: str): + """Add a protocol scaffolding to the configuration file and agent.""" + # check if we already have a protocol with the same name + logger.debug("Protocols already supported by the agent: {}".format(ctx.agent_config.protocols)) + if protocol_name in ctx.agent_config.protocols: + logger.error("A protocol with name '{}' already exists. Aborting...".format(protocol_name)) + exit(-1) + return + + try: + # create the 'protocols' folder if it doesn't exist: + if not os.path.exists("protocols"): + os.makedirs("protocols") + + # create the protocol folder + dest = Path(os.path.join("protocols", protocol_name)) + + # copy the skill package into the agent's supported skills. + src = Path(os.path.join(AEA_DIR, "protocols", "scaffold")) + logger.info("Copying protocol modules. src={} dst={}".format(src, dest)) + try: + shutil.copytree(src, dest) + except Exception as e: + logger.error(e) + exit(-1) + + # add the protocol to the configurations. + logger.info("Registering the protocol into {}".format(DEFAULT_AEA_CONFIG_FILE)) + ctx.agent_config.protocols.add(protocol_name) + ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w")) + + except OSError: + logger.error("Directory already exist. Aborting...") + exit(-1) + except ValidationError as e: + logger.error(str(e)) + shutil.rmtree(protocol_name, ignore_errors=True) + exit(-1) + except Exception as e: + logger.exception(e) + shutil.rmtree(protocol_name, ignore_errors=True) + exit(-1) + + +@scaffold.command() +@click.argument('skill_name', type=str, required=True) +@pass_ctx +def skill(ctx: Context, skill_name: str): + """Add a skill scaffolding to the configuration file and agent.""" + # check if we already have a skill with the same name + logger.debug("Skills already supported by the agent: {}".format(ctx.agent_config.skills)) + if skill_name in ctx.agent_config.skills: + logger.error("A skill with name '{}' already exists. Aborting...".format(skill_name)) + exit(-1) + return + + try: + # create the 'skills' folder if it doesn't exist: + if not os.path.exists("skills"): + os.makedirs("skills") + + # create the skill folder + dest = Path(os.path.join("skills", skill_name)) + + # copy the skill package into the agent's supported skills. + src = Path(os.path.join(AEA_DIR, "skills", "scaffold")) + logger.info("Copying skill modules. src={} dst={}".format(src, dest)) + try: + shutil.copytree(src, dest) + except Exception as e: + logger.error(e) + exit(-1) + + # add the skill to the configurations. + logger.info("Registering the protocol into {}".format(DEFAULT_AEA_CONFIG_FILE)) + ctx.agent_config.skills.add(skill_name) + ctx.agent_loader.dump(ctx.agent_config, open(os.path.join(ctx.cwd, DEFAULT_AEA_CONFIG_FILE), "w")) + + except OSError: + logger.error("Directory already exist. Aborting...") + exit(-1) + except ValidationError as e: + logger.error(str(e)) + shutil.rmtree(skill_name, ignore_errors=True) + exit(-1) + except Exception as e: + logger.exception(e) + shutil.rmtree(skill_name, ignore_errors=True) + exit(-1) diff --git a/aea/configurations/base.py b/aea/configurations/base.py index 3d14357889..3c15d896f9 100644 --- a/aea/configurations/base.py +++ b/aea/configurations/base.py @@ -23,10 +23,14 @@ from typing import TypeVar, Generic, Optional, List, Tuple, Dict, Set, cast DEFAULT_AEA_CONFIG_FILE = "aea-config.yaml" +DEFAULT_SKILL_CONFIG_FILE = "skill.yaml" +DEFAULT_CONNECTION_CONFIG_FILE = 'connection.yaml' +DEFAULT_PROTOCOL_CONFIG_FILE = 'protocol.yaml' T = TypeVar('T') Address = str ProtocolId = str +SkillId = str class JSONSerializable(ABC): @@ -106,7 +110,7 @@ def __init__(self, license: str = "", url: str = "", class_name: str = "", - supported_protocols: Optional[List[ProtocolId]] = None, + supported_protocols: Optional[List[str]] = None, **config): """Initialize a connection configuration object.""" self.name = name @@ -128,7 +132,7 @@ def json(self) -> Dict: "license": self.license, "url": self.url, "class_name": self.class_name, - "supported_protocol": self.supported_protocols, + "supported_protocols": self.supported_protocols, "config": self.config } @@ -304,6 +308,7 @@ def __init__(self, version: str = "", license: str = "", url: str = "", + registry_path: str = "", private_key_pem_path: str = ""): """Instantiate the agent configuration object.""" self.agent_name = agent_name @@ -312,6 +317,7 @@ def __init__(self, self.version = version self.license = license self.url = url + self.registry_path = registry_path self.private_key_pem_path = private_key_pem_path self._default_connection = None # type: Optional[str] self.connections = set() # type: Set[str] @@ -344,6 +350,7 @@ def json(self) -> Dict: "version": self.version, "license": self.license, "url": self.url, + "registry_path": self.registry_path, "private_key_pem_path": self.private_key_pem_path, "default_connection": self.default_connection, "connections": sorted(self.connections), @@ -361,6 +368,7 @@ def from_json(cls, obj: Dict): version=cast(str, obj.get("version")), license=cast(str, obj.get("license")), url=cast(str, obj.get("url")), + registry_path=cast(str, obj.get("registry_path")), private_key_pem_path=cast(str, obj.get("private_key_pem_path")), ) diff --git a/aea/configurations/schemas/aea-config_schema.json b/aea/configurations/schemas/aea-config_schema.json index 0a35522f22..da1b8aa897 100644 --- a/aea/configurations/schemas/aea-config_schema.json +++ b/aea/configurations/schemas/aea-config_schema.json @@ -11,6 +11,7 @@ "version": {"type": "string"}, "license": {"type": "string"}, "url": {"type": "string"}, + "registry_path": {"type": "string"}, "private_key_pem_path": {"type": "string"}, "connections": { "type": "array", diff --git a/aea/channels/__init__.py b/aea/connections/__init__.py similarity index 100% rename from aea/channels/__init__.py rename to aea/connections/__init__.py diff --git a/aea/connections/base.py b/aea/connections/base.py new file mode 100644 index 0000000000..541eb8810d --- /dev/null +++ b/aea/connections/base.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""The base connection package.""" + +from abc import abstractmethod, ABC +from queue import Queue +from typing import Optional, TYPE_CHECKING + +from aea.configurations.base import ConnectionConfig +if TYPE_CHECKING: + from aea.mail.base import Envelope + + +class Channel(ABC): + """Abstract definition of a channel.""" + + @abstractmethod + def connect(self) -> Optional[Queue]: + """ + Set up the connection. + + :return: A queue or None. + """ + + @abstractmethod + def disconnect(self) -> None: + """ + Tear down the connection. + + :return: None. + """ + + @abstractmethod + def send(self, envelope: 'Envelope') -> None: + """ + Send an envelope. + + :param envelope: the envelope to send. + :return: None. + """ + + +class Connection(ABC): + """Abstract definition of a connection.""" + + channel: Channel + + def __init__(self): + """Initialize the connection.""" + self.in_queue = Queue() + self.out_queue = Queue() + + @abstractmethod + def connect(self): + """Set up the connection.""" + + @abstractmethod + def disconnect(self): + """Tear down the connection.""" + + @property + @abstractmethod + def is_established(self) -> bool: + """Check if the connection is established.""" + + @abstractmethod + def send(self, envelope: 'Envelope'): + """Send a message.""" + + @classmethod + @abstractmethod + def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': + """ + Initialize a connection instance from a configuration. + + :param public_key: the public key of the agent. + :param connection_configuration: the connection configuration. + :return: an instance of the concrete connection class. + """ diff --git a/aea/channels/local/__init__.py b/aea/connections/local/__init__.py similarity index 100% rename from aea/channels/local/__init__.py rename to aea/connections/local/__init__.py diff --git a/aea/channels/local/connection.py b/aea/connections/local/connection.py similarity index 99% rename from aea/channels/local/connection.py rename to aea/connections/local/connection.py index 71366d2bed..2b59091788 100644 --- a/aea/channels/local/connection.py +++ b/aea/connections/local/connection.py @@ -27,11 +27,12 @@ from threading import Thread from typing import Dict, List, Optional, cast -from aea.mail.base import Envelope, Channel, Connection +from aea.configurations.base import ConnectionConfig +from aea.connections.base import Channel, Connection +from aea.mail.base import Envelope from aea.protocols.oef.message import OEFMessage from aea.protocols.oef.models import Description, Query from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF -from aea.configurations.base import ConnectionConfig logger = logging.getLogger(__name__) diff --git a/aea/channels/local/connection.yaml b/aea/connections/local/connection.yaml similarity index 100% rename from aea/channels/local/connection.yaml rename to aea/connections/local/connection.yaml diff --git a/aea/channels/oef/__init__.py b/aea/connections/oef/__init__.py similarity index 100% rename from aea/channels/oef/__init__.py rename to aea/connections/oef/__init__.py diff --git a/aea/channels/oef/connection.py b/aea/connections/oef/connection.py similarity index 99% rename from aea/channels/oef/connection.py rename to aea/connections/oef/connection.py index 543a066f43..5ea6aeef12 100644 --- a/aea/channels/oef/connection.py +++ b/aea/connections/oef/connection.py @@ -40,13 +40,14 @@ ) from oef.schema import Description as OEFDescription, DataModel as OEFDataModel, AttributeSchema as OEFAttribute -from aea.mail.base import Channel, Connection, MailBox, Envelope +from aea.configurations.base import ConnectionConfig +from aea.connections.base import Channel, Connection +from aea.mail.base import MailBox, Envelope from aea.protocols.fipa.message import FIPAMessage from aea.protocols.fipa.serialization import FIPASerializer from aea.protocols.oef.message import OEFMessage from aea.protocols.oef.models import Description, Attribute, DataModel, Query, ConstraintExpr, And, Or, Not, Constraint from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF -from aea.configurations.base import ConnectionConfig logger = logging.getLogger(__name__) diff --git a/aea/channels/oef/connection.yaml b/aea/connections/oef/connection.yaml similarity index 100% rename from aea/channels/oef/connection.yaml rename to aea/connections/oef/connection.yaml diff --git a/aea/connections/scaffold/__init__.py b/aea/connections/scaffold/__init__.py new file mode 100644 index 0000000000..daeb757378 --- /dev/null +++ b/aea/connections/scaffold/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""Scaffold of a connection.""" diff --git a/aea/connections/scaffold/connection.py b/aea/connections/scaffold/connection.py new file mode 100644 index 0000000000..81f0ec0752 --- /dev/null +++ b/aea/connections/scaffold/connection.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""Scaffold connection and channel.""" + +import logging +from queue import Queue +from typing import Optional + +from aea.configurations.base import ConnectionConfig +from aea.connections.base import Channel, Connection +from aea.mail.base import Envelope + +logger = logging.getLogger(__name__) + + +class MyScaffoldChannel(Channel): + """A wrapper for an SDK or API.""" + + def __init__(self, public_key: str): + """ + Initialize a channel. + + :param public_key: the public key + """ + self.public_key = public_key + + def connect(self) -> Optional[Queue]: + """ + Connect. + + :return: an asynchronous queue, that constitutes the communication channel. + """ + raise NotImplementedError + + def send(self, envelope: Envelope) -> None: + """ + Process the envelopes. + + :param envelope: the envelope + :return: None + """ + raise NotImplementedError + + def disconnect(self) -> None: + """ + Disconnect. + + :return: None + """ + raise NotImplementedError + + +class MyScaffoldConnection(Connection): + """Proxy to the functionality of the SDK or API.""" + + def __init__(self, public_key: str): + """ + Initialize a connection to an SDK or API. + + :param public_key: the public key used in the protocols. + """ + super().__init__() + self.public_key = public_key + self.channel = MyScaffoldChannel(public_key) + + @property + def is_established(self) -> bool: + """Return True if the connection has been established, False otherwise.""" + raise NotImplementedError + + def connect(self) -> None: + """ + Connect to the gym. + + :return: None + """ + raise NotImplementedError + + def disconnect(self) -> None: + """ + Disconnect from the gym. + + :return: None + """ + raise NotImplementedError + + def send(self, envelope: Envelope) -> None: + """ + Send an envelope. + + :param envelope: the envelop + :return: None + """ + raise NotImplementedError + + @classmethod + def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': + """ + Get the Gym connection from the connection configuration. + + :param public_key: the public key of the agent. + :param connection_configuration: the connection configuration object. + :return: the connection object + """ + raise NotImplementedError diff --git a/aea/connections/scaffold/connection.yaml b/aea/connections/scaffold/connection.yaml new file mode 100644 index 0000000000..44b5fab177 --- /dev/null +++ b/aea/connections/scaffold/connection.yaml @@ -0,0 +1,9 @@ +name: scaffold +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +class_name: MyScaffoldConnection +supported_protocols: [] +config: + foo: bar diff --git a/aea/helpers/dialogue/base.py b/aea/helpers/dialogue/base.py index 747d244a5e..7b0409b78b 100644 --- a/aea/helpers/dialogue/base.py +++ b/aea/helpers/dialogue/base.py @@ -29,7 +29,7 @@ from abc import abstractmethod from typing import Dict, List -from aea.protocols.base.message import Message +from aea.protocols.base import Message class DialogueLabel: diff --git a/aea/mail/base.py b/aea/mail/base.py index d5e83ed7d0..6d861e9914 100644 --- a/aea/mail/base.py +++ b/aea/mail/base.py @@ -21,12 +21,13 @@ """Mail module abstract base classes.""" import logging -from abc import abstractmethod, ABC from queue import Queue -from typing import Optional +from typing import Optional, TYPE_CHECKING +from aea.configurations.base import Address, ProtocolId from aea.mail import base_pb2 -from aea.configurations.base import ConnectionConfig, Address, ProtocolId +if TYPE_CHECKING: + from aea.connections.base import Connection logger = logging.getLogger(__name__) @@ -50,12 +51,6 @@ def __init__(self, to: Address, self._sender = sender self._protocol_id = protocol_id self._message = message - assert type(self._to) == str or self._to is None - try: - if self._to is not None and type(self._to) == str: - self._to.encode('utf-8') - except Exception: - assert False @property def to(self) -> Address: @@ -137,7 +132,8 @@ def decode(cls, envelope_bytes: bytes) -> 'Envelope': protocol_id = envelope_pb.protocol_id message = envelope_pb.message - envelope = Envelope(to=to, sender=sender, protocol_id=protocol_id, message=message) + envelope = Envelope(to=to, sender=sender, + protocol_id=protocol_id, message=message) return envelope @@ -155,7 +151,7 @@ def __init__(self, queue: Queue): def empty(self) -> bool: """ - Check for a message on the in queue. + Check for a envelope on the in queue. :return: boolean indicating whether there is a message or not """ @@ -163,28 +159,28 @@ def empty(self) -> bool: def get(self, block: bool = True, timeout: Optional[float] = None) -> Envelope: """ - Check for a message on the in queue. + Check for a envelope on the in queue. :param block: if true makes it blocking. :param timeout: times out the block after timeout seconds. - :return: the message object. + :return: the envelope object. :raises Empty: if the attempt to get a message fails. """ logger.debug("Checks for message from the in queue...") - msg = self._queue.get(block=block, timeout=timeout) + envelope = self._queue.get(block=block, timeout=timeout) logger.debug("Incoming message: to='{}' sender='{}' protocol_id='{}' message='{}'" - .format(msg.to, msg.sender, msg.protocol_id, msg.message)) - return msg + .format(envelope.to, envelope.sender, envelope.protocol_id, envelope.message)) + return envelope def get_nowait(self) -> Optional[Envelope]: """ - Check for a message on the in queue and wait for no time. + Check for a envelope on the in queue and wait for no time. - :return: the message object + :return: the envelope object """ - item = self._queue.get_nowait() - return item + envelope = self._queue.get_nowait() + return envelope class OutBox(object): @@ -201,22 +197,22 @@ def __init__(self, queue: Queue) -> None: def empty(self) -> bool: """ - Check for a message on the out queue. + Check for a envelope on the out queue. - :return: boolean indicating whether there is a message or not + :return: boolean indicating whether there is a envelope or not """ return self._queue.empty() - def put(self, item: Envelope) -> None: + def put(self, envelope: Envelope) -> None: """ - Put an item into the queue. + Put an envelope into the queue. - :param item: the message. + :param envelope: the envelope. :return: None """ - logger.debug("Put a message in the queue: to='{}' sender='{}' protocol_id='{}' message='{}'..." - .format(item.to, item.sender, item.protocol_id, item.message)) - self._queue.put(item) + logger.debug("Put an envelope in the queue: to='{}' sender='{}' protocol_id='{}' message='{}'..." + .format(envelope.to, envelope.sender, envelope.protocol_id, envelope.message)) + self._queue.put(envelope) def put_message(self, to: Address, sender: Address, protocol_id: ProtocolId, message: bytes) -> None: @@ -229,82 +225,15 @@ def put_message(self, to: Address, sender: Address, :param message: the content of the message. :return: None """ - envelope = Envelope(to=to, sender=sender, protocol_id=protocol_id, message=message) + envelope = Envelope(to=to, sender=sender, + protocol_id=protocol_id, message=message) self._queue.put(envelope) -class Channel(ABC): - """Abstract definition of a channel.""" - - @abstractmethod - def connect(self) -> Optional[Queue]: - """ - Set up the connection. - - :return: A queue or None. - """ - - @abstractmethod - def disconnect(self) -> None: - """ - Tear down the connection. - - :return: None. - """ - - @abstractmethod - def send(self, envelope: Envelope) -> None: - """ - Send an envelope. - - :param envelope: the envelope to send. - :return: None. - """ - - -class Connection(ABC): - """Abstract definition of a connection.""" - - channel: Channel - - def __init__(self): - """Initialize the connection.""" - self.in_queue = Queue() - self.out_queue = Queue() - - @abstractmethod - def connect(self): - """Set up the connection.""" - - @abstractmethod - def disconnect(self): - """Tear down the connection.""" - - @property - @abstractmethod - def is_established(self) -> bool: - """Check if the connection is established.""" - - @abstractmethod - def send(self, envelope: Envelope): - """Send a message.""" - - @classmethod - @abstractmethod - def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': - """ - Initialize a connection instance from a configuration. - - :param public_key: the public key of the agent. - :param connection_configuration: the connection configuration. - :return: an instance of the concrete connection class. - """ - - class MailBox(object): """Abstract definition of a mailbox.""" - def __init__(self, connection: Connection): + def __init__(self, connection: 'Connection'): """Initialize the mailbox.""" self._connection = connection diff --git a/aea/protocols/base/serialization.py b/aea/protocols/base.py similarity index 50% rename from aea/protocols/base/serialization.py rename to aea/protocols/base.py index e5605a38aa..d01eb812b2 100644 --- a/aea/protocols/base/serialization.py +++ b/aea/protocols/base.py @@ -18,13 +18,79 @@ # # ------------------------------------------------------------------------------ -"""Serializer for base.""" -import json +"""This module contains the base message and serialization definition.""" + from abc import abstractmethod, ABC +from copy import copy +import json +from typing import Any, Dict, Optional from google.protobuf.struct_pb2 import Struct -from aea.protocols.base.message import Message + +class Message: + """This class implements a message.""" + + def __init__(self, body: Optional[Dict] = None, + **kwargs): + """ + Initialize a Message object. + + :param body: the dictionary of values to hold. + :param kwargs: any additional value to add to the body. It will overwrite the body values. + """ + self._body = copy(body) if body else {} # type: Dict[str, Any] + self._body.update(kwargs) + + @property + def body(self) -> Dict: + """ + Get the body of the message (in dictionary form). + + :return: the body + """ + return self._body + + @body.setter + def body(self, body: Dict) -> None: + """ + Set the body of hte message. + + :param body: the body. + :return: None + """ + self._body = body + + def set(self, key: str, value: Any) -> None: + """ + Set key and value pair. + + :param key: the key. + :param value: the value. + :return: None + """ + self._body[key] = value + + def get(self, key: str) -> Optional[Any]: + """Get value for key.""" + return self._body.get(key, None) + + def unset(self, key: str) -> None: + """Unset valye for key.""" + self._body.pop(key, None) + + def is_set(self, key: str) -> bool: + """Check value is set for key.""" + return key in self._body + + def check_consistency(self) -> bool: + """Check that the data is consistent.""" + return True + + def __eq__(self, other): + """Compare with another object.""" + return isinstance(other, Message) \ + and self.body == other.body class Encoder(ABC): @@ -107,3 +173,43 @@ def decode(self, obj: bytes) -> Message: """ json_msg = json.loads(obj.decode("utf-8")) return Message(json_msg) + + +class Protocol(ABC): + """ + This class implements a specifications for a protocol. + + It includes: + - a serializer, to encode/decode a message. + - a 'check' abstract method (to be implemented) to check if a message is allowed for the protocol. + """ + + def __init__(self, id: str, serializer: Serializer): + """ + Initialize the protocol manager. + + :param id: the protocol id. + :param serializer: the serializer. + """ + self._id = id + self._serializer = serializer + + @property + def id(self): + """Get the name.""" + return self._id + + @property + def serializer(self) -> Serializer: + """Get the serializer.""" + return self._serializer + + def check(self, msg: Message) -> bool: + """ + Check whether the message belongs to the allowed messages. + + :param msg: the message. + :return: True if the message is valid wrt the protocol, False otherwise. + """ + # TODO 'check' should check the message against the protocol rules, if such rules are provided. + return True diff --git a/aea/protocols/base/message.py b/aea/protocols/base/message.py deleted file mode 100644 index b66db24f79..0000000000 --- a/aea/protocols/base/message.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# 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. -# -# ------------------------------------------------------------------------------ - -"""This module contains the base message definition.""" -from copy import copy -from typing import Any, Dict, Optional - - -class Message: - """This class implements a message.""" - - def __init__(self, body: Optional[Dict] = None, - **kwargs): - """ - Initialize a Message object. - - :param body: the dictionary of values to hold. - :param kwargs: any additional value to add to the body. It will overwrite the body values. - """ - self._body = copy(body) if body else {} # type: Dict[str, Any] - self._body.update(kwargs) - - @property - def body(self) -> Dict: - """ - Get the body of the message (in dictionary form). - - :return: the body - """ - return self._body - - @body.setter - def body(self, body: Dict) -> None: - """ - Set the body of hte message. - - :param body: the body. - :return: None - """ - self._body = body - - def set(self, key: str, value: Any) -> None: - """ - Set key and value pair. - - :param key: the key. - :param value: the value. - :return: None - """ - self._body[key] = value - - def get(self, key: str) -> Optional[Any]: - """Get value for key.""" - return self._body.get(key, None) - - def unset(self, key: str) -> None: - """Unset valye for key.""" - self._body.pop(key, None) - - def is_set(self, key: str) -> bool: - """Check value is set for key.""" - return key in self._body - - def check_consistency(self) -> bool: - """Check that the data is consistent.""" - return True - - def __eq__(self, other): - """Compare with another object.""" - return isinstance(other, Message) \ - and self.body == other.body diff --git a/aea/protocols/base/protocol.py b/aea/protocols/base/protocol.py deleted file mode 100644 index 5554405506..0000000000 --- a/aea/protocols/base/protocol.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# 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. -# -# ------------------------------------------------------------------------------ - -"""This module contains the implementation of a protocol manager.""" -from abc import ABC - -from aea.protocols.base.message import Message -from aea.protocols.base.serialization import Serializer - - -class Protocol(ABC): - """ - This class implements a specifications for a protocol. - - It includes: - - a serializer, to encode/decode a message. - - a 'check' abstract method (to be implemented) to check if a message is allowed for the protocol. - """ - - def __init__(self, id: str, serializer: Serializer): - """ - Initialize the protocol manager. - - :param id: the protocol id. - :param serializer: the serializer. - """ - self._id = id - self._serializer = serializer - - @property - def id(self): - """Get the name.""" - return self._id - - @property - def serializer(self) -> Serializer: - """Get the serializer.""" - return self._serializer - - def check(self, msg: Message) -> bool: - """ - Check whether the message belongs to the allowed messages. - - :param msg: the message. - :return: True if the message is valid wrt the protocol, False otherwise. - """ - # TODO 'check' should be an abstract method, and every protocol should provide a concrete Protocol class that implements it. - return True diff --git a/aea/protocols/default/message.py b/aea/protocols/default/message.py index a04b7f5c23..475714a2f0 100644 --- a/aea/protocols/default/message.py +++ b/aea/protocols/default/message.py @@ -22,7 +22,7 @@ from enum import Enum from typing import Optional -from aea.protocols.base.message import Message +from aea.protocols.base import Message class DefaultMessage(Message): @@ -56,3 +56,4 @@ def __init__(self, type: Optional[Type] = None, :param type: the type. """ super().__init__(type=type, **kwargs) + assert self.check_consistency(), "DefaultMessage initialization inconsistent." diff --git a/aea/protocols/default/protocol.yaml b/aea/protocols/default/protocol.yaml new file mode 100644 index 0000000000..6e9fd1dc97 --- /dev/null +++ b/aea/protocols/default/protocol.yaml @@ -0,0 +1,5 @@ +name: 'default' +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" diff --git a/aea/protocols/default/serialization.py b/aea/protocols/default/serialization.py index 3656f1e419..080b8f386b 100644 --- a/aea/protocols/default/serialization.py +++ b/aea/protocols/default/serialization.py @@ -23,8 +23,8 @@ import json from typing import cast -from aea.protocols.base.message import Message -from aea.protocols.base.serialization import Serializer +from aea.protocols.base import Message +from aea.protocols.base import Serializer from aea.protocols.default.message import DefaultMessage diff --git a/aea/protocols/fipa/message.py b/aea/protocols/fipa/message.py index f2d46373c6..729d8deba5 100644 --- a/aea/protocols/fipa/message.py +++ b/aea/protocols/fipa/message.py @@ -22,7 +22,7 @@ from enum import Enum from typing import Optional, Union -from aea.protocols.base.message import Message +from aea.protocols.base import Message from aea.protocols.oef.models import Description @@ -62,6 +62,7 @@ def __init__(self, message_id: Optional[int] = None, target=target, performative=FIPAMessage.Performative(performative), **kwargs) + assert self.check_consistency(), "FIPAMessage initialization inconsistent." def check_consistency(self) -> bool: """Check that the data is consistent.""" diff --git a/aea/protocols/fipa/protocol.yaml b/aea/protocols/fipa/protocol.yaml new file mode 100644 index 0000000000..8719746ed9 --- /dev/null +++ b/aea/protocols/fipa/protocol.yaml @@ -0,0 +1,5 @@ +name: 'fipa' +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" diff --git a/aea/protocols/fipa/serialization.py b/aea/protocols/fipa/serialization.py index 1b3be0e1ae..65fe2b56a9 100644 --- a/aea/protocols/fipa/serialization.py +++ b/aea/protocols/fipa/serialization.py @@ -22,8 +22,8 @@ import pickle from typing import cast -from aea.protocols.base.message import Message -from aea.protocols.base.serialization import Serializer +from aea.protocols.base import Message +from aea.protocols.base import Serializer from aea.protocols.fipa import fipa_pb2 from aea.protocols.fipa.message import FIPAMessage from aea.protocols.oef.models import Description diff --git a/aea/protocols/oef/message.py b/aea/protocols/oef/message.py index 7132348757..91982cd114 100644 --- a/aea/protocols/oef/message.py +++ b/aea/protocols/oef/message.py @@ -22,7 +22,7 @@ from enum import Enum from typing import Optional, List, cast -from aea.protocols.base.message import Message +from aea.protocols.base import Message from aea.protocols.oef.models import Description, Query @@ -66,6 +66,7 @@ def __init__(self, oef_type: Optional[Type] = None, :param oef_type: the type of OEF message. """ super().__init__(type=oef_type, **kwargs) + assert self.check_consistency(), "OEFMessage initialization inconsistent." def check_consistency(self) -> bool: """Check that the data is consistent.""" diff --git a/aea/protocols/oef/protocol.yaml b/aea/protocols/oef/protocol.yaml new file mode 100644 index 0000000000..8719746ed9 --- /dev/null +++ b/aea/protocols/oef/protocol.yaml @@ -0,0 +1,5 @@ +name: 'fipa' +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" diff --git a/aea/protocols/oef/serialization.py b/aea/protocols/oef/serialization.py index c512d74706..fafeaed44d 100644 --- a/aea/protocols/oef/serialization.py +++ b/aea/protocols/oef/serialization.py @@ -24,8 +24,8 @@ import json import pickle -from aea.protocols.base.message import Message -from aea.protocols.base.serialization import Serializer +from aea.protocols.base import Message +from aea.protocols.base import Serializer from aea.protocols.oef.message import OEFMessage from aea.protocols.oef.models import Description, Query diff --git a/aea/protocols/scaffold/__init__.py b/aea/protocols/scaffold/__init__.py new file mode 100644 index 0000000000..430a610c95 --- /dev/null +++ b/aea/protocols/scaffold/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the scaffold protocol.""" diff --git a/aea/protocols/scaffold/message.py b/aea/protocols/scaffold/message.py new file mode 100644 index 0000000000..1071213cab --- /dev/null +++ b/aea/protocols/scaffold/message.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the scaffold message definition.""" + +from enum import Enum +from typing import Optional + +from aea.protocols.base import Message + + +class MyScaffoldMessage(Message): + """The scaffold message class.""" + + protocol_id = "my_scaffold_protocol" + + class Type(Enum): + """Scaffold Message types.""" + + def __str__(self): + """Get string representation.""" + return self.value + + def __init__(self, oef_type: Optional[Type] = None, + **kwargs): + """ + Initialize. + + :param oef_type: the type of message. + """ + super().__init__(type=oef_type, **kwargs) + assert self.check_consistency(), "MyScaffoldMessage initialization inconsistent." + + def check_consistency(self) -> bool: + """Check that the data is consistent.""" + try: + raise NotImplementedError + except (AssertionError, ValueError): + return False + + return True diff --git a/aea/protocols/scaffold/protocol.yaml b/aea/protocols/scaffold/protocol.yaml new file mode 100644 index 0000000000..8719746ed9 --- /dev/null +++ b/aea/protocols/scaffold/protocol.yaml @@ -0,0 +1,5 @@ +name: 'fipa' +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" diff --git a/aea/protocols/scaffold/serialization.py b/aea/protocols/scaffold/serialization.py new file mode 100644 index 0000000000..21557f22f5 --- /dev/null +++ b/aea/protocols/scaffold/serialization.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""Serialization for the scaffold protocol.""" + + +from aea.protocols.base import Message +from aea.protocols.base import Serializer + + +class MyScaffoldSerializer(Serializer): + """Serialization for the scaffold protocol.""" + + def encode(self, msg: Message) -> bytes: + """ + Decode the message. + + :param msg: the message object + :return: the bytes + """ + raise NotImplementedError + + def decode(self, obj: bytes) -> Message: + """ + Decode the message. + + :param obj: the bytes object + :return: the message + """ + raise NotImplementedError diff --git a/aea/registries/__init__.py b/aea/registries/__init__.py new file mode 100644 index 0000000000..b54ba1c104 --- /dev/null +++ b/aea/registries/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the registries used by the framework.""" diff --git a/aea/registries/base.py b/aea/registries/base.py new file mode 100644 index 0000000000..7d5a2aec30 --- /dev/null +++ b/aea/registries/base.py @@ -0,0 +1,464 @@ +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains registries.""" + +import importlib.util +import inspect +import logging +import os +import pprint +import re +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Optional, List, Dict, Any, Tuple, cast + +from aea.configurations.base import ProtocolId, SkillId +from aea.protocols.base import Protocol +from aea.skills.base import Handler, Behaviour, Task, Skill, AgentContext + +logger = logging.getLogger(__name__) + + +class Registry(ABC): + """This class implements an abstract registry.""" + + @abstractmethod + def register(self, id: Tuple[Any, Any], item: Any) -> None: + """ + Register an item. + + :param id: the identifier of the item. + :param item: the item. + :return: None + """ + + @abstractmethod + def unregister(self, id: Any) -> None: + """ + Unregister an item. + + :param id: the identifier of the item. + :return: None + """ + + @abstractmethod + def fetch(self, id: Any) -> Optional[Any]: + """ + Fetch an item. + + :param id: the identifier of the item. + :return: the Item + """ + + @abstractmethod + def fetch_all(self) -> Optional[List[Any]]: + """ + Fetch all the items. + + :return: the list of items. + """ + + @abstractmethod + def teardown(self) -> None: + """ + Teardown the registry. + + :return: None + """ + + +class ProtocolRegistry(Registry): + """This class implements the handlers registry.""" + + def __init__(self) -> None: + """ + Instantiate the registry. + + :return: None + """ + self._protocols = {} # type: Dict[ProtocolId, Protocol] + + def register(self, ids: Tuple[ProtocolId, None], protocol: Protocol) -> None: + """ + Register a protocol. + + :param ids: the tuple of ids + """ + protocol_id = ids[0] + self._protocols[protocol_id] = protocol + + def unregister(self, protocol_id: ProtocolId) -> None: + """Unregister a protocol.""" + self._protocols.pop(protocol_id, None) + + def fetch(self, protocol_id: ProtocolId) -> Optional[Protocol]: + """ + Fetch the protocol for the envelope. + + :pass protocol_id: the protocol id + :return: the protocol id or None if the protocol is not registered + """ + return self._protocols.get(protocol_id, None) + + def fetch_all(self) -> List[Protocol]: + """Fetch all the protocols.""" + return list(self._protocols.values()) + + def populate(self, directory: str) -> None: + """ + Load the handlers as specified in the config and apply consistency checks. + + :param directory: the filepath to the agent's resource directory. + :return: None + """ + protocols_spec = importlib.util.spec_from_file_location("protocols", + os.path.join(directory, "protocols", "__init__.py")) + path = cast(str, protocols_spec.origin) + if protocols_spec is None or not os.path.exists(path): + logger.warning("No protocol found.") + return + + protocols_packages = list(filter(lambda x: not x.startswith("__"), protocols_spec.loader.contents())) # type: ignore + logger.debug("Processing the following protocol package: {}".format(protocols_packages)) + for protocol_name in protocols_packages: + try: + self._add_protocol(directory, protocol_name) + except Exception: + logger.exception("Not able to add protocol {}.".format(protocol_name)) + + def teardown(self) -> None: + """ + Teardown the registry. + + :return: None + """ + self._protocols = {} + + def _add_protocol(self, directory: str, protocol_name: str): + """ + Add a protocol. + + :param directory: the agent's resources directory. + :param protocol_name: the name of the protocol to be added. + :return: None + """ + # get the serializer + serialization_spec = importlib.util.spec_from_file_location("serialization", + os.path.join(directory, "protocols", protocol_name, "serialization.py")) + serialization_module = importlib.util.module_from_spec(serialization_spec) + serialization_spec.loader.exec_module(serialization_module) # type: ignore + classes = inspect.getmembers(serialization_module, inspect.isclass) + serializer_classes = list(filter(lambda x: re.match("\\w+Serializer", x[0]), classes)) + serializer_class = serializer_classes[0][1] + + logger.debug("Found serializer class {serializer_class} for protocol {protocol_name}" + .format(serializer_class=serializer_class, protocol_name=protocol_name)) + serializer = serializer_class() + + # instantiate the protocol manager. + protocol = Protocol(protocol_name, serializer) + self.register((protocol_name, None), protocol) + + +class HandlerRegistry(Registry): + """This class implements the handlers registry.""" + + def __init__(self) -> None: + """ + Instantiate the registry. + + :return: None + """ + self._handlers = {} # type: Dict[ProtocolId, Dict[SkillId, Handler]] + + def register(self, ids: Tuple[ProtocolId, SkillId], handler: Handler) -> None: + """ + Register a handler. + + :param ids: the pair (protocol id, skill id). + :param handler: the handler. + :return: None + """ + protocol_id, skill_id = ids + if protocol_id in self._handlers.keys(): + logger.info("More than one handler registered against protocol with id '{}'".format(protocol_id)) + if protocol_id not in self._handlers.keys(): + self._handlers[protocol_id] = {} + self._handlers[protocol_id][skill_id] = handler + + def unregister(self, skill_id: SkillId) -> None: + """ + Unregister a handler. + + :param skill_id: the skill id. + :return: None + """ + for protocol_id, skill_to_handler_dict in self._handlers.items(): + if skill_id in skill_to_handler_dict.keys(): + self._handlers[protocol_id].pop(skill_id, None) + if self._handlers[protocol_id] == {}: + self._handlers.pop(protocol_id, None) + + def fetch(self, protocol_id: ProtocolId) -> Optional[List[Handler]]: + """ + Fetch the handlers for the protocol_id. + + :param protocol_id: the protocol id + :return: the list of handlers registered for the protocol_id + """ + result = self._handlers.get(protocol_id, None) + if result is None: + return None + else: + # TODO: introduce a controller class which intelligently selects the appropriate handler. + return list(result.values()) + + def fetch_by_skill(self, protocol_id: ProtocolId, skill_id: SkillId) -> Optional[Handler]: + """ + Fetch the handler for the protocol_id and skill id. + + :param protocol_id: the protocol id + :param skill_id: the skill id + :return: the handler registered for the protocol_id and skill_id + """ + result = self._handlers.get(protocol_id, None) + if result is None: + return None + else: + return result.get(skill_id, None) + + def fetch_all(self) -> Optional[List[Handler]]: + """Fetch all the handlers.""" + if self._handlers.values() is None: + return None + else: + result = [] + for skill_id_to_handler_dict in self._handlers.values(): + result.extend(list(skill_id_to_handler_dict.values())) + return result + + def teardown(self) -> None: + """ + Teardown the registry. + + :return: None + """ + if self._handlers.values() is not None: + for skill_id_to_handler_dict in self._handlers.values(): + for handler in skill_id_to_handler_dict.values(): + handler.teardown() + self._handlers = {} + + +class BehaviourRegistry(Registry): + """This class implements the behaviour registry.""" + + def __init__(self) -> None: + """ + Instantiate the registry. + + :return: None + """ + self._behaviours = {} # type: Dict[SkillId, List[Behaviour]] + + def register(self, ids: Tuple[None, SkillId], behaviours: List[Behaviour]) -> None: + """ + Register a behaviour. + + :param skill_id: the skill id. + :param behaviours: the behaviours of the skill. + :return: None + """ + skill_id = ids[1] + if skill_id in self._behaviours.keys(): + logger.warning("Behaviours already registered with skill id '{}'".format(skill_id)) + self._behaviours[skill_id] = behaviours + + def unregister(self, skill_id: SkillId) -> None: + """ + Unregister a behaviour. + + :param skill_id: the skill id. + :return: None + """ + self._behaviours.pop(skill_id, None) + + def fetch(self, skill_id: SkillId) -> Optional[List[Behaviour]]: + """ + Return a behaviour. + + :return: the list of behaviours + """ + return self._behaviours.get(skill_id, None) + + def fetch_all(self) -> List[Behaviour]: + """Fetch all the behaviours.""" + return [b for skill_behaviours in self._behaviours.values() for b in skill_behaviours] + + def teardown(self) -> None: + """ + Teardown the registry. + + :return: None + """ + for behaviours in self._behaviours.values(): + for behaviour in behaviours: + behaviour.teardown() + self._behaviours = {} + + +class TaskRegistry(Registry): + """This class implements the task registry.""" + + def __init__(self) -> None: + """ + Instantiate the registry. + + :return: None + """ + self._tasks = {} # type: Dict[SkillId, List[Task]] + + def register(self, ids: Tuple[None, SkillId], tasks: List[Task]) -> None: + """ + Register a task. + + :param skill_id: the skill id. + :param tasks: the tasks list. + :return: None + """ + skill_id = ids[1] + if skill_id in self._tasks.keys(): + logger.warning("Tasks already registered with skill id '{}'".format(skill_id)) + self._tasks[skill_id] = tasks + + def unregister(self, skill_id: SkillId) -> None: + """ + Unregister a task. + + :param skill_id: the skill id. + :return: None + """ + self._tasks.pop(skill_id, None) + + def fetch(self, skill_id: SkillId) -> Optional[List[Task]]: + """ + Return a task. + + :return: the list of tasks + """ + return self._tasks.get(skill_id, None) + + def fetch_all(self) -> List[Task]: + """ + Return a list of tasks for processing. + + :return: a list of tasks. + """ + return [t for skill_tasks in self._tasks.values() for t in skill_tasks] + + def teardown(self) -> None: + """ + Teardown the registry. + + :return: None + """ + for tasks in self._tasks.values(): + for task in tasks: + task.teardown() + self._tasks = {} + + +class Resources(object): + """This class implements the resources of an AEA.""" + + def __init__(self): + """Instantiate the resources.""" + self.protocol_registry = ProtocolRegistry() + self.handler_registry = HandlerRegistry() + self.behaviour_registry = BehaviourRegistry() + self.task_registry = TaskRegistry() + self._skills = dict() # type: Dict[SkillId, Skill] + + self._registries = [self.protocol_registry, self.handler_registry, self.behaviour_registry, self.task_registry] + + @classmethod + def from_resource_dir(cls, directory: str, agent_context: AgentContext) -> Optional['Resources']: + """ + Parse the resource directory. + + :param directory: the agent's resources directory. + :param agent_context: the agent's context object + :return: None + """ + resources = Resources() + resources.protocol_registry.populate(directory) + resources.populate_skills(directory, agent_context) + return resources + + def populate_skills(self, directory: str, agent_context: AgentContext) -> None: + """ + Populate skills. + + :param directory: the agent's resources directory. + :param agent_context: the agent's context object + :return: None + """ + root_skill_directory = os.path.join(directory, "skills") + if not os.path.exists(root_skill_directory): + logger.warning("No skill found.") + return + + skill_directories = [str(x) for x in Path(root_skill_directory).iterdir() if x.is_dir()] + logger.debug("Processing the following skill directories: {}".format(pprint.pformat(skill_directories))) + for skill_directory in skill_directories: + try: + skill = Skill.from_dir(skill_directory, agent_context) + assert skill is not None + self.add_skill(skill) + except Exception as e: + logger.warning("A problem occurred while parsing the skill directory {}. Exception: {}" + .format(skill_directory, str(e))) + + def add_skill(self, skill: Skill): + """Add a skill to the set of resources.""" + skill_id = skill.config.name + self._skills[skill_id] = skill + if skill.handler is not None: + protocol_id = skill.config.protocol + self.handler_registry.register((protocol_id, skill_id), cast(Handler, skill.handler)) + if skill.behaviours is not None: + self.behaviour_registry.register((None, skill_id), cast(List[Behaviour], skill.behaviours)) + if skill.tasks is not None: + self.task_registry.register((None, skill_id), cast(List[Task], skill.tasks)) + + def remove_skill(self, skill_id: SkillId): + """Remove a skill from the set of resources.""" + self._skills.pop(skill_id, None) + self.handler_registry.unregister(skill_id) + self.behaviour_registry.unregister(skill_id) + self.task_registry.unregister(skill_id) + + def teardown(self): + """ + Teardown the resources. + + :return: None + """ + for r in self._registries: + r.teardown() diff --git a/aea/skills/base.py b/aea/skills/base.py index f56a44c31a..e5dfe2f680 100644 --- a/aea/skills/base.py +++ b/aea/skills/base.py @@ -22,24 +22,17 @@ import inspect import logging import os -import pprint import re import sys from abc import ABC, abstractmethod -from pathlib import Path -from typing import Optional, List, Dict, Any, Tuple, cast +from typing import Optional, List, Dict, Any, cast -from aea.configurations.base import BehaviourConfig, HandlerConfig, TaskConfig, SkillConfig, ProtocolId +from aea.configurations.base import BehaviourConfig, HandlerConfig, TaskConfig, SkillConfig, ProtocolId, DEFAULT_SKILL_CONFIG_FILE from aea.configurations.loader import ConfigLoader from aea.mail.base import OutBox, Envelope -from aea.protocols.base.protocol import Protocol logger = logging.getLogger(__name__) -DEFAULT_SKILL_CONFIG_FILE = "skill.yaml" -DEFAULT_CONNECTION_CONFIG_FILE = "connection.yaml" -SkillId = str - class AgentContext: """Save relevant data for the agent.""" @@ -395,425 +388,3 @@ def from_dir(cls, directory: str, agent_context: AgentContext) -> Optional['Skil skill_context._skill = skill return skill - - -class Controller: - """This class implements the controller.""" - - def __init__(self): - """Initialize the controller.""" - - -class Registry(ABC): - """This class implements an abstract registry.""" - - @abstractmethod - def register(self, id: Tuple[Any, Any], item: Any) -> None: - """ - Register an item. - - :param id: the identifier of the item. - :param item: the item. - :return: None - """ - - @abstractmethod - def unregister(self, id: Any) -> None: - """ - Unregister an item. - - :param id: the identifier of the item. - :return: None - """ - - @abstractmethod - def fetch(self, id: Any) -> Optional[Any]: - """ - Fetch an item. - - :param id: the identifier of the item. - :return: the Item - """ - - @abstractmethod - def fetch_all(self) -> Optional[List[Any]]: - """ - Fetch all the items. - - :return: the list of items. - """ - - @abstractmethod - def teardown(self) -> None: - """ - Teardown the registry. - - :return: None - """ - - -class ProtocolRegistry(Registry): - """This class implements the handlers registry.""" - - def __init__(self) -> None: - """ - Instantiate the registry. - - :return: None - """ - self._protocols = {} # type: Dict[ProtocolId, Protocol] - - def register(self, ids: Tuple[ProtocolId, None], protocol: Protocol) -> None: - """ - Register a protocol. - - :param ids: the tuple of ids - """ - protocol_id = ids[0] - self._protocols[protocol_id] = protocol - - def unregister(self, protocol_id: ProtocolId) -> None: - """Unregister a protocol.""" - self._protocols.pop(protocol_id, None) - - def fetch(self, protocol_id: ProtocolId) -> Optional[Protocol]: - """ - Fetch the protocol for the envelope. - - :pass protocol_id: the protocol id - :return: the protocol id or None if the protocol is not registered - """ - return self._protocols.get(protocol_id, None) - - def fetch_all(self) -> List[Protocol]: - """Fetch all the protocols.""" - return list(self._protocols.values()) - - def populate(self, directory: str) -> None: - """ - Load the handlers as specified in the config and apply consistency checks. - - :param directory: the filepath to the agent's resource directory. - :return: None - """ - protocols_spec = importlib.util.spec_from_file_location("protocols", - os.path.join(directory, "protocols", "__init__.py")) - path = cast(str, protocols_spec.origin) - if protocols_spec is None or not os.path.exists(path): - logger.warning("No protocol found.") - return - - protocols_packages = list(filter(lambda x: not x.startswith("__"), protocols_spec.loader.contents())) # type: ignore - logger.debug("Processing the following protocol package: {}".format(protocols_packages)) - for protocol_name in protocols_packages: - try: - self._add_protocol(directory, protocol_name) - except Exception: - logger.exception("Not able to add protocol {}.".format(protocol_name)) - - def teardown(self) -> None: - """ - Teardown the registry. - - :return: None - """ - self._protocols = {} - - def _add_protocol(self, directory: str, protocol_name: str): - """ - Add a protocol. - - :param directory: the agent's resources directory. - :param protocol_name: the name of the protocol to be added. - :return: None - """ - # get the serializer - serialization_spec = importlib.util.spec_from_file_location("serialization", - os.path.join(directory, "protocols", protocol_name, "serialization.py")) - serialization_module = importlib.util.module_from_spec(serialization_spec) - serialization_spec.loader.exec_module(serialization_module) # type: ignore - classes = inspect.getmembers(serialization_module, inspect.isclass) - serializer_classes = list(filter(lambda x: re.match("\\w+Serializer", x[0]), classes)) - serializer_class = serializer_classes[0][1] - - logger.debug("Found serializer class {serializer_class} for protocol {protocol_name}" - .format(serializer_class=serializer_class, protocol_name=protocol_name)) - serializer = serializer_class() - - # instantiate the protocol manager. - protocol = Protocol(protocol_name, serializer) - self.register((protocol_name, None), protocol) - - -class HandlerRegistry(Registry): - """This class implements the handlers registry.""" - - def __init__(self) -> None: - """ - Instantiate the registry. - - :return: None - """ - self._handlers = {} # type: Dict[ProtocolId, Dict[SkillId, Handler]] - - def register(self, ids: Tuple[ProtocolId, SkillId], handler: Handler) -> None: - """ - Register a handler. - - :param ids: the pair (protocol id, skill id). - :param handler: the handler. - :return: None - """ - protocol_id, skill_id = ids - if protocol_id in self._handlers.keys(): - logger.warning("Another handler also registered against protocol id '{}'".format(protocol_id)) - if protocol_id not in self._handlers.keys(): - self._handlers[protocol_id] = {} - self._handlers[protocol_id][skill_id] = handler - - def unregister(self, skill_id: SkillId) -> None: - """ - Unregister a handler. - - :param skill_id: the skill id. - :return: None - """ - for protocol_id, skill_to_handler_dict in self._handlers.items(): - if skill_id in skill_to_handler_dict.keys(): - self._handlers[protocol_id].pop(skill_id, None) - if self._handlers[protocol_id] == {}: - self._handlers.pop(protocol_id, None) - - def fetch(self, protocol_id: ProtocolId) -> Optional[List[Handler]]: - """ - Fetch the handlers for the protocol_id. - - :param protocol_id: the protocol id - :return: the list of handlers registered for the protocol_id - """ - result = self._handlers.get(protocol_id, None) - if result is None: - return None - else: - # TODO: introduce a controller class which intelligently selects the appropriate handler. - return list(result.values()) - - def fetch_all(self) -> Optional[List[Handler]]: - """Fetch all the handlers.""" - if self._handlers.values() is None: - return None - else: - result = [] - for skill_id_to_handler_dict in self._handlers.values(): - result.extend(list(skill_id_to_handler_dict.values())) - return result - - def teardown(self) -> None: - """ - Teardown the registry. - - :return: None - """ - if self._handlers.values() is not None: - for skill_id_to_handler_dict in self._handlers.values(): - for handler in skill_id_to_handler_dict.values(): - handler.teardown() - self._handlers = {} - - -class BehaviourRegistry(Registry): - """This class implements the behaviour registry.""" - - def __init__(self) -> None: - """ - Instantiate the registry. - - :return: None - """ - self._behaviours = {} # type: Dict[SkillId, List[Behaviour]] - - def register(self, ids: Tuple[None, SkillId], behaviours: List[Behaviour]) -> None: - """ - Register a behaviour. - - :param skill_id: the skill id. - :param behaviours: the behaviours of the skill. - :return: None - """ - skill_id = ids[1] - if skill_id in self._behaviours.keys(): - logger.warning("Behaviours already registered with skill id '{}'".format(skill_id)) - self._behaviours[skill_id] = behaviours - - def unregister(self, skill_id: SkillId) -> None: - """ - Unregister a behaviour. - - :param skill_id: the skill id. - :return: None - """ - self._behaviours.pop(skill_id, None) - - def fetch(self, skill_id: SkillId) -> Optional[List[Behaviour]]: - """ - Return a behaviour. - - :return: the list of behaviours - """ - return self._behaviours.get(skill_id, None) - - def fetch_all(self) -> List[Behaviour]: - """Fetch all the behaviours.""" - return [b for skill_behaviours in self._behaviours.values() for b in skill_behaviours] - - def teardown(self) -> None: - """ - Teardown the registry. - - :return: None - """ - for behaviours in self._behaviours.values(): - for behaviour in behaviours: - behaviour.teardown() - self._behaviours = {} - - -class TaskRegistry(Registry): - """This class implements the task registry.""" - - def __init__(self) -> None: - """ - Instantiate the registry. - - :return: None - """ - self._tasks = {} # type: Dict[SkillId, List[Task]] - - def register(self, ids: Tuple[None, SkillId], tasks: List[Task]) -> None: - """ - Register a task. - - :param skill_id: the skill id. - :param tasks: the tasks list. - :return: None - """ - skill_id = ids[1] - if skill_id in self._tasks.keys(): - logger.warning("Tasks already registered with skill id '{}'".format(skill_id)) - self._tasks[skill_id] = tasks - - def unregister(self, skill_id: SkillId) -> None: - """ - Unregister a task. - - :param skill_id: the skill id. - :return: None - """ - self._tasks.pop(skill_id, None) - - def fetch(self, skill_id: SkillId) -> Optional[List[Task]]: - """ - Return a task. - - :return: the list of tasks - """ - return self._tasks.get(skill_id, None) - - def fetch_all(self) -> List[Task]: - """ - Return a list of tasks for processing. - - :return: a list of tasks. - """ - return [t for skill_tasks in self._tasks.values() for t in skill_tasks] - - def teardown(self) -> None: - """ - Teardown the registry. - - :return: None - """ - for tasks in self._tasks.values(): - for task in tasks: - task.teardown() - self._tasks = {} - - -class Resources(object): - """This class implements the resources of an AEA.""" - - def __init__(self): - """Instantiate the resources.""" - self.protocol_registry = ProtocolRegistry() - self.handler_registry = HandlerRegistry() - self.behaviour_registry = BehaviourRegistry() - self.task_registry = TaskRegistry() - self._skills = dict() # type: Dict[SkillId, Skill] - - self._registries = [self.protocol_registry, self.handler_registry, self.behaviour_registry, self.task_registry] - - @classmethod - def from_resource_dir(cls, directory: str, agent_context: AgentContext) -> Optional['Resources']: - """ - Parse the resource directory. - - :param directory: the agent's resources directory. - :param agent_context: the agent's context object - :return: None - """ - resources = Resources() - resources.protocol_registry.populate(directory) - resources.populate_skills(directory, agent_context) - return resources - - def populate_skills(self, directory: str, agent_context: AgentContext) -> None: - """ - Populate skills. - - :param directory: the agent's resources directory. - :param agent_context: the agent's context object - :return: None - """ - root_skill_directory = os.path.join(directory, "skills") - if not os.path.exists(root_skill_directory): - logger.warning("No skill found.") - return - - skill_directories = [str(x) for x in Path(root_skill_directory).iterdir() if x.is_dir()] - logger.debug("Processing the following skill directories: {}".format(pprint.pformat(skill_directories))) - for skill_directory in skill_directories: - try: - skill = Skill.from_dir(skill_directory, agent_context) - assert skill is not None - self.add_skill(skill) - except Exception as e: - logger.warning("A problem occurred while parsing the skill directory {}. Exception: {}" - .format(skill_directory, str(e))) - - def add_skill(self, skill: Skill): - """Add a skill to the set of resources.""" - skill_id = skill.config.name - self._skills[skill_id] = skill - if skill.handler is not None: - protocol_id = skill.config.protocol - self.handler_registry.register((protocol_id, skill_id), cast(Handler, skill.handler)) - if skill.behaviours is not None: - self.behaviour_registry.register((None, skill_id), cast(List[Behaviour], skill.behaviours)) - if skill.tasks is not None: - self.task_registry.register((None, skill_id), cast(List[Task], skill.tasks)) - - def remove_skill(self, skill_id: SkillId): - """Remove a skill from the set of resources.""" - self._skills.pop(skill_id, None) - self.handler_registry.unregister(skill_id) - self.behaviour_registry.unregister(skill_id) - self.task_registry.unregister(skill_id) - - def teardown(self): - """ - Teardown the resources. - - :return: None - """ - for r in self._registries: - r.teardown() diff --git a/aea/skills/error/__init__.py b/aea/skills/error/__init__.py new file mode 100644 index 0000000000..96c80ac32c --- /dev/null +++ b/aea/skills/error/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the implementation of the error skill.""" diff --git a/aea/skills/error/behaviours.py b/aea/skills/error/behaviours.py new file mode 100644 index 0000000000..97650c4526 --- /dev/null +++ b/aea/skills/error/behaviours.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This package contains the error behaviours.""" + +from aea.skills.base import Behaviour + + +class ErrorBehaviour(Behaviour): + """This class implements the error behaviour.""" + + def act(self) -> None: + """ + Implement the act. + + :return: None + """ + pass + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + pass diff --git a/aea/skills/default/handler.py b/aea/skills/error/handler.py similarity index 87% rename from aea/skills/default/handler.py rename to aea/skills/error/handler.py index 692440bad7..e7af794df3 100644 --- a/aea/skills/default/handler.py +++ b/aea/skills/error/handler.py @@ -22,9 +22,9 @@ import logging from typing import Optional -from aea.mail.base import Envelope from aea.configurations.base import ProtocolId -from aea.protocols.base.protocol import Protocol +from aea.mail.base import Envelope +from aea.protocols.base import Protocol from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer from aea.skills.base import Handler @@ -32,8 +32,8 @@ logger = logging.getLogger(__name__) -class DefaultHandler(Handler): - """This class implements the default handler.""" +class ErrorHandler(Handler): + """This class implements the error handler.""" SUPPORTED_PROTOCOL = 'default' # type: Optional[ProtocolId] @@ -44,7 +44,7 @@ def handle_envelope(self, envelope: Envelope) -> None: :param envelope: the envelope :return: None """ - pass # TODO: this could be the ping method? > no, then on error from other agent causes endless loop; just print out the response and raise a warning? + pass def teardown(self) -> None: """ @@ -63,11 +63,11 @@ def send_unsupported_protocol(self, envelope: Envelope) -> None: """ logger.warning("Unsupported protocol: {}".format(envelope.protocol_id)) reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL, + error_code=DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL.value, error_msg="Unsupported protocol.", error_data={"protocol_id": envelope.protocol_id}) - # TODO: outbox not available in handler yet - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_name, protocol_id=DefaultMessage.protocol_id, + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(reply)) def send_decoding_error(self, envelope: Envelope) -> None: @@ -80,10 +80,11 @@ def send_decoding_error(self, envelope: Envelope) -> None: logger.warning("Decoding error: {}.".format(envelope)) encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.DECODING_ERROR, + error_code=DefaultMessage.ErrorCode.DECODING_ERROR.value, error_msg="Decoding error.", error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_name, protocol_id=DefaultMessage.protocol_id, + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(reply)) def send_invalid_message(self, envelope: Envelope) -> None: @@ -96,10 +97,11 @@ def send_invalid_message(self, envelope: Envelope) -> None: logger.warning("Invalid message wrt protocol: {}.".format(envelope.protocol_id)) encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.INVALID_MESSAGE, + error_code=DefaultMessage.ErrorCode.INVALID_MESSAGE.value, error_msg="Invalid message.", error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_name, protocol_id=DefaultMessage.protocol_id, + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(reply)) def send_unsupported_skill(self, envelope: Envelope, protocol: Protocol) -> None: @@ -113,8 +115,9 @@ def send_unsupported_skill(self, envelope: Envelope, protocol: Protocol) -> None logger.warning("Cannot handle envelope: no handler registered for the protocol '{}'.".format(protocol.id)) encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") reply = DefaultMessage(type=DefaultMessage.Type.ERROR, - error_code=DefaultMessage.ErrorCode.UNSUPPORTED_SKILL, + error_code=DefaultMessage.ErrorCode.UNSUPPORTED_SKILL.value, error_msg="Unsupported skill.", error_data={"envelope": encoded_envelope}) - self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_name, protocol_id=DefaultMessage.protocol_id, + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, message=DefaultSerializer().encode(reply)) diff --git a/aea/skills/default/skill.yaml b/aea/skills/error/skill.yaml similarity index 56% rename from aea/skills/default/skill.yaml rename to aea/skills/error/skill.yaml index a8660963ac..e62d327565 100644 --- a/aea/skills/default/skill.yaml +++ b/aea/skills/error/skill.yaml @@ -1,10 +1,12 @@ -name: default +name: error authors: Fetch.AI Limited version: 0.1.0 license: Apache 2.0 url: "" behaviours: [] handler: - class_name: DefaultHandler + class_name: ErrorHandler + args: + foo: bar tasks: [] -protocol: "default" +protocol: 'default' diff --git a/aea/skills/error/tasks.py b/aea/skills/error/tasks.py new file mode 100644 index 0000000000..be7ceb7b9e --- /dev/null +++ b/aea/skills/error/tasks.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This package contains the implementation of the error tasks.""" + +from aea.skills.base import Task + + +class ErrorTask(Task): + """This class implements the error task.""" + + def execute(self) -> None: + """ + Implement the task execution. + + :param envelope: the envelope + :return: None + """ + pass + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + pass diff --git a/aea/skills/default/__init__.py b/aea/skills/scaffold/__init__.py similarity index 100% rename from aea/skills/default/__init__.py rename to aea/skills/scaffold/__init__.py diff --git a/aea/skills/scaffold/behaviours.py b/aea/skills/scaffold/behaviours.py new file mode 100644 index 0000000000..ae2d7f2885 --- /dev/null +++ b/aea/skills/scaffold/behaviours.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This package contains a scaffold of a behaviour.""" + +from aea.skills.base import Behaviour + + +class MyScaffoldBehaviour(Behaviour): + """This class scaffolds a behaviour.""" + + def act(self) -> None: + """ + Implement the act. + + :return: None + """ + raise NotImplementedError + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + raise NotImplementedError diff --git a/aea/skills/scaffold/handler.py b/aea/skills/scaffold/handler.py new file mode 100644 index 0000000000..e873f075ad --- /dev/null +++ b/aea/skills/scaffold/handler.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This package contains a scaffold of a handler.""" + +from typing import Optional + +from aea.configurations.base import ProtocolId +from aea.mail.base import Envelope +from aea.skills.base import Handler + + +class MyScaffoldHandler(Handler): + """This class scaffolds a handler.""" + + SUPPORTED_PROTOCOL = '' # type: Optional[ProtocolId] + + def handle_envelope(self, envelope: Envelope) -> None: + """ + Implement the reaction to an envelope. + + :param envelope: the envelope + :return: None + """ + raise NotImplementedError + + def teardown(self) -> None: + """ + Implement the handler teardown. + + :return: None + """ + raise NotImplementedError diff --git a/aea/skills/scaffold/skill.yaml b/aea/skills/scaffold/skill.yaml new file mode 100644 index 0000000000..719bf99ac9 --- /dev/null +++ b/aea/skills/scaffold/skill.yaml @@ -0,0 +1,20 @@ +name: '' +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +behaviours: + - behaviour: + class_name: MyScaffoldBehaviour + args: + foo: bar +handler: + class_name: MyScaffoldHandler + args: + foo: bar +tasks: + - task: + class_name: MyScaffoldTask + args: + foo: bar +protocol: '' diff --git a/aea/skills/scaffold/tasks.py b/aea/skills/scaffold/tasks.py new file mode 100644 index 0000000000..eb20bd37f8 --- /dev/null +++ b/aea/skills/scaffold/tasks.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This package contains a scaffold of a task.""" + +from aea.skills.base import Task + + +class MyScaffoldTask(Task): + """This class scaffolds a task.""" + + def execute(self) -> None: + """ + Implement the task execution. + + :param envelope: the envelope + :return: None + """ + raise NotImplementedError + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + raise NotImplementedError diff --git a/develop-image/docker-env.sh b/develop-image/docker-env.sh index 1f4988b44f..bd7f005cfb 100755 --- a/develop-image/docker-env.sh +++ b/develop-image/docker-env.sh @@ -1,5 +1,5 @@ #!/bin/bash -DOCKER_IMAGE_TAG=aea-develop:0.1.4 +DOCKER_IMAGE_TAG=aea-develop:0.1.5 DOCKER_BUILD_CONTEXT_DIR=.. DOCKERFILE=./Dockerfile diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000000..1d8d5322d3 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,53 @@ + +!!! Note + Work in progress. + + + +## Core components + +The `Envelope` is the core object which agents use to communicate with each other. An `Envelope` has four attributes: + +* `to`: defines the destination address + +* `sender`: defines the sender address + +* `protocol_id`: defines the protocol_id + +* `message`: is a `bytes` field to hold the message in serialized form. + +### Protocols + +Protocols define how messages are represented and encoded for transport. They also define the rules to which messages have to adhere in a message sequence. For instance, a protocol might have a message of type START and FINISH. Then the rules could prescribe that a message of type FINISH must be preceded by a message of type START. + +### Connections + +A connection allows the AEA to connect to an external service which has a Python SDK or API. A connection wraps an external SDK or API. + +### Skills + +A skill can encapsulate any code and ideally delivers economic value to the AEA. Each skill has at most a single Handler and potentially multiple Behaviours and Tasks. The Handler is responsible for dealing with messages of the protocol type for which this skill is registered, as such it encapsulates `reactions`. A Behaviour encapsulates `actions`, that is sequences of interactions with other agents initiated by the AEA. Finally, a Task encapsulates background work which is internal to the AEA. + + +## File structure + +An agent is structured in a directory with a configuration file, a directory with skills, a directory with protocols, a directory with connections and a main logic file that is used when running aea run. + +``` bash +agentName/ + agent.yml YAML configuration of the agent + connections/ Directory containing all the supported connections + connection1/ First connection + ... ... + connectionN/ nth connection + protocols/ Directory containing all supported protocols + protocol1/ First protocol + ... ... + protocolK/ kth protocol + skills/ Directory containing all the skill components + skill1/ First skill + ... ... + skillN/ nth skill +``` + +
diff --git a/docs/assets/echo.png b/docs/assets/echo.png new file mode 100644 index 0000000000..9d23fe9756 Binary files /dev/null and b/docs/assets/echo.png differ diff --git a/docs/assets/full-scaffold.png b/docs/assets/full-scaffold.png new file mode 100644 index 0000000000..14e902d74f Binary files /dev/null and b/docs/assets/full-scaffold.png differ diff --git a/docs/assets/visdom_ui.png b/docs/assets/visdom_ui.png new file mode 100644 index 0000000000..aff425db25 Binary files /dev/null and b/docs/assets/visdom_ui.png differ diff --git a/docs/cli_overview.md b/docs/cli_overview.md index b891b55883..0aa94dc0a0 100644 --- a/docs/cli_overview.md +++ b/docs/cli_overview.md @@ -1,15 +1,22 @@ -# Command Line Interface - -The command line interface of the AEA helps you quickly assemble autonomous economic agents. - -Command | Description ----------------------------------------------- | ----------------------------------------------------------------- -create [name] | Creates a new aea project -fetch [name] | Fetches an aea project -scaffold connection/protocol/skill [name] | Scaffolds a new connection, protocol or skill project -publish agent/connection/protocol/skill [name] | Publishes agent, connection, protocol or skill project -add connection/protocol/skill [name] | Adds connection, protocol or skill to agent -remove connection/protocol/skill [name] | Removes connection, protocol or skill from agent -run {using [connection, ...]} | Runs the agent on the Fetch.AI network with the default or specified connections. -deploy {using [connection, ...]} | Deploys the agent to a server and runs it on the Fetch.AI network with the default or specified connections. -delete [name] | Delete an aea project +# CLI commands + + +Command | Description +---------| ----------------------------------------------------------------- +`create [name]` | Create a new aea project. +`fetch [name]` | Fetch an aea project. +`scaffold connection/protocol/skill [name]` | Scaffold a new connection, protocol, or skill. +`publish agent/connection/protocol/skill [name]` | Publish agent, connection, protocol, or skill. +`add connection/protocol/skill [name]` | Add connection, protocol, or skill to agent. +`remove connection/protocol/skill [name]` | Remove connection, protocol, or skill from agent. +`run {using [connection, ...]}` | Run the agent on the Fetch.AI network with default or specified connections. +`-v DEBUG run` | Run with debugging. +`deploy {using [connection, ...]}` | Deploy the agent to a server and run it on the Fetch.AI network with default or specified connections. +`delete [name]` | Delete an aea project. + + + + + + +
\ No newline at end of file diff --git a/docs/css/my-styles.css b/docs/css/my-styles.css new file mode 100644 index 0000000000..40aba59612 --- /dev/null +++ b/docs/css/my-styles.css @@ -0,0 +1,27 @@ +pre { + background-color: #f8f8f7; +} + +code { + background-color: #0083fb; +} + +/* this doesn't work now +.md-nav__link { + text-transform: uppercase; + color: #0083fb; +} +*/ + +/* Katharine's css additions */ +.md-header, .md-tabs, .md-footer-meta, .md-footer-nav, .md-footer-nav__inner { + background-color: #172b6e; +} + +.md-nav__title { + color: #172b6e; +} + +.md-icon { + ./assets/images/favicon.ico +} \ No newline at end of file diff --git a/docs/ex_rl.md b/docs/ex_rl.md deleted file mode 100644 index 5c6bdd606d..0000000000 --- a/docs/ex_rl.md +++ /dev/null @@ -1,11 +0,0 @@ -# Reinforcement Learning and the AEA Framework - -We provide two examples to demonstrate the utility of our framework to RL developers. - -## Gym Example - -The `train.py` file [here](https://github.com/fetchai/agents-aea/tree/master/examples/gym_ex/train.py) shows that all the RL developer needs to do is add one line of code `(proxy_env = ...)` to introduce our agent as a proxy layer between an OpenAI `gym.Env` and a standard RL agent. The `gym_ex` just serves as a demonstration and helps on-boarding, there is no immediate use case for it as you can train your RL agent without our proxy layer just fine (and faster). However, it decouples the RL agent from the `gym.Env` allowing the two do run in separate environments, potentially owned by different entities. - -## Gym Skill - -The `gym_skill` [here](https://github.com/fetchai/agents-aea/tree/master/examples/gym_skill) lets an RL developer embed their RL agent inside an AEA as a skill. diff --git a/docs/gym-plugin.md b/docs/gym-plugin.md new file mode 100644 index 0000000000..2269840d14 --- /dev/null +++ b/docs/gym-plugin.md @@ -0,0 +1,58 @@ +The `gym_ex` example demonstrates to Reinforcement Learning developers the AEA framework's flexibility. + +There is no immediate use case for this example as you can train an RL agent without the AEA proxy layer just fine (and faster). + +However, the example decouples the RL agent from the `gym.Env` allowing them to run in separate environments, potentially owned by different entities. + + +## Quick start + +### Dependencies + +``` bash +pip install numpy gym +``` + +### Files + +You will have already downloaded the `examples` directory during the AEA quick start demo. + +``` bash +cd examples/gym_ex +``` + +### Run the example + +``` bash +python train.py +``` + +Notice the usual RL setup, i.e. the fit method of the RL agent has the typical signature and a familiar implementation. + +Note how `train.py` demonstrates how easy it is to use an AEA agent as a proxy layer between an OpenAI `gym.Env` and a standard RL agent. + +It is just one line of code! + +``` python +from gyms.env import BanditNArmedRandom +from proxy.env import ProxyEnv +from rl.agent import RLAgent + + +if __name__ == "__main__": + NB_GOODS = 10 + NB_PRICES_PER_GOOD = 100 + NB_STEPS = 4000 + + # Use any gym.Env compatible environment: + gym_env = BanditNArmedRandom(nb_bandits=NB_GOODS, nb_prices_per_bandit=NB_PRICES_PER_GOOD) + + # Pass the gym environment to a proxy environment: + proxy_env = ProxyEnv(gym_env) + + # Use any RL agent compatible with the gym environment and call the fit method: + rl_agent = RLAgent(nb_goods=NB_GOODS) + rl_agent.fit(env=proxy_env, nb_steps=NB_STEPS) +``` + +
diff --git a/docs/gym-skill.md b/docs/gym-skill.md new file mode 100644 index 0000000000..1972b8379a --- /dev/null +++ b/docs/gym-skill.md @@ -0,0 +1,85 @@ +The AEA gym skill demonstrates how a custom Reinforcement Learning agent may be embedded into an Autonomous Economic Agent. + + +## Demo instructions + +Follow the Preliminaries and Installation instructions here. + +Create and launch a virtual environment. + +``` bash +pipenv --python 3.7 && pipenv shell +``` + +Install the gym library. + +``` bash +pip install gym +``` + +Then, download the examples and channels directory. +``` bash +svn export https://github.com/fetchai/agents-aea.git/trunk/examples +``` + + + + +### Create the agent +In the root directory, create the gym agent. +``` bash +aea create my_gym_agent +``` + + +### Add the gym skill +``` bash +cd my_gym_agent +aea add skill gym +``` + + +### Copy the gym environment to the agent directory +``` bash +mkdir gyms +cp -a ../examples/gym_ex/gyms/. gyms/ +``` + + +### Add a gym connection +``` bash +aea add connection gym +``` + + +### Update the connection config +``` bash +nano connections/gym/connection.yaml +env: gyms.env.BanditNArmedRandom +``` + + + +### Run the agent with the gym connection + +``` bash +aea run --connection gym +``` + + + + +### Delete the agent + +When you're done, you can delete the agent. + +``` bash +aea delete my_first_agent +``` + + +
\ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 61eb022ed5..87f5c4dd12 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,71 +1,36 @@ -# AEA - Autonomous Economic Agent Framework +The AEA is a framework for autonomous economic agent (AEA) development. It gives developers a quick and efficient way to build autonomous economic agents. -The AEA framework allows you to quickly assemble autonomous economic agents. Through its modularity AEAs are easily extenable and highly composable. +The framework is super modular, easily extensible, and highly composable. It is ideal for Reinforcement Learning scenarios. -## Quickstart -` -pip install -i https://test.pypi.org/simple/ aea -` +## Our vision -1. create a new AEA project +Fetch.AI intends the AEA framework to have two focused commercial roles. -` -aea create my_first_agent -` +### Open source company -2. CD into the project folder and add the echo skill to the agent +We want to build infrastructure with which external parties build their own solutions. -` -aea add skill echo -` +### Platform for start ups -3. run the agent on a local network +By operating as a platform for start ups, we hope to solve the chicken-or-egg problem through incentive schemes. -` -aea run -` -## AEA file structure -An agent is structured in a directory with a configuration file, a directory with skills, a directory with protocols, a directory with connections and a main logic file that is used when running aea run. +## Agents -agentName/ | The root of the agent ----------------------------------------------- | ----------------------------------------------------------------- -agent.yml | YAML configuration of the agent -connections/ | Directory containing all the supported connections - connection1/ | Connection 1 - ... | ... - connectionN/ | Connection N -protocols/ | Directory containing all supported protocols - protocol1/ | Protocol 1 - ... | ... - protocolK/ | Protocol K -skills/ | Directory containing all the skill components - skill1/ | Skill 1 - ... | ... - skillN/ | Skill L +An autonomous economic agent (AEA) is an intelligent agent whose goal is to generate economic value for its owner. Their super powers lie in their ability to autonomously acquire new skills. -## AEA Core Components +AEAs achieve their goals with the help of the OEF and the Fetch.AI Ledger. -The `Envelope` is the core object which agents use to communicate with each other. An `Envelope` has four attributes: +Third party systems, such as Ethereum, may also allow AEA integration. -* `to`: defines the destination address -* `sender`: defines the sender address -* `protocol_id`: defines the protocol_id +!!! Note + Work in progress. -* `message`: is a `bytes` field to hold the message in serialized form. -### AEA Protocols +
-Protocols define how messages are represented and encoded for transport. They also define the rules to which messages have to adhere in a message sequence. For instance, a protocol might have a message of type START and FINISH. Then the rules could prescribe that a message of type FINISH must be preceded by a message of type START. -### AEA Connections - -A connection allows the AEA to connect to an external service which has a Python SDK or API. A connection wraps an external SDK or API. - -### AEA Skills - -A skill can encapsulate any code and ideally delivers economic value to the AEA. Each skill has at most a single Handler and potentially multiple Behaviours and Tasks. The Handler is responsible for dealing with messages of the protocol type for which this skill is registered, as such it encapsulates `reactions`. A Behaviour encapsulates `actions`, that is sequences of interactions with other agents initiated by the AEA. Finally, a Task encapsulates background work which is internal to the AEA. diff --git a/docs/integration.md b/docs/integration.md new file mode 100644 index 0000000000..e2dfe4a7f9 --- /dev/null +++ b/docs/integration.md @@ -0,0 +1,22 @@ +In this section, we show you how to integrate the AEA with third party ledgers. + + +## Fetch.AI Ledger + +!!! Note + Coming soon. + + +## Ethereum + +!!! Note + Coming soon. + + +## Etc. + +!!! Note + Coming soon. + + +
\ No newline at end of file diff --git a/docs/protocol.md b/docs/protocol.md new file mode 100644 index 0000000000..cb1504869d --- /dev/null +++ b/docs/protocol.md @@ -0,0 +1,8 @@ +## Envelope + +!!! Todo + + +## Sending stuff around + +!!! Todo \ No newline at end of file diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000000..7057121b6a --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,96 @@ +## Preliminaries + +Create and cd into a new working directory. + +``` bash +mkdir aea/ +cd aea/ +``` + +Check you have `pipenv`. + +``` bash +which pipenv +``` + +If you don't have it, install it. Instructions are here. + +Once installed, create a new environment and open it. + +``` bash +pipenv --python 3.7 && pipenv shell +``` + + +## Installation + +Install the Autonomous Economic Agent framework. + +The following installs the basic application. +``` bash +pip install aea +``` + +The following installs the whole package. +``` bash +pip install aea[all] + +``` + +The following installs just the cli. +``` bash +pip install aea[cli] +``` + + +## Echo Agent demo +### Download the examples and scripts directories. +``` bash +svn export https://github.com/fetchai/agents-aea.git/trunk/examples +svn export https://github.com/fetchai/agents-aea.git/trunk/scripts +``` + +### Create a new agent +``` bash +aea create my_first_agent +``` + +### Add the echo skill + +``` bash +cd my_first_agent +aea add skill echo +``` + +### Launch the OEF + + +Open a new terminal and launch the OEF. + +``` bash +python scripts/oef/launch.py -c ./scripts/oef/launch_config.json +``` + +### Run the agent locally + +Go back to the other terminal and run the agent. + +``` bash +aea run +``` + +You will see the echo task running in the terminal window. + +
![The echo call and response log](assets/echo.png)
+ + +### Delete the agent + +When you're done, you can delete the agent. + +``` bash +aea delete my_first_agent +``` + + +
\ No newline at end of file diff --git a/docs/scaffolding.md b/docs/scaffolding.md new file mode 100644 index 0000000000..788602fc17 --- /dev/null +++ b/docs/scaffolding.md @@ -0,0 +1,39 @@ +## Scaffold generator + +The scaffold generator builds out the entire directory structure required for creating a particular skill. + +For example, create a new AEA project. + +``` bash +aea create new_project +cd new_project +``` + +Then, cd into your project directory and scaffold your project skill, protocol, or connection. + +You will see the directories filled out with the files required and the skill, protocol, or connection registered in the top level `aea-config.yaml`. + + +### Scaffold a skill + +``` bash +aea scaffold skill new_skill +``` + + +### Scaffold a protocol + +``` bash +aea scaffold protocol new_protocol +``` + + +### Scaffold a connection + +``` bash +aea scaffold connection new_connection +``` + +After running the above commands, you will have the fully constructured file system required by the AEA. + +
![The echo call and response log](assets/full-scaffold.png)
\ No newline at end of file diff --git a/docs/skill.md b/docs/skill.md new file mode 100644 index 0000000000..9a455a7156 --- /dev/null +++ b/docs/skill.md @@ -0,0 +1,11 @@ +## How? + +!!! Todo + +## Why? + +!!! Todo + +## Example + +!!! Todo \ No newline at end of file diff --git a/docs/tac.md b/docs/tac.md new file mode 100644 index 0000000000..15f2fa8709 --- /dev/null +++ b/docs/tac.md @@ -0,0 +1,108 @@ +TAC has its own repo. + +Follow the instructions below to build and run the TAC demo. + + +## Requirements + +Make sure you are running Docker and Docker Compose. + + +## Quick start + +Clone the repo to include submodules. + +``` bash +git clone git@github.com:fetchai/agents-tac.git --recursive && cd agents-tac +``` + +Check you have `pipenv`. + +``` bash +which pipenv +``` + +If you don't have it, install it. Instructions are here. + + +Create and launch a virtual environment. + +``` bash +pipenv --python 3.7 && pipenv shell +``` + +Install the dependencies. + +``` bash +pipenv install +``` + + +Install the package. +``` bash +python setup.py install +``` + + +Run the launch script. This may take a while. + +``` bash +python scripts/launch.py +``` + +The visdom server is now running. + +The controller GUI at http://localhost:8097 provides real time insights. + +In the Environment tab, make sure you have the `tac_controller` environment selected. + +
![AEA Visdom UI](assets/visdom_ui.png)
+ + +## Alternative build and run - WIP + +In a new terminal window, clone the repo, build the sandbox, and launch it. + +``` bash +git clone git@github.com:fetchai/agents-tac.git --recursive && cd agents-tac +pipenv --python 3.7 && pipenv shell +python setup.py install +cd sandbox && docker-compose build +docker-compose up +``` + +In a new terminal window, enter the virtual environment, and connect a template agent to the sandbox. + +``` bash +pipenv shell +python templates/v1/basic.py --name my_agent --dashboard +``` + + +Click through to the controller GUI. + + + +## Launcher GUI + +!!! Todo + + +## Possible gotchas + +Stop all running containers before restart. + +``` bash +docker stop $(docker ps -q) +``` + +To remove all images, run the following command. + +``` bash +# mac +docker ps -q | xargs docker stop ; docker system prune -a +``` + + + +
\ No newline at end of file diff --git a/docs/version.md b/docs/version.md new file mode 100644 index 0000000000..5bbeedf9ba --- /dev/null +++ b/docs/version.md @@ -0,0 +1 @@ +Current version of the Autonomous Econonmic Agent application is `0.1`. \ No newline at end of file diff --git a/examples/echo_skill/README.md b/examples/echo_skill/README.md index 6ba589bc47..e4fb84e13c 100644 --- a/examples/echo_skill/README.md +++ b/examples/echo_skill/README.md @@ -10,7 +10,7 @@ This quick start explains how to create and launch an agent with the cli. aea create my_first_agent - a directory named `my_first_agent` will be created. + This command will create a directory named `my_first_agent`. It will further create the `my_first_agent/skills` folder, with the `error` skill package inside. It will also create the `my_first_agent/protocols` folder, with the `default` protocol package inside. Finally, it will create the `my_first_agent/connections` folder, with the `oef` connection package inside. - enter into the agent's directory: @@ -18,9 +18,9 @@ This quick start explains how to create and launch an agent with the cli. - add a skill to the agent, e.g.: - aea add skill echo_skill ../examples/echo_skill + aea add skill echo - This command will create the `my_first_agent/skills` folder, with the `echo_skill` skill package inside. It will also create the `my_first_agent/protocols` folder, with the `default` protocol package inside. + This command will add the `echo` skill package to the `my_first_agent/skills` folder. - start an oef from a separate terminal: diff --git a/examples/gym_ex/README.md b/examples/gym_ex/README.md index 5e3f362351..9dc9519601 100644 --- a/examples/gym_ex/README.md +++ b/examples/gym_ex/README.md @@ -6,10 +6,10 @@ This example requires `numpy` and `gym` in addition to the dependencies of the ` ## Run -Cd into the directory and execute: +From root execute: ` -python train.py +python examples/gym_ex/train.py ` which has the usual RL setup (that is, the `fit` method of the `RLAgent` has the typical signature and familiar implementation). \ No newline at end of file diff --git a/examples/gym_ex/proxy/agent.py b/examples/gym_ex/proxy/agent.py index 92cf0e5198..4424719d44 100644 --- a/examples/gym_ex/proxy/agent.py +++ b/examples/gym_ex/proxy/agent.py @@ -20,14 +20,19 @@ """This contains the proxy agent class.""" -import gym +import sys from queue import Queue from typing import Optional +import gym + from aea.agent import Agent -from aea.channels.gym.connection import GymConnection +from aea.helpers.base import locate from aea.mail.base import Envelope, MailBox +sys.modules["gym_connection"] = locate("packages.connections.gym") +from gym_connection.connection import GymConnection # noqa: E402 + class ProxyAgent(Agent): """This class implements a proxy agent to be used by a proxy environment.""" diff --git a/examples/gym_ex/proxy/env.py b/examples/gym_ex/proxy/env.py index 69f1b1ba54..d7dc5f1aec 100755 --- a/examples/gym_ex/proxy/env.py +++ b/examples/gym_ex/proxy/env.py @@ -20,17 +20,24 @@ """This contains the proxy gym environment.""" +import sys + +from aea.helpers.base import locate + import gym from queue import Queue from threading import Thread from typing import Any, Tuple, cast from aea.mail.base import Envelope -from aea.protocols.base.message import Message -from aea.protocols.gym.message import GymMessage -from aea.protocols.gym.serialization import GymSerializer +from aea.protocols.base import Message + +sys.modules["gym_connection"] = locate("packages.connections.gym") +sys.modules["gym_protocol"] = locate("packages.protocols.gym") +from gym_protocol.message import GymMessage # noqa: E402 +from gym_protocol.serialization import GymSerializer # noqa: E402 -from .agent import ProxyAgent +from .agent import ProxyAgent # noqa: E402 Action = Any Observation = Any diff --git a/examples/gym_ex/train.py b/examples/gym_ex/train.py index 0f9c6d188f..f9ae1afebc 100644 --- a/examples/gym_ex/train.py +++ b/examples/gym_ex/train.py @@ -24,7 +24,6 @@ from proxy.env import ProxyEnv from rl.agent import RLAgent - if __name__ == "__main__": NB_GOODS = 10 NB_PRICES_PER_GOOD = 100 diff --git a/examples/gym_skill/README.md b/examples/gym_skill/README.md index 4adfde3341..7f28bd12f7 100644 --- a/examples/gym_skill/README.md +++ b/examples/gym_skill/README.md @@ -14,9 +14,7 @@ A guide to create an AEA with the gym_skill. - Add the 'gym' skill: - aea add skill gym ../examples/gym_skill - - This command will create the `my_gym_agent/skills` folder, with the `gym_skill` skill package inside. It will also create the `my_first_agent/protocols` folder, with the `gym` protocol package inside. + aea add skill gym - Copy the gym environment to the agent directory: @@ -25,7 +23,7 @@ A guide to create an AEA with the gym_skill. - Add a gym connection: - aea add connection ../aea/channels/gym + aea add connection gym - Update the connection config `my_gym_agent/connections/gym/connection.yaml`: diff --git a/mkdocs.yml b/mkdocs.yml index 96495d6983..e554f6df08 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,45 @@ -site_name: AEA - Autonomous Economic Agent Framework +site_name: Fetch.AI Developer Documentation +site_url: https://docs.fetch.ai/ +site_description: Everything you need to know about Fetch.AI. +#repo_url: https://github.com/fetchai/docs // commented out to remove edit option +#repo_name: 'GitHub' +site_author: katharine.murphy@fetch.ai + + +theme: + name: 'material' + logo: + icon: code + feature: + tabs: true + + + nav: - - Home: index.md - - CLI: - - 'Overview': cli_overview.md - - Examples: - - 'Reinforcement Learning': ex_rl.md -theme: readthedocs + + - Autonomous Economic Agent Framework: + - Welcome: + - "What is the AEA?": 'index.md' + - "Architecture": 'architecture.md' + - "Version": 'version.md' + - AEA quick start: 'quickstart.md' + - Get developing: + - "Protocol": 'protocol.md' + - "Skill": 'skill.md' + - "Scaffolding": 'scaffolding.md' + - "CLI commands": 'cli_overview.md' + - Integration: + - "Integrate with third parties": 'integration.md' + - Demos: + - "AEA gym skill": 'gym-skill.md' + - "Gym efficiency demo": 'gym-plugin.md' + - "TAC": 'tac.md' + + + +markdown_extensions: + - pymdownx.superfences + - admonition + +extra_css: + - css/my-styles.css \ No newline at end of file diff --git a/mypy.ini b/mypy.ini index 2b686cfaf2..bcd76fbc12 100644 --- a/mypy.ini +++ b/mypy.ini @@ -21,9 +21,6 @@ ignore_missing_imports = True [mypy-cryptography.hazmat.primitives.serialization] ignore_missing_imports = True -[mypy-aea/protocols/tac/tac_pb2] -ignore_errors = True - [mypy-aea/protocols/fipa/fipa_pb2] ignore_errors = True @@ -54,3 +51,17 @@ ignore_missing_imports = True [mypy-docker.models.containers] ignore_missing_imports = True + +# Package ignores + +[mypy-packages/protocols/tac/tac_pb2] +ignore_errors = True + +[mypy-tac_protocol.*] +ignore_missing_imports = True + +[mypy-gym_protocol.*] +ignore_missing_imports = True + +[mypy-gym_skill.*] +ignore_missing_imports = True diff --git a/packages/__init__.py b/packages/__init__.py new file mode 100644 index 0000000000..28f842ac66 --- /dev/null +++ b/packages/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""Package registry for the AEA framework.""" diff --git a/packages/connections/__init__.py b/packages/connections/__init__.py new file mode 100644 index 0000000000..dad7e82c9b --- /dev/null +++ b/packages/connections/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the connection registry for the AEA framework.""" diff --git a/aea/channels/gym/__init__.py b/packages/connections/gym/__init__.py similarity index 100% rename from aea/channels/gym/__init__.py rename to packages/connections/gym/__init__.py diff --git a/aea/channels/gym/connection.py b/packages/connections/gym/connection.py similarity index 97% rename from aea/channels/gym/connection.py rename to packages/connections/gym/connection.py index bb4c167212..7001a2aafe 100644 --- a/aea/channels/gym/connection.py +++ b/packages/connections/gym/connection.py @@ -28,11 +28,12 @@ import gym -from aea.helpers.base import locate -from aea.mail.base import Envelope, Channel, Connection -from aea.protocols.gym.message import GymMessage -from aea.protocols.gym.serialization import GymSerializer from aea.configurations.base import ConnectionConfig +from aea.connections.base import Channel, Connection +from aea.helpers.base import locate +from aea.mail.base import Envelope +from gym_protocol.message import GymMessage +from gym_protocol.serialization import GymSerializer logger = logging.getLogger(__name__) diff --git a/aea/channels/gym/connection.yaml b/packages/connections/gym/connection.yaml similarity index 100% rename from aea/channels/gym/connection.yaml rename to packages/connections/gym/connection.yaml diff --git a/packages/protocols/__init__.py b/packages/protocols/__init__.py new file mode 100644 index 0000000000..24b27d9b56 --- /dev/null +++ b/packages/protocols/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the protocol registry for the AEA framework.""" diff --git a/aea/protocols/gym/__init__.py b/packages/protocols/gym/__init__.py similarity index 100% rename from aea/protocols/gym/__init__.py rename to packages/protocols/gym/__init__.py diff --git a/aea/protocols/gym/message.py b/packages/protocols/gym/message.py similarity index 95% rename from aea/protocols/gym/message.py rename to packages/protocols/gym/message.py index 79c32febe6..616b3e8226 100644 --- a/aea/protocols/gym/message.py +++ b/packages/protocols/gym/message.py @@ -22,7 +22,7 @@ from enum import Enum from typing import Optional, Union -from aea.protocols.base.message import Message +from aea.protocols.base import Message class GymMessage(Message): @@ -49,6 +49,7 @@ def __init__(self, performative: Optional[Union[str, Performative]] = None, **kw :param type: the type. """ super().__init__(performative=GymMessage.Performative(performative), **kwargs) + assert self.check_consistency(), "GymMessage initialization inconsistent." def check_consistency(self) -> bool: """Check that the data is consistent.""" diff --git a/packages/protocols/gym/protocol.yaml b/packages/protocols/gym/protocol.yaml new file mode 100644 index 0000000000..5241b7956a --- /dev/null +++ b/packages/protocols/gym/protocol.yaml @@ -0,0 +1,5 @@ +name: gym +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" diff --git a/aea/protocols/gym/serialization.py b/packages/protocols/gym/serialization.py similarity index 96% rename from aea/protocols/gym/serialization.py rename to packages/protocols/gym/serialization.py index a2e3e11a2d..8bb0d02adf 100644 --- a/aea/protocols/gym/serialization.py +++ b/packages/protocols/gym/serialization.py @@ -25,9 +25,9 @@ import pickle from typing import Any -from aea.protocols.base.message import Message -from aea.protocols.base.serialization import Serializer -from aea.protocols.gym.message import GymMessage +from aea.protocols.base import Message +from aea.protocols.base import Serializer +from gym_protocol.message import GymMessage class GymSerializer(Serializer): diff --git a/aea/protocols/tac/__init__.py b/packages/protocols/tac/__init__.py similarity index 100% rename from aea/protocols/tac/__init__.py rename to packages/protocols/tac/__init__.py diff --git a/aea/protocols/tac/message.py b/packages/protocols/tac/message.py similarity index 97% rename from aea/protocols/tac/message.py rename to packages/protocols/tac/message.py index 94102be923..07a100d90e 100644 --- a/aea/protocols/tac/message.py +++ b/packages/protocols/tac/message.py @@ -22,7 +22,7 @@ from enum import Enum from typing import Dict, Optional, cast -from aea.protocols.base.message import Message +from aea.protocols.base import Message class TACMessage(Message): @@ -82,6 +82,7 @@ def __init__(self, tac_type: Optional[Type] = None, :param tac_type: the type of TAC message. """ super().__init__(type=tac_type, **kwargs) + assert self.check_consistency(), "TACMessage initialization inconsistent." def check_consistency(self) -> bool: """Check that the data is consistent.""" diff --git a/packages/protocols/tac/protocol.yaml b/packages/protocols/tac/protocol.yaml new file mode 100644 index 0000000000..13a3854a18 --- /dev/null +++ b/packages/protocols/tac/protocol.yaml @@ -0,0 +1,5 @@ +name: tac +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" diff --git a/aea/protocols/tac/serialization.py b/packages/protocols/tac/serialization.py similarity index 98% rename from aea/protocols/tac/serialization.py rename to packages/protocols/tac/serialization.py index 992e0060f7..d86177bf0a 100644 --- a/aea/protocols/tac/serialization.py +++ b/packages/protocols/tac/serialization.py @@ -22,10 +22,10 @@ from typing import Any, Dict, List, cast -from aea.protocols.base.message import Message -from aea.protocols.base.serialization import Serializer -from aea.protocols.tac import tac_pb2 -from aea.protocols.tac.message import TACMessage +from aea.protocols.base import Message +from aea.protocols.base import Serializer +from tac_protocol import tac_pb2 +from tac_protocol.message import TACMessage # type: ignore def _from_dict_to_pairs(d): diff --git a/aea/protocols/tac/tac.proto b/packages/protocols/tac/tac.proto similarity index 100% rename from aea/protocols/tac/tac.proto rename to packages/protocols/tac/tac.proto diff --git a/aea/protocols/tac/tac_pb2.py b/packages/protocols/tac/tac_pb2.py similarity index 100% rename from aea/protocols/tac/tac_pb2.py rename to packages/protocols/tac/tac_pb2.py diff --git a/packages/skills/__init__.py b/packages/skills/__init__.py new file mode 100644 index 0000000000..f7348fb9f0 --- /dev/null +++ b/packages/skills/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the skill registry for the AEA framework.""" diff --git a/examples/echo_skill/__init__.py b/packages/skills/echo/__init__.py similarity index 100% rename from examples/echo_skill/__init__.py rename to packages/skills/echo/__init__.py diff --git a/examples/echo_skill/behaviours.py b/packages/skills/echo/behaviours.py similarity index 100% rename from examples/echo_skill/behaviours.py rename to packages/skills/echo/behaviours.py diff --git a/examples/echo_skill/handler.py b/packages/skills/echo/handler.py similarity index 100% rename from examples/echo_skill/handler.py rename to packages/skills/echo/handler.py diff --git a/examples/echo_skill/skill.yaml b/packages/skills/echo/skill.yaml similarity index 100% rename from examples/echo_skill/skill.yaml rename to packages/skills/echo/skill.yaml diff --git a/examples/echo_skill/tasks.py b/packages/skills/echo/tasks.py similarity index 100% rename from examples/echo_skill/tasks.py rename to packages/skills/echo/tasks.py diff --git a/examples/gym_skill/__init__.py b/packages/skills/gym/__init__.py similarity index 100% rename from examples/gym_skill/__init__.py rename to packages/skills/gym/__init__.py diff --git a/examples/gym_skill/behaviours.py b/packages/skills/gym/behaviours.py similarity index 100% rename from examples/gym_skill/behaviours.py rename to packages/skills/gym/behaviours.py diff --git a/examples/gym_skill/handler.py b/packages/skills/gym/handler.py similarity index 95% rename from examples/gym_skill/handler.py rename to packages/skills/gym/handler.py index a734897068..db796b202d 100644 --- a/examples/gym_skill/handler.py +++ b/packages/skills/gym/handler.py @@ -22,8 +22,8 @@ from aea.mail.base import Envelope from aea.skills.base import Handler -from aea.protocols.gym.message import GymMessage -from aea.protocols.gym.serialization import GymSerializer +from gym_protocol.message import GymMessage +from gym_protocol.serialization import GymSerializer from gym_skill.tasks import GymTask diff --git a/examples/gym_skill/helpers.py b/packages/skills/gym/helpers.py similarity index 97% rename from examples/gym_skill/helpers.py rename to packages/skills/gym/helpers.py index 596159febe..db945d0b40 100644 --- a/examples/gym_skill/helpers.py +++ b/packages/skills/gym/helpers.py @@ -26,9 +26,9 @@ from aea.mail.base import Envelope from aea.skills.base import SkillContext -from aea.protocols.base.message import Message -from aea.protocols.gym.message import GymMessage -from aea.protocols.gym.serialization import GymSerializer +from aea.protocols.base import Message +from gym_protocol.message import GymMessage +from gym_protocol.serialization import GymSerializer Action = Any Observation = Any diff --git a/examples/gym_skill/rl_agent.py b/packages/skills/gym/rl_agent.py similarity index 100% rename from examples/gym_skill/rl_agent.py rename to packages/skills/gym/rl_agent.py diff --git a/examples/gym_skill/skill.yaml b/packages/skills/gym/skill.yaml similarity index 100% rename from examples/gym_skill/skill.yaml rename to packages/skills/gym/skill.yaml diff --git a/examples/gym_skill/tasks.py b/packages/skills/gym/tasks.py similarity index 97% rename from examples/gym_skill/tasks.py rename to packages/skills/gym/tasks.py index 7402fb84a5..fee8aa4aec 100644 --- a/examples/gym_skill/tasks.py +++ b/packages/skills/gym/tasks.py @@ -69,4 +69,4 @@ def _stop_training(self) -> None: self.is_rl_agent_training = False self._proxy_env.close() self._rl_agent_training_thread.join() - print("Training finished.") + print("Training finished. You can exit now via CTRL+C.") diff --git a/setup.py b/setup.py index ae07bffb87..b68ddb3f74 100644 --- a/setup.py +++ b/setup.py @@ -33,9 +33,9 @@ def get_aea_extras() -> Dict[str, List[str]]: """Parse extra dependencies from aea channels and protocols.""" result = {} - # parse channel dependencies - channel_module = importlib.import_module("aea.channels") - channel_dependencies = {k.split("_")[0] + "-channel": v for k, v in vars(channel_module).items() if re.match(".+_dependencies", k)} + # parse connections dependencies + channel_module = importlib.import_module("aea.connections") + channel_dependencies = {k.split("_")[0] + "-connection": v for k, v in vars(channel_module).items() if re.match(".+_dependencies", k)} result.update(channel_dependencies) # parse protocols dependencies @@ -43,6 +43,11 @@ def get_aea_extras() -> Dict[str, List[str]]: protocols_dependencies = {k.split("_")[0] + "-protocol": v for k, v in vars(protocols_module).items() if re.match(".+_dependencies", k)} result.update(protocols_dependencies) + # parse skills dependencies + skills_module = importlib.import_module("aea.skills") + skills_dependencies = {k.split("_")[0] + "-skill": v for k, v in vars(protocols_module).items() if re.match(".+_dependencies", k)} + result.update(skills_dependencies) + return result diff --git a/tests/conftest.py b/tests/conftest.py index 37a5db0782..2aa5f5f625 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,6 +36,11 @@ CUR_PATH = os.path.dirname(inspect.getfile(inspect.currentframe())) ROOT_DIR = os.path.join(CUR_PATH, "..") +CONFIGURATION_SCHEMA_DIR = os.path.join(ROOT_DIR, "aea", "configurations", "schemas") +AGENT_CONFIGURATION_SCHEMA = os.path.join(CONFIGURATION_SCHEMA_DIR, "aea-config_schema.json") +SKILL_CONFIGURATION_SCHEMA = os.path.join(CONFIGURATION_SCHEMA_DIR, "skill-config_schema.json") +CONNECTION_CONFIGURATION_SCHEMA = os.path.join(CONFIGURATION_SCHEMA_DIR, "connection-config_schema.json") + def pytest_addoption(parser): """Add options to the parser.""" diff --git a/tests/data/dummy_aea/aea-config.yaml b/tests/data/dummy_aea/aea-config.yaml new file mode 100644 index 0000000000..8245e4741c --- /dev/null +++ b/tests/data/dummy_aea/aea-config.yaml @@ -0,0 +1,17 @@ +aea_version: 0.1.4 +agent_name: Agent0 +authors: Fetch.AI Limited +connections: +- local +default_connection: local +license: Apache 2.0 +private_key_pem_path: '' +protocols: +- fipa +- default +registry_path: aea +skills: +- error +- dummy +url: '' +version: v1 diff --git a/tests/data/dummy_aea/connections/__init__.py b/tests/data/dummy_aea/connections/__init__.py new file mode 100644 index 0000000000..a6ea43d075 --- /dev/null +++ b/tests/data/dummy_aea/connections/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the implementation of the connections.""" diff --git a/tests/data/dummy_aea/connections/local/__init__.py b/tests/data/dummy_aea/connections/local/__init__.py new file mode 100644 index 0000000000..3a0058ec7a --- /dev/null +++ b/tests/data/dummy_aea/connections/local/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""Implementation of the Local OEF connection.""" diff --git a/tests/data/dummy_aea/connections/local/connection.py b/tests/data/dummy_aea/connections/local/connection.py new file mode 100644 index 0000000000..2b59091788 --- /dev/null +++ b/tests/data/dummy_aea/connections/local/connection.py @@ -0,0 +1,365 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""Extension to the Local Node.""" +import logging +import queue +import threading +from collections import defaultdict +from queue import Queue +from threading import Thread +from typing import Dict, List, Optional, cast + +from aea.configurations.base import ConnectionConfig +from aea.connections.base import Channel, Connection +from aea.mail.base import Envelope +from aea.protocols.oef.message import OEFMessage +from aea.protocols.oef.models import Description, Query +from aea.protocols.oef.serialization import OEFSerializer, DEFAULT_OEF + +logger = logging.getLogger(__name__) + +STUB_DIALOGUE_ID = 0 + + +class LocalNode: + """A light-weight local implementation of a OEF Node.""" + + def __init__(self): + """Initialize a local (i.e. non-networked) implementation of an OEF Node.""" + self.agents = dict() # type: Dict[str, Description] + self.services = defaultdict(lambda: []) # type: Dict[str, List[Description]] + self._lock = threading.Lock() + + self._queues = {} # type: Dict[str, Queue] + + def connect(self, public_key: str) -> Optional[Queue]: + """ + Connect a public key to the node. + + :param public_key: the public key of the agent. + :return: an asynchronous queue, that constitutes the communication channel. + """ + if public_key in self._queues: + return None + + q = Queue() # type: Queue + self._queues[public_key] = q + return q + + def send(self, envelope: Envelope) -> None: + """ + Process the incoming messages. + + :return: None + """ + sender = envelope.sender + logger.debug("Processing message from {}: {}".format(sender, envelope)) + self._decode_envelope(envelope) + + def _decode_envelope(self, envelope: Envelope) -> None: + """ + Decode the envelope. + + :param envelope: the envelope + :return: None + """ + if envelope.protocol_id == "oef": + self.handle_oef_message(envelope) + else: + self.handle_agent_message(envelope) + + def handle_oef_message(self, envelope: Envelope) -> None: + """ + Handle oef messages. + + :param envelope: the envelope + :return: None + """ + oef_message = OEFSerializer().decode(envelope.message) + sender = envelope.sender + request_id = cast(int, oef_message.get("id")) + oef_type = OEFMessage.Type(oef_message.get("type")) + if oef_type == OEFMessage.Type.REGISTER_SERVICE: + self.register_service(sender, cast(Description, oef_message.get("service_description"))) + elif oef_type == OEFMessage.Type.UNREGISTER_SERVICE: + self.unregister_service(sender, request_id, cast(Description, oef_message.get("service_description"))) + elif oef_type == OEFMessage.Type.SEARCH_AGENTS: + self.search_agents(sender, request_id, cast(Query, oef_message.get("query"))) + elif oef_type == OEFMessage.Type.SEARCH_SERVICES: + self.search_services(sender, request_id, cast(Query, oef_message.get("query"))) + else: + # request not recognized + pass + + def handle_agent_message(self, envelope: Envelope) -> None: + """ + Forward an envelope to the right agent. + + :param envelope: the envelope + :return: None + """ + destination = envelope.to + + if destination not in self._queues: + msg = OEFMessage(oef_type=OEFMessage.Type.DIALOGUE_ERROR, id=STUB_DIALOGUE_ID, dialogue_id=STUB_DIALOGUE_ID, origin=destination) + msg_bytes = OEFSerializer().encode(msg) + error_envelope = Envelope(to=destination, sender=envelope.sender, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + self._send(error_envelope) + return + else: + self._send(envelope) + + def register_service(self, public_key: str, service_description: Description): + """ + Register a service agent in the service directory of the node. + + :param public_key: the public key of the service agent to be registered. + :param service_description: the description of the service agent to be registered. + :return: None + """ + with self._lock: + self.services[public_key].append(service_description) + + def register_service_wide(self, public_key: str, service_description: Description): + """Register service wide.""" + raise NotImplementedError + + def unregister_service(self, public_key: str, msg_id: int, service_description: Description) -> None: + """ + Unregister a service agent. + + :param public_key: the public key of the service agent to be unregistered. + :param msg_id: the message id of the request. + :param service_description: the description of the service agent to be unregistered. + :return: None + """ + with self._lock: + if public_key not in self.services: + msg = OEFMessage(oef_type=OEFMessage.Type.OEF_ERROR, id=msg_id, operation=OEFMessage.OEFErrorOperation.UNREGISTER_SERVICE) + msg_bytes = OEFSerializer().encode(msg) + envelope = Envelope(to=public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + self._send(envelope) + else: + self.services[public_key].remove(service_description) + if len(self.services[public_key]) == 0: + self.services.pop(public_key) + + def search_agents(self, public_key: str, search_id: int, query: Query) -> None: + """ + Search the agents in the local Agent Directory, and send back the result. + + This is actually a dummy search, it will return all the registered agents with the specified data model. + If the data model is not specified, it will return all the agents. + + :param public_key: the source of the search request. + :param search_id: the search identifier associated with the search request. + :param query: the query that constitutes the search. + :return: None + """ + result = [] # type: List[str] + if query.model is None: + result = list(set(self.services.keys())) + else: + for agent_public_key, description in self.agents.items(): + if query.model == description.data_model: + result.append(agent_public_key) + + msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=sorted(set(result))) + msg_bytes = OEFSerializer().encode(msg) + envelope = Envelope(to=public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + self._send(envelope) + + def search_services(self, public_key: str, search_id: int, query: Query) -> None: + """ + Search the agents in the local Service Directory, and send back the result. + + This is actually a dummy search, it will return all the registered agents with the specified data model. + If the data model is not specified, it will return all the agents. + + :param public_key: the source of the search request. + :param search_id: the search identifier associated with the search request. + :param query: the query that constitutes the search. + :return: None + """ + result = [] # type: List[str] + if query.model is None: + result = list(set(self.services.keys())) + else: + for agent_public_key, descriptions in self.services.items(): + for description in descriptions: + if description.data_model == query.model: + result.append(agent_public_key) + + msg = OEFMessage(oef_type=OEFMessage.Type.SEARCH_RESULT, id=search_id, agents=sorted(set(result))) + msg_bytes = OEFSerializer().encode(msg) + envelope = Envelope(to=public_key, sender=DEFAULT_OEF, protocol_id=OEFMessage.protocol_id, message=msg_bytes) + self._send(envelope) + + def _send(self, envelope: Envelope): + """Send a message.""" + destination = envelope.to + self._queues[destination].put_nowait(envelope) + + def disconnect(self, public_key: str) -> None: + """ + Disconnect. + + :param public_key: the public key + :return: None + """ + with self._lock: + self._queues.pop(public_key, None) + self.services.pop(public_key, None) + self.agents.pop(public_key, None) + + +class LocalNodeChannel(Channel): + """Channel implementation for the local node.""" + + def __init__(self, public_key: str, local_node: LocalNode): + """ + Initialize a OEF proxy for a local OEF Node (that is, :class:`~oef.proxy.OEFLocalProxy.LocalNode`. + + :param public_key: the public key used in the protocols. + :param local_node: the Local OEF Node object. This reference must be the same across the agents of interest. + """ + self.public_key = public_key + self.local_node = local_node + + def connect(self) -> Optional[Queue]: + """ + Set up the connection. + + :return: A queue or None. + """ + return self.local_node.connect(self.public_key) + + def disconnect(self) -> None: + """ + Tear down the connection. + + :return: None. + """ + return self.local_node.disconnect(self.public_key) + + def send(self, envelope: Envelope) -> None: + """ + Send an envelope. + + :param envelope: the envelope to send. + :return: None. + """ + return self.local_node.send(envelope) + + +class OEFLocalConnection(Connection): + """ + Proxy to the functionality of the OEF. + + It allows the interaction between agents, but not the search functionality. + It is useful for local testing. + """ + + def __init__(self, public_key: str, local_node: LocalNode): + """ + Initialize a OEF proxy for a local OEF Node (that is, :class:`~oef.proxy.OEFLocalProxy.LocalNode`. + + :param public_key: the public key used in the protocols. + :param local_node: the Local OEF Node object. This reference must be the same across the agents of interest. + """ + super().__init__() + self.public_key = public_key + self.channel = LocalNodeChannel(public_key, local_node) + + self._connection = None # type: Optional[Queue] + + self._stopped = True + self.in_thread = None + self.out_thread = None + + def _fetch(self) -> None: + """ + Fetch the messages from the outqueue and send them. + + :return: None + """ + while not self._stopped: + try: + msg = self.out_queue.get(block=True, timeout=2.0) + self.send(msg) + except queue.Empty: + pass + + def _receive_loop(self): + """Receive messages.""" + while not self._stopped: + try: + data = self._connection.get(timeout=2.0) + self.in_queue.put_nowait(data) + except queue.Empty: + pass + + @property + def is_established(self) -> bool: + """Return True if the connection has been established, False otherwise.""" + return self._connection is not None + + def connect(self): + """Connect to the local OEF Node.""" + if self._stopped: + self._stopped = False + self._connection = self.channel.connect() + self.in_thread = Thread(target=self._receive_loop) + self.out_thread = Thread(target=self._fetch) + self.in_thread.start() + self.out_thread.start() + + def disconnect(self): + """Disconnect from the local OEF Node.""" + if not self._stopped: + self._stopped = True + self.in_thread.join() + self.out_thread.join() + self.in_thread = None + self.out_thread = None + self.channel.disconnect() + self.stop() + + def send(self, envelope: Envelope): + """Send a message.""" + if not self.is_established: + raise ConnectionError("Connection not established yet. Please use 'connect()'.") + self.channel.send(envelope) + + def stop(self): + """Tear down the connection.""" + self._connection = None + + @classmethod + def from_config(cls, public_key: str, connection_configuration: ConnectionConfig) -> 'Connection': + """Get the Local OEF connection from the connection configuration. + + :param public_key: the public key of the agent. + :param connection_configuration: the connection configuration object. + :return: the connection object + """ + local_node = LocalNode() + return OEFLocalConnection(public_key, local_node) diff --git a/tests/data/dummy_aea/connections/local/connection.yaml b/tests/data/dummy_aea/connections/local/connection.yaml new file mode 100644 index 0000000000..44a4504b9a --- /dev/null +++ b/tests/data/dummy_aea/connections/local/connection.yaml @@ -0,0 +1,8 @@ +name: local +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +class_name: OEFLocalConnection +supported_protocols: ["oef"] +config: {} \ No newline at end of file diff --git a/tests/data/dummy_aea/protocols/__init__.py b/tests/data/dummy_aea/protocols/__init__.py new file mode 100644 index 0000000000..238c8c9633 --- /dev/null +++ b/tests/data/dummy_aea/protocols/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the implementation of the protocols.""" diff --git a/tests/data/dummy_aea/protocols/default/__init__.py b/tests/data/dummy_aea/protocols/default/__init__.py new file mode 100644 index 0000000000..52e51b51e3 --- /dev/null +++ b/tests/data/dummy_aea/protocols/default/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the default protocol.""" diff --git a/tests/data/dummy_aea/protocols/default/message.py b/tests/data/dummy_aea/protocols/default/message.py new file mode 100644 index 0000000000..475714a2f0 --- /dev/null +++ b/tests/data/dummy_aea/protocols/default/message.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the default message definition.""" +from enum import Enum +from typing import Optional + +from aea.protocols.base import Message + + +class DefaultMessage(Message): + """The Default message class.""" + + protocol_id = "default" + + class Type(Enum): + """Default message types.""" + + BYTES = "bytes" + ERROR = "error" + + def __str__(self): + """Get the string representation.""" + return self.value + + class ErrorCode(Enum): + """The error codes.""" + + UNSUPPORTED_PROTOCOL = -10001 + DECODING_ERROR = -10002 + INVALID_MESSAGE = -10003 + UNSUPPORTED_SKILL = -10004 + + def __init__(self, type: Optional[Type] = None, + **kwargs): + """ + Initialize. + + :param type: the type. + """ + super().__init__(type=type, **kwargs) + assert self.check_consistency(), "DefaultMessage initialization inconsistent." diff --git a/tests/data/dummy_aea/protocols/default/protocol.yaml b/tests/data/dummy_aea/protocols/default/protocol.yaml new file mode 100644 index 0000000000..6e9fd1dc97 --- /dev/null +++ b/tests/data/dummy_aea/protocols/default/protocol.yaml @@ -0,0 +1,5 @@ +name: 'default' +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" diff --git a/tests/data/dummy_aea/protocols/default/serialization.py b/tests/data/dummy_aea/protocols/default/serialization.py new file mode 100644 index 0000000000..080b8f386b --- /dev/null +++ b/tests/data/dummy_aea/protocols/default/serialization.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""Serialization module for the default protocol.""" +import base64 +import json +from typing import cast + +from aea.protocols.base import Message +from aea.protocols.base import Serializer +from aea.protocols.default.message import DefaultMessage + + +class DefaultSerializer(Serializer): + """Serialization for the 'default' protocol.""" + + def encode(self, msg: Message) -> bytes: + """Encode a 'default' message into bytes.""" + body = {} # Dict[str, Any] + + msg_type = DefaultMessage.Type(msg.get("type")) + body["type"] = str(msg_type.value) + + if msg_type == DefaultMessage.Type.BYTES: + content = cast(bytes, msg.get("content")) + body["content"] = base64.b64encode(content).decode("utf-8") + elif msg_type == DefaultMessage.Type.ERROR: + body["error_code"] = cast(str, msg.get("error_code")) + body["error_msg"] = cast(str, msg.get("error_msg")) + body["error_data"] = cast(str, msg.get("error_data")) + else: + raise ValueError("Type not recognized.") + + bytes_msg = json.dumps(body).encode("utf-8") + return bytes_msg + + def decode(self, obj: bytes) -> Message: + """Decode bytes into a 'default' message.""" + json_body = json.loads(obj.decode("utf-8")) + body = {} + + msg_type = DefaultMessage.Type(json_body["type"]) + body["type"] = msg_type + if msg_type == DefaultMessage.Type.BYTES: + content = base64.b64decode(json_body["content"].encode("utf-8")) + body["content"] = content # type: ignore + elif msg_type == DefaultMessage.Type.ERROR: + body["error_code"] = json_body["error_code"] + body["error_msg"] = json_body["error_msg"] + body["error_data"] = json_body["error_data"] + else: + raise ValueError("Type not recognized.") + + return DefaultMessage(type=msg_type, body=body) diff --git a/aea/protocols/base/__init__.py b/tests/data/dummy_aea/protocols/fipa/__init__.py similarity index 92% rename from aea/protocols/base/__init__.py rename to tests/data/dummy_aea/protocols/fipa/__init__.py index 4f1c59df3c..88af132fb8 100644 --- a/aea/protocols/base/__init__.py +++ b/tests/data/dummy_aea/protocols/fipa/__init__.py @@ -18,4 +18,4 @@ # # ------------------------------------------------------------------------------ -"""This module contains the support resources for the base protocol.""" +"""This module contains the support resources for the FIPA protocol.""" diff --git a/tests/data/dummy_aea/protocols/fipa/fipa.proto b/tests/data/dummy_aea/protocols/fipa/fipa.proto new file mode 100644 index 0000000000..69b04db2f9 --- /dev/null +++ b/tests/data/dummy_aea/protocols/fipa/fipa.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; + +package fetch.aea.fipa; + +message FIPAMessage{ + + message CFP{ + message Nothing { + } + oneof query{ + bytes bytes = 2; + Nothing nothing = 3; + } + } + message Propose{ + repeated bytes proposal = 4; + } + message Accept{} + message MatchAccept{} + message Decline{} + + int32 message_id = 1; + int32 dialogue_id = 2; + int32 target = 3; + oneof performative{ + CFP cfp = 4; + Propose propose = 5; + Accept accept = 6; + MatchAccept match_accept = 7; + Decline decline = 8; + } +} diff --git a/tests/data/dummy_aea/protocols/fipa/fipa_pb2.py b/tests/data/dummy_aea/protocols/fipa/fipa_pb2.py new file mode 100644 index 0000000000..6f5d03ae5a --- /dev/null +++ b/tests/data/dummy_aea/protocols/fipa/fipa_pb2.py @@ -0,0 +1,364 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: fipa.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +from google.protobuf import descriptor_pb2 +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='fipa.proto', + package='fetch.aea.fipa', + syntax='proto3', + serialized_pb=_b('\n\nfipa.proto\x12\x0e\x66\x65tch.aea.fipa\"\x96\x04\n\x0b\x46IPAMessage\x12\x12\n\nmessage_id\x18\x01 \x01(\x05\x12\x13\n\x0b\x64ialogue_id\x18\x02 \x01(\x05\x12\x0e\n\x06target\x18\x03 \x01(\x05\x12.\n\x03\x63\x66p\x18\x04 \x01(\x0b\x32\x1f.fetch.aea.fipa.FIPAMessage.CFPH\x00\x12\x36\n\x07propose\x18\x05 \x01(\x0b\x32#.fetch.aea.fipa.FIPAMessage.ProposeH\x00\x12\x34\n\x06\x61\x63\x63\x65pt\x18\x06 \x01(\x0b\x32\".fetch.aea.fipa.FIPAMessage.AcceptH\x00\x12?\n\x0cmatch_accept\x18\x07 \x01(\x0b\x32\'.fetch.aea.fipa.FIPAMessage.MatchAcceptH\x00\x12\x36\n\x07\x64\x65\x63line\x18\x08 \x01(\x0b\x32#.fetch.aea.fipa.FIPAMessage.DeclineH\x00\x1a\x66\n\x03\x43\x46P\x12\x0f\n\x05\x62ytes\x18\x02 \x01(\x0cH\x00\x12:\n\x07nothing\x18\x03 \x01(\x0b\x32\'.fetch.aea.fipa.FIPAMessage.CFP.NothingH\x00\x1a\t\n\x07NothingB\x07\n\x05query\x1a\x1b\n\x07Propose\x12\x10\n\x08proposal\x18\x04 \x03(\x0c\x1a\x08\n\x06\x41\x63\x63\x65pt\x1a\r\n\x0bMatchAccept\x1a\t\n\x07\x44\x65\x63lineB\x0e\n\x0cperformativeb\x06proto3') +) +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + + + + +_FIPAMESSAGE_CFP_NOTHING = _descriptor.Descriptor( + name='Nothing', + full_name='fetch.aea.fipa.FIPAMessage.CFP.Nothing', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=466, + serialized_end=475, +) + +_FIPAMESSAGE_CFP = _descriptor.Descriptor( + name='CFP', + full_name='fetch.aea.fipa.FIPAMessage.CFP', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='bytes', full_name='fetch.aea.fipa.FIPAMessage.CFP.bytes', index=0, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='nothing', full_name='fetch.aea.fipa.FIPAMessage.CFP.nothing', index=1, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[_FIPAMESSAGE_CFP_NOTHING, ], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='query', full_name='fetch.aea.fipa.FIPAMessage.CFP.query', + index=0, containing_type=None, fields=[]), + ], + serialized_start=382, + serialized_end=484, +) + +_FIPAMESSAGE_PROPOSE = _descriptor.Descriptor( + name='Propose', + full_name='fetch.aea.fipa.FIPAMessage.Propose', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='proposal', full_name='fetch.aea.fipa.FIPAMessage.Propose.proposal', index=0, + number=4, type=12, 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, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=486, + serialized_end=513, +) + +_FIPAMESSAGE_ACCEPT = _descriptor.Descriptor( + name='Accept', + full_name='fetch.aea.fipa.FIPAMessage.Accept', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=515, + serialized_end=523, +) + +_FIPAMESSAGE_MATCHACCEPT = _descriptor.Descriptor( + name='MatchAccept', + full_name='fetch.aea.fipa.FIPAMessage.MatchAccept', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=525, + serialized_end=538, +) + +_FIPAMESSAGE_DECLINE = _descriptor.Descriptor( + name='Decline', + full_name='fetch.aea.fipa.FIPAMessage.Decline', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=540, + serialized_end=549, +) + +_FIPAMESSAGE = _descriptor.Descriptor( + name='FIPAMessage', + full_name='fetch.aea.fipa.FIPAMessage', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='message_id', full_name='fetch.aea.fipa.FIPAMessage.message_id', 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, + options=None), + _descriptor.FieldDescriptor( + name='dialogue_id', full_name='fetch.aea.fipa.FIPAMessage.dialogue_id', index=1, + number=2, 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, + options=None), + _descriptor.FieldDescriptor( + name='target', full_name='fetch.aea.fipa.FIPAMessage.target', index=2, + number=3, 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, + options=None), + _descriptor.FieldDescriptor( + name='cfp', full_name='fetch.aea.fipa.FIPAMessage.cfp', index=3, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='propose', full_name='fetch.aea.fipa.FIPAMessage.propose', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='accept', full_name='fetch.aea.fipa.FIPAMessage.accept', index=5, + number=6, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='match_accept', full_name='fetch.aea.fipa.FIPAMessage.match_accept', index=6, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='decline', full_name='fetch.aea.fipa.FIPAMessage.decline', index=7, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[_FIPAMESSAGE_CFP, _FIPAMESSAGE_PROPOSE, _FIPAMESSAGE_ACCEPT, _FIPAMESSAGE_MATCHACCEPT, _FIPAMESSAGE_DECLINE, ], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='performative', full_name='fetch.aea.fipa.FIPAMessage.performative', + index=0, containing_type=None, fields=[]), + ], + serialized_start=31, + serialized_end=565, +) + +_FIPAMESSAGE_CFP_NOTHING.containing_type = _FIPAMESSAGE_CFP +_FIPAMESSAGE_CFP.fields_by_name['nothing'].message_type = _FIPAMESSAGE_CFP_NOTHING +_FIPAMESSAGE_CFP.containing_type = _FIPAMESSAGE +_FIPAMESSAGE_CFP.oneofs_by_name['query'].fields.append( + _FIPAMESSAGE_CFP.fields_by_name['bytes']) +_FIPAMESSAGE_CFP.fields_by_name['bytes'].containing_oneof = _FIPAMESSAGE_CFP.oneofs_by_name['query'] +_FIPAMESSAGE_CFP.oneofs_by_name['query'].fields.append( + _FIPAMESSAGE_CFP.fields_by_name['nothing']) +_FIPAMESSAGE_CFP.fields_by_name['nothing'].containing_oneof = _FIPAMESSAGE_CFP.oneofs_by_name['query'] +_FIPAMESSAGE_PROPOSE.containing_type = _FIPAMESSAGE +_FIPAMESSAGE_ACCEPT.containing_type = _FIPAMESSAGE +_FIPAMESSAGE_MATCHACCEPT.containing_type = _FIPAMESSAGE +_FIPAMESSAGE_DECLINE.containing_type = _FIPAMESSAGE +_FIPAMESSAGE.fields_by_name['cfp'].message_type = _FIPAMESSAGE_CFP +_FIPAMESSAGE.fields_by_name['propose'].message_type = _FIPAMESSAGE_PROPOSE +_FIPAMESSAGE.fields_by_name['accept'].message_type = _FIPAMESSAGE_ACCEPT +_FIPAMESSAGE.fields_by_name['match_accept'].message_type = _FIPAMESSAGE_MATCHACCEPT +_FIPAMESSAGE.fields_by_name['decline'].message_type = _FIPAMESSAGE_DECLINE +_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( + _FIPAMESSAGE.fields_by_name['cfp']) +_FIPAMESSAGE.fields_by_name['cfp'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] +_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( + _FIPAMESSAGE.fields_by_name['propose']) +_FIPAMESSAGE.fields_by_name['propose'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] +_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( + _FIPAMESSAGE.fields_by_name['accept']) +_FIPAMESSAGE.fields_by_name['accept'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] +_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( + _FIPAMESSAGE.fields_by_name['match_accept']) +_FIPAMESSAGE.fields_by_name['match_accept'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] +_FIPAMESSAGE.oneofs_by_name['performative'].fields.append( + _FIPAMESSAGE.fields_by_name['decline']) +_FIPAMESSAGE.fields_by_name['decline'].containing_oneof = _FIPAMESSAGE.oneofs_by_name['performative'] +DESCRIPTOR.message_types_by_name['FIPAMessage'] = _FIPAMESSAGE + +FIPAMessage = _reflection.GeneratedProtocolMessageType('FIPAMessage', (_message.Message,), dict( + + CFP = _reflection.GeneratedProtocolMessageType('CFP', (_message.Message,), dict( + + Nothing = _reflection.GeneratedProtocolMessageType('Nothing', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_CFP_NOTHING, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.CFP.Nothing) + )) + , + DESCRIPTOR = _FIPAMESSAGE_CFP, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.CFP) + )) + , + + Propose = _reflection.GeneratedProtocolMessageType('Propose', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_PROPOSE, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.Propose) + )) + , + + Accept = _reflection.GeneratedProtocolMessageType('Accept', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_ACCEPT, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.Accept) + )) + , + + MatchAccept = _reflection.GeneratedProtocolMessageType('MatchAccept', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_MATCHACCEPT, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.MatchAccept) + )) + , + + Decline = _reflection.GeneratedProtocolMessageType('Decline', (_message.Message,), dict( + DESCRIPTOR = _FIPAMESSAGE_DECLINE, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage.Decline) + )) + , + DESCRIPTOR = _FIPAMESSAGE, + __module__ = 'fipa_pb2' + # @@protoc_insertion_point(class_scope:fetch.aea.fipa.FIPAMessage) + )) +_sym_db.RegisterMessage(FIPAMessage) +_sym_db.RegisterMessage(FIPAMessage.CFP) +_sym_db.RegisterMessage(FIPAMessage.CFP.Nothing) +_sym_db.RegisterMessage(FIPAMessage.Propose) +_sym_db.RegisterMessage(FIPAMessage.Accept) +_sym_db.RegisterMessage(FIPAMessage.MatchAccept) +_sym_db.RegisterMessage(FIPAMessage.Decline) + + +# @@protoc_insertion_point(module_scope) diff --git a/tests/data/dummy_aea/protocols/fipa/message.py b/tests/data/dummy_aea/protocols/fipa/message.py new file mode 100644 index 0000000000..729d8deba5 --- /dev/null +++ b/tests/data/dummy_aea/protocols/fipa/message.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the FIPA message definition.""" +from enum import Enum +from typing import Optional, Union + +from aea.protocols.base import Message +from aea.protocols.oef.models import Description + + +class FIPAMessage(Message): + """The FIPA message class.""" + + protocol_id = "fipa" + + class Performative(Enum): + """FIPA performatives.""" + + CFP = "cfp" + PROPOSE = "propose" + ACCEPT = "accept" + MATCH_ACCEPT = "match_accept" + DECLINE = "decline" + + def __str__(self): + """Get string representation.""" + return self.value + + def __init__(self, message_id: Optional[int] = None, + dialogue_id: Optional[int] = None, + target: Optional[int] = None, + performative: Optional[Union[str, Performative]] = None, + **kwargs): + """ + Initialize. + + :param message_id: the message id. + :param dialogue_id: the dialogue id. + :param target: the message target. + :param performative: the message performative. + """ + super().__init__(id=message_id, + dialogue_id=dialogue_id, + target=target, + performative=FIPAMessage.Performative(performative), + **kwargs) + assert self.check_consistency(), "FIPAMessage initialization inconsistent." + + def check_consistency(self) -> bool: + """Check that the data is consistent.""" + try: + assert self.is_set("target") + performative = FIPAMessage.Performative(self.get("performative")) + if performative == FIPAMessage.Performative.CFP: + query = self.get("query") + assert isinstance(query, dict) or isinstance(query, bytes) or query is None + elif performative == FIPAMessage.Performative.PROPOSE: + proposal = self.get("proposal") + assert type(proposal) == list and all(isinstance(d, Description) or type(d) == bytes for d in proposal) # type: ignore + elif performative == FIPAMessage.Performative.ACCEPT \ + or performative == FIPAMessage.Performative.MATCH_ACCEPT \ + or performative == FIPAMessage.Performative.DECLINE: + pass + else: + raise ValueError("Performative not recognized.") + + except (AssertionError, ValueError, KeyError): + return False + + return True diff --git a/tests/data/dummy_aea/protocols/fipa/protocol.yaml b/tests/data/dummy_aea/protocols/fipa/protocol.yaml new file mode 100644 index 0000000000..8719746ed9 --- /dev/null +++ b/tests/data/dummy_aea/protocols/fipa/protocol.yaml @@ -0,0 +1,5 @@ +name: 'fipa' +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" diff --git a/tests/data/dummy_aea/protocols/fipa/serialization.py b/tests/data/dummy_aea/protocols/fipa/serialization.py new file mode 100644 index 0000000000..65fe2b56a9 --- /dev/null +++ b/tests/data/dummy_aea/protocols/fipa/serialization.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""Serialization for the FIPA protocol.""" +import pickle +from typing import cast + +from aea.protocols.base import Message +from aea.protocols.base import Serializer +from aea.protocols.fipa import fipa_pb2 +from aea.protocols.fipa.message import FIPAMessage +from aea.protocols.oef.models import Description + + +class FIPASerializer(Serializer): + """Serialization for the FIPA protocol.""" + + def encode(self, msg: Message) -> bytes: + """Encode a FIPA message into bytes.""" + fipa_msg = fipa_pb2.FIPAMessage() + fipa_msg.message_id = msg.get("id") + fipa_msg.dialogue_id = msg.get("dialogue_id") + fipa_msg.target = msg.get("target") + + performative_id = FIPAMessage.Performative(msg.get("performative")) + if performative_id == FIPAMessage.Performative.CFP: + performative = fipa_pb2.FIPAMessage.CFP() # type: ignore + query = msg.get("query") + if query is None or query == b"": + nothing = fipa_pb2.FIPAMessage.CFP.Nothing() # type: ignore + performative.nothing.CopyFrom(nothing) + elif type(query) == bytes: + performative.bytes = query + else: + raise ValueError("Query type not supported: {}".format(type(query))) + fipa_msg.cfp.CopyFrom(performative) + elif performative_id == FIPAMessage.Performative.PROPOSE: + performative = fipa_pb2.FIPAMessage.Propose() # type: ignore + proposal = cast(Description, msg.get("proposal")) + p_array_bytes = [pickle.dumps(p) for p in proposal] + performative.proposal.extend(p_array_bytes) + fipa_msg.propose.CopyFrom(performative) + + elif performative_id == FIPAMessage.Performative.ACCEPT: + performative = fipa_pb2.FIPAMessage.Accept() # type: ignore + fipa_msg.accept.CopyFrom(performative) + elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT: + performative = fipa_pb2.FIPAMessage.MatchAccept() # type: ignore + fipa_msg.match_accept.CopyFrom(performative) + elif performative_id == FIPAMessage.Performative.DECLINE: + performative = fipa_pb2.FIPAMessage.Decline() # type: ignore + fipa_msg.decline.CopyFrom(performative) + else: + raise ValueError("Performative not valid: {}".format(performative_id)) + + fipa_bytes = fipa_msg.SerializeToString() + return fipa_bytes + + def decode(self, obj: bytes) -> Message: + """Decode bytes into a FIPA message.""" + fipa_pb = fipa_pb2.FIPAMessage() + fipa_pb.ParseFromString(obj) + message_id = fipa_pb.message_id + dialogue_id = fipa_pb.dialogue_id + target = fipa_pb.target + + performative = fipa_pb.WhichOneof("performative") + performative_id = FIPAMessage.Performative(str(performative)) + performative_content = dict() + if performative_id == FIPAMessage.Performative.CFP: + query_type = fipa_pb.cfp.WhichOneof("query") + if query_type == "nothing": + query = None + elif query_type == "bytes": + query = fipa_pb.cfp.bytes + else: + raise ValueError("Query type not recognized.") + + performative_content["query"] = query + elif performative_id == FIPAMessage.Performative.PROPOSE: + descriptions = [] + for p_bytes in fipa_pb.propose.proposal: + p = pickle.loads(p_bytes) # type: Description + descriptions.append(p) + performative_content["proposal"] = descriptions + elif performative_id == FIPAMessage.Performative.ACCEPT: + pass + elif performative_id == FIPAMessage.Performative.MATCH_ACCEPT: + pass + elif performative_id == FIPAMessage.Performative.DECLINE: + pass + else: + raise ValueError("Performative not valid: {}.".format(performative)) + + return FIPAMessage(message_id=message_id, dialogue_id=dialogue_id, target=target, + performative=performative, **performative_content) diff --git a/tests/data/dummy_aea/skills/__init__.py b/tests/data/dummy_aea/skills/__init__.py new file mode 100644 index 0000000000..8c883d40ef --- /dev/null +++ b/tests/data/dummy_aea/skills/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the implementation of the skills.""" diff --git a/tests/data/dummy_aea/skills/dummy/__init__.py b/tests/data/dummy_aea/skills/dummy/__init__.py new file mode 100644 index 0000000000..2d8bbdabc2 --- /dev/null +++ b/tests/data/dummy_aea/skills/dummy/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains a dummy skill for an AEA.""" diff --git a/tests/data/dummy_aea/skills/dummy/behaviours.py b/tests/data/dummy_aea/skills/dummy/behaviours.py new file mode 100644 index 0000000000..6c544b2ee1 --- /dev/null +++ b/tests/data/dummy_aea/skills/dummy/behaviours.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the behaviours for the 'echo' skill.""" + +from aea.skills.base import Behaviour + + +class DummyBehaviour(Behaviour): + """Dummy behaviour.""" + + def __init__(self, **kwargs): + """Initialize the echo behaviour.""" + super().__init__(**kwargs) + self.kwargs = kwargs + self.nb_act_called = 0 + self.nb_teardown_called = 0 + + def act(self) -> None: + """Act according to the behaviour.""" + self.nb_act_called += 1 + + def teardown(self) -> None: + """Teardown the behaviour.""" + self.nb_teardown_called += 1 diff --git a/tests/data/dummy_aea/skills/dummy/handler.py b/tests/data/dummy_aea/skills/dummy/handler.py new file mode 100644 index 0000000000..2d5c508d16 --- /dev/null +++ b/tests/data/dummy_aea/skills/dummy/handler.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the handler for the 'echo' skill.""" + +from aea.mail.base import Envelope +from aea.skills.base import Handler + + +class DummyHandler(Handler): + """Echo handler.""" + + SUPPORTED_PROTOCOL = "default" + + def __init__(self, **kwargs): + """Initialize the handler.""" + super().__init__(**kwargs) + self.kwargs = kwargs + self.handled_envelopes = [] + self.nb_teardown_called = 0 + + def handle_envelope(self, envelope: Envelope) -> None: + """ + Handle envelopes. + + :param envelope: the envelope + :return: None + """ + self.handled_envelopes.append(envelope) + + def teardown(self) -> None: + """ + Teardown the handler. + + :return: None + """ + self.nb_teardown_called += 1 diff --git a/tests/data/dummy_aea/skills/dummy/skill.yaml b/tests/data/dummy_aea/skills/dummy/skill.yaml new file mode 100644 index 0000000000..98f1fc2221 --- /dev/null +++ b/tests/data/dummy_aea/skills/dummy/skill.yaml @@ -0,0 +1,23 @@ +name: dummy +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +behaviours: + - behaviour: + class_name: DummyBehaviour + args: + behaviour_arg_1: 1 + behaviour_arg_2: "2" +handler: + class_name: DummyHandler + args: + handler_arg_1: 1 + handler_arg_2: "2" +tasks: + - task: + class_name: DummyTask + args: + task_arg_1: 1 + task_arg_2: "2" +protocol: "default" diff --git a/tests/data/dummy_aea/skills/dummy/tasks.py b/tests/data/dummy_aea/skills/dummy/tasks.py new file mode 100644 index 0000000000..9d9a388175 --- /dev/null +++ b/tests/data/dummy_aea/skills/dummy/tasks.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the tasks for the 'echo' skill.""" +from aea.skills.base import Task + + +class DummyTask(Task): + """Dummy task.""" + + def __init__(self, **kwargs): + """Initialize the task.""" + super().__init__(**kwargs) + self.kwargs = kwargs + self.nb_execute_called = 0 + self.nb_teardown_called = 0 + + def execute(self) -> None: + """Execute the task.""" + self.nb_execute_called += 1 + + def teardown(self) -> None: + """Teardown the task.""" + self.nb_teardown_called += 1 diff --git a/tests/data/dummy_aea/skills/error/__init__.py b/tests/data/dummy_aea/skills/error/__init__.py new file mode 100644 index 0000000000..96c80ac32c --- /dev/null +++ b/tests/data/dummy_aea/skills/error/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the implementation of the error skill.""" diff --git a/tests/data/dummy_aea/skills/error/behaviours.py b/tests/data/dummy_aea/skills/error/behaviours.py new file mode 100644 index 0000000000..97650c4526 --- /dev/null +++ b/tests/data/dummy_aea/skills/error/behaviours.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This package contains the error behaviours.""" + +from aea.skills.base import Behaviour + + +class ErrorBehaviour(Behaviour): + """This class implements the error behaviour.""" + + def act(self) -> None: + """ + Implement the act. + + :return: None + """ + pass + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + pass diff --git a/tests/data/dummy_aea/skills/error/handler.py b/tests/data/dummy_aea/skills/error/handler.py new file mode 100644 index 0000000000..e7af794df3 --- /dev/null +++ b/tests/data/dummy_aea/skills/error/handler.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This package contains the implementation of the handler for the 'default' protocol.""" +import base64 +import logging +from typing import Optional + +from aea.configurations.base import ProtocolId +from aea.mail.base import Envelope +from aea.protocols.base import Protocol +from aea.protocols.default.message import DefaultMessage +from aea.protocols.default.serialization import DefaultSerializer +from aea.skills.base import Handler + +logger = logging.getLogger(__name__) + + +class ErrorHandler(Handler): + """This class implements the error handler.""" + + SUPPORTED_PROTOCOL = 'default' # type: Optional[ProtocolId] + + def handle_envelope(self, envelope: Envelope) -> None: + """ + Implement the reaction to an envelope. + + :param envelope: the envelope + :return: None + """ + pass + + def teardown(self) -> None: + """ + Implement the handler teardown. + + :return: None + """ + pass + + def send_unsupported_protocol(self, envelope: Envelope) -> None: + """ + Handle the received envelope in case the protocol is not supported. + + :param envelope: the envelope + :return: None + """ + logger.warning("Unsupported protocol: {}".format(envelope.protocol_id)) + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.UNSUPPORTED_PROTOCOL.value, + error_msg="Unsupported protocol.", + error_data={"protocol_id": envelope.protocol_id}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) + + def send_decoding_error(self, envelope: Envelope) -> None: + """ + Handle a decoding error. + + :param envelope: the envelope + :return: None + """ + logger.warning("Decoding error: {}.".format(envelope)) + encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.DECODING_ERROR.value, + error_msg="Decoding error.", + error_data={"envelope": encoded_envelope}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) + + def send_invalid_message(self, envelope: Envelope) -> None: + """ + Handle an message that is invalid wrt a protocol. + + :param envelope: the envelope + :return: None + """ + logger.warning("Invalid message wrt protocol: {}.".format(envelope.protocol_id)) + encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.INVALID_MESSAGE.value, + error_msg="Invalid message.", + error_data={"envelope": encoded_envelope}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) + + def send_unsupported_skill(self, envelope: Envelope, protocol: Protocol) -> None: + """ + Handle the received envelope in case the skill is not supported. + + :param envelope: the envelope + :param protocol: the protocol + :return: None + """ + logger.warning("Cannot handle envelope: no handler registered for the protocol '{}'.".format(protocol.id)) + encoded_envelope = base64.b85encode(envelope.encode()).decode("utf-8") + reply = DefaultMessage(type=DefaultMessage.Type.ERROR, + error_code=DefaultMessage.ErrorCode.UNSUPPORTED_SKILL.value, + error_msg="Unsupported skill.", + error_data={"envelope": encoded_envelope}) + self.context.outbox.put_message(to=envelope.sender, sender=self.context.agent_public_key, + protocol_id=DefaultMessage.protocol_id, + message=DefaultSerializer().encode(reply)) diff --git a/tests/data/dummy_aea/skills/error/skill.yaml b/tests/data/dummy_aea/skills/error/skill.yaml new file mode 100644 index 0000000000..e62d327565 --- /dev/null +++ b/tests/data/dummy_aea/skills/error/skill.yaml @@ -0,0 +1,12 @@ +name: error +authors: Fetch.AI Limited +version: 0.1.0 +license: Apache 2.0 +url: "" +behaviours: [] +handler: + class_name: ErrorHandler + args: + foo: bar +tasks: [] +protocol: 'default' diff --git a/tests/data/dummy_aea/skills/error/tasks.py b/tests/data/dummy_aea/skills/error/tasks.py new file mode 100644 index 0000000000..be7ceb7b9e --- /dev/null +++ b/tests/data/dummy_aea/skills/error/tasks.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This package contains the implementation of the error tasks.""" + +from aea.skills.base import Task + + +class ErrorTask(Task): + """This class implements the error task.""" + + def execute(self) -> None: + """ + Implement the task execution. + + :param envelope: the envelope + :return: None + """ + pass + + def teardown(self) -> None: + """ + Implement the task teardown. + + :return: None + """ + pass diff --git a/tests/test_aea.py b/tests/test_aea.py new file mode 100644 index 0000000000..0e4695e5de --- /dev/null +++ b/tests/test_aea.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests for aea.aea.py.""" + +from aea.aea import AEA +from aea.mail.base import MailBox, Envelope +from aea.protocols.default.message import DefaultMessage +from aea.protocols.default.serialization import DefaultSerializer +from aea.protocols.fipa.message import FIPAMessage +from aea.protocols.fipa.serialization import FIPASerializer +from aea.connections.local.connection import LocalNode, OEFLocalConnection +from aea.crypto.helpers import _create_temporary_private_key_pem_path +from aea.crypto.base import Crypto + +import time +from threading import Thread +from pathlib import Path + + +def test_initialiseAeA(): + """Tests the initialisation of the AeA.""" + node = LocalNode() + public_key_1 = "mailbox1" + path = "/tests/aea/" + mailbox1 = MailBox(OEFLocalConnection(public_key_1, node)) + myAea = AEA("Agent0", mailbox1, directory=str(Path(".").absolute()) + path) + assert AEA("Agent0", mailbox1), "Agent is not inisialised" + print(myAea.context) + assert myAea.context == myAea._context, "Cannot access the Agent's Context" + myAea.setup() + assert myAea.resources is not None,\ + "Resources must not be None after setup" + + +def test_act(): + """Tests the act function of the AeA.""" + node = LocalNode() + agent_name = "MyAgent" + path = "/tests/data/dummy_aea/" + private_key_pem_path = _create_temporary_private_key_pem_path() + crypto = Crypto(private_key_pem_path=private_key_pem_path) + public_key = crypto.public_key + mailbox = MailBox(OEFLocalConnection(public_key, node)) + + agent = AEA( + agent_name, + mailbox, + private_key_pem_path=private_key_pem_path, + directory=str(Path(".").absolute()) + path) + t = Thread(target=agent.start) + t.start() + time.sleep(1) + + behaviour = agent.resources.behaviour_registry.fetch("dummy") + assert behaviour[0].nb_act_called > 0, "Act() wasn't called" + agent.stop() + t.join() + + +def test_react(): + """Tests income messages.""" + node = LocalNode() + agent_name = "MyAgent" + path = "/tests/data/dummy_aea/" + private_key_pem_path = _create_temporary_private_key_pem_path() + crypto = Crypto(private_key_pem_path=private_key_pem_path) + public_key = crypto.public_key + mailbox = MailBox(OEFLocalConnection(public_key, node)) + + msg = DefaultMessage(type=DefaultMessage.Type.BYTES, content=b"hello") + message_bytes = DefaultSerializer().encode(msg) + + envelope = Envelope( + to="Agent1", + sender=public_key, + protocol_id="default", + message=message_bytes) + + agent = AEA( + agent_name, + mailbox, + private_key_pem_path=private_key_pem_path, + directory=str(Path(".").absolute()) + path) + t = Thread(target=agent.start) + t.start() + agent.mailbox.inbox._queue.put(envelope) + time.sleep(1) + handler = agent.resources\ + .handler_registry.fetch_by_skill('default', "dummy") + assert envelope in handler.handled_envelopes,\ + "The envelope is not inside the handled_envelopes." + agent.stop() + t.join() + + +def test_handle(): + """Tests handle method of an agent.""" + node = LocalNode() + agent_name = "MyAgent" + path = "/tests/data/dummy_aea/" + private_key_pem_path = _create_temporary_private_key_pem_path() + crypto = Crypto(private_key_pem_path=private_key_pem_path) + public_key = crypto.public_key + mailbox = MailBox(OEFLocalConnection(public_key, node)) + + msg = DefaultMessage(type=DefaultMessage.Type.BYTES, content=b"hello") + message_bytes = DefaultSerializer().encode(msg) + + envelope = Envelope( + to="Agent1", + sender=public_key, + protocol_id="unknown_protocol", + message=message_bytes) + + agent = AEA( + agent_name, + mailbox, + private_key_pem_path=private_key_pem_path, + directory=str(Path(".").absolute()) + path) + t = Thread(target=agent.start) + t.start() + agent.mailbox.inbox._queue.put(envelope) + env = agent.mailbox.outbox._queue.get(block=True, timeout=1) + assert env.protocol_id == "default",\ + "The envelope is not the expected protocol (Unsupported protocol)" + +# DECODING ERROR + msg = "hello".encode("utf-8") + envelope = Envelope( + to=public_key, + sender=public_key, + protocol_id='default', + message=msg) + agent.mailbox.inbox._queue.put(envelope) +# UNSUPPORTED SKILL + msg = FIPASerializer().encode( + FIPAMessage(performative=FIPAMessage.Performative.ACCEPT, + message_id=0, + dialogue_id=0, + destination=public_key, + target=1)) + envelope = Envelope( + to=public_key, + sender=public_key, + protocol_id="fipa", + message=msg) + agent.mailbox.inbox._queue.put(envelope) + agent.stop() + t.join() + +# unsupported skill for protocol oef +# msg = FIPASerializer().encode(\ +# FIPAMessage(performative=FIPAMessage.Performative.ACCEPT, +# message_id=0, dialogue_id=0, destination=public_key, target=1)) +# agent.outbox.put_message(to=public_key, sender=public_key,\ +# protocol_id="fipa", message=msg) +# env = agent.mailbox.outbox._queue.get(block=True, timeout=1) diff --git a/tests/test_agent.py b/tests/test_agent.py new file mode 100644 index 0000000000..22dd6d0282 --- /dev/null +++ b/tests/test_agent.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the tests of the agent module.""" + +import time +from threading import Thread + +from aea.agent import Agent, AgentState +from aea.connections.local.connection import LocalNode, OEFLocalConnection +from aea.crypto.base import Crypto +from aea.mail.base import MailBox, InBox, OutBox + + +class DummyAgent(Agent): + """A dummy agent for testing.""" + + def __init__(self, *args, **kwargs): + """Initialize the agent.""" + super().__init__(*args, **kwargs) + + def setup(self) -> None: + """Set up the agent.""" + pass + + def act(self) -> None: + """Act.""" + pass + + def react(self) -> None: + """React to events.""" + pass + + def update(self) -> None: + """Update the state of the agent.""" + pass + + def teardown(self) -> None: + """Tear down the agent.""" + pass + + +def test_run_agent(): + """Test that we can set up and then run the agent.""" + agent_name = "dummyagent" + agent = DummyAgent(agent_name) + mailbox = MailBox(OEFLocalConnection("mypbk", LocalNode())) + agent.mailbox = mailbox + assert agent.name == agent_name + assert isinstance(agent.crypto, Crypto) + assert agent.agent_state == AgentState.INITIATED,\ + "Agent state must be 'initiated'" + + agent.mailbox.connect() + assert agent.agent_state == AgentState.CONNECTED,\ + "Agent state must be 'connected'" + + assert isinstance(agent.inbox, InBox) + assert isinstance(agent.outbox, OutBox) + + agent_thread = Thread(target=agent.start) + agent_thread.start() + time.sleep(1) + + assert agent.agent_state == AgentState.RUNNING,\ + "Agent state must be 'running'" + + agent.stop() + agent.mailbox.disconnect() + agent_thread.join() diff --git a/tests/test_channel/test_gym.py b/tests/test_channel/test_gym.py deleted file mode 100644 index ba75cf4600..0000000000 --- a/tests/test_channel/test_gym.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# 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. -# -# ------------------------------------------------------------------------------ - -"""This test module contains the tests for the Gym channel and connection.""" - -import time -from typing import Any, Tuple - -from aea.channels.gym.connection import GymConnection, DEFAULT_GYM -from aea.mail.base import Envelope, MailBox -from aea.protocols.gym.message import GymMessage -from aea.protocols.gym.serialization import GymSerializer - - -Action = Any -Observation = Any -Reward = Any -Done = bool -Info = Any -Feedback = Tuple[Observation, Reward, Done, Info] - - -class GymEnvStub: - """Stubs a Gym Env.""" - - def step(self, action: Action) -> Feedback: - """Take a step.""" - return None, 1, False, {} - - -def test_connection(): - """Test that two mailbox can connect to the node.""" - mailbox = MailBox(GymConnection("agent_public_key", GymEnvStub())) - - mailbox.connect() - - mailbox.disconnect() - - -def test_communication(): - """Test that the gym can be communicated with.""" - mailbox = MailBox(GymConnection("agent_public_key", GymEnvStub())) - - mailbox.connect() - - msg = GymMessage(performative=GymMessage.Performative.ACT, action='some_action', step_id=1) - msg_bytes = GymSerializer().encode(msg) - envelope = Envelope(to=DEFAULT_GYM, sender="agent_public_key", protocol_id=GymMessage.protocol_id, message=msg_bytes) - mailbox.send(envelope) - - time.sleep(1.0) - - envelope = mailbox.inbox.get(block=True, timeout=1.0) - assert envelope.sender == DEFAULT_GYM - assert envelope.to == "agent_public_key" - msg = GymSerializer().decode(envelope.message) - assert envelope.protocol_id == "gym" - assert msg.get("observation") is None - assert msg.get("reward") == 1 - assert msg.get("done") is False - assert msg.get("info") == {} - assert msg.get("step_id") == 1 - - mailbox.disconnect() diff --git a/tests/test_cli.py b/tests/test_cli.py deleted file mode 100644 index f1dbcbad08..0000000000 --- a/tests/test_cli.py +++ /dev/null @@ -1,142 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# 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. -# -# ------------------------------------------------------------------------------ - -"""This test module contains the tests for the `aea` command-line tool.""" -import json -import os -import pprint - -import yaml -from click.testing import CliRunner -from jsonschema import validate, Draft7Validator # type: ignore - -from aea.cli import cli -from .conftest import CUR_PATH, ROOT_DIR - - -def test_no_argument(): - """Test that if we run the cli tool without arguments, it exits gracefully.""" - runner = CliRunner() - result = runner.invoke(cli, []) - assert result.exit_code == 0 - - -# def test_use_case(): -# """Test a common use case for the 'aea' tool.""" -# runner = CliRunner() -# agent_name = "myagent" -# with runner.isolated_filesystem() as t: -# configs = dict(stdout=subprocess.PIPE) -# -# # create an agent -# proc = subprocess.Popen(["aea", "create", agent_name], cwd=t, **configs) -# proc.wait(timeout=1) -# assert proc.returncode == 0 -# -# # add protocol oef -# proc = subprocess.Popen(["aea", "add", "protocol", "oef"], cwd=os.path.join(t, agent_name), **configs) -# proc.wait(timeout=1) -# assert proc.returncode == 0 -# -# # add protocol tac -# proc = subprocess.Popen(["aea", "add", "protocol", "tac"], cwd=os.path.join(t, agent_name), **configs) -# proc.wait(timeout=1) -# assert proc.returncode == 0 -# -# # add protocol default -# proc = subprocess.Popen(["aea", "add", "protocol", "default"], cwd=os.path.join(t, agent_name), **configs) -# proc.wait(timeout=1) -# assert proc.returncode == 0 -# -# # remove protocol default -# proc = subprocess.Popen(["aea", "remove", "protocol", "default"], cwd=os.path.join(t, agent_name), **configs) -# proc.wait(timeout=1) -# assert proc.returncode == 0 -# -# # add dummy skill -# proc = subprocess.Popen(["aea", "add", "skill", "dummy_skill", os.path.join(CUR_PATH, "data", "dummy_skill")], -# cwd=os.path.join(t, agent_name), **configs) -# proc.wait(timeout=1) -# assert proc.returncode == 0 -# -# # remove dummy skill -# proc = subprocess.Popen(["aea", "remove", "skill", "dummy_skill"], -# cwd=os.path.join(t, agent_name), **configs) -# proc.wait(timeout=1) -# assert proc.returncode == 0 -# -# # add dummy skill -# proc = subprocess.Popen(["aea", "add", "skill", "dummy_skill", os.path.join(CUR_PATH, "data", "dummy_skill")], -# cwd=os.path.join(t, agent_name), **configs) -# proc.wait(timeout=1) -# assert proc.returncode == 0 -# -# # run agent -# proc = subprocess.Popen(["aea", "run"], -# cwd=os.path.join(t, agent_name), **configs) -# time.sleep(2.0) -# proc.terminate() -# proc.wait(5.0) -# -# # delete agent -# proc = subprocess.Popen(["aea", "delete", agent_name], cwd=t, **configs) -# proc.wait(timeout=1) -# assert proc.returncode == 0 - - -def test_agent_configuration_schema_is_valid_wrt_draft_07(): - """Test that the JSON schema for the agent configuration file is compliant with the specification Draft 07.""" - agent_config_schema = json.load(open(os.path.join(ROOT_DIR, "aea", "configurations", "schemas", "aea-config_schema.json"))) - Draft7Validator.check_schema(agent_config_schema) - - -def test_skill_configuration_schema_is_valid_wrt_draft_07(): - """Test that the JSON schema for the skill configuration file is compliant with the specification Draft 07.""" - skill_config_schema = json.load(open(os.path.join(ROOT_DIR, "aea", "configurations", "schemas", "skill-config_schema.json"))) - Draft7Validator.check_schema(skill_config_schema) - - -def test_connection_configuration_schema_is_valid_wrt_draft_07(): - """Test that the JSON schema for the connection configuration file is compliant with the specification Draft 07.""" - connection_config_schema = json.load(open(os.path.join(ROOT_DIR, "aea", "configurations", "schemas", "connection-config_schema.json"))) - Draft7Validator.check_schema(connection_config_schema) - - -def test_validate_agent_config(): - """Test that the validation of the agent configuration file works correctly.""" - agent_config_schema = json.load(open(os.path.join(ROOT_DIR, "aea", "configurations", "schemas", "aea-config_schema.json"))) - agent_config_file = yaml.safe_load(open(os.path.join(CUR_PATH, "data", "aea-config.example.yaml"))) - pprint.pprint(agent_config_file) - validate(instance=agent_config_file, schema=agent_config_schema) - - -def test_validate_skill_config(): - """Test that the validation of the skill configuration file works correctly.""" - skill_config_schema = json.load(open(os.path.join(ROOT_DIR, "aea", "configurations", "schemas", "skill-config_schema.json"))) - skill_config_file = yaml.safe_load(open(os.path.join(CUR_PATH, "data", "dummy_skill", "skill.yaml"))) - pprint.pprint(skill_config_file) - validate(instance=skill_config_file, schema=skill_config_schema) - - -def test_validate_connection_config(): - """Test that the validation of the connection configuration file works correctly.""" - connection_config_schema = json.load(open(os.path.join(ROOT_DIR, "aea", "configurations", "schemas", "connection-config_schema.json"))) - connection_config_file = yaml.safe_load(open(os.path.join(CUR_PATH, "data", "dummy_connection", "connection.yaml"))) - pprint.pprint(connection_config_file) - validate(instance=connection_config_file, schema=connection_config_schema) diff --git a/tests/test_cli/__init__.py b/tests/test_cli/__init__.py new file mode 100644 index 0000000000..9cb426a26b --- /dev/null +++ b/tests/test_cli/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea` command-line tool.""" diff --git a/tests/test_cli/test_commands/__init__.py b/tests/test_cli/test_commands/__init__.py new file mode 100644 index 0000000000..f0ecfd0b69 --- /dev/null +++ b/tests/test_cli/test_commands/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the cli tool commands.""" diff --git a/tests/test_cli/test_commands/test_add/__init__.py b/tests/test_cli/test_commands/test_add/__init__.py new file mode 100644 index 0000000000..05323fb25c --- /dev/null +++ b/tests/test_cli/test_commands/test_add/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea add` sub-command.""" diff --git a/tests/test_cli/test_commands/test_add/test_connection.py b/tests/test_cli/test_commands/test_add/test_connection.py new file mode 100644 index 0000000000..0022c3a566 --- /dev/null +++ b/tests/test_cli/test_commands/test_add/test_connection.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea add connection` sub-command.""" +import os +import shutil +import tempfile +import unittest.mock +from pathlib import Path + +from click.testing import CliRunner +from jsonschema import ValidationError + +import aea +import aea.cli.common +import aea.configurations.base +from aea.cli import cli + + +class TestAddConnectionFailsWhenConnectionAlreadyExists: + """Test that the command 'aea add connection' fails when the connection already exists.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.connection_name = "local" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + # add connection first time + result = cls.runner.invoke(cli, ["add", "connection", cls.connection_name]) + assert result.exit_code == 0 + # add connection again + cls.result = cls.runner.invoke(cli, ["add", "connection", cls.connection_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_error_message_connection_already_existing(self): + """Test that the log error message is fixed. + + The expected message is: 'A connection with name '{connection_name}' already exists. Aborting...' + """ + s = "A connection with name '{}' already exists. Aborting...".format(self.connection_name) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestAddConnectionFailsWhenConnectionNotInRegistry: + """Test that the command 'aea add connection' fails when the connection is not in the registry.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.connection_name = "unknown_connection" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + cls.result = cls.runner.invoke(cli, ["add", "connection", cls.connection_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_error_message_connection_already_existing(self): + """Test that the log error message is fixed. + + The expected message is: 'Cannot find connection: '{connection_name}'' + """ + s = "Cannot find connection: '{}'.".format(self.connection_name) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestAddConnectionFailsWhenConfigFileIsNotCompliant: + """Test that the command 'aea add connection' fails when the configuration file is not compliant with the schema.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.connection_name = "local" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + + # change the serialization of the AgentConfig class so to make the parsing to fail. + cls.patch = unittest.mock.patch.object(aea.configurations.base.ConnectionConfig, "from_json", + side_effect=ValidationError("test error message")) + cls.patch.__enter__() + + os.chdir(cls.agent_name) + cls.result = cls.runner.invoke(cli, ["add", "connection", cls.connection_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_configuration_file_not_valid(self): + """Test that the log error message is fixed. + + The expected message is: 'Cannot find connection: '{connection_name}'' + """ + self.mocked_logger_error.assert_called_once_with("Connection configuration file not valid: test error message") + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestAddConnectionFailsWhenDirectoryAlreadyExists: + """Test that the command 'aea add connection' fails when the destination directory already exists.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.connection_name = "local" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + + os.chdir(cls.agent_name) + Path("connections", cls.connection_name).mkdir(parents=True, exist_ok=True) + cls.result = cls.runner.invoke(cli, ["add", "connection", cls.connection_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_file_exists_error(self): + """Test that the log error message is fixed. + + The expected message is: 'Cannot find connection: '{connection_name}'' + """ + s = "[Errno 17] File exists: './connections/{}'".format(self.connection_name) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass diff --git a/tests/test_cli/test_commands/test_add/test_protocol.py b/tests/test_cli/test_commands/test_add/test_protocol.py new file mode 100644 index 0000000000..52e6fff807 --- /dev/null +++ b/tests/test_cli/test_commands/test_add/test_protocol.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea add protocol` sub-command.""" +import os +import shutil +import tempfile +import unittest.mock +from pathlib import Path + +from click.testing import CliRunner + +import aea +import aea.cli.common +from aea.cli import cli + + +class TestAddProtocolFailsWhenProtocolAlreadyExists: + """Test that the command 'aea add protocol' fails when the protocol already exists.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.protocol_name = "oef" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + result = cls.runner.invoke(cli, ["add", "protocol", cls.protocol_name]) + assert result.exit_code == 0 + cls.result = cls.runner.invoke(cli, ["add", "protocol", cls.protocol_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_error_message_protocol_already_existing(self): + """Test that the log error message is fixed. + + The expected message is: 'A protocol with name '{protocol_name}' already exists. Aborting...' + """ + s = "A protocol with name '{}' already exists. Aborting...".format(self.protocol_name) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestAddProtocolFailsWhenProtocolNotInRegistry: + """Test that the command 'aea add protocol' fails when the protocol is not in the registry.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.protocol_name = "unknown_protocol" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + cls.result = cls.runner.invoke(cli, ["add", "protocol", cls.protocol_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_error_message_protocol_already_existing(self): + """Test that the log error message is fixed. + + The expected message is: 'Cannot find protocol: '{protocol_name}'' + """ + s = "Cannot find protocol: '{}'.".format(self.protocol_name) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestAddProtocolFailsWhenDirectoryAlreadyExists: + """Test that the command 'aea add protocol' fails when the destination directory already exists.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.protocol_name = "oef" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + + os.chdir(cls.agent_name) + Path("protocols", cls.protocol_name).mkdir(parents=True, exist_ok=True) + cls.result = cls.runner.invoke(cli, ["add", "protocol", cls.protocol_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_file_exists_error(self): + """Test that the log error message is fixed. + + The expected message is: 'Cannot find protocol: '{protocol_name}'' + """ + s = "[Errno 17] File exists: './protocols/{}'".format(self.protocol_name) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass diff --git a/tests/test_cli/test_commands/test_add/test_skill.py b/tests/test_cli/test_commands/test_add/test_skill.py new file mode 100644 index 0000000000..df6087e7f7 --- /dev/null +++ b/tests/test_cli/test_commands/test_add/test_skill.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea add skill` sub-command.""" +import os +import shutil +import tempfile +import unittest.mock +from pathlib import Path + +import yaml +from click.testing import CliRunner +from jsonschema import ValidationError + +import aea +import aea.cli.common +from aea.cli import cli +from aea.configurations.base import AgentConfig, DEFAULT_AEA_CONFIG_FILE +from ....conftest import ROOT_DIR + + +class TestAddSkillFailsWhenSkillAlreadyExists: + """Test that the command 'aea add skill' fails when the skill already exists.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.skill_name = "error" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + # this also by default adds the oef connection and error skill + assert result.exit_code == 0 + os.chdir(cls.agent_name) + + # change default registry path + config = AgentConfig.from_json(yaml.safe_load(open(DEFAULT_AEA_CONFIG_FILE))) + config.registry_path = os.path.join(ROOT_DIR, "packages") + yaml.safe_dump(config.json, open(DEFAULT_AEA_CONFIG_FILE, "w")) + + # add the error skill again + cls.result = cls.runner.invoke(cli, ["add", "skill", cls.skill_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_error_message_skill_already_existing(self): + """Test that the log error message is fixed. + + The expected message is: 'A skill with name '{skill_name}' already exists. Aborting...' + """ + s = "A skill with name '{}' already exists. Aborting...".format(self.skill_name) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestAddSkillFailsWhenSkillNotInRegistry: + """Test that the command 'aea add skill' fails when the skill is not in the registry.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.skill_name = "unknown_skill" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + cls.result = cls.runner.invoke(cli, ["add", "skill", cls.skill_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_error_message_skill_already_existing(self): + """Test that the log error message is fixed. + + The expected message is: 'Cannot find skill: '{skill_name}'' + """ + s = "Cannot find skill: '{}'.".format(self.skill_name) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestAddSkillFailsWhenConfigFileIsNotCompliant: + """Test that the command 'aea add connection' fails when the configuration file is not compliant with the schema.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.skill_name = "echo" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + + # change default registry path + config = AgentConfig.from_json(yaml.safe_load(open(DEFAULT_AEA_CONFIG_FILE))) + config.registry_path = os.path.join(ROOT_DIR, "packages") + yaml.safe_dump(config.json, open(DEFAULT_AEA_CONFIG_FILE, "w")) + + # change the serialization of the AgentConfig class so to make the parsing to fail. + cls.patch = unittest.mock.patch.object(aea.configurations.base.SkillConfig, "from_json", + side_effect=ValidationError("test error message")) + cls.patch.__enter__() + + cls.result = cls.runner.invoke(cli, ["add", "skill", cls.skill_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_configuration_file_not_valid(self): + """Test that the log error message is fixed. + + The expected message is: 'Cannot find skill: '{skill_name}'' + """ + self.mocked_logger_error.assert_called_once_with("Skill configuration file not valid: test error message") + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestAddSkillFailsWhenDirectoryAlreadyExists: + """Test that the command 'aea add skill' fails when the destination directory already exists.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.skill_name = "echo" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + + # change default registry path + config = AgentConfig.from_json(yaml.safe_load(open(DEFAULT_AEA_CONFIG_FILE))) + config.registry_path = os.path.join(ROOT_DIR, "packages") + yaml.safe_dump(config.json, open(DEFAULT_AEA_CONFIG_FILE, "w")) + + Path("skills", cls.skill_name).mkdir(parents=True, exist_ok=True) + cls.result = cls.runner.invoke(cli, ["add", "skill", cls.skill_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_file_exists_error(self): + """Test that the log error message is fixed. + + The expected message is: 'Cannot find skill: '{skill_name}'' + """ + s = "[Errno 17] File exists: './skills/{}'".format(self.skill_name) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass diff --git a/tests/test_cli/test_commands/test_create.py b/tests/test_cli/test_commands/test_create.py new file mode 100644 index 0000000000..df54b7371a --- /dev/null +++ b/tests/test_cli/test_commands/test_create.py @@ -0,0 +1,289 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea create` sub-command.""" +import filecmp +import json +import os +import shutil +import tempfile +import unittest +from pathlib import Path +from typing import Dict +from unittest.mock import patch + +import jsonschema +import pytest +import yaml +from click.testing import CliRunner + +import aea +import aea.cli.common +from aea.cli import cli +from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE +from aea.configurations.loader import ConfigLoader +from ...conftest import AGENT_CONFIGURATION_SCHEMA, ROOT_DIR + + +class TestCreate: + """Test that the command 'aea create ' works as expected.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + cls.result = cls.runner.invoke(cli, ["create", cls.agent_name]) + + def _load_config_file(self) -> Dict: + """Load a config file.""" + agent_config_file = Path(self.agent_name, DEFAULT_AEA_CONFIG_FILE) # type: ignore + file_pointer = open(agent_config_file, mode="r", encoding="utf-8") + agent_config_instance = yaml.safe_load(file_pointer) + return agent_config_instance + + def test_exit_code_equal_to_zero(self): + """Assert that the exit code is equal to zero (i.e. success).""" + assert self.result.exit_code == 0 + + def test_agent_directory_path_exists(self): + """Check that the agent's directory has been created.""" + agent_dir = Path(self.agent_name) + assert agent_dir.exists() + assert agent_dir.is_dir() + + def test_configuration_file_has_been_created(self): + """Check that an agent's configuration file has been created.""" + agent_config_file = Path(self.agent_name, DEFAULT_AEA_CONFIG_FILE) + assert agent_config_file.exists() + assert agent_config_file.is_file() + + def test_configuration_file_is_compliant_to_schema(self): + """Check that the agent's configuration file is compliant with the schema.""" + agent_config_instance = self._load_config_file() + agent_config_schema = json.load(open(AGENT_CONFIGURATION_SCHEMA)) + + try: + jsonschema.validate(instance=agent_config_instance, schema=agent_config_schema) + except jsonschema.exceptions.ValidationError as e: + pytest.fail("Configuration file is not compliant with the schema. Exception: {}".format(str(e))) + + def test_aea_version_is_correct(self): + """Check that the aea version in the configuration file is correct, i.e. the same of the installed package.""" + agent_config_instance = self._load_config_file() + assert agent_config_instance["aea_version"] == aea.__version__ + + def test_agent_name_is_correct(self): + """Check that the agent name in the configuration file is correct.""" + agent_config_instance = self._load_config_file() + assert agent_config_instance["agent_name"] == self.agent_name + + def test_authors_field_is_empty_string(self): + """Check that the 'authors' field in the config file is the empty string.""" + agent_config_instance = self._load_config_file() + assert agent_config_instance["authors"] == "" + + def test_connections_contains_only_oef(self): + """Check that the 'connections' list contains only the 'oef' connection.""" + agent_config_instance = self._load_config_file() + assert agent_config_instance["connections"] == ["oef"] + + def test_default_connection_field_is_oef(self): + """Check that the 'default_connection' is the 'oef' connection.""" + agent_config_instance = self._load_config_file() + assert agent_config_instance["default_connection"] == "oef" + + def test_license_field_is_empty_string(self): + """Check that the 'license' is the empty string.""" + agent_config_instance = self._load_config_file() + assert agent_config_instance["license"] == "" + + def test_private_key_pem_path_field_is_empty_string(self): + """Check that the 'private_key_pem_path' is the empty string.""" + agent_config_instance = self._load_config_file() + assert agent_config_instance["private_key_pem_path"] == "" + + def test_protocols_field_is_empty_list(self): + """Check that the 'protocols' field is a list with the 'default' protocol.""" + agent_config_instance = self._load_config_file() + assert agent_config_instance["protocols"] == ["default"] + + def test_skills_field_is_empty_list(self): + """Check that the 'skills' field is a list with the 'error' skill.""" + agent_config_instance = self._load_config_file() + assert agent_config_instance["skills"] == ["error"] + + def test_url_field_is_empty_string(self): + """Check that the 'url' field is the empty string.""" + agent_config_instance = self._load_config_file() + assert agent_config_instance["url"] == "" + + def test_version_field_is_equal_to_v1(self): + """Check that the 'version' field is equal to the string 'v1'.""" + agent_config_instance = self._load_config_file() + assert agent_config_instance["version"] == "v1" + + def test_connections_directory_exists(self): + """Check that the connections directory exists.""" + connections_dirpath = Path(self.agent_name, "connections") + assert connections_dirpath.exists() + assert connections_dirpath.is_dir() + + def test_connections_contains_oef_connection(self): + """Check that the connections directory contains the oef directory.""" + oef_connection_dirpath = Path(self.agent_name, "connections", "oef") + assert oef_connection_dirpath.exists() + assert oef_connection_dirpath.is_dir() + + def test_oef_connection_directory_is_equal_to_library_oef_connection(self): + """Check that the oef connection directory is equal to the package's one (aea.connections.oef).""" + oef_connection_dirpath = Path(self.agent_name, "connections", "oef") + comparison = filecmp.dircmp(str(oef_connection_dirpath), str(Path(ROOT_DIR, "aea", "connections", "oef"))) + assert comparison.diff_files == [] + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestCreateFailsWhenDirectoryAlreadyExists: + """Test that 'aea create' sub-command fails when the directory with the agent name in input already exists.""" + + @classmethod + def setup_class(cls): + """Set up the test class.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + + # create a directory with the agent name -> make 'aea create fail. + os.mkdir(cls.agent_name) + cls.result = cls.runner.invoke(cli, ["create", cls.agent_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the error code is equal to -1.""" + assert self.result.exit_code == -1 + + def test_log_error_message(self): + """Test that the log error message is fixed. + + The expected message is: 'Directory already exist. Aborting...' + """ + s = "Directory already exist. Aborting..." + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestCreateFailsWhenConfigFileIsNotCompliant: + """Test that 'aea create' sub-command fails when the generated configuration file is not compliant with the schema.""" + + @classmethod + def setup_class(cls): + """Set up the test class.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + + # change the serialization of the AgentConfig class so to make the parsing to fail. + cls.patch = patch.object(aea.configurations.base.AgentConfig, "json", return_value={"hello": "world"}) + cls.patch.__enter__() + + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + + cls.result = cls.runner.invoke(cli, ["create", cls.agent_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the error code is equal to -1.""" + assert self.result.exit_code == -1 + + def test_agent_folder_is_not_created(self): + """Test that the agent folder is removed.""" + assert not Path(self.agent_name).exists() + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestCreateFailsWhenExceptionOccurs: + """Test that 'aea create' sub-command fails when the generated configuration file is not compliant with the schema.""" + + @classmethod + def setup_class(cls): + """Set up the test class.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + + # change the serialization of the AgentConfig class so to make the parsing to fail. + cls.patch = patch.object(ConfigLoader, "dump", side_effect=Exception) + cls.patch.__enter__() + + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + + cls.result = cls.runner.invoke(cli, ["create", cls.agent_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the error code is equal to -1.""" + assert self.result.exit_code == -1 + + def test_agent_folder_is_not_created(self): + """Test that the agent folder is removed.""" + assert not Path(self.agent_name).exists() + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass diff --git a/tests/test_cli/test_commands/test_delete.py b/tests/test_cli/test_commands/test_delete.py new file mode 100644 index 0000000000..d3d993b084 --- /dev/null +++ b/tests/test_cli/test_commands/test_delete.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea delete` sub-command.""" +import os +import shutil +import tempfile +import unittest +from pathlib import Path + +from click.testing import CliRunner + +import aea +import aea.cli.common +from aea.cli import cli + + +class TestDelete: + """Test that the command 'aea create ' works as expected.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + + cls.runner.invoke(cli, ["create", cls.agent_name]) + cls.result = cls.runner.invoke(cli, ["delete", cls.agent_name]) + + def test_exit_code_equal_to_zero(self): + """Assert that the exit code is equal to zero (i.e. success).""" + assert self.result.exit_code == 0 + + def test_agent_directory_path_does_not_exists(self): + """Check that the agent's directory has been deleted.""" + agent_dir = Path(self.agent_name) + assert not agent_dir.exists() + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestDeleteFailsWhenDirectoryDoesNotExist: + """Test that 'aea delete' sub-command fails when the directory with the agent name in input does not exist.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + os.chdir(cls.t) + + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + # agent's directory does not exist -> command will fail. + cls.result = cls.runner.invoke(cli, ["delete", cls.agent_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the error code is equal to -1.""" + assert self.result.exit_code == -1 + + def test_log_error_message(self): + """Test that the log error message is fixed. + + The expected message is: 'Directory already exist. Aborting...' + """ + s = "An error occurred while deleting the agent directory. Aborting..." + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass diff --git a/tests/test_cli/test_commands/test_misc.py b/tests/test_cli/test_commands/test_misc.py new file mode 100644 index 0000000000..9cabeac56c --- /dev/null +++ b/tests/test_cli/test_commands/test_misc.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea` sub-commands.""" + +from click.testing import CliRunner + +from aea.cli import cli + + +def test_no_argument(): + """Test that if we run the cli tool without arguments, it exits gracefully.""" + runner = CliRunner() + result = runner.invoke(cli, []) + assert result.exit_code == 0 diff --git a/tests/test_cli/test_commands/test_remove/__init__.py b/tests/test_cli/test_commands/test_remove/__init__.py new file mode 100644 index 0000000000..0bbf2e44f6 --- /dev/null +++ b/tests/test_cli/test_commands/test_remove/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea remove` sub-command.""" diff --git a/tests/test_cli/test_commands/test_remove/test_connection.py b/tests/test_cli/test_commands/test_remove/test_connection.py new file mode 100644 index 0000000000..ad7d034a60 --- /dev/null +++ b/tests/test_cli/test_commands/test_remove/test_connection.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea remove connection` sub-command.""" +import os +import shutil +import tempfile +import unittest.mock +from pathlib import Path + +import yaml +from click.testing import CliRunner + +import aea +import aea.cli.common +import aea.configurations.base +from aea.cli import cli +from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE + + +class TestRemoveConnection: + """Test that the command 'aea remove connection' works correctly.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.connection_name = "local" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + result = cls.runner.invoke(cli, ["add", "connection", cls.connection_name]) + assert result.exit_code == 0 + cls.result = cls.runner.invoke(cli, ["remove", "connection", cls.connection_name]) + + def test_exit_code_equal_to_zero(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == 0 + + def test_directory_does_not_exist(self): + """Test that the directory of the removed connection does not exist.""" + assert not Path("connections", self.connection_name).exists() + + def test_connection_not_present_in_agent_config(self): + """Test that the name of the removed connection is not present in the agent configuration file.""" + agent_config = aea.configurations.base.AgentConfig.from_json(yaml.safe_load(open(DEFAULT_AEA_CONFIG_FILE))) + assert self.connection_name not in agent_config.connections + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestRemoveConnectionFailsWhenConnectionDoesNotExist: + """Test that the command 'aea remove connection' fails when the connection does not exist.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.connection_name = "local" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + + cls.result = cls.runner.invoke(cli, ["remove", "connection", cls.connection_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_error_message_connection_not_existing(self): + """Test that the log error message is fixed. + + The expected message is: 'Connection '{connection_name}' not found.' + """ + s = "Connection '{}' not found.".format(self.connection_name) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestRemoveConnectionFailsWhenExceptionOccurs: + """Test that the command 'aea remove connection' fails when an exception occurs while removing the directory.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.connection_name = "local" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + result = cls.runner.invoke(cli, ["add", "connection", cls.connection_name]) + assert result.exit_code == 0 + + cls.patch = unittest.mock.patch("shutil.rmtree", side_effect=BaseException("an exception")) + cls.patch.__enter__() + + cls.result = cls.runner.invoke(cli, ["remove", "connection", cls.connection_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass diff --git a/tests/test_cli/test_commands/test_remove/test_protocol.py b/tests/test_cli/test_commands/test_remove/test_protocol.py new file mode 100644 index 0000000000..6703a0f38d --- /dev/null +++ b/tests/test_cli/test_commands/test_remove/test_protocol.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea remove protocol` sub-command.""" +import os +import shutil +import tempfile +import unittest.mock +from pathlib import Path + +import yaml +from click.testing import CliRunner + +import aea +import aea.cli.common +import aea.configurations.base +from aea.cli import cli +from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE + + +class TestRemoveProtocol: + """Test that the command 'aea remove protocol' works correctly.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.protocol_name = "oef" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + result = cls.runner.invoke(cli, ["add", "protocol", cls.protocol_name]) + assert result.exit_code == 0 + cls.result = cls.runner.invoke(cli, ["remove", "protocol", cls.protocol_name]) + + def test_exit_code_equal_to_zero(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == 0 + + def test_directory_does_not_exist(self): + """Test that the directory of the removed protocol does not exist.""" + assert not Path("protocols", self.protocol_name).exists() + + def test_protocol_not_present_in_agent_config(self): + """Test that the name of the removed protocol is not present in the agent configuration file.""" + agent_config = aea.configurations.base.AgentConfig.from_json(yaml.safe_load(open(DEFAULT_AEA_CONFIG_FILE))) + assert self.protocol_name not in agent_config.protocols + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestRemoveProtocolFailsWhenProtocolDoesNotExist: + """Test that the command 'aea remove protocol' fails when the protocol does not exist.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.protocol_name = "oef" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + + cls.result = cls.runner.invoke(cli, ["remove", "protocol", cls.protocol_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_error_message_protocol_not_existing(self): + """Test that the log error message is fixed. + + The expected message is: 'Protocol '{protocol_name}' not found.' + """ + s = "Protocol '{}' not found.".format(self.protocol_name) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestRemoveProtocolFailsWhenExceptionOccurs: + """Test that the command 'aea remove protocol' fails when an exception occurs while removing the directory.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.protocol_name = "oef" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + result = cls.runner.invoke(cli, ["add", "protocol", cls.protocol_name]) + assert result.exit_code == 0 + + cls.patch = unittest.mock.patch("shutil.rmtree", side_effect=BaseException("an exception")) + cls.patch.__enter__() + + cls.result = cls.runner.invoke(cli, ["remove", "protocol", cls.protocol_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass diff --git a/tests/test_cli/test_commands/test_remove/test_skill.py b/tests/test_cli/test_commands/test_remove/test_skill.py new file mode 100644 index 0000000000..2cd0a55a09 --- /dev/null +++ b/tests/test_cli/test_commands/test_remove/test_skill.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the `aea remove skill` sub-command.""" +import os +import shutil +import tempfile +import unittest.mock +from pathlib import Path + +import yaml +from click.testing import CliRunner + +import aea +import aea.cli.common +import aea.configurations.base +from aea.cli import cli +from aea.configurations.base import DEFAULT_AEA_CONFIG_FILE, AgentConfig +from ....conftest import ROOT_DIR + + +class TestRemoveSkill: + """Test that the command 'aea remove skill' works correctly.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.skill_name = "gym" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + + # change default registry path + config = AgentConfig.from_json(yaml.safe_load(open(DEFAULT_AEA_CONFIG_FILE))) + config.registry_path = os.path.join(ROOT_DIR, "packages") + yaml.safe_dump(config.json, open(DEFAULT_AEA_CONFIG_FILE, "w")) + + result = cls.runner.invoke(cli, ["add", "skill", cls.skill_name]) + assert result.exit_code == 0 + cls.result = cls.runner.invoke(cli, ["remove", "skill", cls.skill_name]) + + def test_exit_code_equal_to_zero(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == 0 + + def test_directory_does_not_exist(self): + """Test that the directory of the removed skill does not exist.""" + assert not Path("skills", self.skill_name).exists() + + def test_skill_not_present_in_agent_config(self): + """Test that the name of the removed skill is not present in the agent configuration file.""" + agent_config = aea.configurations.base.AgentConfig.from_json(yaml.safe_load(open(DEFAULT_AEA_CONFIG_FILE))) + assert self.skill_name not in agent_config.skills + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestRemoveSkillFailsWhenSkillIsNotSupported: + """Test that the command 'aea remove skill' fails when the skill is not supported.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.skill_name = "gym" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + + cls.result = cls.runner.invoke(cli, ["remove", "skill", cls.skill_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + def test_error_message_skill_not_existing(self): + """Test that the log error message is fixed. + + The expected message is: 'The skill '{skill_name}' is not supported.' + """ + s = "The skill '{}' is not supported.".format(self.skill_name) + self.mocked_logger_error.assert_called_once_with(s) + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass + + +class TestRemoveSkillFailsWhenExceptionOccurs: + """Test that the command 'aea remove skill' fails when an exception occurs while removing the directory.""" + + @classmethod + def setup_class(cls): + """Set the test up.""" + cls.runner = CliRunner() + cls.agent_name = "myagent" + cls.cwd = os.getcwd() + cls.t = tempfile.mkdtemp() + cls.skill_name = "gym" + cls.patch = unittest.mock.patch.object(aea.cli.common.logger, 'error') + cls.mocked_logger_error = cls.patch.__enter__() + + os.chdir(cls.t) + result = cls.runner.invoke(cli, ["create", cls.agent_name]) + assert result.exit_code == 0 + os.chdir(cls.agent_name) + + # change default registry path + config = AgentConfig.from_json(yaml.safe_load(open(DEFAULT_AEA_CONFIG_FILE))) + config.registry_path = os.path.join(ROOT_DIR, "packages") + yaml.safe_dump(config.json, open(DEFAULT_AEA_CONFIG_FILE, "w")) + + result = cls.runner.invoke(cli, ["add", "skill", cls.skill_name]) + assert result.exit_code == 0 + + cls.patch = unittest.mock.patch("shutil.rmtree", side_effect=BaseException("an exception")) + cls.patch.__enter__() + + cls.result = cls.runner.invoke(cli, ["remove", "skill", cls.skill_name]) + + def test_exit_code_equal_to_minus_1(self): + """Test that the exit code is equal to minus 1.""" + assert self.result.exit_code == -1 + + @classmethod + def teardown_class(cls): + """Teardowm the test.""" + cls.patch.__exit__() + os.chdir(cls.cwd) + try: + shutil.rmtree(cls.t) + except (OSError, IOError): + pass diff --git a/tests/test_cli/test_schema.py b/tests/test_cli/test_schema.py new file mode 100644 index 0000000000..e36ec28095 --- /dev/null +++ b/tests/test_cli/test_schema.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This test module contains the tests for the JSON schemas of the configuration files.""" +import json +import os +import pprint + +import yaml +from jsonschema import validate, Draft7Validator # type: ignore + +from ..conftest import CUR_PATH, ROOT_DIR, AGENT_CONFIGURATION_SCHEMA, SKILL_CONFIGURATION_SCHEMA, \ + CONNECTION_CONFIGURATION_SCHEMA + + +def test_agent_configuration_schema_is_valid_wrt_draft_07(): + """Test that the JSON schema for the agent configuration file is compliant with the specification Draft 07.""" + agent_config_schema = json.load(open(os.path.join(ROOT_DIR, "aea", "configurations", "schemas", "aea-config_schema.json"))) + Draft7Validator.check_schema(agent_config_schema) + + +def test_skill_configuration_schema_is_valid_wrt_draft_07(): + """Test that the JSON schema for the skill configuration file is compliant with the specification Draft 07.""" + skill_config_schema = json.load(open(os.path.join(ROOT_DIR, "aea", "configurations", "schemas", "skill-config_schema.json"))) + Draft7Validator.check_schema(skill_config_schema) + + +def test_connection_configuration_schema_is_valid_wrt_draft_07(): + """Test that the JSON schema for the connection configuration file is compliant with the specification Draft 07.""" + connection_config_schema = json.load(open(os.path.join(ROOT_DIR, "aea", "configurations", "schemas", "connection-config_schema.json"))) + Draft7Validator.check_schema(connection_config_schema) + + +def test_validate_agent_config(): + """Test that the validation of the agent configuration file works correctly.""" + agent_config_schema = json.load(open(AGENT_CONFIGURATION_SCHEMA)) + agent_config_file = yaml.safe_load(open(os.path.join(CUR_PATH, "data", "aea-config.example.yaml"))) + pprint.pprint(agent_config_file) + validate(instance=agent_config_file, schema=agent_config_schema) + + +def test_validate_skill_config(): + """Test that the validation of the skill configuration file works correctly.""" + skill_config_schema = json.load(open(SKILL_CONFIGURATION_SCHEMA)) + skill_config_file = yaml.safe_load(open(os.path.join(CUR_PATH, "data", "dummy_skill", "skill.yaml"))) + pprint.pprint(skill_config_file) + validate(instance=skill_config_file, schema=skill_config_schema) + + +def test_validate_connection_config(): + """Test that the validation of the connection configuration file works correctly.""" + connection_config_schema = json.load(open(CONNECTION_CONFIGURATION_SCHEMA)) + connection_config_file = yaml.safe_load(open(os.path.join(CUR_PATH, "data", "dummy_connection", "connection.yaml"))) + pprint.pprint(connection_config_file) + validate(instance=connection_config_file, schema=connection_config_schema) diff --git a/tests/test_channel/__init__.py b/tests/test_connections/__init__.py similarity index 100% rename from tests/test_channel/__init__.py rename to tests/test_connections/__init__.py diff --git a/tests/test_channel/test_local/__init__.py b/tests/test_connections/test_local/__init__.py similarity index 100% rename from tests/test_channel/test_local/__init__.py rename to tests/test_connections/test_local/__init__.py diff --git a/tests/test_channel/test_local/test_misc.py b/tests/test_connections/test_local/test_misc.py similarity index 98% rename from tests/test_channel/test_local/test_misc.py rename to tests/test_connections/test_local/test_misc.py index 31c2f55f99..36aca131f6 100644 --- a/tests/test_channel/test_local/test_misc.py +++ b/tests/test_connections/test_local/test_misc.py @@ -20,7 +20,7 @@ """This module contains the tests of the local OEF node implementation.""" import time -from aea.channels.local.connection import LocalNode, OEFLocalConnection +from aea.connections.local.connection import LocalNode, OEFLocalConnection from aea.mail.base import Envelope, MailBox from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer diff --git a/tests/test_channel/test_local/test_search_services.py b/tests/test_connections/test_local/test_search_services.py similarity index 96% rename from tests/test_channel/test_local/test_search_services.py rename to tests/test_connections/test_local/test_search_services.py index 01e64df13c..e068ac517c 100644 --- a/tests/test_channel/test_local/test_search_services.py +++ b/tests/test_connections/test_local/test_search_services.py @@ -19,7 +19,7 @@ """This module contains the tests for the search feature of the local OEF node.""" -from aea.channels.local.connection import LocalNode, OEFLocalConnection +from aea.connections.local.connection import LocalNode, OEFLocalConnection from aea.mail.base import MailBox, Envelope from aea.protocols.oef.message import OEFMessage from aea.protocols.oef.models import Query, DataModel, Description @@ -80,9 +80,10 @@ def setup_class(cls): # register a service. request_id = 1 + service_id = '' cls.data_model = DataModel("foobar", attributes=[]) service_description = Description({"foo": 1, "bar": "baz"}, data_model=cls.data_model) - register_service_request = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, id=request_id, service_description=service_description) + register_service_request = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, id=request_id, service_description=service_description, service_id=service_id) msg_bytes = OEFSerializer().encode(register_service_request) envelope = Envelope(to=DEFAULT_OEF, sender=cls.public_key_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) cls.mailbox1.send(envelope) @@ -130,9 +131,10 @@ def setup_class(cls): # register 'mailbox2' as a service 'foobar'. request_id = 1 + service_id = '' cls.data_model_foobar = DataModel("foobar", attributes=[]) service_description = Description({"foo": 1, "bar": "baz"}, data_model=cls.data_model_foobar) - register_service_request = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, id=request_id, service_description=service_description) + register_service_request = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, id=request_id, service_description=service_description, service_id=service_id) msg_bytes = OEFSerializer().encode(register_service_request) envelope = Envelope(to=DEFAULT_OEF, sender=cls.public_key_1, protocol_id=OEFMessage.protocol_id, message=msg_bytes) cls.mailbox1.send(envelope) @@ -140,7 +142,7 @@ def setup_class(cls): # register 'mailbox2' as a service 'barfoo'. cls.data_model_barfoo = DataModel("barfoo", attributes=[]) service_description = Description({"foo": 1, "bar": "baz"}, data_model=cls.data_model_barfoo) - register_service_request = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, id=request_id, service_description=service_description) + register_service_request = OEFMessage(oef_type=OEFMessage.Type.REGISTER_SERVICE, id=request_id, service_description=service_description, service_id=service_id) msg_bytes = OEFSerializer().encode(register_service_request) envelope = Envelope(to=DEFAULT_OEF, sender=cls.public_key_2, protocol_id=OEFMessage.protocol_id, message=msg_bytes) cls.mailbox2.send(envelope) diff --git a/tests/test_channel/test_oef/__init__.py b/tests/test_connections/test_oef/__init__.py similarity index 100% rename from tests/test_channel/test_oef/__init__.py rename to tests/test_connections/test_oef/__init__.py diff --git a/tests/test_channel/test_oef/test_communication.py b/tests/test_connections/test_oef/test_communication.py similarity index 99% rename from tests/test_channel/test_oef/test_communication.py rename to tests/test_connections/test_oef/test_communication.py index 2c3b39b925..2709ea6d2a 100644 --- a/tests/test_channel/test_oef/test_communication.py +++ b/tests/test_connections/test_oef/test_communication.py @@ -23,7 +23,7 @@ import pytest from oef.query import Eq -from aea.channels.oef.connection import OEFMailBox +from aea.connections.oef.connection import OEFMailBox from aea.crypto.base import Crypto from aea.protocols.default.message import DefaultMessage from aea.protocols.default.serialization import DefaultSerializer @@ -269,7 +269,7 @@ def test_accept(self): def test_match_accept(self): """Test that a match accept can be sent correctly.""" - # TODO since the OEF SDK doesn't support the match accept, we have to use a fixed message id! + # NOTE since the OEF SDK doesn't support the match accept, we have to use a fixed message id! match_accept = FIPAMessage(message_id=4, dialogue_id=0, target=3, performative=FIPAMessage.Performative.MATCH_ACCEPT) self.mailbox1.outbox.put_message(to=self.crypto2.public_key, sender=self.crypto1.public_key, protocol_id=FIPAMessage.protocol_id, message=FIPASerializer().encode(match_accept)) envelope = self.mailbox2.inbox.get(block=True, timeout=2.0) diff --git a/tests/test_channel/test_oef/test_models.py b/tests/test_connections/test_oef/test_models.py similarity index 98% rename from tests/test_channel/test_oef/test_models.py rename to tests/test_connections/test_oef/test_models.py index 75582d35ce..5abe6d4c74 100644 --- a/tests/test_channel/test_oef/test_models.py +++ b/tests/test_connections/test_oef/test_models.py @@ -23,7 +23,7 @@ import pytest from oef.query import Gt, Eq, LtEq -from aea.channels.oef.connection import OEFObjectTranslator +from aea.connections.oef.connection import OEFObjectTranslator from aea.protocols.oef.models import Attribute, DataModel, Description, Query, And, Or, Not, Constraint diff --git a/tests/test_crypto/__init__.py b/tests/test_crypto/__init__.py new file mode 100644 index 0000000000..101d9853b5 --- /dev/null +++ b/tests/test_crypto/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains a test for aea.crypto.""" diff --git a/tests/test_crypto.py b/tests/test_crypto/test_crypto.py similarity index 98% rename from tests/test_crypto.py rename to tests/test_crypto/test_crypto.py index cd1c348bbd..c9131e16d3 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto/test_crypto.py @@ -24,7 +24,7 @@ load_pem_private_key from aea.crypto.base import Crypto -from .conftest import ROOT_DIR +from ..conftest import ROOT_DIR def test_initialization_from_existing_private_key(): diff --git a/tests/test_mail/test_mail.py b/tests/test_mail/test_mail.py new file mode 100644 index 0000000000..ad84b53a59 --- /dev/null +++ b/tests/test_mail/test_mail.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""This module contains the tests for Envelope of mail.base.py.""" + +from queue import Queue + +from aea.connections.local.connection import LocalNode, OEFLocalConnection +from aea.mail.base import Envelope, MailBox, InBox, OutBox +from aea.protocols.base import Message +from aea.protocols.base import ProtobufSerializer + + +def test_envelope_initialisation(): + """Testing the envelope initialisation.""" + msg = Message(content='hello') + message_bytes = ProtobufSerializer().encode(msg) + assert Envelope(to="Agent1", sender="Agent0", + protocol_id="my_own_protocol", + message=message_bytes), "Cannot generate a new envelope" + + envelope = Envelope(to="Agent1", sender="Agent0", + protocol_id="my_own_protocol", message=message_bytes) + + envelope.to = "ChangedAgent" + envelope.sender = "ChangedSender" + envelope.protocol_id = "my_changed_protocol" + envelope.message = b"HelloWorld" + + assert envelope.to == "ChangedAgent", "Cannot set to value on Envelope" + assert envelope.sender == "ChangedSender",\ + "Cannot set sender value on Envelope" + assert envelope.protocol_id == "my_changed_protocol",\ + "Cannot set protocol_id on Envelope " + assert envelope.message == b"HelloWorld", "Cannot set message on Envelope" + + +def test_inbox_empty(): + """Tests if the inbox is empty.""" + my_queue = Queue() + _inbox = InBox(my_queue) + assert _inbox.empty(), "Inbox is not empty" + + +def test_inbox_nowait(): + """Tests the inbox without waiting.""" + msg = Message(content="hello") + message_bytes = ProtobufSerializer().encode(msg) + my_queue = Queue() + envelope = Envelope(to="Agent1", sender="Agent0", + protocol_id="my_own_protocol", message=message_bytes) + my_queue.put(envelope) + _inbox = InBox(my_queue) + assert _inbox.get_nowait( + ) == envelope, "Check for a message on the in queue and wait for no time." + + +def test_inbox_get(): + """Tests for a envelope on the in queue.""" + msg = Message(content="hello") + message_bytes = ProtobufSerializer().encode(msg) + my_queue = Queue() + envelope = Envelope(to="Agent1", sender="Agent0", + protocol_id="my_own_protocol", message=message_bytes) + my_queue.put(envelope) + _inbox = InBox(my_queue) + + assert _inbox.get() == envelope,\ + "Checks if the returned envelope is the same with the queued envelope." + + +def test_outbox_put(): + """Tests that an envelope is putted into the queue.""" + msg = Message(content="hello") + message_bytes = ProtobufSerializer().encode(msg) + my_queue = Queue() + envelope = Envelope(to="Agent1", sender="Agent0", + protocol_id="my_own_protocol", message=message_bytes) + my_queue.put(envelope) + _outbox = OutBox(my_queue) + _outbox.put(envelope) + assert _outbox.empty() is False,\ + "Oubox must not be empty after putting an envelope" + + +def test_outbox_put_message(): + """Tests that an envelope is created from the message is in the queue.""" + msg = Message(content="hello") + message_bytes = ProtobufSerializer().encode(msg) + my_queue = Queue() + envelope = Envelope(to="Agent1", sender="Agent0", + protocol_id="my_own_protocol", message=message_bytes) + my_queue.put(envelope) + _outbox = OutBox(my_queue) + _outbox.put_message("Agent1", "Agent0", "my_own_protocol", message_bytes) + assert _outbox.empty() is False,\ + "Outbox will not be empty after putting a message." + + +def test_outbox_empty(): + """Test thet the outbox queue is empty.""" + my_queue = Queue() + _outbox = OutBox(my_queue) + assert _outbox.empty(), "The outbox is not empty" + + +def test_mailBox(): + """Tests if the mailbox is connected.""" + node = LocalNode() + public_key_1 = "mailbox1" + mailbox1 = MailBox(OEFLocalConnection(public_key_1, node)) + mailbox1.connect() + assert mailbox1.is_connected,\ + "Mailbox cannot connect to the specific Connection(OEFLocalConnection)" + mailbox1.disconnect() diff --git a/tests/test_protocols/test_base.py b/tests/test_protocols/test_base.py index 0233668fbd..05c3101331 100644 --- a/tests/test_protocols/test_base.py +++ b/tests/test_protocols/test_base.py @@ -19,8 +19,8 @@ """This module contains the tests of the messages module.""" from aea.mail.base import Envelope -from aea.protocols.base.message import Message -from aea.protocols.base.serialization import ProtobufSerializer, JSONSerializer +from aea.protocols.base import Message +from aea.protocols.base import ProtobufSerializer, JSONSerializer class TestBaseSerializations: diff --git a/tests/test_protocols/test_tac.py b/tests/test_protocols/test_tac.py deleted file mode 100644 index 5a8fd165a8..0000000000 --- a/tests/test_protocols/test_tac.py +++ /dev/null @@ -1,140 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2018-2019 Fetch.AI Limited -# -# 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. -# -# ------------------------------------------------------------------------------ - -"""This module contains the tests for the TAC protocol.""" - -import pytest - -from aea.protocols.tac.message import TACMessage -from aea.protocols.tac.serialization import TACSerializer - - -def test_register_serialization(): - """Test that the serialization of the 'Register' message works.""" - tac_message = TACMessage(tac_type=TACMessage.Type.REGISTER, agent_name="my_agent") - tac_message_bytes = TACSerializer().encode(tac_message) - expected_tac_message = TACSerializer().decode(tac_message_bytes) - print(tac_message.body) - print(expected_tac_message.body) - assert expected_tac_message == tac_message - - -def test_unregister_serialization(): - """Test that the serialization of the 'Unregister' message works.""" - tac_message = TACMessage(tac_type=TACMessage.Type.UNREGISTER) - tac_message_bytes = TACSerializer().encode(tac_message) - expected_tac_message = TACSerializer().decode(tac_message_bytes) - assert expected_tac_message == tac_message - - -def test_transaction_serialization(): - """Test that the serialization of the 'Transaction' message works.""" - tac_message = TACMessage(tac_type=TACMessage.Type.TRANSACTION, - transaction_id="transaction_id", - is_sender_buyer=True, - counterparty="seller", - amount=10.0, - quantities_by_good_pbk={'tac_good_0_pbk': 1, 'tac_good_1_pbk': 1}) - tac_message_bytes = TACSerializer().encode(tac_message) - expected_msg = TACSerializer().decode(tac_message_bytes) - - assert expected_msg == tac_message - - -def test_get_state_update_serialization(): - """Test that the serialization of the 'GetStateUpdate' message works.""" - tac_message = TACMessage(tac_type=TACMessage.Type.GET_STATE_UPDATE) - tac_message_bytes = TACSerializer().encode(tac_message) - expected_msg = TACSerializer().decode(tac_message_bytes) - - assert expected_msg == tac_message - - -def test_cancelled_serialization(): - """Test that the serialization of the 'Cancelled' message works.""" - tac_message = TACMessage(tac_type=TACMessage.Type.CANCELLED) - tac_message_bytes = TACSerializer().encode(tac_message) - expected_msg = TACSerializer().decode(tac_message_bytes) - - assert expected_msg == tac_message - - -@pytest.mark.parametrize("error_code", list(TACMessage.ErrorCode)) -def test_error_serialization(error_code): - """Test that the serialization of the 'Error' message works.""" - tac_message = TACMessage(tac_type=TACMessage.Type.TAC_ERROR, - error_code=error_code, - details={"foo": "bar"}) - tac_message_bytes = TACSerializer().encode(tac_message) - expected_msg = TACSerializer().decode(tac_message_bytes) - - assert expected_msg == tac_message - - -def test_game_data_serialization(): - """Test that the serialization of the 'GameData' message works.""" - tac_message = TACMessage(tac_type=TACMessage.Type.GAME_DATA, - money=10.0, - endowment=[1, 1, 2], - utility_params=[0.04, 0.80, 0.16], - nb_agents=3, - nb_goods=3, - tx_fee=1.0, - agent_pbk_to_name={'tac_agent_0_pbk': 'tac_agent_0', 'tac_agent_1_pbk': 'tac_agent_1', 'tac_agent_2_pbk': 'tac_agent_2'}, - good_pbk_to_name={'tag_good_0_pbk': 'tag_good_0', 'tag_good_1_pbk': 'tag_good_1', 'tag_good_2_pbk': 'tag_good_2'}) - tac_message_bytes = TACSerializer().encode(tac_message) - expected_msg = TACSerializer().decode(tac_message_bytes) - - assert expected_msg == tac_message - - -def test_transaction_confirmation_serialization(): - """Test that the serialization of the 'TransactionConfirmation' message works.""" - tac_message = TACMessage(tac_type=TACMessage.Type.TRANSACTION_CONFIRMATION, transaction_id="transaction_id") - tac_message_bytes = TACSerializer().encode(tac_message) - expected_msg = TACSerializer().decode(tac_message_bytes) - - assert expected_msg == tac_message - - -def test_state_update_serialization(): - """Test that the serialization of the 'StateUpdate' message works.""" - game_state = dict( - money=10.0, - endowment=[1, 1, 2], - utility_params=[0.04, 0.80, 0.16], - nb_agents=3, - nb_goods=3, - tx_fee=1.0, - agent_pbk_to_name={'tac_agent_0_pbk': 'tac_agent_0', 'tac_agent_1_pbk': 'tac_agent_1', 'tac_agent_2_pbk': 'tac_agent_2'}, - good_pbk_to_name={'tag_good_0_pbk': 'tag_good_0', 'tag_good_1_pbk': 'tag_good_1', 'tag_good_2_pbk': 'tag_good_2'}) - - transactions = [dict( - transaction_id="transaction_id", - is_sender_buyer=True, - counterparty="seller", - amount=10.0, - quantities_by_good_pbk={"tac_good_0_pbk": 1} - )] - - tac_message = TACMessage(tac_type=TACMessage.Type.STATE_UPDATE, initial_state=game_state, transactions=transactions) - tac_message_bytes = TACSerializer().encode(tac_message) - expected_msg = TACSerializer().decode(tac_message_bytes) - - assert expected_msg == tac_message diff --git a/tests/test_scaffold_message.py b/tests/test_scaffold_message.py new file mode 100644 index 0000000000..0954bff0de --- /dev/null +++ b/tests/test_scaffold_message.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# 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. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests for aea.aea.py.""" diff --git a/tox.ini b/tox.ini index 95ad404deb..7e7254eac1 100644 --- a/tox.ini +++ b/tox.ini @@ -20,23 +20,27 @@ basepython = python3.7 deps = flake8 flake8-docstrings pydocstyle==3.0.0 -commands = flake8 aea examples scripts tests --exclude=.md,aea/*_pb2.py,aea/__init__.py,aea/cli/__init__.py,tests/common//oef_search_pluto_scripts,scripts/oef/launch.py --ignore=E501,E701 +commands = flake8 aea examples packages scripts tests --exclude=.md,*_pb2.py,aea/__init__.py,aea/cli/__init__.py,tests/common//oef_search_pluto_scripts,scripts/oef/launch.py --ignore=E501,E701 [testenv:mypy] basepython = python3.7 deps = mypy -commands = mypy aea examples tests scripts +commands = mypy aea packages tests scripts [testenv:docs] description = Build the documentation. basepython = python3.7 deps = mkdocs + mkdocs-material + pymdown-extensions commands = mkdocs build --clean [testenv:docs-serve] description = Run a development server for working on documentation. basepython = python3.7 deps = mkdocs + mkdocs-material + pymdown-extensions commands = mkdocs build --clean python -c 'print("###### Starting local server. Press Control+C to stop server ######")' mkdocs serve -a localhost:8080