├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── app ├── __init__.py ├── data.txt ├── models.py ├── schemas.py └── settings.py ├── docker-compose.yml ├── run.py └── wait-for.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __pycache__/ 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.6-alpine3.11 2 | 3 | RUN apk add --no-cache build-base gcc libffi-dev libressl-dev 4 | 5 | COPY Pipfile* /tmp/ 6 | WORKDIR /tmp/ 7 | RUN pip install pipenv 8 | RUN pipenv install --system 9 | 10 | COPY . /app/ 11 | WORKDIR /app/ 12 | 13 | # we have to wait even after wait-for.sh 14 | # because neo4j doesn't work when it starts listening to a port 15 | CMD echo "Waiting for $NEO4J_HOST:$NEO4J_PORT..." && \ 16 | /app/wait-for.sh -t 60 $NEO4J_HOST:$NEO4J_PORT && \ 17 | sleep 15 && \ 18 | python run.py 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004-2018, Elements Interactive B.V., the Netherlands 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | * Neither the name of the owner nor the names of its contributors may be 14 | used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 21 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | docker-build: 2 | docker build -f Dockerfile . -t elementsinteractive/flask-graphql-neo4j:latest 3 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | flask = "*" 8 | graphene = "*" 9 | py2neo = "*" 10 | flask-graphql = "*" 11 | maya = "*" 12 | environs = "*" 13 | 14 | [dev-packages] 15 | ipdb = "*" 16 | "flake8" = "*" 17 | "autopep8" = "*" 18 | 19 | [requires] 20 | python_version = "3.7" 21 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "4a3c2aa950b51c7b4c3e4cbbbae2d9167819f915de0a8604d79099baf9ddf1a9" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aniso8601": { 20 | "hashes": [ 21 | "sha256:513d2b6637b7853806ae79ffaca6f3e8754bdd547048f5ccc1420aec4b714f1e", 22 | "sha256:d10a4bf949f619f719b227ef5386e31f49a2b6d453004b21f02661ccc8670c7b" 23 | ], 24 | "version": "==7.0.0" 25 | }, 26 | "certifi": { 27 | "hashes": [ 28 | "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", 29 | "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" 30 | ], 31 | "version": "==2019.9.11" 32 | }, 33 | "click": { 34 | "hashes": [ 35 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 36 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 37 | ], 38 | "version": "==7.0" 39 | }, 40 | "colorama": { 41 | "hashes": [ 42 | "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", 43 | "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" 44 | ], 45 | "version": "==0.4.1" 46 | }, 47 | "dateparser": { 48 | "hashes": [ 49 | "sha256:983d84b5e3861cb0aa240cad07f12899bb10b62328aae188b9007e04ce37d665", 50 | "sha256:e1eac8ef28de69a554d5fcdb60b172d526d61924b1a40afbbb08df459a36006b" 51 | ], 52 | "version": "==0.7.2" 53 | }, 54 | "environs": { 55 | "hashes": [ 56 | "sha256:59bea3c982fbcc9d0755d160f67e725b70227ecbc14f7507ac81f484c6fd8334", 57 | "sha256:e0a6c53b05effbb7521374e564eb751ba2311bd36dcde2c7d914246175b8e02d" 58 | ], 59 | "index": "pypi", 60 | "version": "==6.0.0" 61 | }, 62 | "flask": { 63 | "hashes": [ 64 | "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", 65 | "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" 66 | ], 67 | "index": "pypi", 68 | "version": "==1.1.1" 69 | }, 70 | "flask-graphql": { 71 | "hashes": [ 72 | "sha256:b4e23dfd767f7028835208db0bfa7145f59467225882e6d4f560d15c09f32de8" 73 | ], 74 | "index": "pypi", 75 | "version": "==2.0.0" 76 | }, 77 | "graphene": { 78 | "hashes": [ 79 | "sha256:09165f03e1591b76bf57b133482db9be6dac72c74b0a628d3c93182af9c5a896", 80 | "sha256:2cbe6d4ef15cfc7b7805e0760a0e5b80747161ce1b0f990dfdc0d2cf497c12f9" 81 | ], 82 | "index": "pypi", 83 | "version": "==2.1.8" 84 | }, 85 | "graphql-core": { 86 | "hashes": [ 87 | "sha256:1488f2a5c2272dc9ba66e3042a6d1c30cea0db4c80bd1e911c6791ad6187d91b", 88 | "sha256:da64c472d720da4537a2e8de8ba859210b62841bd47a9be65ca35177f62fe0e4" 89 | ], 90 | "version": "==2.2.1" 91 | }, 92 | "graphql-relay": { 93 | "hashes": [ 94 | "sha256:0e94201af4089e1f81f07d7bd8f84799768e39d70fa1ea16d1df505b46cc6335", 95 | "sha256:75aa0758971e252964cb94068a4decd472d2a8295229f02189e3cbca1f10dbb5", 96 | "sha256:7fa74661246e826ef939ee92e768f698df167a7617361ab399901eaebf80dce6" 97 | ], 98 | "version": "==2.0.0" 99 | }, 100 | "graphql-server-core": { 101 | "hashes": [ 102 | "sha256:e5f82add4b3d5580aa1f1e7d9f00e944ad3abe1b65eb337e611d6a77cc20f231" 103 | ], 104 | "version": "==1.1.1" 105 | }, 106 | "humanize": { 107 | "hashes": [ 108 | "sha256:a43f57115831ac7c70de098e6ac46ac13be00d69abbf60bdcac251344785bb19" 109 | ], 110 | "version": "==0.5.1" 111 | }, 112 | "itsdangerous": { 113 | "hashes": [ 114 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 115 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 116 | ], 117 | "version": "==1.1.0" 118 | }, 119 | "jinja2": { 120 | "hashes": [ 121 | "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", 122 | "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" 123 | ], 124 | "version": "==2.10.1" 125 | }, 126 | "markupsafe": { 127 | "hashes": [ 128 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 129 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 130 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 131 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 132 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 133 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 134 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 135 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 136 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 137 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 138 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 139 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 140 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 141 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 142 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 143 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 144 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 145 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 146 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 147 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 148 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 149 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 150 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 151 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 152 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 153 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 154 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 155 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" 156 | ], 157 | "version": "==1.1.1" 158 | }, 159 | "marshmallow": { 160 | "hashes": [ 161 | "sha256:6dca0125320c15795c4f2dfe8f2f5c37a96916e7a09a123eba05ef24d3126b94", 162 | "sha256:801a7c70f0596b812a086773b9d7ba85b4bbad1becab14cde460ab7798511409" 163 | ], 164 | "version": "==3.2.0" 165 | }, 166 | "maya": { 167 | "hashes": [ 168 | "sha256:7f53e06d5a123613dce7c270cbc647643a6942590dba7a19ec36194d0338c3f4", 169 | "sha256:fa90d8c6c9a730a7f740dec6e1c7d3da8ca10159e40bb843e4e72772f5e3a9a3" 170 | ], 171 | "index": "pypi", 172 | "version": "==0.6.1" 173 | }, 174 | "neobolt": { 175 | "hashes": [ 176 | "sha256:fa9efe4a4defbdc63fc3f1e552d503727049586c59d8a3acf5188a2cf1a45dce" 177 | ], 178 | "version": "==1.7.13" 179 | }, 180 | "neotime": { 181 | "hashes": [ 182 | "sha256:4e0477ba0f24e004de2fa79a3236de2bd941f20de0b5db8d976c52a86d7363eb" 183 | ], 184 | "version": "==1.7.4" 185 | }, 186 | "pendulum": { 187 | "hashes": [ 188 | "sha256:1cde6e3c6310fb882c98f373795f807cb2bd6af01f34d2857e6e283b5ee91e09", 189 | "sha256:485aef2089defee88607d37d5bc238934d0b90993d7bf9ceb36e481af41e9c66", 190 | "sha256:57801754e05f30e8a7e4d24734c9fad82c6c3ec489151555f0fc66bb32ba6d6d", 191 | "sha256:7ee344bc87cb425b04717b90d14ffde14c1dd64eaa73060b3772edcf57f3e866", 192 | "sha256:c460f4d8dc41ec3c4377ac1807678cd72fe5e973cc2943c104ffdeaac32dacb7", 193 | "sha256:d3078e007315a959989c41cee5cfd63cfeeca21dd3d8295f4bc24199489e9b6c" 194 | ], 195 | "version": "==2.0.5" 196 | }, 197 | "promise": { 198 | "hashes": [ 199 | "sha256:2ebbfc10b7abf6354403ed785fe4f04b9dfd421eb1a474ac8d187022228332af", 200 | "sha256:348f5f6c3edd4fd47c9cd65aed03ac1b31136d375aa63871a57d3e444c85655c" 201 | ], 202 | "version": "==2.2.1" 203 | }, 204 | "prompt-toolkit": { 205 | "hashes": [ 206 | "sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780", 207 | "sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1", 208 | "sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55" 209 | ], 210 | "version": "==2.0.9" 211 | }, 212 | "py2neo": { 213 | "hashes": [ 214 | "sha256:a218ccb4b636e3850faa6b74ebad80f00600217172a57f745cf223d38a219222" 215 | ], 216 | "index": "pypi", 217 | "version": "==4.3.0" 218 | }, 219 | "pygments": { 220 | "hashes": [ 221 | "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", 222 | "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" 223 | ], 224 | "version": "==2.3.1" 225 | }, 226 | "python-dateutil": { 227 | "hashes": [ 228 | "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", 229 | "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" 230 | ], 231 | "version": "==2.8.0" 232 | }, 233 | "python-dotenv": { 234 | "hashes": [ 235 | "sha256:debd928b49dbc2bf68040566f55cdb3252458036464806f4094487244e2a4093", 236 | "sha256:f157d71d5fec9d4bd5f51c82746b6344dffa680ee85217c123f4a0c8117c4544" 237 | ], 238 | "version": "==0.10.3" 239 | }, 240 | "pytz": { 241 | "hashes": [ 242 | "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", 243 | "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7" 244 | ], 245 | "version": "==2019.2" 246 | }, 247 | "pytzdata": { 248 | "hashes": [ 249 | "sha256:84c52b9a47d097fcd483f047a544979de6c3a86e94c845e3569e9f8acd0fa071", 250 | "sha256:fac06f7cdfa903188dc4848c655e4adaee67ee0f2fe08e7daf815cf2a761ee5e" 251 | ], 252 | "version": "==2019.3" 253 | }, 254 | "regex": { 255 | "hashes": [ 256 | "sha256:1e9f9bc44ca195baf0040b1938e6801d2f3409661c15fe57f8164c678cfc663f", 257 | "sha256:587b62d48ca359d2d4f02d486f1f0aa9a20fbaf23a9d4198c4bed72ab2f6c849", 258 | "sha256:835ccdcdc612821edf132c20aef3eaaecfb884c9454fdc480d5887562594ac61", 259 | "sha256:93f6c9da57e704e128d90736430c5c59dd733327882b371b0cae8833106c2a21", 260 | "sha256:a46f27d267665016acb3ec8c6046ec5eae8cf80befe85ba47f43c6f5ec636dcd", 261 | "sha256:c5c8999b3a341b21ac2c6ec704cfcccbc50f1fedd61b6a8ee915ca7fd4b0a557", 262 | "sha256:d4d1829cf97632673aa49f378b0a2c3925acd795148c5ace8ef854217abbee89", 263 | "sha256:d96479257e8e4d1d7800adb26bf9c5ca5bab1648a1eddcac84d107b73dc68327", 264 | "sha256:f20f4912daf443220436759858f96fefbfc6c6ba9e67835fd6e4e9b73582791a", 265 | "sha256:f2b37b5b2c2a9d56d9e88efef200ec09c36c7f323f9d58d0b985a90923df386d", 266 | "sha256:fe765b809a1f7ce642c2edeee351e7ebd84391640031ba4b60af8d91a9045890" 267 | ], 268 | "version": "==2019.8.19" 269 | }, 270 | "rx": { 271 | "hashes": [ 272 | "sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23", 273 | "sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105" 274 | ], 275 | "version": "==1.6.1" 276 | }, 277 | "six": { 278 | "hashes": [ 279 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 280 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 281 | ], 282 | "version": "==1.12.0" 283 | }, 284 | "snaptime": { 285 | "hashes": [ 286 | "sha256:e3f1eb89043d58d30721ab98cb65023f1a4c2740e3b197704298b163c92d508b" 287 | ], 288 | "version": "==0.2.4" 289 | }, 290 | "tzlocal": { 291 | "hashes": [ 292 | "sha256:11c9f16e0a633b4b60e1eede97d8a46340d042e67b670b290ca526576e039048", 293 | "sha256:949b9dd5ba4be17190a80c0268167d7e6c92c62b30026cf9764caf3e308e5590" 294 | ], 295 | "version": "==2.0.0" 296 | }, 297 | "urllib3": { 298 | "hashes": [ 299 | "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", 300 | "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" 301 | ], 302 | "version": "==1.24.3" 303 | }, 304 | "wcwidth": { 305 | "hashes": [ 306 | "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", 307 | "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" 308 | ], 309 | "version": "==0.1.7" 310 | }, 311 | "werkzeug": { 312 | "hashes": [ 313 | "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", 314 | "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4" 315 | ], 316 | "version": "==0.16.0" 317 | } 318 | }, 319 | "develop": { 320 | "appnope": { 321 | "hashes": [ 322 | "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", 323 | "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" 324 | ], 325 | "markers": "sys_platform == 'darwin'", 326 | "version": "==0.1.0" 327 | }, 328 | "autopep8": { 329 | "hashes": [ 330 | "sha256:4d8eec30cc81bc5617dbf1218201d770dc35629363547f17577c61683ccfb3ee" 331 | ], 332 | "index": "pypi", 333 | "version": "==1.4.4" 334 | }, 335 | "backcall": { 336 | "hashes": [ 337 | "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", 338 | "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2" 339 | ], 340 | "version": "==0.1.0" 341 | }, 342 | "decorator": { 343 | "hashes": [ 344 | "sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", 345 | "sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6" 346 | ], 347 | "version": "==4.4.0" 348 | }, 349 | "entrypoints": { 350 | "hashes": [ 351 | "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", 352 | "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" 353 | ], 354 | "version": "==0.3" 355 | }, 356 | "flake8": { 357 | "hashes": [ 358 | "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", 359 | "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" 360 | ], 361 | "index": "pypi", 362 | "version": "==3.7.8" 363 | }, 364 | "ipdb": { 365 | "hashes": [ 366 | "sha256:473fdd798a099765f093231a8b1fabfa95b0b682fce12de0c74b61a4b4d8ee57" 367 | ], 368 | "index": "pypi", 369 | "version": "==0.12.2" 370 | }, 371 | "ipython": { 372 | "hashes": [ 373 | "sha256:c4ab005921641e40a68e405e286e7a1fcc464497e14d81b6914b4fd95e5dee9b", 374 | "sha256:dd76831f065f17bddd7eaa5c781f5ea32de5ef217592cf019e34043b56895aa1" 375 | ], 376 | "version": "==7.8.0" 377 | }, 378 | "ipython-genutils": { 379 | "hashes": [ 380 | "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", 381 | "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" 382 | ], 383 | "version": "==0.2.0" 384 | }, 385 | "jedi": { 386 | "hashes": [ 387 | "sha256:786b6c3d80e2f06fd77162a07fed81b8baa22dde5d62896a790a331d6ac21a27", 388 | "sha256:ba859c74fa3c966a22f2aeebe1b74ee27e2a462f56d3f5f7ca4a59af61bfe42e" 389 | ], 390 | "version": "==0.15.1" 391 | }, 392 | "mccabe": { 393 | "hashes": [ 394 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 395 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 396 | ], 397 | "version": "==0.6.1" 398 | }, 399 | "parso": { 400 | "hashes": [ 401 | "sha256:63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc", 402 | "sha256:666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c" 403 | ], 404 | "version": "==0.5.1" 405 | }, 406 | "pexpect": { 407 | "hashes": [ 408 | "sha256:2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1", 409 | "sha256:9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb" 410 | ], 411 | "markers": "sys_platform != 'win32'", 412 | "version": "==4.7.0" 413 | }, 414 | "pickleshare": { 415 | "hashes": [ 416 | "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", 417 | "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" 418 | ], 419 | "version": "==0.7.5" 420 | }, 421 | "prompt-toolkit": { 422 | "hashes": [ 423 | "sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780", 424 | "sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1", 425 | "sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55" 426 | ], 427 | "version": "==2.0.9" 428 | }, 429 | "ptyprocess": { 430 | "hashes": [ 431 | "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", 432 | "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" 433 | ], 434 | "version": "==0.6.0" 435 | }, 436 | "pycodestyle": { 437 | "hashes": [ 438 | "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", 439 | "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" 440 | ], 441 | "version": "==2.5.0" 442 | }, 443 | "pyflakes": { 444 | "hashes": [ 445 | "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", 446 | "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" 447 | ], 448 | "version": "==2.1.1" 449 | }, 450 | "pygments": { 451 | "hashes": [ 452 | "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", 453 | "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" 454 | ], 455 | "version": "==2.3.1" 456 | }, 457 | "six": { 458 | "hashes": [ 459 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 460 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 461 | ], 462 | "version": "==1.12.0" 463 | }, 464 | "traitlets": { 465 | "hashes": [ 466 | "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", 467 | "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9" 468 | ], 469 | "version": "==4.3.2" 470 | }, 471 | "wcwidth": { 472 | "hashes": [ 473 | "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", 474 | "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" 475 | ], 476 | "version": "==0.1.7" 477 | } 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Diving into GraphQL and Neo4j with Python 2 | 3 | Simple Flask proof of concept financial API to test-drive GraphQL and Neo4j with Python. 4 | 5 | ### Graph Model 6 | 7 | ![](https://i.imgur.com/hEK4e1E.png) 8 | 9 | You can read more about this in our [blog post](https://medium.com/elements/diving-into-graphql-and-neo4j-with-python-244ec39ddd94). 10 | 11 | ### Usage 12 | 13 | The simplest way is: 14 | 15 | ``` shell 16 | docker-compose up 17 | ``` 18 | 19 | The Neo4j web ui will be available at localhost:7474 and the API under localhost:8080. 20 | 21 | ### License 22 | 23 | This code is provided to the open source community under the [3-clause BSD license](LICENSE). 24 | 25 | ### Authors 26 | 27 | * [Charles David de Moraes](https://github.com/streeck) 28 | * [Yahia El Sherbini](https://github.com/yelsherbini) 29 | * [Alexander Akhmetov](https://github.com/alexander-akhmetov) 30 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify 2 | from flask_graphql import GraphQLView 3 | 4 | from .schemas import schema 5 | 6 | 7 | def create_app(): 8 | app = Flask(__name__) 9 | app.add_url_rule('/graphql', view_func=GraphQLView.as_view( 10 | 'graphql', schema=schema, graphiql=True) 11 | ) 12 | 13 | @app.errorhandler(404) 14 | def page_not_found(e): 15 | return jsonify({'message': 'The requested URL was not found on the server.'}), 404 16 | 17 | return app 18 | -------------------------------------------------------------------------------- /app/data.txt: -------------------------------------------------------------------------------- 1 | CREATE (rec1:Receipt {total_amount: 10.37, timestamp: "28/04/2018 19:19"}) 2 | CREATE (rec2:Receipt {total_amount: 3.85, timestamp: "26/04/2018 20:18"}) 3 | 4 | CREATE (st1:Store {name: "LIDL", address: "Somewhere in this beautiful planet"}) 5 | CREATE (st2:Store {name: "ALDI", address: "Somewhere different"}) 6 | CREATE (st3:Store {name: "Albert Heijn", address: "A fancy place"}) 7 | 8 | CREATE (pr1:Product {name: "Indian Pale Ale", brand: "Elements", category: "Beer"}) 9 | CREATE (pr2:Product {name: "Parmesan Cheese", brand: "Faixa Azul", category: "Cheese"}) 10 | CREATE (pr3:Product {name: "Old Amsterdam 48+", brand: "Old Amsterdam", category: "Cheese"}) 11 | CREATE (pr4:Product {name: "Skimmed Milk", brand: "CowPowers", category: "Milk"}) 12 | CREATE (pr5:Product {name: "Hummus", brand: "Egyptian", category: "Spread"}) 13 | 14 | CREATE (customer:Customer {name: "Charles", email: "charles@gmail.com"}) 15 | 16 | CREATE 17 | (customer)-[:HAS]->(rec1), 18 | (customer)-[:HAS]->(rec2), 19 | (customer)-[:BOUGHT]->(pr1), 20 | (customer)-[:BOUGHT]->(pr2), 21 | (customer)-[:BOUGHT]->(pr3), 22 | (customer)-[:BOUGHT]->(pr4), 23 | (rec1)-[:HAS {price: 2.35}]->(pr1), 24 | (rec1)-[:HAS {price: 4.18}]->(pr3), 25 | (rec1)-[:HAS {price: 1.49}]->(pr4), 26 | (rec2)-[:HAS {price: 3.85}]->(pr2), 27 | (st1)-[:EMITTED]->(rec1), 28 | (st2)-[:EMITTED]->(rec2), 29 | (customer)-[:GOES_TO]->(st1), 30 | (customer)-[:GOES_TO]->(st2), 31 | (st1)-[:SELLS]->(pr1), 32 | (st1)-[:SELLS]->(pr2), 33 | (st1)-[:SELLS]->(pr3), 34 | (st1)-[:SELLS]->(pr4), 35 | (st2)-[:SELLS]->(pr1), 36 | (st2)-[:SELLS]->(pr2), 37 | (st2)-[:SELLS]->(pr3), 38 | (st2)-[:SELLS]->(pr4), 39 | (st3)-[:SELLS]->(pr5) 40 | -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | import maya 2 | from graphql import GraphQLError 3 | from py2neo import Graph 4 | from py2neo.ogm import GraphObject, Property, RelatedTo 5 | 6 | from app import settings 7 | 8 | 9 | graph = Graph( 10 | host=settings.NEO4J_HOST, 11 | port=settings.NEO4J_PORT, 12 | user=settings.NEO4J_USER, 13 | password=settings.NEO4J_PASSWORD, 14 | ) 15 | 16 | 17 | class BaseModel(GraphObject): 18 | """ 19 | Implements some basic functions to guarantee some standard functionality 20 | across all models. The main purpose here is also to compensate for some 21 | missing basic features that we expected from GraphObjects, and improve the 22 | way we interact with them. 23 | """ 24 | 25 | def __init__(self, **kwargs): 26 | for key, value in kwargs.items(): 27 | if hasattr(self, key): 28 | setattr(self, key, value) 29 | 30 | @property 31 | def all(self): 32 | return self.match(graph) 33 | 34 | def save(self): 35 | graph.push(self) 36 | 37 | 38 | class Product(BaseModel): 39 | __primarykey__ = 'name' 40 | 41 | name = Property() 42 | brand = Property() 43 | category = Property() 44 | 45 | def as_dict(self): 46 | return { 47 | 'name': self.name, 48 | 'brand': self.brand, 49 | 'category': self.category 50 | } 51 | 52 | def fetch(self): 53 | return self.match(graph, self.name).first() 54 | 55 | 56 | class Store(BaseModel): 57 | name = Property() 58 | address = Property() 59 | 60 | products = RelatedTo('Product', 'SELLS') 61 | receipts = RelatedTo('Product', 'EMITTED') 62 | 63 | def fetch(self, _id): 64 | return Store.match(graph, _id).first() 65 | 66 | def fetch_by_name_and_address(self): 67 | return Store.match(graph).where( 68 | f'_.name = "{self.name}" AND _.address = "{self.address}"' 69 | ).first() 70 | 71 | def fetch_products(self): 72 | return [{ 73 | **product[0].as_dict(), 74 | **product[1] 75 | } for product in self.products._related_objects] 76 | 77 | def as_dict(self): 78 | return { 79 | '_id': self.__primaryvalue__, 80 | 'name': self.name, 81 | 'address': self.address 82 | } 83 | 84 | 85 | class Receipt(BaseModel): 86 | total_amount = Property() 87 | timestamp = Property() 88 | 89 | products = RelatedTo('Product', 'HAS') 90 | 91 | def __init__(self, **kwargs): 92 | super().__init__(**kwargs) 93 | 94 | if kwargs.get('validate', False): 95 | self.__validate_timestamp() 96 | 97 | def as_dict(self): 98 | return { 99 | '_id': self.__primaryvalue__, 100 | 'total_amount': self.total_amount, 101 | 'timestamp': maya.parse(self.timestamp) 102 | } 103 | 104 | def fetch(self, _id): 105 | return self.match(graph, _id).first() 106 | 107 | def fetch_products(self): 108 | return [{ 109 | **product[0].as_dict(), 110 | **product[1] 111 | } for product in self.products._related_objects] 112 | 113 | def __validate_timestamp(self): 114 | try: 115 | maya.parse(self.timestamp, day_first=True, year_first=False) 116 | except Exception: 117 | raise GraphQLError( 118 | 'The timestamp you provided is not within the format: "dd/mm/yyyy hh:mm"' 119 | ) 120 | 121 | 122 | class Customer(BaseModel): 123 | __primarykey__ = 'email' 124 | name = Property() 125 | email = Property() 126 | 127 | receipts = RelatedTo('Receipt', 'HAS') 128 | stores = RelatedTo('Store', 'GOES_TO') 129 | 130 | def fetch(self): 131 | customer = self.match(graph, self.email).first() 132 | if customer is None: 133 | raise GraphQLError(f'"{self.email}" has not been found in our customers list.') 134 | 135 | return customer 136 | 137 | def as_dict(self): 138 | return { 139 | 'email': self.email, 140 | 'name': self.name 141 | } 142 | 143 | def __verify_products(self, products): 144 | _total_amount = 0 145 | for product in products: 146 | _product = Product(name=product.get('name')).fetch() 147 | if _product is None: 148 | raise GraphQLError(f'"{product.name}" has not been found in our products list.') 149 | 150 | _total_amount += product['price'] * product['amount'] 151 | product['product'] = _product 152 | return products, _total_amount 153 | 154 | def __verify_receipt(self, receipt): 155 | customer_properties = f":Customer {{email: '{self.email}'}}" 156 | receipt_properties = f":Receipt {{timestamp: '{receipt.timestamp}', total_amount:{receipt.total_amount}}}" 157 | existing_receipts = graph.run( 158 | f"MATCH ({customer_properties})-[relation:HAS]-({receipt_properties}) RETURN relation").data() 159 | 160 | if existing_receipts: 161 | raise GraphQLError("The receipt you're trying to submit already exists.") 162 | 163 | def __link_products(self, products, total_amount, timestamp): 164 | receipt = Receipt(total_amount=total_amount, timestamp=timestamp, validate=True) 165 | self.__verify_receipt(receipt) 166 | 167 | for item in products: 168 | receipt.products.add(item.pop('product'), properties=item) 169 | 170 | return receipt 171 | 172 | def __verify_store(self, store): 173 | _store = Store(**store).fetch_by_name_and_address() 174 | if _store is None: 175 | raise GraphQLError(f"The store \"{store['name']}\" does not exist in our stores list.") 176 | 177 | return _store 178 | 179 | def __add_links(self, store, receipt): 180 | store.receipts.add(receipt) 181 | self.stores.add(store) 182 | self.receipts.add(receipt) 183 | 184 | def submit_receipt(self, products, timestamp, store): 185 | self.__add_links( 186 | self.__verify_store(store), 187 | self.__link_products(*self.__verify_products(products), timestamp) 188 | ) 189 | 190 | self.save() 191 | -------------------------------------------------------------------------------- /app/schemas.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from .models import Customer, Store, Receipt, Product 4 | 5 | 6 | class ProductSchema(graphene.ObjectType): 7 | name = graphene.String() 8 | brand = graphene.String() 9 | category = graphene.String() 10 | 11 | price = graphene.Float() 12 | amount = graphene.Int() 13 | 14 | 15 | class ProductInput(graphene.InputObjectType): 16 | name = graphene.String(required=True) 17 | price = graphene.Float(required=True) 18 | amount = graphene.Int(required=True) 19 | 20 | 21 | class StoreSchema(graphene.ObjectType): 22 | name = graphene.String() 23 | address = graphene.String() 24 | 25 | products = graphene.List(ProductSchema) 26 | 27 | def __init__(self, **kwargs): 28 | self._id = kwargs.pop('_id') 29 | super().__init__(**kwargs) 30 | 31 | def resolve_products(self, info): 32 | return [ProductSchema(**product) for product in Store().fetch(self._id).fetch_products()] 33 | 34 | 35 | class StoreInput(graphene.InputObjectType): 36 | name = graphene.String(required=True) 37 | address = graphene.String(required=True) 38 | 39 | 40 | class ReceiptSchema(graphene.ObjectType): 41 | total_amount = graphene.Float() 42 | timestamp = graphene.String() 43 | 44 | products = graphene.List(ProductSchema) 45 | 46 | def __init__(self, **kwargs): 47 | self._id = kwargs.pop('_id') 48 | super().__init__(**kwargs) 49 | 50 | def resolve_products(self, info): 51 | return [ProductSchema(**product) for product in Receipt().fetch(self._id).fetch_products()] 52 | 53 | 54 | class SubmitReceipt(graphene.Mutation): 55 | class Arguments: 56 | customer_email = graphene.String(required=True) 57 | products = graphene.List(graphene.NonNull(ProductInput)) 58 | store = StoreInput(required=True) 59 | timestamp = graphene.String(required=True) 60 | 61 | success = graphene.Boolean() 62 | 63 | def mutate(self, info, **kwargs): 64 | customer = Customer(email=kwargs.pop('customer_email')).fetch() 65 | customer.submit_receipt(**kwargs) 66 | 67 | return SubmitReceipt(success=True) 68 | 69 | 70 | class CustomerSchema(graphene.ObjectType): 71 | email = graphene.String() 72 | name = graphene.String() 73 | 74 | stores = graphene.List(StoreSchema) 75 | receipts = graphene.List(ReceiptSchema) 76 | products = graphene.List(ProductSchema) 77 | 78 | def __init__(self, **kwargs): 79 | super().__init__(**kwargs) 80 | self.customer = Customer(email=self.email).fetch() 81 | 82 | def resolve_stores(self, info): 83 | return [StoreSchema(**store.as_dict()) for store in self.customer.stores] 84 | 85 | def resolve_receipts(self, info): 86 | return [ReceiptSchema(**receipt.as_dict()) for receipt in self.customer.receipts] 87 | 88 | def resolve_products(self, info): 89 | return [ProductSchema(**product.as_dict()) for product in self.customer.products] 90 | 91 | 92 | class CreateCustomer(graphene.Mutation): 93 | class Arguments: 94 | name = graphene.String(required=True) 95 | email = graphene.String(required=True) 96 | 97 | success = graphene.Boolean() 98 | customer = graphene.Field(lambda: CustomerSchema) 99 | 100 | def mutate(self, info, **kwargs): 101 | customer = Customer(**kwargs) 102 | customer.save() 103 | 104 | return CreateCustomer(customer=customer, success=True) 105 | 106 | 107 | class Query(graphene.ObjectType): 108 | customer = graphene.Field(lambda: CustomerSchema, email=graphene.String()) 109 | stores = graphene.List(lambda: StoreSchema) 110 | products = graphene.List(lambda: ProductSchema) 111 | 112 | def resolve_customer(self, info, email): 113 | customer = Customer(email=email).fetch() 114 | return CustomerSchema(**customer.as_dict()) 115 | 116 | def resolve_stores(self, info): 117 | return [StoreSchema(**store.as_dict()) for store in Store().all] 118 | 119 | def resolve_products(self, info): 120 | return [ProductSchema(**product.as_dict()) for product in Product().all] 121 | 122 | 123 | class Mutations(graphene.ObjectType): 124 | create_customer = CreateCustomer.Field() 125 | submit_receipt = SubmitReceipt.Field() 126 | 127 | 128 | schema = graphene.Schema(query=Query, mutation=Mutations, auto_camelcase=False) 129 | -------------------------------------------------------------------------------- /app/settings.py: -------------------------------------------------------------------------------- 1 | from environs import Env 2 | 3 | 4 | env = Env() 5 | 6 | 7 | DEBUG = env.bool('DEBUG', default=False) 8 | BIND_HOST = env('BIND_HOST', default='127.0.0.1') 9 | BIND_PORT = env.int('BIND_PORT', default=5000) 10 | 11 | NEO4J_HOST = env('NEO4J_HOST', default='localhost') 12 | NEO4J_PORT = env.int('NEO4J_PORT', default=7687) 13 | NEO4J_USER = env('NEO4J_USER', default='neo4j') 14 | NEO4J_PASSWORD = env('NEO4J_PASSWORD', default='admin') 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | app: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | networks: 8 | - flask-graphql-neo4j-dev 9 | environment: 10 | - NEO4J_HOST=neo4j 11 | - NEO4J_PORT=7687 12 | - BIND_HOST=0.0.0.0 13 | - BIND_PORT=8080 14 | ports: 15 | - 127.0.0.1:8080:8080 16 | 17 | neo4j: 18 | image: neo4j:3.5.11 19 | networks: 20 | - flask-graphql-neo4j-dev 21 | environment: 22 | - NEO4J_AUTH=none 23 | ports: 24 | - 127.0.0.1:7474:7474 25 | - 127.0.0.1:7687:7687 26 | 27 | networks: 28 | flask-graphql-neo4j-dev: {} 29 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from app import create_app, settings 2 | 3 | 4 | app = create_app() 5 | 6 | 7 | if __name__ == '__main__': 8 | app.run( 9 | host=settings.BIND_HOST, 10 | port=settings.BIND_PORT, 11 | debug=settings.DEBUG, 12 | ) 13 | -------------------------------------------------------------------------------- /wait-for.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TIMEOUT=15 4 | QUIET=0 5 | 6 | echoerr() { 7 | if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi 8 | } 9 | 10 | usage() { 11 | exitcode="$1" 12 | cat << USAGE >&2 13 | Usage: 14 | $cmdname host:port [-t timeout] [-- command args] 15 | -q | --quiet Do not output any status messages 16 | -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout 17 | -- COMMAND ARGS Execute command with args after the test finishes 18 | USAGE 19 | exit "$exitcode" 20 | } 21 | 22 | wait_for() { 23 | for i in `seq $TIMEOUT` ; do 24 | nc -z "$HOST" "$PORT" > /dev/null 2>&1 25 | 26 | result=$? 27 | if [ $result -eq 0 ] ; then 28 | if [ $# -gt 0 ] ; then 29 | exec "$@" 30 | fi 31 | exit 0 32 | fi 33 | sleep 1 34 | done 35 | echo "Operation timed out" >&2 36 | exit 1 37 | } 38 | 39 | while [ $# -gt 0 ] 40 | do 41 | case "$1" in 42 | *:* ) 43 | HOST=$(printf "%s\n" "$1"| cut -d : -f 1) 44 | PORT=$(printf "%s\n" "$1"| cut -d : -f 2) 45 | shift 1 46 | ;; 47 | -q | --quiet) 48 | QUIET=1 49 | shift 1 50 | ;; 51 | -t) 52 | TIMEOUT="$2" 53 | if [ "$TIMEOUT" = "" ]; then break; fi 54 | shift 2 55 | ;; 56 | --timeout=*) 57 | TIMEOUT="${1#*=}" 58 | shift 1 59 | ;; 60 | --) 61 | shift 62 | break 63 | ;; 64 | --help) 65 | usage 0 66 | ;; 67 | *) 68 | echoerr "Unknown argument: $1" 69 | usage 1 70 | ;; 71 | esac 72 | done 73 | 74 | if [ "$HOST" = "" -o "$PORT" = "" ]; then 75 | echoerr "Error: you need to provide a host and port to test." 76 | usage 2 77 | fi 78 | 79 | wait_for "$@" 80 | --------------------------------------------------------------------------------