diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..be0c949 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,19 @@ +name: main + +on: + push: + +jobs: + spec: + runs-on: ubuntu-latest + container: python:3.8 + steps: + - uses: actions/checkout@v2 + - name: Install dependencies + run: | + pip install pipenv + pipenv install --dev + - name: Spec + run: pipenv run spec + - name: Lint + run: pipenv run format-check \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..410f456 --- /dev/null +++ b/.gitignore @@ -0,0 +1,137 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + + +# remove outputted graphml files +*.graphml + +.DS_Store + +.vscode \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e99277f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Rowan Twell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..99f821a --- /dev/null +++ b/Pipfile @@ -0,0 +1,26 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +mamba = "*" +expects = "*" +black = "*" +pylint = "*" +rope = "*" + +[packages] +bunnyplot = {editable = true, path = "."} + +[requires] +python_version = "3.8" + +[scripts] +format = "black bunnyplot" +format-check = "black bunnyplot --check" +spec = "mamba --format=documentation" +itests = "mamba integration_tests" + +[pipenv] +allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..9b9b887 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,385 @@ +{ + "_meta": { + "hash": { + "sha256": "02f617bc4eda559d2bb758130a59095fa92129ec4e39549004f0b1fece176dc5" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aiohttp": { + "hashes": [ + "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", + "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", + "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", + "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", + "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", + "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", + "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", + "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", + "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", + "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", + "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", + "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" + ], + "markers": "python_full_version >= '3.5.3'", + "version": "==3.6.2" + }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "markers": "python_full_version >= '3.5.3'", + "version": "==3.0.1" + }, + "attrs": { + "hashes": [ + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.2.0" + }, + "bunnyplot": { + "editable": true, + "path": "." + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==7.1.2" + }, + "decorator": { + "hashes": [ + "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760", + "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7" + ], + "version": "==4.4.2" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, + "multidict": { + "hashes": [ + "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", + "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", + "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", + "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", + "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", + "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", + "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", + "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", + "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", + "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", + "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", + "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", + "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", + "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", + "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", + "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", + "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" + ], + "markers": "python_version >= '3.5'", + "version": "==4.7.6" + }, + "networkx": { + "hashes": [ + "sha256:7978955423fbc9639c10498878be59caf99b44dc304c2286162fd24b458c1602", + "sha256:8c5812e9f798d37c50570d15c4a69d5710a18d77bafc903ee9c5fba7454c616c" + ], + "markers": "python_version >= '3.6'", + "version": "==2.5" + }, + "yarl": { + "hashes": [ + "sha256:04a54f126a0732af75e5edc9addeaa2113e2ca7c6fce8974a63549a70a25e50e", + "sha256:3cc860d72ed989f3b1f3abbd6ecf38e412de722fb38b8f1b1a086315cf0d69c5", + "sha256:5d84cc36981eb5a8533be79d6c43454c8e6a39ee3118ceaadbd3c029ab2ee580", + "sha256:5e447e7f3780f44f890360ea973418025e8c0cdcd7d6a1b221d952600fd945dc", + "sha256:61d3ea3c175fe45f1498af868879c6ffeb989d4143ac542163c45538ba5ec21b", + "sha256:67c5ea0970da882eaf9efcf65b66792557c526f8e55f752194eff8ec722c75c2", + "sha256:6f6898429ec3c4cfbef12907047136fd7b9e81a6ee9f105b45505e633427330a", + "sha256:7ce35944e8e61927a8f4eb78f5bc5d1e6da6d40eadd77e3f79d4e9399e263921", + "sha256:b7c199d2cbaf892ba0f91ed36d12ff41ecd0dde46cbf64ff4bfe997a3ebc925e", + "sha256:c15d71a640fb1f8e98a1423f9c64d7f1f6a3a168f803042eaf3a5b5022fde0c1", + "sha256:c22607421f49c0cb6ff3ed593a49b6a99c6ffdeaaa6c944cdda83c2393c8864d", + "sha256:c604998ab8115db802cc55cb1b91619b2831a6128a62ca7eea577fc8ea4d3131", + "sha256:d088ea9319e49273f25b1c96a3763bf19a882cff774d1792ae6fba34bd40550a", + "sha256:db9eb8307219d7e09b33bcb43287222ef35cbcf1586ba9472b0a4b833666ada1", + "sha256:e31fef4e7b68184545c3d68baec7074532e077bd1906b040ecfba659737df188", + "sha256:e32f0fb443afcfe7f01f95172b66f279938fbc6bdaebe294b0ff6747fb6db020", + "sha256:fcbe419805c9b20db9a51d33b942feddbf6e7fb468cb20686fd7089d4164c12a" + ], + "markers": "python_version >= '3.5'", + "version": "==1.6.0" + } + }, + "develop": { + "appdirs": { + "hashes": [ + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" + ], + "version": "==1.4.4" + }, + "args": { + "hashes": [ + "sha256:a785b8d837625e9b61c39108532d95b85274acd679693b71ebb5156848fcf814" + ], + "version": "==0.1.0" + }, + "astroid": { + "hashes": [ + "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", + "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" + ], + "markers": "python_version >= '3.5'", + "version": "==2.4.2" + }, + "black": { + "hashes": [ + "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea" + ], + "index": "pypi", + "version": "==20.8b1" + }, + "click": { + "hashes": [ + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==7.1.2" + }, + "clint": { + "hashes": [ + "sha256:05224c32b1075563d0b16d0015faaf9da43aa214e4a2140e51f08789e7a4c5aa" + ], + "version": "==0.5.1" + }, + "coverage": { + "hashes": [ + "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", + "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259", + "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9", + "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097", + "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0", + "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", + "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", + "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", + "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", + "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", + "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", + "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978", + "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9", + "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f", + "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9", + "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822", + "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418", + "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82", + "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f", + "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d", + "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221", + "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4", + "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21", + "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709", + "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54", + "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d", + "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270", + "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24", + "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751", + "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a", + "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", + "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", + "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", + "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==5.3" + }, + "expects": { + "hashes": [ + "sha256:419902ccafe81b7e9559eeb6b7a07ef9d5c5604eddb93000f0642b3b2d594f4c" + ], + "index": "pypi", + "version": "==0.9.0" + }, + "isort": { + "hashes": [ + "sha256:6187a9f1ce8784cbc6d1b88790a43e6083a6302f03e9ae482acc0f232a98c843", + "sha256:c16eaa7432a1c004c585d79b12ad080c6c421dd18fe27982ca11f95e6898e432" + ], + "markers": "python_version >= '3.6' and python_version < '4.0'", + "version": "==5.5.3" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", + "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", + "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", + "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", + "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", + "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", + "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", + "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", + "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", + "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", + "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", + "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", + "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", + "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", + "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", + "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", + "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", + "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", + "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", + "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", + "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.4.3" + }, + "mamba": { + "hashes": [ + "sha256:f976735949bc9a8731cc0876aaea2720949bd3d1554b0e94004c91a4f61abecb" + ], + "index": "pypi", + "version": "==0.11.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "pathspec": { + "hashes": [ + "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", + "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" + ], + "version": "==0.8.0" + }, + "pylint": { + "hashes": [ + "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", + "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f" + ], + "index": "pypi", + "version": "==2.6.0" + }, + "regex": { + "hashes": [ + "sha256:088afc8c63e7bd187a3c70a94b9e50ab3f17e1d3f52a32750b5b77dbe99ef5ef", + "sha256:1fe0a41437bbd06063aa184c34804efa886bcc128222e9916310c92cd54c3b4c", + "sha256:41bb65f54bba392643557e617316d0d899ed5b4946dccee1cb6696152b29844b", + "sha256:4318d56bccfe7d43e5addb272406ade7a2274da4b70eb15922a071c58ab0108c", + "sha256:4707f3695b34335afdfb09be3802c87fa0bc27030471dbc082f815f23688bc63", + "sha256:5533a959a1748a5c042a6da71fe9267a908e21eded7a4f373efd23a2cbdb0ecc", + "sha256:5f18875ac23d9aa2f060838e8b79093e8bb2313dbaaa9f54c6d8e52a5df097be", + "sha256:60b0e9e6dc45683e569ec37c55ac20c582973841927a85f2d8a7d20ee80216ab", + "sha256:84e9407db1b2eb368b7ecc283121b5e592c9aaedbe8c78b1a2f1102eb2e21d19", + "sha256:8d69cef61fa50c8133382e61fd97439de1ae623fe943578e477e76a9d9471637", + "sha256:9a02d0ae31d35e1ec12a4ea4d4cca990800f66a917d0fb997b20fbc13f5321fc", + "sha256:9bc13e0d20b97ffb07821aa3e113f9998e84994fe4d159ffa3d3a9d1b805043b", + "sha256:a6f32aea4260dfe0e55dc9733ea162ea38f0ea86aa7d0f77b15beac5bf7b369d", + "sha256:ae91972f8ac958039920ef6e8769277c084971a142ce2b660691793ae44aae6b", + "sha256:c570f6fa14b9c4c8a4924aaad354652366577b4f98213cf76305067144f7b100", + "sha256:d23a18037313714fb3bb5a94434d3151ee4300bae631894b1ac08111abeaa4a3", + "sha256:eaf548d117b6737df379fdd53bdde4f08870e66d7ea653e230477f071f861121", + "sha256:ebbe29186a3d9b0c591e71b7393f1ae08c83cb2d8e517d2a822b8f7ec99dfd8b", + "sha256:eda4771e0ace7f67f58bc5b560e27fb20f32a148cbc993b0c3835970935c2707", + "sha256:f1b3afc574a3db3b25c89161059d857bd4909a1269b0b3cb3c904677c8c4a3f7", + "sha256:f2388013e68e750eaa16ccbea62d4130180c26abb1d8e5d584b9baf69672b30f" + ], + "version": "==2020.9.27" + }, + "six": { + "hashes": [ + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" + }, + "toml": { + "hashes": [ + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + ], + "version": "==0.10.1" + }, + "typed-ast": { + "hashes": [ + "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", + "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", + "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", + "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", + "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", + "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", + "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", + "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", + "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", + "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", + "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", + "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", + "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", + "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" + ], + "version": "==1.4.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", + "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", + "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + ], + "version": "==3.7.4.3" + }, + "wrapt": { + "hashes": [ + "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" + ], + "version": "==1.12.1" + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..f1161e3 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# bunnyplot [![pypi version](https://img.shields.io/pypi/v/bunnyplot)](https://pypi.org/project/bunnyplot) +A utility for producting GraphML graphs and other visualizations of RabbitMQ + +![A rabbitMQ graph](bunnyplot.png) + +## Usage +``` +% bunnyplot --help +Usage: bunnyplot [OPTIONS] URL OUTPUT_PATH + +Options: + -u, --username TEXT + -p, --password TEXT + --help Show this message and exit. +``` + +### Usage with yEd +By default, yEd will not recognize node labels or properties. These must be mapped using the Properties Mapper(`Edit > Properties Mapper...`). + +bunnyplot provides a preset for mapping properties. [bunnyplot.cnfx](bunnyplot.cnfx). Import this to your properties mapper and apply the changes. You should see the nodes change size, shape and colour. + +You may also need to rearrange them into a better layout. Use the `Layout` menu to select a type e.g. `Organic` and apply. + + +## Development + +This project uses pipenv to manage it's **development** environment. + +Development dependencies should be specified in `setup.py`. Package dependencies should not change in the Pipfile. + +Code should be formatted with `black`. Use the pipenv script `format-check` and `format` to check and format code, respectively. + diff --git a/bunnyplot.cnfx b/bunnyplot.cnfx new file mode 100644 index 0000000..c99ac7f --- /dev/null +++ b/bunnyplot.cnfx @@ -0,0 +1,261 @@ + + + + diff --git a/bunnyplot.png b/bunnyplot.png new file mode 100644 index 0000000..d36f498 Binary files /dev/null and b/bunnyplot.png differ diff --git a/bunnyplot/__init__.py b/bunnyplot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bunnyplot/cli.py b/bunnyplot/cli.py new file mode 100644 index 0000000..1316245 --- /dev/null +++ b/bunnyplot/cli.py @@ -0,0 +1,41 @@ +import asyncio +import click +import logging + +import bunnyplot.log +from bunnyplot.graph import build_graph, write_graphml, info +from bunnyplot.rabbitmq import RabbitMQApi + +logger = logging.getLogger("bunnyplot") + + +@click.command() +@click.argument("url", envvar="BUNNYPLOT_URL") +@click.argument("output_path", envvar="BUNNYPLOT_OUTPUT") +@click.option( + "--username", "-u", prompt=True, hide_input=True, envvar="BUNNYPLOT_USERNAME" +) +@click.option( + "--password", "-p", prompt=True, hide_input=True, envvar="BUNNYPLOT_PASSWORD" +) +def cli(url, output_path, username, password): + async def main(): + api = RabbitMQApi(url, username, password) + + logger.info("fetching data...") + consumers, definitions = await asyncio.gather( + api.get_consumers(), api.get_definitions() + ) + + graph = build_graph(consumers, definitions) + + logger.info(info(graph)) + + logger.info("writing to %s", output_path) + write_graphml(graph, output_path) + + asyncio.run(main()) + + +if __name__ == "__main__": + cli() diff --git a/bunnyplot/graph.py b/bunnyplot/graph.py new file mode 100644 index 0000000..8cc812d --- /dev/null +++ b/bunnyplot/graph.py @@ -0,0 +1,43 @@ +import networkx as nx + + +def info(graph): + return nx.info(graph) + + +def write_graphml(graph, path): + return nx.write_graphml(graph, path) + + +def build_graph(consumers, definitions): + queues = definitions["queues"] + exchanges = definitions["exchanges"] + bindings = definitions["bindings"] + + graph = nx.DiGraph(name=f"RabbitMQ graph") + for q in queues: + id = q["name"] + graph.add_node(id) + graph.nodes[id]["name"] = id + graph.nodes[id]["type"] = "queue" + + for x in exchanges: + id = x["name"] + graph.add_node(id) + graph.nodes[id]["name"] = id + graph.nodes[id]["type"] = "exchange" + + for qx in bindings: + source_id = qx["source"] + destination_id = qx["destination"] + graph.add_edge(source_id, destination_id) + + for c in consumers: + id = c["channel_details"]["user"] + q_id = c["queue"]["name"] + graph.add_node(id) + graph.nodes[id]["name"] = id + graph.nodes[id]["type"] = "consumer" + graph.add_edge(q_id, id) + + return graph diff --git a/bunnyplot/log.py b/bunnyplot/log.py new file mode 100644 index 0000000..b7f0d9e --- /dev/null +++ b/bunnyplot/log.py @@ -0,0 +1,14 @@ +import logging +import sys + +""" +static logging config +""" +root = logging.getLogger() +root.setLevel(logging.DEBUG) + +handler = logging.StreamHandler(sys.stderr) +handler.setLevel(logging.DEBUG) +formatter = logging.Formatter("%(message)s") +handler.setFormatter(formatter) +root.addHandler(handler) diff --git a/bunnyplot/rabbitmq.py b/bunnyplot/rabbitmq.py new file mode 100644 index 0000000..999fa7f --- /dev/null +++ b/bunnyplot/rabbitmq.py @@ -0,0 +1,36 @@ +import asyncio +import aiohttp +import async_timeout +import logging +from base64 import b64encode + +TIMEOUT = 10 +logger = logging.Logger(__name__) + + +class RabbitMQApi: + def __init__(self, url, username, password): + self.__url = url + basic_credentials = b64encode(f"{username}:{password}".encode()).decode() + self.__auth = f"Basic {basic_credentials}" + + def __get_url(self, endpoint): + return self.__url + endpoint + + async def get_definitions(self): + async with aiohttp.ClientSession() as session: + async with async_timeout.timeout(TIMEOUT): + async with session.get( + self.__get_url("/api/definitions"), + headers={"Authorization": self.__auth}, + ) as response: + return await response.json() + + async def get_consumers(self): + async with aiohttp.ClientSession() as session: + async with async_timeout.timeout(TIMEOUT): + async with session.get( + self.__get_url("/api/consumers"), + headers={"Authorization": self.__auth}, + ) as response: + return await response.json() diff --git a/integration_tests/.docker/rabbitmq/definitions.json b/integration_tests/.docker/rabbitmq/definitions.json new file mode 100644 index 0000000..0e92de7 --- /dev/null +++ b/integration_tests/.docker/rabbitmq/definitions.json @@ -0,0 +1,236 @@ +{ + "rabbit_version": "3.6.5", + "users": [ + { + "name": "guest", + "password_hash": "o2MJjT8UKSRM7eoLDMWvm4LxqaFvDxd2wLg1KQQQ2jXfG5UE", + "hashing_algorithm": "rabbit_password_hashing_sha256", + "tags": "administrator" + } + ], + "vhosts": [ + { + "name": "/" + } + ], + "permissions": [ + { + "user": "guest", + "vhost": "/", + "configure": ".*", + "write": ".*", + "read": ".*" + } + ], + "parameters": [], + "policies": [], + "exchanges": [ + { + "vhost": "/", + "name": "e", + "type": "fanout", + "durable": true, + "auto_delete": false, + "internal": false, + "arguments": {} + }, + { + "vhost": "/", + "name": "e-dl", + "type": "fanout", + "durable": true, + "auto_delete": false, + "internal": false, + "arguments": {} + } + ], + "queues": [ + { + "vhost": "/", + "name": "q1", + "durable": true, + "auto_delete": false, + "arguments": { + "x-dead-letter-exchange": "e-dl" + } + }, + { + "vhost": "/", + "name": "q2", + "durable": true, + "auto_delete": false, + "arguments": { + "x-dead-letter-exchange": "e-dl" + } + }, + { + "vhost": "/", + "name": "q3", + "durable": true, + "auto_delete": false, + "arguments": { + "x-dead-letter-exchange": "e-dl" + } + }, + { + "vhost": "/", + "name": "q4", + "durable": true, + "auto_delete": false, + "arguments": { + "x-dead-letter-exchange": "e-dl" + } + }, + { + "vhost": "/", + "name": "q5", + "durable": true, + "auto_delete": false, + "arguments": { + "x-dead-letter-exchange": "e-dl" + } + }, + { + "vhost": "/", + "name": "q6", + "durable": true, + "auto_delete": false, + "arguments": { + "x-dead-letter-exchange": "e-dl" + } + }, + { + "vhost": "/", + "name": "q7", + "durable": true, + "auto_delete": false, + "arguments": { + "x-dead-letter-exchange": "e-dl" + } + }, + { + "vhost": "/", + "name": "q8", + "durable": true, + "auto_delete": false, + "arguments": { + "x-dead-letter-exchange": "e-dl" + } + }, + { + "vhost": "/", + "name": "q9", + "durable": true, + "auto_delete": false, + "arguments": { + "x-dead-letter-exchange": "e-dl" + } + }, + { + "vhost": "/", + "name": "q10", + "durable": true, + "auto_delete": false, + "arguments": { + "x-dead-letter-exchange": "e-dl" + } + }, + { + "vhost": "/", + "name": "q-dl", + "durable": true, + "auto_delete": false, + "arguments": {} + } + ], + "bindings": [ + { + "vhost": "/", + "source": "e", + "destination": "q1", + "destination_type": "queue", + "routing_key": "1", + "arguments": {} + }, + { + "vhost": "/", + "source": "e", + "destination": "q2", + "destination_type": "queue", + "routing_key": "1", + "arguments": {} + }, + { + "vhost": "/", + "source": "e", + "destination": "q3", + "destination_type": "queue", + "routing_key": "1", + "arguments": {} + }, + { + "vhost": "/", + "source": "e", + "destination": "q4", + "destination_type": "queue", + "routing_key": "1", + "arguments": {} + }, + { + "vhost": "/", + "source": "e", + "destination": "q5", + "destination_type": "queue", + "routing_key": "1", + "arguments": {} + }, + { + "vhost": "/", + "source": "e", + "destination": "q6", + "destination_type": "queue", + "routing_key": "1", + "arguments": {} + }, + { + "vhost": "/", + "source": "e", + "destination": "q7", + "destination_type": "queue", + "routing_key": "1", + "arguments": {} + }, + { + "vhost": "/", + "source": "e", + "destination": "q8", + "destination_type": "queue", + "routing_key": "1", + "arguments": {} + }, + { + "vhost": "/", + "source": "e", + "destination": "q9", + "destination_type": "queue", + "routing_key": "1", + "arguments": {} + }, + { + "vhost": "/", + "source": "e", + "destination": "q10", + "destination_type": "queue", + "routing_key": "1", + "arguments": {} + }, + { + "vhost": "/", + "source": "e-dl", + "destination": "q-dl", + "destination_type": "queue", + "routing_key": "", + "arguments": {} + } + ] +} \ No newline at end of file diff --git a/integration_tests/.docker/rabbitmq/rabbitmq.config b/integration_tests/.docker/rabbitmq/rabbitmq.config new file mode 100644 index 0000000..3257e3c --- /dev/null +++ b/integration_tests/.docker/rabbitmq/rabbitmq.config @@ -0,0 +1,12 @@ +[ + {rabbit, [ + {loopback_users, []}, + {vm_memory_high_watermark, 0.7}, + {vm_memory_high_watermark_paging_ratio, 0.8}, + {log_levels, [{channel, warning}, {connection, warning}, {federation, warning}, {mirroring, info}]}, + {heartbeat, 10} + ]}, + {rabbitmq_management, [ + {load_definitions, "/opt/definitions.json"} + ]} + ]. \ No newline at end of file diff --git a/integration_tests/README.md b/integration_tests/README.md new file mode 100644 index 0000000..ecadcd0 --- /dev/null +++ b/integration_tests/README.md @@ -0,0 +1,4 @@ + +``` +run mamba integration_tests/**/*_test.py +``` \ No newline at end of file diff --git a/integration_tests/cli_spec.py b/integration_tests/cli_spec.py new file mode 100644 index 0000000..1269729 --- /dev/null +++ b/integration_tests/cli_spec.py @@ -0,0 +1,18 @@ +from mamba import description, before, context, it, _it +from expects import expect, equal, raise_error, have_len +import xml.etree.ElementTree as ET +import subprocess + +with description("Integration tests") as self: + with it("should produce the expected graphml"): + result = subprocess.check_output("bunnyplot http://localhost:15672 ./out.graphml -u guest -p guest", shell=True) + + ns = {'g': 'http://graphml.graphdrawing.org/xmlns'} + root = ET.parse('./out.graphml').getroot() + + print(root) + nodes = root.findall(".//g:node", ns) + edges = root.findall(".//g:edge", ns) + + expect(nodes).to(have_len(13)) + expect(edges).to(have_len(11)) \ No newline at end of file diff --git a/integration_tests/docker-compose.yml b/integration_tests/docker-compose.yml new file mode 100644 index 0000000..cfc3889 --- /dev/null +++ b/integration_tests/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3" +services: + rabbitmq: + image: rabbitmq:3-management-alpine + container_name: rabbitmq + volumes: + - ./.docker/rabbitmq/definitions.json:/opt/definitions.json:ro + - ./.docker/rabbitmq/rabbitmq.config:/etc/rabbitmq/rabbitmq.config:ro + ports: + - 5672:5672 + - 15672:15672 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..3c92755 --- /dev/null +++ b/setup.py @@ -0,0 +1,31 @@ +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +setuptools.setup( + name="bunnyplot", + version="0.1.0", + author="Rowan Twell", + author_email="rowantwell@gmail.com", + description="A utility for producting GraphML from RabbitMQ", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/jerometwell/bunnyplot", + packages=setuptools.find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + install_requires=[ + "click", + "networkx", + "aiohttp", + "async_timeout" + ], + entry_points = { + 'console_scripts': ['bunnyplot=bunnyplot.cli:cli'], + }, + python_requires='>=3.6', +) \ No newline at end of file diff --git a/spec/cli_spec.py b/spec/cli_spec.py new file mode 100644 index 0000000..be29c7e --- /dev/null +++ b/spec/cli_spec.py @@ -0,0 +1,17 @@ +from mamba import description, before, context, it, _it +from expects import expect, equal, raise_error +from unittest.mock import MagicMock, Mock, patch, AsyncMock, call +from click.testing import CliRunner +import asyncio + +from bunnyplot.cli import cli + +with description(cli) as self: + with before.all: + self.loop = asyncio.get_event_loop() + + with before.each: + self.runner = CliRunner() + + with after.all: + self.loop.close() \ No newline at end of file diff --git a/spec/graph_spec.py b/spec/graph_spec.py new file mode 100644 index 0000000..99c57e8 --- /dev/null +++ b/spec/graph_spec.py @@ -0,0 +1,12 @@ +from mamba import description, before, context, it, _it +from expects import expect, equal, raise_error +from unittest.mock import MagicMock, Mock, patch, AsyncMock, call + +from bunnyplot.graph import build_graph + +with description(build_graph) as self: + with _it("should build a DiGraph"): + pass + + with _it("should contain the correct nodes and edges"): + pass \ No newline at end of file diff --git a/spec/rabbitmq_spec.py b/spec/rabbitmq_spec.py new file mode 100644 index 0000000..11d5c49 --- /dev/null +++ b/spec/rabbitmq_spec.py @@ -0,0 +1,23 @@ +from mamba import description, before, context, it, _it +from expects import expect, equal, raise_error +from unittest.mock import MagicMock, Mock, patch, AsyncMock, call +import asyncio + +from bunnyplot.rabbitmq import RabbitMQApi + +with description(RabbitMQApi) as self: + with before.each: + self.api = RabbitMQApi("http://localhost", "test_username", "test_password") + + with description("#get_definitions"): + with _it("should make a get request to /api/definitions"): + pass + with _it("should authorize using the correct Basic credentials"): + pass + + with description("#get_consumers"): + with _it("should make a get request to /api/consumers"): + pass + with _it("should authorize using the correct Basic credentials"): + pass + \ No newline at end of file