From 7b1ed65a56b74fc5e0d5d9dbd7762be57936d361 Mon Sep 17 00:00:00 2001 From: Igor Carrasco Date: Sat, 25 Jul 2020 18:26:41 -0400 Subject: [PATCH] Merge work into master (#1) * makes everything nice and async * actually runs teardown * adds a requests session to the connector, with optional Pool parameter * ignore dependency management * getting it back to the initial repo state Co-authored-by: Carrasco --- .coverage | Bin 0 -> 53248 bytes .gitignore | 8 +- Pipfile | 21 ++ Pipfile.lock | 445 ++++++++++++++++++++++++++++++++++++++++ bin/test.sh | 4 + neo4j/__init__.py | 96 ++++----- setup.py | 24 +-- tests/test_connector.py | 117 +++-------- 8 files changed, 561 insertions(+), 154 deletions(-) create mode 100644 .coverage create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 bin/test.sh diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..8cd35c7ebb5a864b602b470a57ba03de80a78e40 GIT binary patch literal 53248 zcmeI)%WoS+90%~-tZThqI)DWN5P$##AP@@lKA6hnPo2{5-U)cU zEdrlg!jC?4m#(g@tgo^4m3OYJv1lKgpJ23-*YN~$J(|V7Sok>V0$hTTf*uFqOoTIiwZ4S2ybU;D6Q+N zK-Z&~@ws;B3?lClGY%d{Zc^r4FJT=-RHvrMVV>QtQz!BpU8kcehhju04!lt@N5u`i zxUl=Kn?34sP~M^HOvqJ@MzOlCeXA?_m0x6on8mOdu2*EL5Yz%rPp0#4p42sJm10ZY zYSS058t(>PxLu>luT|+|>dO=3nf%4bqU?DvgcJLYS50nxaPcb?R$Lx1trt;cg?oOsT7HlqwDRs!yu(L}LdHR2e9cVL3Kj@J00Izz00bZa0SG_<0uX?}5fYe6 z8M9_G^I`E-!3t5(l1RnISzp2gc0*`ldYl}+0Zy1XsiON5Jm?ZU7sdH(p;yDi{TDg_*_l$Pni!j^P%~V z`G@)I5vpPo1p*L&00bZa0SG_<0uX=z1R!vP1*VKyJ^5iHHEvAn>URxe(wNcXA2G%z zjoJRM7t#Fx2b%fd2$P^_5P$##AOHafKmY;|fB*y_0D(g*u&SrDUT)XigH<7K23}|H zsRosBrT|SMuw6R{ClRRU|GN33MlUQ7fB*y_009U<00Izz00bZa0SFvCfz?zmm;Jx@ z|M>s^4qn9~M-YGj1Rwwb2tWV=5P$##AOL|EC}8M@mW!VMKhex5FEC{!1px>^00Izz z00bZa0SG_<0uX?}fe|o_TsC_C|41_*9hht(BM^W91Rwwb2tWV=5P$##AOHaf>`P!? zPi3>~f1TE}?+TjM`28Bi`c_v_f68jwZ|i^leCO*ETI+RfEPDR`STi5*D`5nJ00bZa z0SG_<0uX=z1Rwwb2teR)38d&Zg7EpjcKmQ<9Jz%61Rwwb2tWV=5P$##AOHafK;ZcV z)boGb|39A@LPG!o5P$##AOHafKmY;|fB*yzn}GWF|G57@Y_*2`LI45~fB*y_009U< N00Izz00f>-;6H~cwORlG literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore index 8c74c17..76c732d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,10 @@ venv/ _build/ build/ dist/ -neo4j_connector.egg-info/ \ No newline at end of file +neo4j_connector.egg-info/ +.pytest_cache/ +.mypy_cache/ +data/ +.vscode/ +Pipfile +Pipfile.lock diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..bb46e92 --- /dev/null +++ b/Pipfile @@ -0,0 +1,21 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +black = "*" +mypy = "*" +pytest = "*" +pytest-asyncio = "*" +pytest-cov = "*" + +[packages] +httpx = "*" +pytest-asyncio = "*" + +[requires] +python_version = "3.7" + +[pipenv] +allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..e7bda3e --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,445 @@ +{ + "_meta": { + "hash": { + "sha256": "4a7a1ae4ffc1663ca234b5bb177bce6ed207401068d6c4f669a68e02fcc2aa2c" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, + "certifi": { + "hashes": [ + "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", + "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" + ], + "version": "==2019.11.28" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "h11": { + "hashes": [ + "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1", + "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1" + ], + "version": "==0.9.0" + }, + "h2": { + "hashes": [ + "sha256:ac377fcf586314ef3177bfd90c12c7826ab0840edeb03f0f24f511858326049e", + "sha256:b8a32bd282594424c0ac55845377eea13fa54fe4a8db012f3a198ed923dc3ab4" + ], + "version": "==3.1.1" + }, + "hpack": { + "hashes": [ + "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89", + "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2" + ], + "version": "==3.0.0" + }, + "hstspreload": { + "hashes": [ + "sha256:5892f7b903f337fd96bb96ee4c28a1960f301479718409d7a3f7d1c1c20e7901" + ], + "version": "==2020.1.22" + }, + "httpx": { + "hashes": [ + "sha256:1d3893d3e4244c569764a6bae5c5a9fbbc4a6ec3825450b5696602af7a275576", + "sha256:7d2bfb726eeed717953d15dddb22da9c2fcf48a4d70ba1456aa0a7faeda33cf7" + ], + "index": "pypi", + "version": "==0.11.1" + }, + "hyperframe": { + "hashes": [ + "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40", + "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f" + ], + "version": "==5.2.0" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "importlib-metadata": { + "hashes": [ + "sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359", + "sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8" + ], + "markers": "python_version < '3.8'", + "version": "==1.4.0" + }, + "more-itertools": { + "hashes": [ + "sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39", + "sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288" + ], + "version": "==8.1.0" + }, + "packaging": { + "hashes": [ + "sha256:aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb", + "sha256:fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8" + ], + "version": "==20.0" + }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "version": "==0.13.1" + }, + "py": { + "hashes": [ + "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", + "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" + ], + "version": "==1.8.1" + }, + "pyparsing": { + "hashes": [ + "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", + "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" + ], + "version": "==2.4.6" + }, + "pytest": { + "hashes": [ + "sha256:1d122e8be54d1a709e56f82e2d85dcba3018313d64647f38a91aec88c239b600", + "sha256:c13d1943c63e599b98cf118fcb9703e4d7bde7caa9a432567bcdcae4bf512d20" + ], + "version": "==5.3.4" + }, + "pytest-asyncio": { + "hashes": [ + "sha256:9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf", + "sha256:d734718e25cfc32d2bf78d346e99d33724deeba774cc4afdf491530c6184b63b" + ], + "index": "pypi", + "version": "==0.10.0" + }, + "rfc3986": { + "hashes": [ + "sha256:0344d0bd428126ce554e7ca2b61787b6a28d2bbd19fc70ed2dd85efe31176405", + "sha256:df4eba676077cefb86450c8f60121b9ae04b94f65f85b69f3f731af0516b7b18" + ], + "version": "==1.3.2" + }, + "six": { + "hashes": [ + "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", + "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" + ], + "version": "==1.14.0" + }, + "sniffio": { + "hashes": [ + "sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5", + "sha256:8e3810100f69fe0edd463d02ad407112542a11ffdc29f67db2bf3771afb87a21" + ], + "version": "==1.1.0" + }, + "urllib3": { + "hashes": [ + "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", + "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" + ], + "version": "==1.25.8" + }, + "wcwidth": { + "hashes": [ + "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603", + "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8" + ], + "version": "==0.1.8" + }, + "zipp": { + "hashes": [ + "sha256:57147f6b0403b59f33fd357f169f860e031303415aeb7d04ede4839d23905ab8", + "sha256:7ae5ccaca427bafa9760ac3cd8f8c244bfc259794b5b6bb9db4dda2241575d09" + ], + "version": "==2.0.0" + } + }, + "develop": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, + "black": { + "hashes": [ + "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", + "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" + ], + "index": "pypi", + "version": "==19.10b0" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "coverage": { + "hashes": [ + "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3", + "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c", + "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0", + "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477", + "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a", + "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf", + "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691", + "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73", + "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987", + "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894", + "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e", + "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef", + "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf", + "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68", + "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8", + "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954", + "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2", + "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40", + "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc", + "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc", + "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e", + "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d", + "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f", + "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc", + "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301", + "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea", + "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb", + "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af", + "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52", + "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37", + "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0" + ], + "version": "==5.0.3" + }, + "importlib-metadata": { + "hashes": [ + "sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359", + "sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8" + ], + "markers": "python_version < '3.8'", + "version": "==1.4.0" + }, + "more-itertools": { + "hashes": [ + "sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39", + "sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288" + ], + "version": "==8.1.0" + }, + "mypy": { + "hashes": [ + "sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a", + "sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7", + "sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2", + "sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474", + "sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0", + "sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217", + "sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749", + "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6", + "sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf", + "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36", + "sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b", + "sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72", + "sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1", + "sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1" + ], + "index": "pypi", + "version": "==0.761" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "packaging": { + "hashes": [ + "sha256:aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb", + "sha256:fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8" + ], + "version": "==20.0" + }, + "pathspec": { + "hashes": [ + "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424", + "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96" + ], + "version": "==0.7.0" + }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "version": "==0.13.1" + }, + "py": { + "hashes": [ + "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", + "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" + ], + "version": "==1.8.1" + }, + "pyparsing": { + "hashes": [ + "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", + "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" + ], + "version": "==2.4.6" + }, + "pytest": { + "hashes": [ + "sha256:1d122e8be54d1a709e56f82e2d85dcba3018313d64647f38a91aec88c239b600", + "sha256:c13d1943c63e599b98cf118fcb9703e4d7bde7caa9a432567bcdcae4bf512d20" + ], + "version": "==5.3.4" + }, + "pytest-asyncio": { + "hashes": [ + "sha256:9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf", + "sha256:d734718e25cfc32d2bf78d346e99d33724deeba774cc4afdf491530c6184b63b" + ], + "index": "pypi", + "version": "==0.10.0" + }, + "pytest-cov": { + "hashes": [ + "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", + "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626" + ], + "index": "pypi", + "version": "==2.8.1" + }, + "regex": { + "hashes": [ + "sha256:07b39bf943d3d2fe63d46281d8504f8df0ff3fe4c57e13d1656737950e53e525", + "sha256:0932941cdfb3afcbc26cc3bcf7c3f3d73d5a9b9c56955d432dbf8bbc147d4c5b", + "sha256:0e182d2f097ea8549a249040922fa2b92ae28be4be4895933e369a525ba36576", + "sha256:10671601ee06cf4dc1bc0b4805309040bb34c9af423c12c379c83d7895622bb5", + "sha256:23e2c2c0ff50f44877f64780b815b8fd2e003cda9ce817a7fd00dea5600c84a0", + "sha256:26ff99c980f53b3191d8931b199b29d6787c059f2e029b2b0c694343b1708c35", + "sha256:27429b8d74ba683484a06b260b7bb00f312e7c757792628ea251afdbf1434003", + "sha256:3e77409b678b21a056415da3a56abfd7c3ad03da71f3051bbcdb68cf44d3c34d", + "sha256:4e8f02d3d72ca94efc8396f8036c0d3bcc812aefc28ec70f35bb888c74a25161", + "sha256:4eae742636aec40cf7ab98171ab9400393360b97e8f9da67b1867a9ee0889b26", + "sha256:6a6ae17bf8f2d82d1e8858a47757ce389b880083c4ff2498dba17c56e6c103b9", + "sha256:6a6ba91b94427cd49cd27764679024b14a96874e0dc638ae6bdd4b1a3ce97be1", + "sha256:7bcd322935377abcc79bfe5b63c44abd0b29387f267791d566bbb566edfdd146", + "sha256:98b8ed7bb2155e2cbb8b76f627b2fd12cf4b22ab6e14873e8641f266e0fb6d8f", + "sha256:bd25bb7980917e4e70ccccd7e3b5740614f1c408a642c245019cff9d7d1b6149", + "sha256:d0f424328f9822b0323b3b6f2e4b9c90960b24743d220763c7f07071e0778351", + "sha256:d58e4606da2a41659c84baeb3cfa2e4c87a74cec89a1e7c56bee4b956f9d7461", + "sha256:e3cd21cc2840ca67de0bbe4071f79f031c81418deb544ceda93ad75ca1ee9f7b", + "sha256:e6c02171d62ed6972ca8631f6f34fa3281d51db8b326ee397b9c83093a6b7242", + "sha256:e7c7661f7276507bce416eaae22040fd91ca471b5b33c13f8ff21137ed6f248c", + "sha256:ecc6de77df3ef68fee966bb8cb4e067e84d4d1f397d0ef6fce46913663540d77" + ], + "version": "==2020.1.8" + }, + "six": { + "hashes": [ + "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", + "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" + ], + "version": "==1.14.0" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "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:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2", + "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d", + "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575" + ], + "version": "==3.7.4.1" + }, + "wcwidth": { + "hashes": [ + "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603", + "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8" + ], + "version": "==0.1.8" + }, + "zipp": { + "hashes": [ + "sha256:57147f6b0403b59f33fd357f169f860e031303415aeb7d04ede4839d23905ab8", + "sha256:7ae5ccaca427bafa9760ac3cd8f8c244bfc259794b5b6bb9db4dda2241575d09" + ], + "version": "==2.0.0" + } + } +} diff --git a/bin/test.sh b/bin/test.sh new file mode 100644 index 0000000..f283ece --- /dev/null +++ b/bin/test.sh @@ -0,0 +1,4 @@ +pipenv run python -m pytest --cov=neo4j\ + --cov-report=term-missing \ + --durations=0 \ + -s diff --git a/neo4j/__init__.py b/neo4j/__init__.py index 40503ea..6015be7 100644 --- a/neo4j/__init__.py +++ b/neo4j/__init__.py @@ -3,32 +3,28 @@ library. """ -import requests import sys -from typing import List, Tuple from collections import namedtuple +from typing import List, Optional, Tuple +from urllib.parse import urlparse + +import requests class Statement(dict): """Class that helps transform a cypher query plus optional parameters into the dictionary structure that Neo4j expects. The values can easily be accessed as shown in the last code example. - Args: cypher (str): the Cypher statement parameters (dict): [optional] parameters that are merged into the statement at the server-side. Parameters help with speeding up queries because the execution plan for identical Cypher statements is cached. - Example code: - >>> # create simple statement >>> statement = Statement("MATCH () RETURN COUNT(*) AS node_count") - >>> # create parametrized statement >>> statement = Statement("MATCH (n:node {uuid: {uuid}}) RETURN n", {'uuid': '123abc'}) - >>> # create multiple parametrized statements >>> statements = [Statement("MATCH (n:node {uuid: {uuid}}) RETURN n", {'uuid': uuid}) for uuid in ['123abc', '456def']] - >>> # print individual Statement values >>> statement = Statement("MATCH (n:node {uuid: {uuid}}) RETURN n", {'uuid': '123abc'}) >>> print("Cypher statement: {}".format(statement['statement'])) @@ -38,70 +34,75 @@ class Statement(dict): def __init__(self, cypher: str, parameters: dict = None): super().__init__(statement=cypher) if parameters: - self['parameters'] = parameters + self["parameters"] = parameters class Connector: """Class that abstracts communication with neo4j into up-front setup and then executes one or more :class:`Statement`. The connector doesn't maintain an open connection and thus doesn't need to be closed after use. - Args: endpoint (str): the fully qualified endpoint to send messages to credentials (tuple[str, str]): the credentials that are used to authenticate the requests verbose_errors (bool): if set to True the :class:`Connector` prints :class:`Neo4jErrors` messages and codes to the standard error output in a bit nicer format than the stack trace. - + pool (optional[int]): will determine the size of the underlying http connection pool. Useful for multithreaded + applications. Example code: - >>> # default connector >>> connector = Connector() - >>> # localhost connector, custom credentials >>> connector = Connector(credentials=('username', 'password')) - >>> # custom connector >>> connector = Connector('http://mydomain:7474', ('username', 'password')) """ - # default endpoint of localhost - default_host = 'http://localhost:7474' - default_path = '/db/data/transaction/commit' + # default host and endpoint + default_host = "http://localhost:7474" + default_path = "/db/data/transaction/commit" # default credentials - default_credentials = ('neo4j', 'neo4j') - - def __init__(self, host: str = default_host, credentials: Tuple[str, str] = default_credentials, - verbose_errors=False): + default_credentials = ("neo4j", "neo4j") + + def __init__( + self, + host: str = default_host, + credentials: Tuple[str, str] = default_credentials, + pool: Optional[int] = None, + verbose_errors: bool = False, + ): self.endpoint = host + self.default_path self.credentials = credentials self.verbose_errors = verbose_errors + self.session = requests.Session() + if pool: + self.session.mount( + prefix=f"{urlparse(host).scheme}://", + adapter=requests.adapters.HTTPAdapter( + pool_connections=pool, pool_maxsize=pool + ), + ) + + self.session.auth = credentials def run(self, cypher: str, parameters: dict = None): """ Method that runs a single statement against Neo4j in a single transaction. This method builds the :class:`Statement` object for the user. - Args: cypher (str): the Cypher statement parameters (dict): [optional] parameters that are merged into the statement at the server-side. Parameters help with speeding up queries because the execution plan for identical Cypher statements is cached. - Returns: list[dict]: a list of dictionaries, one dictionary for each row in the result. The keys in the dictionary are defined in the Cypher statement - Raises: Neo4jErrors - Example code: - >>> # retrieve all nodes' properties >>> all_nodes = [row['n'] for row in connector.run("MATCH (n) RETURN n")] - >>> # single row result >>> node_count = connector.run("MATCH () RETURN COUNT(*) AS node_count")[0]['node_count'] - >>> # get a single node's properties with a statement + parameter >>> # in this case we're assuming: CONSTRAINT ON (node:node) ASSERT node.uuid IS UNIQUE >>> single_node_properties_by_uuid = connector.run("MATCH (n:node {uuid: {uuid}}) RETURN n", {'uuid': '123abc'})[0]['n'] @@ -109,33 +110,29 @@ def run(self, cypher: str, parameters: dict = None): response = self.post([Statement(cypher, parameters)]) return self._clean_results(response)[0] - def run_multiple(self, statements: List[Statement], batch_size: int = None) -> List[List[dict]]: + def run_multiple( + self, statements: List[Statement], batch_size: int = None + ) -> List[List[dict]]: """ Method that runs multiple :class:`Statement`\ s against Neo4j in a single transaction or several batches. - Args: statements (list[Statement]): the statements to execute batch_size (int): [optional] number of statements to send to Neo4j per batch. In case the batch_size is omitted (i.e. None) then all statements are sent as a single batch. This parameter can help make large jobs manageable for Neo4j (e.g not running out of memory). - Returns: list[list[dict]]: a list of statement results, each containing a list of dictionaries, one dictionary for each row in the result. The keys in the dictionary are defined in the Cypher statement. The statement results have the same order as the corresponding :class:`Statement`\ s - Raises: Neo4jErrors - Example code: - >>> cypher = "MATCH (n:node {uuid: {uuid}}) RETURN n" >>> statements = [Statement(cypher, {'uuid': uuid}) for uuid in ['123abc', '456def'] >>> statements_responses = connector.run_multiple(statements) >>> for statement_responses in statements_responses: >>> for row in statement_responses: >>> print(row) - >>> # we can use batches if we're likely to overwhelm neo4j by sending everything in a single request >>> # note that this has no effect on the returned data structure >>> cypher = "MATCH (n:node {uuid: {uuid}}) RETURN n" @@ -144,7 +141,6 @@ def run_multiple(self, statements: List[Statement], batch_size: int = None) -> L >>> for statement_responses in statements_responses: >>> for row in statement_responses: >>> print(row) - >>> # we can easily re-use some information from the statement in the next example >>> cypher = "MATCH (language {name: {name}})-->(word:word)) RETURN word" >>> statements = [Statement(cypher, {'name': lang}) for lang in ['en', 'nl'] @@ -167,18 +163,13 @@ def post(self, statements: List[Statement]): `_. This specifically includes the metadata per row and has a separate entry for the result names and the actual values. - Args: statements (list[Statement]): the statements that are POST-ed to Neo4j - Returns: dict: the parsed Neo4j HTTP API response - Raises: Neo4jErrors - Example code: - >>> cypher = "MATCH (n:node {uuid: {uuid}}) RETURN n" >>> statements = [Statement(cypher, {'uuid': uuid}) for uuid in ['123abc', '456def'] >>> statements_responses = connector.run_multiple(statements) @@ -186,7 +177,7 @@ def post(self, statements: List[Statement]): >>> for datum in result['data']: >>> print(datum['row'][0]) #n is the first item in the row """ - response = requests.post(self.endpoint, json={'statements': statements}, auth=self.credentials) + response = self.session.post(self.endpoint, json={"statements": statements}) json_response = response.json() self._check_for_errors(json_response) @@ -205,10 +196,10 @@ def make_batches(statements: List[Statement], batch_size: int = None) -> List: raise ValueError("batchsize should be >= 1") for start_idx in range(0, len(statements), batch_size): - yield statements[start_idx:start_idx + batch_size] + yield statements[start_idx : start_idx + batch_size] def _check_for_errors(self, json_response): - errors = json_response.get('errors') + errors = json_response.get("errors") if errors: neo4j_errors = Neo4jErrors(errors) if self.verbose_errors: @@ -220,23 +211,17 @@ def _check_for_errors(self, json_response): @staticmethod def _clean_results(response): return [ - [ - dict(zip(result['columns'], datum['row'])) - for datum in result['data'] - ] - for result in response['results'] + [dict(zip(result["columns"], datum["row"])) for datum in result["data"]] + for result in response["results"] ] class Neo4jErrors(Exception): """Exception that is raised when Neo4j responds to a request with one or more error message. Iterate over this object to get the individual :class:`Neo4jError` objects - Args: errors (list(dict)): A list of dictionaries that contain the 'code' and 'message' properties - Example code: - >>> try: >>> connector.run(...) >>> except Neo4jErrors as neo4j_errors: @@ -246,23 +231,20 @@ class Neo4jErrors(Exception): """ def __init__(self, errors: List[dict]): - self.errors = [Neo4jError(error['code'], error['message']) for error in errors] + self.errors = [Neo4jError(error["code"], error["message"]) for error in errors] def __iter__(self): return iter(self.errors) # wrapped the namedtuple in a class so it gets documented properly -class Neo4jError(namedtuple('Neo4jError', ['code', 'message'])): +class Neo4jError(namedtuple("Neo4jError", ["code", "message"])): """namedtuple that contains the code and message of a Neo4j error - Args: code (str): Error status code as defined in https://neo4j.com/docs/status-codes/3.5/ message (str): Descriptive message. For Cypher syntax errors this will contain a separate line (delimited by \\\\n) that contains the '^' character to point to the problem - Example code: - >>> print(neo4j_error.code, file=sys.stderr) >>> print(neo4j_error.message, file=sys.stderr) """ diff --git a/setup.py b/setup.py index 6aef0e5..3d1977e 100644 --- a/setup.py +++ b/setup.py @@ -1,24 +1,24 @@ from setuptools import setup -with open('README.rst') as readme_file: +with open("README.rst") as readme_file: long_description = readme_file.read() -with open('requirements.txt') as requirements_file: +with open("requirements.txt") as requirements_file: install_requires = requirements_file.read() setup( - name='neo4j-connector', - version='1.1.0', - description='Connector with single-request transactions for Neo4j 3.0 and above', + name="neo4j-connector", + version="1.1.0", + description="Connector with single-request transactions for Neo4j 3.0 and above", long_description=long_description, long_description_content_type="text/x-rst", - author='Jelle Jan Bankert (Textkernel BV)', - author_email='bankert@textkernel.com', - url='https://github.com/textkernel/neo4j-connector', - license='MIT', - packages=['neo4j'], + author="Jelle Jan Bankert (Textkernel BV)", + author_email="bankert@textkernel.com", + url="https://github.com/textkernel/neo4j-connector", + license="MIT", + packages=["neo4j"], install_requires=install_requires, - test_suite='tests', + test_suite="tests", classifiers=[ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", @@ -30,5 +30,5 @@ "Topic :: Database", "Topic :: Software Development", "Topic :: Utilities", - ] + ], ) diff --git a/tests/test_connector.py b/tests/test_connector.py index 6a92c34..d3413cf 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -1,131 +1,80 @@ -from unittest import TestCase, mock import neo4j +import pytest +import asyncio -class ConnectorBasicsTestCase(TestCase): +class TestConnectorBasics: def test_basic_parameters_existence(self): - hostname = 'http://domain:7474' + hostname = "http://domain:7474" connector = neo4j.Connector(hostname) - self.assertTrue(connector.endpoint.startswith(hostname)) - self.assertTrue(connector.endpoint.endswith(connector.default_path)) - self.assertIsNotNone(connector.credentials) - self.assertIsNotNone(connector.verbose_errors) + assert connector.endpoint.startswith(hostname) + assert connector.endpoint.endswith(connector.default_path) + assert connector.credentials != None + assert connector.verbose_errors != None -class ErrorHandlingTestCase(TestCase): - def setUp(self): +class TestErrorHandling: + def setup(self): self.connector = neo4j.Connector() def test_no_error_response(self): - self.connector._check_for_errors({'errors': []}) + self.connector._check_for_errors({"errors": []}) self.connector._check_for_errors({}) def test_single_error_response(self): - errors = [ - {'code': 'code', 'message': 'message'} - ] + errors = [{"code": "code", "message": "message"}] try: - self.connector._check_for_errors({ - 'errors': errors - }) + self.connector._check_for_errors({"errors": errors}) self.fail("Expected a raised Neo4jErrors, but didn't get one") except neo4j.Neo4jErrors as neo4j_errors: - self.assertEqual(len(neo4j_errors.errors), len(errors)) + assert len(neo4j_errors.errors) == len(errors) def test_multiple_error_response(self): errors = [ - {'code': 'code', 'message': 'message'}, - {'code': 'code2', 'message': 'message2'} + {"code": "code", "message": "message"}, + {"code": "code2", "message": "message2"}, ] try: - self.connector._check_for_errors({ - 'errors': errors - }) + self.connector._check_for_errors({"errors": errors}) self.fail("Expected a raised Neo4jErrors, but didn't get one") except neo4j.Neo4jErrors as neo4j_errors: - self.assertEqual(len(neo4j_errors.errors), len(errors)) - - -def mock_requests_post(*args, **kwargs): - # Inspired by https://stackoverflow.com/a/28507806/803466 - - class MockResponse: - def __init__(self, json_data, status_code): - self.json_data = json_data - self.status_code = status_code - - def json(self): - return self.json_data - - def get_results(statement_ids): - return [{ - 'columns': [ - f'key-{id}' - ], - 'data': [ - { - 'row': [f'value-{id}'], - 'meta': [f'meta-{id}'] - } - ] - } for id in statement_ids] + assert len(neo4j_errors.errors) == len(errors) - statements = [statement_obj['statement'] for statement_obj in kwargs['json']['statements']] - return MockResponse({'results': get_results(statements), 'errors': []}, 200) +class TestPostingStatements: + cypher1 = "MERGE (a:Foo) RETURN a" + cypher2 = "MERGE (b:Bar) RETURN b" -class PostingStatementsTestCase(TestCase): - cypher1 = 'cypher-1' - cypher2 = 'cypher-2' - - def setUp(self): + def setup(self): self.connector = neo4j.Connector() - @mock.patch('neo4j.requests.post', side_effect=mock_requests_post) - def test_post(self, mock_get): - hostname = 'hostname' - credentials = ('username', 'password') - - connector = neo4j.Connector(hostname, credentials) - connector.post([neo4j.Statement(self.cypher1)]) - - expected_endpoint = hostname + connector.default_path - - mock_get.assert_called_once_with(expected_endpoint, auth=credentials, - json={'statements': [{'statement': self.cypher1}]}) + def teardown(self): + self.connector.run("MATCH (n) DELETE n") - @mock.patch('neo4j.requests.post', side_effect=mock_requests_post) - def test_run_single(self, mock_get): + def test_run_single(self): response = self.connector.run(self.cypher1) row = response[0] - self.assertEqual(row['key-cypher-1'], 'value-cypher-1') - @mock.patch('neo4j.requests.post', side_effect=mock_requests_post) - def test_run_multiple(self, mock_get): + assert "a" in row.keys() + + def test_run_multiple(self): statements = [neo4j.Statement(self.cypher1), neo4j.Statement(self.cypher2)] response = self.connector.run_multiple(statements) statement_1_first_row = response[0][0] - self.assertEqual(statement_1_first_row['key-cypher-1'], 'value-cypher-1') + assert "a" in statement_1_first_row.keys() statement_2_first_row = response[1][0] - self.assertEqual(statement_2_first_row['key-cypher-2'], 'value-cypher-2') - - # check that post has been called 1 times (i.e. all statements in a single post request) - self.assertEqual(mock_get.call_count, 1) + assert "b" in statement_2_first_row.keys() - @mock.patch('neo4j.requests.post', side_effect=mock_requests_post) - def test_run_multiple_batch(self, mock_get): + def test_run_multiple_batch(self): statements = [neo4j.Statement(self.cypher1), neo4j.Statement(self.cypher2)] response = self.connector.run_multiple(statements, batch_size=1) statement_1_first_row = response[0][0] - self.assertEqual(statement_1_first_row['key-cypher-1'], 'value-cypher-1') + assert "a" in statement_1_first_row.keys() statement_2_first_row = response[1][0] - self.assertEqual(statement_2_first_row['key-cypher-2'], 'value-cypher-2') - - # check that post has been called 2 times (i.e. 2 batches of a single statement per post request) - self.assertEqual(mock_get.call_count, 2) + assert "b" in statement_2_first_row.keys()