commit
dfacf8d338
|
@ -16,5 +16,9 @@ install:
|
||||||
- cd $TRAVIS_BUILD_DIR
|
- cd $TRAVIS_BUILD_DIR
|
||||||
- pipenv install --dev -e . --deploy
|
- pipenv install --dev -e . --deploy
|
||||||
|
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $HOME/.config/piker/
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- pipenv run pytest tests/
|
- pipenv run pytest tests/
|
||||||
|
|
|
@ -16,9 +16,9 @@
|
||||||
"default": {
|
"default": {
|
||||||
"asks": {
|
"asks": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1679e5bd1dfa6c5d2220bdf2b8921c9c0d063d08370a7c66b9e167113681406f"
|
"sha256:d67aecaa02d0c67fa761dfdb23854391e6996c6045fb9385b1f508b0956a190d"
|
||||||
],
|
],
|
||||||
"version": "==2.2.0"
|
"version": "==2.2.1"
|
||||||
},
|
},
|
||||||
"async-generator": {
|
"async-generator": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -50,37 +50,37 @@
|
||||||
},
|
},
|
||||||
"cython": {
|
"cython": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1327655db47beb665961d3dc0365e20c9e8e80c234513ab2c7d06ec0dd9d63eb",
|
"sha256:0154d3eead9432dfbef489fecf3a9d9202da0ab4966b796c319c4a3048ff2c03",
|
||||||
"sha256:142400f13102403f43576bb92d808a668e29deda5625388cfa39fe0bcf37b3d1",
|
"sha256:0355e23994919a6abfce3b9493062f69317f2057560bde694493fa18306b7824",
|
||||||
"sha256:1b4204715141281a631337378f0c15fe660b35e1b6888ca05f1f3f49df3b97d5",
|
"sha256:06c0c1332ce36bb6feb6c3590cd72c0b4fa59b34202b1975d484319595e2a548",
|
||||||
"sha256:23aabaaf8887e6db99df2145de6742f8c92830134735778bf2ae26338f2b406f",
|
"sha256:0970fc905136b520a7595e1d43ff465a8ab24103ac54da801f9bb25be940bb5b",
|
||||||
"sha256:2a724c6f21fdf4e3c1e8c5c862ff87f5420fdaecf53a5a0417915e483d90217f",
|
"sha256:1242351548eeb2c99ca2958fa2eebae08fc361f30d56588ff4f28cdb63a440c1",
|
||||||
"sha256:2c9c8c1c6e8bd3587e5f5db6f865a42195ff2dedcaf5cdb63fdea10c98bd6246",
|
"sha256:17a573b551aa34878eba7e0b34a774b18e4b2b35943b2e7d2ae0a31ac5446e39",
|
||||||
"sha256:3a1be38b774423605189d60652b3d8a324fc81d213f96569720c8093784245ab",
|
"sha256:28025cd1d36df61257646d97325046ee894118e267a49d19fd321fbca413c3df",
|
||||||
"sha256:46be5297a76513e4d5d6e746737d4866a762cfe457e57d7c54baa7ef8fea7e9a",
|
"sha256:37d1d560d49985b87629785cf9971add6dd621fc9db1505a5811dcb0feb34a94",
|
||||||
"sha256:48dc2ea4c4d3f34ddcad5bc71b1f1cf49830f868832d3e5df803c811e7395b6e",
|
"sha256:3f6ed611cf01e7bbd852bb4f77bea05f0fcca0556926aa0de21a20f719c4abc5",
|
||||||
"sha256:53f33e04d2ed078ac02841741bcd536b546e1f416608084468ab30a87638a466",
|
"sha256:5254de3aecc883d89243f37da74ceff70d9bf459b94ad816f889c794a51a3e76",
|
||||||
"sha256:57b10588618ca19a4cc870f381aa8805bc5fe0c62d19d7f940232ff8a373887c",
|
"sha256:54484d6b3c102c1e52ebb5dbcee4b7b42efae96ac3d1a2e1d640acab8d7c6fbe",
|
||||||
"sha256:6001038341b52301450bb9c62e5d5da825788944572679277e137ffb3596e718",
|
"sha256:6760738fab5d44e3615fb4c3a12dd5b766850e79dc1bd2ecb4c1df361871f1c3",
|
||||||
"sha256:70bef52e735607060f327d729be35c820d9018d260a875e4f98b20ba8c4fff96",
|
"sha256:69c3cd2fe8c2db18a2042aaeb8b3bb0a9ea214c1612c8431fab0acb5ed434b07",
|
||||||
"sha256:7d0f76b251699be8f1f1064dcb12d4b3b2b676ce15ff30c104e0c2091a015142",
|
"sha256:73d3e28f9fb445bf67cc753c826e63ce9c3308d62d7e642dfb8cc3556f3ac685",
|
||||||
"sha256:9440b64c1569c26a184b7c778bb221cf9987c5c8486d32cda02302c66ea78980",
|
"sha256:74763f2ac133aabb1a8260ff00303571b91b7c866e0bbcd05159dc72a67f9911",
|
||||||
"sha256:956cc97eac6f9d3b16e3b2d2a94c5586af3403ba97945e9d88a4a0f029899646",
|
"sha256:764049a11173b2039674879b1be0d73e2288af4fc1ad8177aa99cdc0de335b31",
|
||||||
"sha256:ae430ad8cce937e07ea566d1d7899eef1fedc8ec512b4d5fa37ebf2c1f879936",
|
"sha256:9d5290d749099a8e446422adfb0aa2142c711284800fb1eb70f595101e32cbf1",
|
||||||
"sha256:bdb575149881978d62167dd8427402a5872a79bd83e9d51219680670e9f80b40",
|
"sha256:a12a83d72aa1298236b63c1d5e95de6230c634cc9c3eb06be51c67f88ccedd92",
|
||||||
"sha256:c0ffcddd3dbdf22aae3980931112cc8b2732315a6273988f3205cf5dacf36f45",
|
"sha256:aa83ce29f04c3d83d51863819281be8bf35d22ae1b8fba9a32cd8a84cd471998",
|
||||||
"sha256:c133e2efc57426974366ac74f2ef0f1171b860301ac27f72316deacff4ccdc17",
|
"sha256:ab3d291304c4e4160276533d3e4e36380ba18dacf2c8d6573980f2ef168f3afb",
|
||||||
"sha256:c6e9521d0b77eb1da89e8264eb98c8f5cda7c49a49b8128acfd35f0ca50e56d0",
|
"sha256:b1b4ffcb39e77e29862e23d3a25a7c307cac85c8d0654d51547059c053060fc5",
|
||||||
"sha256:c7cac0220ecb733024e8acfcfb6b593a007185690f2ea470d2392b72510b7187",
|
"sha256:c0ef97e126831ec8ae616c5a4d9b321b6e792cf48a1bf473fd6555226c91839f",
|
||||||
"sha256:d53483820ac28f2be2ff13eedd56c0f36a4c583727b551d3d468023556e2336a",
|
"sha256:c15c3fe45855d985922c0f74f8c282b126b3458d5662c4875ae0d088d12b7c3f",
|
||||||
"sha256:d60210784186d61e0ec808d5dbee5d661c7457a57f93cb5fdc456394607ce98c",
|
"sha256:cdafe6f7f7dd32ce79b9d5dade7045de7c89d747bce4804f41be84027dc23312",
|
||||||
"sha256:d687fb1cd9df28c1515666174c62e54bd894a6a6d0862f89705063cd47739f83",
|
"sha256:d4d9a9531d3f5990f2f043288359c83527ab927ef4ad9c55a831166d68a53baa",
|
||||||
"sha256:d926764d9c768a48b0a16a91696aaa25498057e060934f968fa4c5629b942d85",
|
"sha256:f5722b4eb8052405c314dfae8a1a6e27ee493d051354c53f1ceb8f4e1fd3f075",
|
||||||
"sha256:d94a2f4ad74732f58d1c771fc5d90a62c4fe4c98d0adfecbc76cd0d8d14bf044",
|
"sha256:f581171b9c3b4d5048ce64634b210bfccec06ef3a7422f1807a2a8de31a3c075",
|
||||||
"sha256:def76a546eeec059666f5f4117dfdf9c78e50fa1f95bdd23b04618c7adf845cd"
|
"sha256:f8971da715deda1670e2383185c0c2a1ea819fb17221953f4d0c20d0f14ef24d"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.29.3"
|
"version": "==0.29.5"
|
||||||
},
|
},
|
||||||
"e1839a8": {
|
"e1839a8": {
|
||||||
"editable": true,
|
"editable": true,
|
||||||
|
@ -135,37 +135,37 @@
|
||||||
},
|
},
|
||||||
"multio": {
|
"multio": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:e8bce12aa8d2e076d96f4c4b6bfb70c01e0e0af9892f9ffc4ec868854e1b877e"
|
"sha256:fdcd9bd48d053da9f44b1ca56c2f7cf0902d4eb76b41cf16b684166d7180f79f"
|
||||||
],
|
],
|
||||||
"version": "==0.2.4"
|
"version": "==0.2.5"
|
||||||
},
|
},
|
||||||
"numpy": {
|
"numpy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:00a458d6821b1e87be873f2126d5646b901047a7480e8ae9773ecf214f0e19f3",
|
"sha256:0cdbbaa30ae69281b18dd995d3079c4e552ad6d5426977f66b9a2a95f11f552a",
|
||||||
"sha256:0470c5dc32212a08ebc2405f32e8ceb9a5b1c8ac61a2daf9835ec0856a220495",
|
"sha256:2b0cca1049bd39d1879fa4d598624cafe82d35529c72de1b3d528d68031cdd95",
|
||||||
"sha256:24a9c287a4a1c427c2d45bf7c4fc6180c52a08fa0990d4c94e4c86a9b1e23ba5",
|
"sha256:31d3fe5b673e99d33d70cfee2ea8fe8dccd60f265c3ed990873a88647e3dd288",
|
||||||
"sha256:25600e8901012180a1b7cd1ac3e27e7793586ecd432383191929ac2edf37ff5d",
|
"sha256:34dd4922aab246c39bf5df03ca653d6265e65971deca6784c956bf356bca6197",
|
||||||
"sha256:2d279bd99329e72c30937bdef82b6dc7779c7607c5a379bab1bf76be1f4c1422",
|
"sha256:384e2dfa03da7c8d54f8f934f61b6a5e4e1ebb56a65b287567629d6c14578003",
|
||||||
"sha256:32af2bcf4bb7631dac19736a6e092ec9715e770dcaa1f85fcd99dec5040b2a4d",
|
"sha256:392e2ea22b41a22c0289a88053204b616181288162ba78e6823e1760309d5277",
|
||||||
"sha256:3e90a9fce378114b6c2fc01fff7423300515c7b54b7cc71b02a22bc0bd7dfdd8",
|
"sha256:4341a39fc085f31a583be505eabf00e17c619b469fef78dc7e8241385bfddaa4",
|
||||||
"sha256:5774d49516c37fd3fc1f232e033d2b152f3323ca4c7bfefd7277e4c67f3c08b4",
|
"sha256:45080f065dcaa573ebecbfe13cdd86e8c0a68c4e999aa06bd365374ea7137706",
|
||||||
"sha256:64ff21aac30d40c20ba994c94a08d439b8ced3b9c704af897e9e4ba09d10e62c",
|
"sha256:485cb1eb4c9962f4cd042fed9424482ec1d83fee5dc2ef3f2552ac47852cb259",
|
||||||
"sha256:803b2af862dcad6c11231ea3cd1015d1293efd6c87088be33d713a9b23e9e419",
|
"sha256:575cefd28d3e0da85b0864506ae26b06483ee4a906e308be5a7ad11083f9d757",
|
||||||
"sha256:95c830b09626508f7808ce7f1344fb98068e63143e6050e5dc3063142fc60007",
|
"sha256:62784b35df7de7ca4d0d81c5b6af5983f48c5cdef32fc3635b445674e56e3266",
|
||||||
"sha256:96e49a0c82b4e3130093002f625545104037c2d25866fa2e0c90d6e54f5a1fbc",
|
"sha256:69c152f7c11bf3b4fc11bc4cc62eb0334371c0db6844ebace43b7c815b602805",
|
||||||
"sha256:a1dd8221f0e69038748f47b8bb3248d0b9ecdf13fe837440951c3d5ff72639bb",
|
"sha256:6ccfdcefd287f252cf1ea7a3f1656070da330c4a5658e43ad223269165cdf977",
|
||||||
"sha256:a80ecac5664f420556a725a5646f2d1c60a7c0489d68a38b5056393e949e27ac",
|
"sha256:7298fbd73c0b3eff1d53dc9b9bdb7add8797bb55eeee38c8ccd7906755ba28af",
|
||||||
"sha256:b19a47ff1bd2fca0cacdfa830c967746764c32dca6a0c0328d9c893f4bfe2f6b",
|
"sha256:79463d918d1bf3aeb9186e3df17ddb0baca443f41371df422f99ee94f4f2bbfe",
|
||||||
"sha256:be43df2c563e264b38e3318574d80fc8f365df3fb745270934d2dbe54e006f41",
|
"sha256:8bbee788d82c0ac656536de70e817af09b7694f5326b0ef08e5c1014fcb96bb3",
|
||||||
"sha256:c40cb17188f6ae3c5b6efc6f0fd43a7ddd219b7807fe179e71027849a9b91afc",
|
"sha256:a863957192855c4c57f60a75a1ac06ce5362ad18506d362dd807e194b4baf3ce",
|
||||||
"sha256:c6251e0f0ecac53ba2b99d9f0cc16fa9021914a78869c38213c436ba343641f0",
|
"sha256:ae602ba425fb2b074e16d125cdce4f0194903da935b2e7fe284ebecca6d92e76",
|
||||||
"sha256:cb189bd98b2e7ac02df389b6212846ab20661f4bafe16b5a70a6f1728c1cc7cb",
|
"sha256:b13faa258b20fa66d29011f99fdf498641ca74a0a6d9266bc27d83c70fea4a6a",
|
||||||
"sha256:ef4ae41add536cb825d8aa029c15ef510aead06ea5b68daea64f0b9ecbff17db",
|
"sha256:c2c39d69266621dd7464e2bb740d6eb5abc64ddc339cc97aa669f3bb4d75c103",
|
||||||
"sha256:f00a2c21f60284e024bba351875f3501c6d5817d64997a0afe4f4355161a8889",
|
"sha256:e9c88f173d31909d881a60f08a8494e63f1aff2a4052476b24d4f50e82c47e24",
|
||||||
"sha256:f1232f98a6bbd6d1678249f94028bccc541bbc306aa5c4e1471a881b0e5a3409",
|
"sha256:f1a29267ac29fff0913de0f11f3a9edfcd3f39595f467026c29376fad243ebe3",
|
||||||
"sha256:fea682f6ddc09517df0e6d5caad9613c6d91a42232aeb082df67e4d205de19cc"
|
"sha256:f69dde0c5a137d887676a8129373e44366055cf19d1b434e853310c7a1e68f93"
|
||||||
],
|
],
|
||||||
"version": "==1.16.0"
|
"version": "==1.16.1"
|
||||||
},
|
},
|
||||||
"outcome": {
|
"outcome": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -176,35 +176,35 @@
|
||||||
},
|
},
|
||||||
"pandas": {
|
"pandas": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:02d34a55e85819a7eab096f391f8dcc237876e8b3cdaf1fba964f5fb59af9acf",
|
"sha256:02c830f951f3dc8c3164e2639a8961881390f7492f71a7835c2330f54539ad57",
|
||||||
"sha256:0dbcf78e68f619840184ce661c68c1760de403b0f69d81905d6b9a699d1861d6",
|
"sha256:179015834c72a577486337394493cc2969feee9a04a2ea09f50c724e4b52ab42",
|
||||||
"sha256:174c3974da26fd778ac8537d74efb17d4cef59e6b3e81e3c59690f39a6f6b73d",
|
"sha256:3894960d43c64cfea5142ac783b101362f5008ee92e962392156a3f8d1558995",
|
||||||
"sha256:3a8ab5c350131ba273d3f8eb430343304d6c2138a61d34e4a11ebd75f8bf3e7e",
|
"sha256:435821cb2501eabbcee7e83614bd710940dc0cf28b5afbc4bdb816c31cec71af",
|
||||||
"sha256:560074ce9ff95409b233c0a8d143a2546a2d71d636d583172252dc0021fdb11b",
|
"sha256:8294dea9aa1811f93558702856e3b68dd1dfd7e9dbc8e0865918a07ee0f21c2c",
|
||||||
"sha256:5bded8cb431705609dbd9048114f1d6d59bef2f1ca95a8c58bd649442c9dc16c",
|
"sha256:844e745ab27a9a01c86925fe776f9d2e09575e65f0bf8eba5090edddd655dffc",
|
||||||
"sha256:8a8748684787792f3a643a7e0530c3024301f3e5799a199a5c2c526c07f712ba",
|
"sha256:a08d49f5fa2a2243262fe5581cb89f6c0c7cc525b8d6411719ab9400a9dc4a82",
|
||||||
"sha256:8c7e43c4b7920fc02ce7743b976aca15bd45293ed298d84793307bc9799df3f6",
|
"sha256:a435c251246075337eb9fdc4160fd15c8a87cc0679d8d61fb5255d8d5a12f044",
|
||||||
"sha256:9bd9ef3e183b7b1ce90b7ab5e8672907cd73dc36f036fc6714f0e7a5f9852da0",
|
"sha256:a799f03c0ec6d8687f425d7d6c075e8055a9a808f1ba87604d91f20507631d8d",
|
||||||
"sha256:d3f27e276c8557c15c19c5c9a414e77b893d39fce6e6e40e5c46fcf5eeffe028",
|
"sha256:aea72ce5b3a016b578cc05c04a2f68d9cafacf5d784b6fe832e66381cb62c719",
|
||||||
"sha256:d40b82a4aee4ca968348e41bf6588ed9cadd171c7da8b671ed31d3fd967de703",
|
"sha256:c145e94c6da2af7eaf1fd827293ac1090a61a9b80150bebe99f8966a02378db9",
|
||||||
"sha256:d8cf054a099ff694a0e75386471bdde098efe7c350548ec6b899f169bef1a859",
|
"sha256:c8a7b470c88c779301b73b23cabdbbd94b83b93040b2ccffa409e06df23831c0",
|
||||||
"sha256:dd9f4843aa59f09698679b64064f11f51d60e45358ab45299de4dcff90524be3",
|
"sha256:c9e31b36abbd7b94c547d9047f13e1546e3ba967044cf4f9718575fcb7b81bb6",
|
||||||
"sha256:e6f9f5ad4e73f5eecaa66e9c9d30ff8661c400190a6079ee170e37a466457e31",
|
"sha256:d960b7a03c33c328c723cfc2f8902a6291645f4efa0a5c1d4c5fa008cdc1ea77",
|
||||||
"sha256:e9989e17f203900b2c7add53fa17d6686e66282598359b43fb12260ae8bf7eba",
|
"sha256:da21fae4c173781b012217c9444f13c67449957a4d45184a9718268732c09564",
|
||||||
"sha256:eadc9d19b25420e1ae77f0a11b779d4e71f47c3aa1953c218e8fe812d1f5341e",
|
"sha256:db26c0fea0bd7d33c356da98bafd2c0dfb8f338e45e2824ff8f4f3e61b5c5f25",
|
||||||
"sha256:ecb630a99b0ab6c178b5c2988ca8c5b98f6ec2fd9e172c2873a5df44b261310f",
|
"sha256:dc296c3f16ec620cfb4daf0f672e3c90f3920ece8261b2760cd0ebd9cd4daa55",
|
||||||
"sha256:f8eb9308bd64abf71dda77b823913696cd85c4f36c026acee0a64d8834a09b43",
|
"sha256:e8da67cb2e9333ec30d53cfb96e27a4865d1648688e5471699070d35d8ab38cf",
|
||||||
"sha256:fe71a037ce866d9fb717fd3a792d46c744433179bf3f25da48af8f46cee20c3e",
|
"sha256:fb4f047a63f91f22aade4438aaf790400b96644e802daab4293e9b799802f93f",
|
||||||
"sha256:ff0d83306bfda4639fac2a4f8df2c51eb2bbdda540a74490703e8a6b413a37eb"
|
"sha256:fef9939176cba0c2526ebeefffb8b9807543dc0954877b7226f751ec1294a869"
|
||||||
],
|
],
|
||||||
"version": "==0.24.0"
|
"version": "==0.24.1"
|
||||||
},
|
},
|
||||||
"pdbpp": {
|
"pdbpp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:535085916fcfb768690ba0aeab2967c2a2163a0a60e5b703776846873e171399"
|
"sha256:438bb2c885e40e9dcf649d9b598e4fe30fd1e3558c89a6ad3f447a9839a04e9f"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.9.3"
|
"version": "==0.9.6"
|
||||||
},
|
},
|
||||||
"pygments": {
|
"pygments": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -215,10 +215,10 @@
|
||||||
},
|
},
|
||||||
"python-dateutil": {
|
"python-dateutil": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93",
|
"sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
|
||||||
"sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02"
|
"sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
|
||||||
],
|
],
|
||||||
"version": "==2.7.5"
|
"version": "==2.8.0"
|
||||||
},
|
},
|
||||||
"pytz": {
|
"pytz": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -250,14 +250,15 @@
|
||||||
},
|
},
|
||||||
"tractor": {
|
"tractor": {
|
||||||
"git": "git://github.com/tgoodlet/tractor.git",
|
"git": "git://github.com/tgoodlet/tractor.git",
|
||||||
"ref": "977eaedb0bd4b235a5ac07da318f4c1d3be3749a"
|
"ref": "a927966170ea092be003b72029ca5d432c5e6239"
|
||||||
},
|
},
|
||||||
"trio": {
|
"trio": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:d323cc15f6406d15954af91e5e34af2001cc24163fdde29e3f88a227a1b53ab0"
|
"sha256:3796774aedbf5be581c68f98c79b565654876de6e9a01c6a95e3ec6cd4e4b4c3",
|
||||||
|
"sha256:b0c03d312c300a947e54e204be88255992434e824374b7d3cc886876dab9a542"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.10.0"
|
"version": "==0.11.0"
|
||||||
},
|
},
|
||||||
"wmctrl": {
|
"wmctrl": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -269,9 +270,9 @@
|
||||||
"develop": {
|
"develop": {
|
||||||
"asks": {
|
"asks": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1679e5bd1dfa6c5d2220bdf2b8921c9c0d063d08370a7c66b9e167113681406f"
|
"sha256:d67aecaa02d0c67fa761dfdb23854391e6996c6045fb9385b1f508b0956a190d"
|
||||||
],
|
],
|
||||||
"version": "==2.2.0"
|
"version": "==2.2.1"
|
||||||
},
|
},
|
||||||
"async-generator": {
|
"async-generator": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -282,10 +283,10 @@
|
||||||
},
|
},
|
||||||
"atomicwrites": {
|
"atomicwrites": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
|
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
|
||||||
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
|
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
|
||||||
],
|
],
|
||||||
"version": "==1.2.1"
|
"version": "==1.3.0"
|
||||||
},
|
},
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -310,37 +311,37 @@
|
||||||
},
|
},
|
||||||
"cython": {
|
"cython": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1327655db47beb665961d3dc0365e20c9e8e80c234513ab2c7d06ec0dd9d63eb",
|
"sha256:0154d3eead9432dfbef489fecf3a9d9202da0ab4966b796c319c4a3048ff2c03",
|
||||||
"sha256:142400f13102403f43576bb92d808a668e29deda5625388cfa39fe0bcf37b3d1",
|
"sha256:0355e23994919a6abfce3b9493062f69317f2057560bde694493fa18306b7824",
|
||||||
"sha256:1b4204715141281a631337378f0c15fe660b35e1b6888ca05f1f3f49df3b97d5",
|
"sha256:06c0c1332ce36bb6feb6c3590cd72c0b4fa59b34202b1975d484319595e2a548",
|
||||||
"sha256:23aabaaf8887e6db99df2145de6742f8c92830134735778bf2ae26338f2b406f",
|
"sha256:0970fc905136b520a7595e1d43ff465a8ab24103ac54da801f9bb25be940bb5b",
|
||||||
"sha256:2a724c6f21fdf4e3c1e8c5c862ff87f5420fdaecf53a5a0417915e483d90217f",
|
"sha256:1242351548eeb2c99ca2958fa2eebae08fc361f30d56588ff4f28cdb63a440c1",
|
||||||
"sha256:2c9c8c1c6e8bd3587e5f5db6f865a42195ff2dedcaf5cdb63fdea10c98bd6246",
|
"sha256:17a573b551aa34878eba7e0b34a774b18e4b2b35943b2e7d2ae0a31ac5446e39",
|
||||||
"sha256:3a1be38b774423605189d60652b3d8a324fc81d213f96569720c8093784245ab",
|
"sha256:28025cd1d36df61257646d97325046ee894118e267a49d19fd321fbca413c3df",
|
||||||
"sha256:46be5297a76513e4d5d6e746737d4866a762cfe457e57d7c54baa7ef8fea7e9a",
|
"sha256:37d1d560d49985b87629785cf9971add6dd621fc9db1505a5811dcb0feb34a94",
|
||||||
"sha256:48dc2ea4c4d3f34ddcad5bc71b1f1cf49830f868832d3e5df803c811e7395b6e",
|
"sha256:3f6ed611cf01e7bbd852bb4f77bea05f0fcca0556926aa0de21a20f719c4abc5",
|
||||||
"sha256:53f33e04d2ed078ac02841741bcd536b546e1f416608084468ab30a87638a466",
|
"sha256:5254de3aecc883d89243f37da74ceff70d9bf459b94ad816f889c794a51a3e76",
|
||||||
"sha256:57b10588618ca19a4cc870f381aa8805bc5fe0c62d19d7f940232ff8a373887c",
|
"sha256:54484d6b3c102c1e52ebb5dbcee4b7b42efae96ac3d1a2e1d640acab8d7c6fbe",
|
||||||
"sha256:6001038341b52301450bb9c62e5d5da825788944572679277e137ffb3596e718",
|
"sha256:6760738fab5d44e3615fb4c3a12dd5b766850e79dc1bd2ecb4c1df361871f1c3",
|
||||||
"sha256:70bef52e735607060f327d729be35c820d9018d260a875e4f98b20ba8c4fff96",
|
"sha256:69c3cd2fe8c2db18a2042aaeb8b3bb0a9ea214c1612c8431fab0acb5ed434b07",
|
||||||
"sha256:7d0f76b251699be8f1f1064dcb12d4b3b2b676ce15ff30c104e0c2091a015142",
|
"sha256:73d3e28f9fb445bf67cc753c826e63ce9c3308d62d7e642dfb8cc3556f3ac685",
|
||||||
"sha256:9440b64c1569c26a184b7c778bb221cf9987c5c8486d32cda02302c66ea78980",
|
"sha256:74763f2ac133aabb1a8260ff00303571b91b7c866e0bbcd05159dc72a67f9911",
|
||||||
"sha256:956cc97eac6f9d3b16e3b2d2a94c5586af3403ba97945e9d88a4a0f029899646",
|
"sha256:764049a11173b2039674879b1be0d73e2288af4fc1ad8177aa99cdc0de335b31",
|
||||||
"sha256:ae430ad8cce937e07ea566d1d7899eef1fedc8ec512b4d5fa37ebf2c1f879936",
|
"sha256:9d5290d749099a8e446422adfb0aa2142c711284800fb1eb70f595101e32cbf1",
|
||||||
"sha256:bdb575149881978d62167dd8427402a5872a79bd83e9d51219680670e9f80b40",
|
"sha256:a12a83d72aa1298236b63c1d5e95de6230c634cc9c3eb06be51c67f88ccedd92",
|
||||||
"sha256:c0ffcddd3dbdf22aae3980931112cc8b2732315a6273988f3205cf5dacf36f45",
|
"sha256:aa83ce29f04c3d83d51863819281be8bf35d22ae1b8fba9a32cd8a84cd471998",
|
||||||
"sha256:c133e2efc57426974366ac74f2ef0f1171b860301ac27f72316deacff4ccdc17",
|
"sha256:ab3d291304c4e4160276533d3e4e36380ba18dacf2c8d6573980f2ef168f3afb",
|
||||||
"sha256:c6e9521d0b77eb1da89e8264eb98c8f5cda7c49a49b8128acfd35f0ca50e56d0",
|
"sha256:b1b4ffcb39e77e29862e23d3a25a7c307cac85c8d0654d51547059c053060fc5",
|
||||||
"sha256:c7cac0220ecb733024e8acfcfb6b593a007185690f2ea470d2392b72510b7187",
|
"sha256:c0ef97e126831ec8ae616c5a4d9b321b6e792cf48a1bf473fd6555226c91839f",
|
||||||
"sha256:d53483820ac28f2be2ff13eedd56c0f36a4c583727b551d3d468023556e2336a",
|
"sha256:c15c3fe45855d985922c0f74f8c282b126b3458d5662c4875ae0d088d12b7c3f",
|
||||||
"sha256:d60210784186d61e0ec808d5dbee5d661c7457a57f93cb5fdc456394607ce98c",
|
"sha256:cdafe6f7f7dd32ce79b9d5dade7045de7c89d747bce4804f41be84027dc23312",
|
||||||
"sha256:d687fb1cd9df28c1515666174c62e54bd894a6a6d0862f89705063cd47739f83",
|
"sha256:d4d9a9531d3f5990f2f043288359c83527ab927ef4ad9c55a831166d68a53baa",
|
||||||
"sha256:d926764d9c768a48b0a16a91696aaa25498057e060934f968fa4c5629b942d85",
|
"sha256:f5722b4eb8052405c314dfae8a1a6e27ee493d051354c53f1ceb8f4e1fd3f075",
|
||||||
"sha256:d94a2f4ad74732f58d1c771fc5d90a62c4fe4c98d0adfecbc76cd0d8d14bf044",
|
"sha256:f581171b9c3b4d5048ce64634b210bfccec06ef3a7422f1807a2a8de31a3c075",
|
||||||
"sha256:def76a546eeec059666f5f4117dfdf9c78e50fa1f95bdd23b04618c7adf845cd"
|
"sha256:f8971da715deda1670e2383185c0c2a1ea819fb17221953f4d0c20d0f14ef24d"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.29.3"
|
"version": "==0.29.5"
|
||||||
},
|
},
|
||||||
"fancycompleter": {
|
"fancycompleter": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -364,11 +365,11 @@
|
||||||
},
|
},
|
||||||
"more-itertools": {
|
"more-itertools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4",
|
"sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40",
|
||||||
"sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc",
|
"sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"
|
||||||
"sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
|
|
||||||
],
|
],
|
||||||
"version": "==5.0.0"
|
"markers": "python_version > '2.7'",
|
||||||
|
"version": "==6.0.0"
|
||||||
},
|
},
|
||||||
"msgpack": {
|
"msgpack": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -395,37 +396,37 @@
|
||||||
},
|
},
|
||||||
"multio": {
|
"multio": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:e8bce12aa8d2e076d96f4c4b6bfb70c01e0e0af9892f9ffc4ec868854e1b877e"
|
"sha256:fdcd9bd48d053da9f44b1ca56c2f7cf0902d4eb76b41cf16b684166d7180f79f"
|
||||||
],
|
],
|
||||||
"version": "==0.2.4"
|
"version": "==0.2.5"
|
||||||
},
|
},
|
||||||
"numpy": {
|
"numpy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:00a458d6821b1e87be873f2126d5646b901047a7480e8ae9773ecf214f0e19f3",
|
"sha256:0cdbbaa30ae69281b18dd995d3079c4e552ad6d5426977f66b9a2a95f11f552a",
|
||||||
"sha256:0470c5dc32212a08ebc2405f32e8ceb9a5b1c8ac61a2daf9835ec0856a220495",
|
"sha256:2b0cca1049bd39d1879fa4d598624cafe82d35529c72de1b3d528d68031cdd95",
|
||||||
"sha256:24a9c287a4a1c427c2d45bf7c4fc6180c52a08fa0990d4c94e4c86a9b1e23ba5",
|
"sha256:31d3fe5b673e99d33d70cfee2ea8fe8dccd60f265c3ed990873a88647e3dd288",
|
||||||
"sha256:25600e8901012180a1b7cd1ac3e27e7793586ecd432383191929ac2edf37ff5d",
|
"sha256:34dd4922aab246c39bf5df03ca653d6265e65971deca6784c956bf356bca6197",
|
||||||
"sha256:2d279bd99329e72c30937bdef82b6dc7779c7607c5a379bab1bf76be1f4c1422",
|
"sha256:384e2dfa03da7c8d54f8f934f61b6a5e4e1ebb56a65b287567629d6c14578003",
|
||||||
"sha256:32af2bcf4bb7631dac19736a6e092ec9715e770dcaa1f85fcd99dec5040b2a4d",
|
"sha256:392e2ea22b41a22c0289a88053204b616181288162ba78e6823e1760309d5277",
|
||||||
"sha256:3e90a9fce378114b6c2fc01fff7423300515c7b54b7cc71b02a22bc0bd7dfdd8",
|
"sha256:4341a39fc085f31a583be505eabf00e17c619b469fef78dc7e8241385bfddaa4",
|
||||||
"sha256:5774d49516c37fd3fc1f232e033d2b152f3323ca4c7bfefd7277e4c67f3c08b4",
|
"sha256:45080f065dcaa573ebecbfe13cdd86e8c0a68c4e999aa06bd365374ea7137706",
|
||||||
"sha256:64ff21aac30d40c20ba994c94a08d439b8ced3b9c704af897e9e4ba09d10e62c",
|
"sha256:485cb1eb4c9962f4cd042fed9424482ec1d83fee5dc2ef3f2552ac47852cb259",
|
||||||
"sha256:803b2af862dcad6c11231ea3cd1015d1293efd6c87088be33d713a9b23e9e419",
|
"sha256:575cefd28d3e0da85b0864506ae26b06483ee4a906e308be5a7ad11083f9d757",
|
||||||
"sha256:95c830b09626508f7808ce7f1344fb98068e63143e6050e5dc3063142fc60007",
|
"sha256:62784b35df7de7ca4d0d81c5b6af5983f48c5cdef32fc3635b445674e56e3266",
|
||||||
"sha256:96e49a0c82b4e3130093002f625545104037c2d25866fa2e0c90d6e54f5a1fbc",
|
"sha256:69c152f7c11bf3b4fc11bc4cc62eb0334371c0db6844ebace43b7c815b602805",
|
||||||
"sha256:a1dd8221f0e69038748f47b8bb3248d0b9ecdf13fe837440951c3d5ff72639bb",
|
"sha256:6ccfdcefd287f252cf1ea7a3f1656070da330c4a5658e43ad223269165cdf977",
|
||||||
"sha256:a80ecac5664f420556a725a5646f2d1c60a7c0489d68a38b5056393e949e27ac",
|
"sha256:7298fbd73c0b3eff1d53dc9b9bdb7add8797bb55eeee38c8ccd7906755ba28af",
|
||||||
"sha256:b19a47ff1bd2fca0cacdfa830c967746764c32dca6a0c0328d9c893f4bfe2f6b",
|
"sha256:79463d918d1bf3aeb9186e3df17ddb0baca443f41371df422f99ee94f4f2bbfe",
|
||||||
"sha256:be43df2c563e264b38e3318574d80fc8f365df3fb745270934d2dbe54e006f41",
|
"sha256:8bbee788d82c0ac656536de70e817af09b7694f5326b0ef08e5c1014fcb96bb3",
|
||||||
"sha256:c40cb17188f6ae3c5b6efc6f0fd43a7ddd219b7807fe179e71027849a9b91afc",
|
"sha256:a863957192855c4c57f60a75a1ac06ce5362ad18506d362dd807e194b4baf3ce",
|
||||||
"sha256:c6251e0f0ecac53ba2b99d9f0cc16fa9021914a78869c38213c436ba343641f0",
|
"sha256:ae602ba425fb2b074e16d125cdce4f0194903da935b2e7fe284ebecca6d92e76",
|
||||||
"sha256:cb189bd98b2e7ac02df389b6212846ab20661f4bafe16b5a70a6f1728c1cc7cb",
|
"sha256:b13faa258b20fa66d29011f99fdf498641ca74a0a6d9266bc27d83c70fea4a6a",
|
||||||
"sha256:ef4ae41add536cb825d8aa029c15ef510aead06ea5b68daea64f0b9ecbff17db",
|
"sha256:c2c39d69266621dd7464e2bb740d6eb5abc64ddc339cc97aa669f3bb4d75c103",
|
||||||
"sha256:f00a2c21f60284e024bba351875f3501c6d5817d64997a0afe4f4355161a8889",
|
"sha256:e9c88f173d31909d881a60f08a8494e63f1aff2a4052476b24d4f50e82c47e24",
|
||||||
"sha256:f1232f98a6bbd6d1678249f94028bccc541bbc306aa5c4e1471a881b0e5a3409",
|
"sha256:f1a29267ac29fff0913de0f11f3a9edfcd3f39595f467026c29376fad243ebe3",
|
||||||
"sha256:fea682f6ddc09517df0e6d5caad9613c6d91a42232aeb082df67e4d205de19cc"
|
"sha256:f69dde0c5a137d887676a8129373e44366055cf19d1b434e853310c7a1e68f93"
|
||||||
],
|
],
|
||||||
"version": "==1.16.0"
|
"version": "==1.16.1"
|
||||||
},
|
},
|
||||||
"outcome": {
|
"outcome": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -436,35 +437,35 @@
|
||||||
},
|
},
|
||||||
"pandas": {
|
"pandas": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:02d34a55e85819a7eab096f391f8dcc237876e8b3cdaf1fba964f5fb59af9acf",
|
"sha256:02c830f951f3dc8c3164e2639a8961881390f7492f71a7835c2330f54539ad57",
|
||||||
"sha256:0dbcf78e68f619840184ce661c68c1760de403b0f69d81905d6b9a699d1861d6",
|
"sha256:179015834c72a577486337394493cc2969feee9a04a2ea09f50c724e4b52ab42",
|
||||||
"sha256:174c3974da26fd778ac8537d74efb17d4cef59e6b3e81e3c59690f39a6f6b73d",
|
"sha256:3894960d43c64cfea5142ac783b101362f5008ee92e962392156a3f8d1558995",
|
||||||
"sha256:3a8ab5c350131ba273d3f8eb430343304d6c2138a61d34e4a11ebd75f8bf3e7e",
|
"sha256:435821cb2501eabbcee7e83614bd710940dc0cf28b5afbc4bdb816c31cec71af",
|
||||||
"sha256:560074ce9ff95409b233c0a8d143a2546a2d71d636d583172252dc0021fdb11b",
|
"sha256:8294dea9aa1811f93558702856e3b68dd1dfd7e9dbc8e0865918a07ee0f21c2c",
|
||||||
"sha256:5bded8cb431705609dbd9048114f1d6d59bef2f1ca95a8c58bd649442c9dc16c",
|
"sha256:844e745ab27a9a01c86925fe776f9d2e09575e65f0bf8eba5090edddd655dffc",
|
||||||
"sha256:8a8748684787792f3a643a7e0530c3024301f3e5799a199a5c2c526c07f712ba",
|
"sha256:a08d49f5fa2a2243262fe5581cb89f6c0c7cc525b8d6411719ab9400a9dc4a82",
|
||||||
"sha256:8c7e43c4b7920fc02ce7743b976aca15bd45293ed298d84793307bc9799df3f6",
|
"sha256:a435c251246075337eb9fdc4160fd15c8a87cc0679d8d61fb5255d8d5a12f044",
|
||||||
"sha256:9bd9ef3e183b7b1ce90b7ab5e8672907cd73dc36f036fc6714f0e7a5f9852da0",
|
"sha256:a799f03c0ec6d8687f425d7d6c075e8055a9a808f1ba87604d91f20507631d8d",
|
||||||
"sha256:d3f27e276c8557c15c19c5c9a414e77b893d39fce6e6e40e5c46fcf5eeffe028",
|
"sha256:aea72ce5b3a016b578cc05c04a2f68d9cafacf5d784b6fe832e66381cb62c719",
|
||||||
"sha256:d40b82a4aee4ca968348e41bf6588ed9cadd171c7da8b671ed31d3fd967de703",
|
"sha256:c145e94c6da2af7eaf1fd827293ac1090a61a9b80150bebe99f8966a02378db9",
|
||||||
"sha256:d8cf054a099ff694a0e75386471bdde098efe7c350548ec6b899f169bef1a859",
|
"sha256:c8a7b470c88c779301b73b23cabdbbd94b83b93040b2ccffa409e06df23831c0",
|
||||||
"sha256:dd9f4843aa59f09698679b64064f11f51d60e45358ab45299de4dcff90524be3",
|
"sha256:c9e31b36abbd7b94c547d9047f13e1546e3ba967044cf4f9718575fcb7b81bb6",
|
||||||
"sha256:e6f9f5ad4e73f5eecaa66e9c9d30ff8661c400190a6079ee170e37a466457e31",
|
"sha256:d960b7a03c33c328c723cfc2f8902a6291645f4efa0a5c1d4c5fa008cdc1ea77",
|
||||||
"sha256:e9989e17f203900b2c7add53fa17d6686e66282598359b43fb12260ae8bf7eba",
|
"sha256:da21fae4c173781b012217c9444f13c67449957a4d45184a9718268732c09564",
|
||||||
"sha256:eadc9d19b25420e1ae77f0a11b779d4e71f47c3aa1953c218e8fe812d1f5341e",
|
"sha256:db26c0fea0bd7d33c356da98bafd2c0dfb8f338e45e2824ff8f4f3e61b5c5f25",
|
||||||
"sha256:ecb630a99b0ab6c178b5c2988ca8c5b98f6ec2fd9e172c2873a5df44b261310f",
|
"sha256:dc296c3f16ec620cfb4daf0f672e3c90f3920ece8261b2760cd0ebd9cd4daa55",
|
||||||
"sha256:f8eb9308bd64abf71dda77b823913696cd85c4f36c026acee0a64d8834a09b43",
|
"sha256:e8da67cb2e9333ec30d53cfb96e27a4865d1648688e5471699070d35d8ab38cf",
|
||||||
"sha256:fe71a037ce866d9fb717fd3a792d46c744433179bf3f25da48af8f46cee20c3e",
|
"sha256:fb4f047a63f91f22aade4438aaf790400b96644e802daab4293e9b799802f93f",
|
||||||
"sha256:ff0d83306bfda4639fac2a4f8df2c51eb2bbdda540a74490703e8a6b413a37eb"
|
"sha256:fef9939176cba0c2526ebeefffb8b9807543dc0954877b7226f751ec1294a869"
|
||||||
],
|
],
|
||||||
"version": "==0.24.0"
|
"version": "==0.24.1"
|
||||||
},
|
},
|
||||||
"pdbpp": {
|
"pdbpp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:535085916fcfb768690ba0aeab2967c2a2163a0a60e5b703776846873e171399"
|
"sha256:438bb2c885e40e9dcf649d9b598e4fe30fd1e3558c89a6ad3f447a9839a04e9f"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.9.3"
|
"version": "==0.9.6"
|
||||||
},
|
},
|
||||||
"piker": {
|
"piker": {
|
||||||
"editable": true,
|
"editable": true,
|
||||||
|
@ -479,10 +480,10 @@
|
||||||
},
|
},
|
||||||
"py": {
|
"py": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
|
"sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
|
||||||
"sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
|
"sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
|
||||||
],
|
],
|
||||||
"version": "==1.7.0"
|
"version": "==1.8.0"
|
||||||
},
|
},
|
||||||
"pygments": {
|
"pygments": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -493,18 +494,18 @@
|
||||||
},
|
},
|
||||||
"pytest": {
|
"pytest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:41568ea7ecb4a68d7f63837cf65b92ce8d0105e43196ff2b26622995bb3dc4b2",
|
"sha256:067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c",
|
||||||
"sha256:c3c573a29d7c9547fb90217ece8a8843aa0c1328a797e200290dc3d0b4b823be"
|
"sha256:9687049d53695ad45cf5fdc7bbd51f0c49f1ea3ecfc4b7f3fde7501b541f17f4"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==4.1.1"
|
"version": "==4.3.0"
|
||||||
},
|
},
|
||||||
"python-dateutil": {
|
"python-dateutil": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93",
|
"sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
|
||||||
"sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02"
|
"sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
|
||||||
],
|
],
|
||||||
"version": "==2.7.5"
|
"version": "==2.8.0"
|
||||||
},
|
},
|
||||||
"pytz": {
|
"pytz": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -536,10 +537,11 @@
|
||||||
},
|
},
|
||||||
"trio": {
|
"trio": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:d323cc15f6406d15954af91e5e34af2001cc24163fdde29e3f88a227a1b53ab0"
|
"sha256:3796774aedbf5be581c68f98c79b565654876de6e9a01c6a95e3ec6cd4e4b4c3",
|
||||||
|
"sha256:b0c03d312c300a947e54e204be88255992434e824374b7d3cc886876dab9a542"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.10.0"
|
"version": "==0.11.0"
|
||||||
},
|
},
|
||||||
"wmctrl": {
|
"wmctrl": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
|
@ -4,6 +4,10 @@ Broker clients, daemons and general back end machinery.
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
|
||||||
|
# TODO: move to urllib3/requests once supported
|
||||||
|
import asks
|
||||||
|
asks.init('trio')
|
||||||
|
|
||||||
__brokers__ = [
|
__brokers__ = [
|
||||||
'questrade',
|
'questrade',
|
||||||
'robinhood',
|
'robinhood',
|
||||||
|
|
|
@ -24,11 +24,11 @@ def resproc(
|
||||||
if not resp.status_code == 200:
|
if not resp.status_code == 200:
|
||||||
raise BrokerError(resp.body)
|
raise BrokerError(resp.body)
|
||||||
try:
|
try:
|
||||||
data = resp.json()
|
json = resp.json()
|
||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
log.exception(f"Failed to process {resp}:\n{resp.text}")
|
log.exception(f"Failed to process {resp}:\n{resp.text}")
|
||||||
raise BrokerError(resp.text)
|
raise BrokerError(resp.text)
|
||||||
else:
|
else:
|
||||||
log.trace(f"Received json contents:\n{colorize_json(data)}")
|
log.trace(f"Received json contents:\n{colorize_json(json)}")
|
||||||
|
|
||||||
return data if return_json else resp
|
return json if return_json else resp
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Broker configuration mgmt.
|
Broker configuration mgmt.
|
||||||
"""
|
"""
|
||||||
from os import path, makedirs
|
import os
|
||||||
import configparser
|
import configparser
|
||||||
import click
|
import click
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
|
@ -9,28 +9,46 @@ from ..log import get_logger
|
||||||
log = get_logger('broker-config')
|
log = get_logger('broker-config')
|
||||||
|
|
||||||
_config_dir = click.get_app_dir('piker')
|
_config_dir = click.get_app_dir('piker')
|
||||||
_broker_conf_path = path.join(_config_dir, 'brokers.ini')
|
_file_name = 'brokers.ini'
|
||||||
|
|
||||||
|
|
||||||
def load(path: str = None) -> (configparser.ConfigParser, str):
|
def _override_config_dir(
|
||||||
|
path: str
|
||||||
|
) -> None:
|
||||||
|
global _config_dir
|
||||||
|
_config_dir = path
|
||||||
|
|
||||||
|
|
||||||
|
def get_broker_conf_path():
|
||||||
|
return os.path.join(_config_dir, _file_name)
|
||||||
|
|
||||||
|
|
||||||
|
def load(
|
||||||
|
path: str = None
|
||||||
|
) -> (configparser.ConfigParser, str):
|
||||||
"""Load broker config.
|
"""Load broker config.
|
||||||
"""
|
"""
|
||||||
path = path or _broker_conf_path
|
path = path or get_broker_conf_path()
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
read = config.read(path)
|
config.read(path)
|
||||||
log.debug(f"Read config file {path}")
|
log.debug(f"Read config file {path}")
|
||||||
return config, path
|
return config, path
|
||||||
|
|
||||||
|
|
||||||
def write(config: configparser.ConfigParser) -> None:
|
def write(
|
||||||
|
config: configparser.ConfigParser,
|
||||||
|
path: str = None,
|
||||||
|
) -> None:
|
||||||
"""Write broker config to disk.
|
"""Write broker config to disk.
|
||||||
|
|
||||||
Create a ``brokers.ini`` file if one does not exist.
|
Create a ``brokers.ini`` file if one does not exist.
|
||||||
"""
|
"""
|
||||||
if not path.isdir(_config_dir):
|
path = path or get_broker_conf_path()
|
||||||
|
dirname = os.path.dirname(path)
|
||||||
|
if not os.path.isdir(dirname):
|
||||||
log.debug(f"Creating config dir {_config_dir}")
|
log.debug(f"Creating config dir {_config_dir}")
|
||||||
makedirs(_config_dir)
|
os.makedirs(dirname)
|
||||||
|
|
||||||
log.debug(f"Writing config file {_broker_conf_path}")
|
log.debug(f"Writing config file {path}")
|
||||||
with open(_broker_conf_path, 'w') as cf:
|
with open(path, 'w') as cf:
|
||||||
return config.write(cf)
|
return config.write(cf)
|
||||||
|
|
|
@ -5,16 +5,25 @@ import inspect
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
|
from async_generator import asynccontextmanager
|
||||||
|
import tractor
|
||||||
|
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from .data import DataFeed
|
from .data import DataFeed
|
||||||
|
from . import get_brokermod
|
||||||
|
|
||||||
|
|
||||||
log = get_logger('broker.core')
|
log = get_logger('broker.core')
|
||||||
|
_data_mods = [
|
||||||
|
'piker.brokers.core',
|
||||||
|
'piker.brokers.data',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def api(brokermod: ModuleType, methname: str, **kwargs) -> dict:
|
async def api(brokername: str, methname: str, **kwargs) -> dict:
|
||||||
"""Make (proxy through) a broker API call by name and return its result.
|
"""Make (proxy through) a broker API call by name and return its result.
|
||||||
"""
|
"""
|
||||||
|
brokermod = get_brokermod(brokername)
|
||||||
async with brokermod.get_client() as client:
|
async with brokermod.get_client() as client:
|
||||||
|
|
||||||
meth = getattr(client.api, methname, None)
|
meth = getattr(client.api, methname, None)
|
||||||
|
@ -39,6 +48,24 @@ async def api(brokermod: ModuleType, methname: str, **kwargs) -> dict:
|
||||||
return await meth(**kwargs)
|
return await meth(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def maybe_spawn_brokerd_as_subactor(sleep=0.5, tries=10, loglevel=None):
|
||||||
|
"""If no ``brokerd`` daemon-actor can be found spawn one in a
|
||||||
|
local subactor.
|
||||||
|
"""
|
||||||
|
async with tractor.open_nursery() as nursery:
|
||||||
|
async with tractor.find_actor('brokerd') as portal:
|
||||||
|
if not portal:
|
||||||
|
log.info(
|
||||||
|
"No broker daemon could be found, spawning brokerd..")
|
||||||
|
portal = await nursery.start_actor(
|
||||||
|
'brokerd',
|
||||||
|
rpc_module_paths=_data_mods,
|
||||||
|
loglevel=loglevel,
|
||||||
|
)
|
||||||
|
yield portal
|
||||||
|
|
||||||
|
|
||||||
async def stocks_quote(
|
async def stocks_quote(
|
||||||
brokermod: ModuleType,
|
brokermod: ModuleType,
|
||||||
tickers: List[str]
|
tickers: List[str]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Live data feed machinery
|
Real-time data feed machinery
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
@ -9,7 +9,11 @@ import socket
|
||||||
import json
|
import json
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
import typing
|
import typing
|
||||||
from typing import Coroutine, Callable, Dict, List, Any, Tuple
|
from typing import (
|
||||||
|
Coroutine, Callable, Dict,
|
||||||
|
List, Any, Tuple, AsyncGenerator,
|
||||||
|
Sequence,
|
||||||
|
)
|
||||||
import contextlib
|
import contextlib
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
|
@ -73,15 +77,15 @@ class BrokerFeed:
|
||||||
|
|
||||||
|
|
||||||
@tractor.msg.pub(tasks=['stock', 'option'])
|
@tractor.msg.pub(tasks=['stock', 'option'])
|
||||||
async def stream_quotes(
|
async def stream_requests(
|
||||||
get_topics: typing.Callable,
|
get_topics: typing.Callable,
|
||||||
get_quotes: Coroutine,
|
get_quotes: Coroutine,
|
||||||
feed: BrokerFeed,
|
feed: BrokerFeed,
|
||||||
rate: int = 5, # delay between quote requests
|
rate: int = 3, # delay between quote requests
|
||||||
diff_cached: bool = True, # only deliver "new" quotes to the queue
|
diff_cached: bool = True, # only deliver "new" quotes to the queue
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Stream quotes for a sequence of tickers at the given ``rate``
|
"""Stream requests for quotes for a set of symbols at the given
|
||||||
per second.
|
``rate`` (per second).
|
||||||
|
|
||||||
A stock-broker client ``get_quotes()`` async context manager must be
|
A stock-broker client ``get_quotes()`` async context manager must be
|
||||||
provided which returns an async quote retrieval function.
|
provided which returns an async quote retrieval function.
|
||||||
|
@ -135,11 +139,12 @@ async def stream_quotes(
|
||||||
# quote).
|
# quote).
|
||||||
new_quotes.setdefault(quote['key'], []).append(quote)
|
new_quotes.setdefault(quote['key'], []).append(quote)
|
||||||
else:
|
else:
|
||||||
log.info(f"Delivering quotes:\n{quotes}")
|
# log.debug(f"Delivering quotes:\n{quotes}")
|
||||||
for quote in quotes:
|
for quote in quotes:
|
||||||
new_quotes.setdefault(quote['key'], []).append(quote)
|
new_quotes.setdefault(quote['key'], []).append(quote)
|
||||||
|
|
||||||
yield new_quotes
|
if new_quotes:
|
||||||
|
yield new_quotes
|
||||||
|
|
||||||
# latency monitoring
|
# latency monitoring
|
||||||
req_time = round(postquote_start - prequote_start, 3)
|
req_time = round(postquote_start - prequote_start, 3)
|
||||||
|
@ -302,7 +307,7 @@ async def start_quote_stream(
|
||||||
# push initial smoke quote response for client initialization
|
# push initial smoke quote response for client initialization
|
||||||
await ctx.send_yield(payload)
|
await ctx.send_yield(payload)
|
||||||
|
|
||||||
await stream_quotes(
|
await stream_requests(
|
||||||
|
|
||||||
# pub required kwargs
|
# pub required kwargs
|
||||||
task_name=feed_type,
|
task_name=feed_type,
|
||||||
|
@ -341,65 +346,70 @@ class DataFeed:
|
||||||
self._quote_type = None
|
self._quote_type = None
|
||||||
self._symbols = None
|
self._symbols = None
|
||||||
self.quote_gen = None
|
self.quote_gen = None
|
||||||
self._mutex = trio.StrictFIFOLock()
|
|
||||||
self._symbol_data_cache: Dict[str, Any] = {}
|
self._symbol_data_cache: Dict[str, Any] = {}
|
||||||
|
|
||||||
async def open_stream(self, symbols, feed_type, rate=1, test=None):
|
async def open_stream(
|
||||||
|
self,
|
||||||
|
symbols: Sequence[str],
|
||||||
|
feed_type: str,
|
||||||
|
rate: int = 1,
|
||||||
|
diff_cached: bool = True,
|
||||||
|
test: bool = None,
|
||||||
|
) -> (AsyncGenerator, dict):
|
||||||
if feed_type not in self._allowed:
|
if feed_type not in self._allowed:
|
||||||
raise ValueError(f"Only feed types {self._allowed} are supported")
|
raise ValueError(f"Only feed types {self._allowed} are supported")
|
||||||
|
|
||||||
self._quote_type = feed_type
|
self._quote_type = feed_type
|
||||||
|
try:
|
||||||
|
if self.quote_gen is not None and symbols != self._symbols:
|
||||||
|
log.info(
|
||||||
|
f"Stopping existing subscription for {self._symbols}")
|
||||||
|
await self.quote_gen.aclose()
|
||||||
|
self._symbols = symbols
|
||||||
|
|
||||||
async with self._mutex:
|
if feed_type == 'stock' and not (
|
||||||
try:
|
all(symbol in self._symbol_data_cache
|
||||||
if self.quote_gen is not None and symbols != self._symbols:
|
for symbol in symbols)
|
||||||
log.info(
|
):
|
||||||
f"Stopping existing subscription for {self._symbols}")
|
# subscribe for tickers (this performs a possible filtering
|
||||||
await self.quote_gen.aclose()
|
# where invalid symbols are discarded)
|
||||||
self._symbols = symbols
|
sd = await self.portal.run(
|
||||||
|
"piker.brokers.data", 'symbol_data',
|
||||||
|
broker=self.brokermod.name, tickers=symbols)
|
||||||
|
self._symbol_data_cache.update(sd)
|
||||||
|
|
||||||
if feed_type == 'stock' and not (
|
if test:
|
||||||
all(symbol in self._symbol_data_cache
|
# stream from a local test file
|
||||||
for symbol in symbols)
|
quote_gen = await self.portal.run(
|
||||||
):
|
"piker.brokers.data", 'stream_from_file',
|
||||||
# subscribe for tickers (this performs a possible filtering
|
filename=test
|
||||||
# where invalid symbols are discarded)
|
)
|
||||||
sd = await self.portal.run(
|
else:
|
||||||
"piker.brokers.data", 'symbol_data',
|
log.info(f"Starting new stream for {symbols}")
|
||||||
broker=self.brokermod.name, tickers=symbols)
|
# start live streaming from broker daemon
|
||||||
self._symbol_data_cache.update(sd)
|
quote_gen = await self.portal.run(
|
||||||
|
"piker.brokers.data",
|
||||||
|
'start_quote_stream',
|
||||||
|
broker=self.brokermod.name,
|
||||||
|
symbols=symbols,
|
||||||
|
feed_type=feed_type,
|
||||||
|
diff_cached=diff_cached,
|
||||||
|
rate=rate,
|
||||||
|
)
|
||||||
|
|
||||||
if test:
|
# get first quotes response
|
||||||
# stream from a local test file
|
log.debug(f"Waiting on first quote for {symbols}...")
|
||||||
quote_gen = await self.portal.run(
|
quotes = {}
|
||||||
"piker.brokers.data", 'stream_from_file',
|
quotes = await quote_gen.__anext__()
|
||||||
filename=test
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
log.info(f"Starting new stream for {symbols}")
|
|
||||||
# start live streaming from broker daemon
|
|
||||||
quote_gen = await self.portal.run(
|
|
||||||
"piker.brokers.data",
|
|
||||||
'start_quote_stream',
|
|
||||||
broker=self.brokermod.name,
|
|
||||||
symbols=symbols,
|
|
||||||
feed_type=feed_type,
|
|
||||||
rate=rate,
|
|
||||||
)
|
|
||||||
|
|
||||||
# get first quotes response
|
self.quote_gen = quote_gen
|
||||||
log.debug(f"Waiting on first quote for {symbols}...")
|
self.first_quotes = quotes
|
||||||
quotes = {}
|
return quote_gen, quotes
|
||||||
quotes = await quote_gen.__anext__()
|
except Exception:
|
||||||
|
if self.quote_gen:
|
||||||
self.quote_gen = quote_gen
|
await self.quote_gen.aclose()
|
||||||
self.first_quotes = quotes
|
self.quote_gen = None
|
||||||
return quote_gen, quotes
|
raise
|
||||||
except Exception:
|
|
||||||
if self.quote_gen:
|
|
||||||
await self.quote_gen.aclose()
|
|
||||||
self.quote_gen = None
|
|
||||||
raise
|
|
||||||
|
|
||||||
def format_quotes(self, quotes, symbol_data={}):
|
def format_quotes(self, quotes, symbol_data={}):
|
||||||
self._symbol_data_cache.update(symbol_data)
|
self._symbol_data_cache.update(symbol_data)
|
||||||
|
|
|
@ -12,6 +12,7 @@ from typing import List, Tuple, Dict, Any, Iterator, NamedTuple
|
||||||
import trio
|
import trio
|
||||||
from async_generator import asynccontextmanager
|
from async_generator import asynccontextmanager
|
||||||
import wrapt
|
import wrapt
|
||||||
|
import asks
|
||||||
|
|
||||||
from ..calc import humanize, percent_change
|
from ..calc import humanize, percent_change
|
||||||
from . import config
|
from . import config
|
||||||
|
@ -19,13 +20,10 @@ from ._util import resproc, BrokerError
|
||||||
from ..log import get_logger, colorize_json
|
from ..log import get_logger, colorize_json
|
||||||
from .._async_utils import async_lifo_cache
|
from .._async_utils import async_lifo_cache
|
||||||
|
|
||||||
# TODO: move to urllib3/requests once supported
|
|
||||||
import asks
|
|
||||||
asks.init('trio')
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
_refresh_token_ep = 'https://login.questrade.com/oauth2/'
|
_use_practice_account = False
|
||||||
|
_refresh_token_ep = 'https://{}login.questrade.com/oauth2/'
|
||||||
_version = 'v1'
|
_version = 'v1'
|
||||||
|
|
||||||
# stock queries/sec
|
# stock queries/sec
|
||||||
|
@ -102,7 +100,10 @@ class _API:
|
||||||
resp = await self._sess.get(path=f'/{path}', params=params)
|
resp = await self._sess.get(path=f'/{path}', params=params)
|
||||||
return resproc(resp, log)
|
return resproc(resp, log)
|
||||||
|
|
||||||
async def _new_auth_token(self, refresh_token: str) -> dict:
|
async def _new_auth_token(
|
||||||
|
self,
|
||||||
|
refresh_token: str,
|
||||||
|
) -> dict:
|
||||||
"""Request a new api authorization ``refresh_token``.
|
"""Request a new api authorization ``refresh_token``.
|
||||||
|
|
||||||
Gain api access using either a user provided or existing token.
|
Gain api access using either a user provided or existing token.
|
||||||
|
@ -112,18 +113,42 @@ class _API:
|
||||||
http://www.questrade.com/api/documentation/security
|
http://www.questrade.com/api/documentation/security
|
||||||
"""
|
"""
|
||||||
resp = await self._sess.get(
|
resp = await self._sess.get(
|
||||||
_refresh_token_ep + 'token',
|
self.client._auth_ep + 'token',
|
||||||
params={'grant_type': 'refresh_token',
|
params={'grant_type': 'refresh_token',
|
||||||
'refresh_token': refresh_token}
|
'refresh_token': refresh_token}
|
||||||
)
|
)
|
||||||
return resproc(resp, log)
|
return resproc(resp, log)
|
||||||
|
|
||||||
|
async def _revoke_auth_token(
|
||||||
|
self,
|
||||||
|
practise: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""Revoke api access for the current token.
|
||||||
|
"""
|
||||||
|
token = self.access_data['refresh_token']
|
||||||
|
log.debug(f"Revoking token {token}")
|
||||||
|
resp = await asks.post(
|
||||||
|
self.client._auth_ep + 'revoke',
|
||||||
|
headers={'token': token}
|
||||||
|
)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
# accounts end points
|
||||||
|
|
||||||
async def accounts(self) -> dict:
|
async def accounts(self) -> dict:
|
||||||
return await self._get('accounts')
|
return await self._get('accounts')
|
||||||
|
|
||||||
async def time(self) -> dict:
|
async def time(self) -> dict:
|
||||||
return await self._get('time')
|
return await self._get('time')
|
||||||
|
|
||||||
|
async def balances(self, id: str) -> dict:
|
||||||
|
return await self._get(f'accounts/{id}/balances')
|
||||||
|
|
||||||
|
async def postions(self, id: str) -> dict:
|
||||||
|
return await self._get(f'accounts/{id}/positions')
|
||||||
|
|
||||||
|
# market end points
|
||||||
|
|
||||||
async def markets(self) -> dict:
|
async def markets(self) -> dict:
|
||||||
return await self._get('markets')
|
return await self._get('markets')
|
||||||
|
|
||||||
|
@ -146,12 +171,6 @@ class _API:
|
||||||
async def candles(self, id: str, start: str, end, interval) -> dict:
|
async def candles(self, id: str, start: str, end, interval) -> dict:
|
||||||
return await self._get(f'markets/candles/{id}', params={})
|
return await self._get(f'markets/candles/{id}', params={})
|
||||||
|
|
||||||
async def balances(self, id: str) -> dict:
|
|
||||||
return await self._get(f'accounts/{id}/balances')
|
|
||||||
|
|
||||||
async def postions(self, id: str) -> dict:
|
|
||||||
return await self._get(f'accounts/{id}/positions')
|
|
||||||
|
|
||||||
async def option_contracts(self, symbol_id: str) -> dict:
|
async def option_contracts(self, symbol_id: str) -> dict:
|
||||||
"Retrieve all option contract API ids with expiry -> strike prices."
|
"Retrieve all option contract API ids with expiry -> strike prices."
|
||||||
contracts = await self._get(f'symbols/{symbol_id}/options')
|
contracts = await self._get(f'symbols/{symbol_id}/options')
|
||||||
|
@ -189,12 +208,20 @@ class Client:
|
||||||
|
|
||||||
Provides a high-level api which wraps the underlying endpoint calls.
|
Provides a high-level api which wraps the underlying endpoint calls.
|
||||||
"""
|
"""
|
||||||
def __init__(self, config: configparser.ConfigParser):
|
def __init__(
|
||||||
|
self,
|
||||||
|
config: configparser.ConfigParser,
|
||||||
|
):
|
||||||
self._sess = asks.Session()
|
self._sess = asks.Session()
|
||||||
self.api = _API(self)
|
self.api = _API(self)
|
||||||
self._conf = config
|
self._conf = config
|
||||||
|
self._is_practice = _use_practice_account or (
|
||||||
|
config['questrade'].get('is_practice', False)
|
||||||
|
)
|
||||||
|
self._auth_ep = _refresh_token_ep.format(
|
||||||
|
'practice' if self._is_practice else '')
|
||||||
self.access_data = {}
|
self.access_data = {}
|
||||||
self._reload_config(config)
|
self._reload_config(config=config)
|
||||||
self._symbol_cache: Dict[str, int] = {}
|
self._symbol_cache: Dict[str, int] = {}
|
||||||
self._optids2contractinfo = {}
|
self._optids2contractinfo = {}
|
||||||
self._contract2ids = {}
|
self._contract2ids = {}
|
||||||
|
@ -206,27 +233,23 @@ class Client:
|
||||||
self._mutex = trio.StrictFIFOLock()
|
self._mutex = trio.StrictFIFOLock()
|
||||||
|
|
||||||
def _reload_config(self, config=None, **kwargs):
|
def _reload_config(self, config=None, **kwargs):
|
||||||
self._conf = config or get_config(**kwargs)
|
if config:
|
||||||
|
self._conf = config
|
||||||
|
else:
|
||||||
|
self._conf, _ = get_config(**kwargs)
|
||||||
self.access_data = dict(self._conf['questrade'])
|
self.access_data = dict(self._conf['questrade'])
|
||||||
|
|
||||||
async def _revoke_auth_token(self) -> None:
|
|
||||||
"""Revoke api access for the current token.
|
|
||||||
"""
|
|
||||||
token = self.access_data['refresh_token']
|
|
||||||
log.debug(f"Revoking token {token}")
|
|
||||||
resp = await asks.post(
|
|
||||||
_refresh_token_ep + 'revoke',
|
|
||||||
headers={'token': token}
|
|
||||||
)
|
|
||||||
return resp
|
|
||||||
|
|
||||||
def write_config(self):
|
def write_config(self):
|
||||||
"""Save access creds to config file.
|
"""Save access creds to config file.
|
||||||
"""
|
"""
|
||||||
self._conf['questrade'] = self.access_data
|
self._conf['questrade'] = self.access_data
|
||||||
config.write(self._conf)
|
config.write(self._conf)
|
||||||
|
|
||||||
async def ensure_access(self, force_refresh: bool = False) -> dict:
|
async def ensure_access(
|
||||||
|
self,
|
||||||
|
force_refresh: bool = False,
|
||||||
|
ask_user: bool = True,
|
||||||
|
) -> dict:
|
||||||
"""Acquire a new token set (``access_token`` and ``refresh_token``).
|
"""Acquire a new token set (``access_token`` and ``refresh_token``).
|
||||||
|
|
||||||
Checks if the locally cached (file system) ``access_token`` has expired
|
Checks if the locally cached (file system) ``access_token`` has expired
|
||||||
|
@ -256,7 +279,7 @@ class Client:
|
||||||
if not access_token or (
|
if not access_token or (
|
||||||
expires < time.time()
|
expires < time.time()
|
||||||
) or force_refresh:
|
) or force_refresh:
|
||||||
log.info("REFRESHING TOKENS!")
|
log.info("Refreshing API tokens")
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Refreshing access token {access_token} which expired"
|
f"Refreshing access token {access_token} which expired"
|
||||||
f" at {expires_stamp}")
|
f" at {expires_stamp}")
|
||||||
|
@ -278,14 +301,16 @@ class Client:
|
||||||
|
|
||||||
elif msg == 'Bad Request':
|
elif msg == 'Bad Request':
|
||||||
# likely config ``refresh_token`` is expired but
|
# likely config ``refresh_token`` is expired but
|
||||||
# may be updated in the config file via another
|
# may be updated in the config file via
|
||||||
# piker process
|
# another actor
|
||||||
self._reload_config()
|
self._reload_config()
|
||||||
try:
|
try:
|
||||||
data = await self.api._new_auth_token(
|
data = await self.api._new_auth_token(
|
||||||
self.access_data['refresh_token'])
|
self.access_data['refresh_token'])
|
||||||
except BrokerError as qterr:
|
except BrokerError as qterr:
|
||||||
if get_err_msg(qterr) == 'Bad Request':
|
if get_err_msg(qterr) == 'Bad Request' and (
|
||||||
|
ask_user
|
||||||
|
):
|
||||||
# actually expired; get new from user
|
# actually expired; get new from user
|
||||||
self._reload_config(force_from_user=True)
|
self._reload_config(force_from_user=True)
|
||||||
data = await self.api._new_auth_token(
|
data = await self.api._new_auth_token(
|
||||||
|
@ -304,7 +329,7 @@ class Client:
|
||||||
# write to config to disk
|
# write to config to disk
|
||||||
self.write_config()
|
self.write_config()
|
||||||
else:
|
else:
|
||||||
log.info(
|
log.debug(
|
||||||
f"\nCurrent access token {access_token} expires at"
|
f"\nCurrent access token {access_token} expires at"
|
||||||
f" {expires_stamp}\n")
|
f" {expires_stamp}\n")
|
||||||
|
|
||||||
|
@ -501,8 +526,8 @@ def _token_from_user(conf: 'configparser.ConfigParser') -> None:
|
||||||
|
|
||||||
|
|
||||||
def get_config(
|
def get_config(
|
||||||
force_from_user: bool = False,
|
|
||||||
config_path: str = None,
|
config_path: str = None,
|
||||||
|
force_from_user: bool = False,
|
||||||
) -> "configparser.ConfigParser":
|
) -> "configparser.ConfigParser":
|
||||||
"""Load the broker config from disk.
|
"""Load the broker config from disk.
|
||||||
|
|
||||||
|
@ -514,32 +539,39 @@ def get_config(
|
||||||
"""
|
"""
|
||||||
log.debug("Reloading access config data")
|
log.debug("Reloading access config data")
|
||||||
conf, path = config.load(config_path)
|
conf, path = config.load(config_path)
|
||||||
if not conf.has_section('questrade'):
|
if force_from_user:
|
||||||
log.warn(
|
|
||||||
f"No valid refresh token could be found in {path}")
|
|
||||||
elif force_from_user:
|
|
||||||
log.warn(f"Forcing manual token auth from user")
|
log.warn(f"Forcing manual token auth from user")
|
||||||
_token_from_user(conf)
|
_token_from_user(conf)
|
||||||
|
|
||||||
return conf
|
return conf, path
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def get_client() -> Client:
|
async def get_client(
|
||||||
|
config_path: str = None,
|
||||||
|
ask_user: bool = True
|
||||||
|
) -> Client:
|
||||||
"""Spawn a broker client for making requests to the API service.
|
"""Spawn a broker client for making requests to the API service.
|
||||||
"""
|
"""
|
||||||
conf = get_config()
|
conf, path = get_config(config_path)
|
||||||
|
if not conf.has_section('questrade'):
|
||||||
|
raise ValueError(
|
||||||
|
f"No `questrade` section could be found in {path}")
|
||||||
log.debug(f"Loaded config:\n{colorize_json(dict(conf['questrade']))}")
|
log.debug(f"Loaded config:\n{colorize_json(dict(conf['questrade']))}")
|
||||||
client = Client(conf)
|
client = Client(conf)
|
||||||
await client.ensure_access()
|
await client.ensure_access(ask_user=ask_user)
|
||||||
try:
|
try:
|
||||||
log.debug("Check time to ensure access token is valid")
|
log.debug("Check time to ensure access token is valid")
|
||||||
|
# XXX: the `time()` end point requires acc_read Oauth access.
|
||||||
|
# In order to use a client you need at least one key with this
|
||||||
|
# access enabled in order to do symbol searches and id lookups.
|
||||||
await client.api.time()
|
await client.api.time()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
raise
|
||||||
# access token is likely no good
|
# access token is likely no good
|
||||||
log.warn(f"Access tokens {client.access_data} seem"
|
log.warn(f"Access tokens {client.access_data} seem"
|
||||||
f" expired, forcing refresh")
|
f" expired, forcing refresh")
|
||||||
await client.ensure_access(force_refresh=True)
|
await client.ensure_access(force_refresh=True, ask_user=ask_user)
|
||||||
await client.api.time()
|
await client.api.time()
|
||||||
try:
|
try:
|
||||||
yield client
|
yield client
|
||||||
|
|
|
@ -9,15 +9,14 @@ from functools import partial
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from async_generator import asynccontextmanager
|
from async_generator import asynccontextmanager
|
||||||
# TODO: move to urllib3/requests once supported
|
|
||||||
import asks
|
import asks
|
||||||
|
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from ._util import resproc, BrokerError
|
from ._util import resproc, BrokerError
|
||||||
from ..calc import percent_change
|
from ..calc import percent_change
|
||||||
|
|
||||||
asks.init('trio')
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
_service_ep = 'https://api.robinhood.com'
|
_service_ep = 'https://api.robinhood.com'
|
||||||
|
|
||||||
|
|
||||||
|
|
146
piker/cli.py
146
piker/cli.py
|
@ -11,21 +11,27 @@ import click
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import trio
|
import trio
|
||||||
import tractor
|
import tractor
|
||||||
from async_generator import asynccontextmanager
|
|
||||||
|
|
||||||
from . import watchlists as wl
|
from . import watchlists as wl
|
||||||
from .brokers import core, get_brokermod, data
|
|
||||||
from .log import get_console_log, colorize_json, get_logger
|
from .log import get_console_log, colorize_json, get_logger
|
||||||
|
from .brokers import core, get_brokermod, data, config
|
||||||
|
from .brokers.core import maybe_spawn_brokerd_as_subactor, _data_mods
|
||||||
|
|
||||||
log = get_logger('cli')
|
log = get_logger('cli')
|
||||||
DEFAULT_BROKER = 'robinhood'
|
DEFAULT_BROKER = 'questrade'
|
||||||
|
|
||||||
_config_dir = click.get_app_dir('piker')
|
_config_dir = click.get_app_dir('piker')
|
||||||
_watchlists_data_path = os.path.join(_config_dir, 'watchlists.json')
|
_watchlists_data_path = os.path.join(_config_dir, 'watchlists.json')
|
||||||
_data_mods = [
|
_context_defaults = dict(
|
||||||
'piker.brokers.core',
|
default_map={
|
||||||
'piker.brokers.data',
|
'monitor': {
|
||||||
]
|
'rate': 3,
|
||||||
|
},
|
||||||
|
'optschain': {
|
||||||
|
'rate': 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
|
@ -43,24 +49,38 @@ def pikerd(loglevel, host, tl):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group(context_settings=_context_defaults)
|
||||||
def cli():
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
@click.option('--broker', '-b', default=DEFAULT_BROKER,
|
@click.option('--broker', '-b', default=DEFAULT_BROKER,
|
||||||
help='Broker backend to use')
|
help='Broker backend to use')
|
||||||
@click.option('--loglevel', '-l', default='warning', help='Logging level')
|
@click.option('--loglevel', '-l', default='warning', help='Logging level')
|
||||||
|
@click.option('--configdir', '-c', help='Configuration directory')
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx, broker, loglevel, configdir):
|
||||||
|
if configdir is not None:
|
||||||
|
assert os.path.isdir(configdir), f"`{configdir}` is not a valid path"
|
||||||
|
config._override_config_dir(configdir)
|
||||||
|
|
||||||
|
# ensure that ctx.obj exists even though we aren't using it (yet)
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
ctx.obj.update({
|
||||||
|
'broker': broker,
|
||||||
|
'brokermod': get_brokermod(broker),
|
||||||
|
'loglevel': loglevel,
|
||||||
|
'log': get_console_log(loglevel),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
@click.option('--keys', '-k', multiple=True,
|
@click.option('--keys', '-k', multiple=True,
|
||||||
help='Return results only for these keys')
|
help='Return results only for these keys')
|
||||||
@click.argument('meth', nargs=1)
|
@click.argument('meth', nargs=1)
|
||||||
@click.argument('kwargs', nargs=-1)
|
@click.argument('kwargs', nargs=-1)
|
||||||
def api(meth, kwargs, loglevel, broker, keys):
|
@click.pass_obj
|
||||||
|
def api(config, meth, kwargs, keys):
|
||||||
"""client for testing broker API methods with pretty printing of output.
|
"""client for testing broker API methods with pretty printing of output.
|
||||||
"""
|
"""
|
||||||
get_console_log(loglevel)
|
# global opts
|
||||||
brokermod = get_brokermod(broker)
|
broker = config['broker']
|
||||||
|
|
||||||
_kwargs = {}
|
_kwargs = {}
|
||||||
for kwarg in kwargs:
|
for kwarg in kwargs:
|
||||||
|
@ -71,7 +91,7 @@ def api(meth, kwargs, loglevel, broker, keys):
|
||||||
_kwargs[key] = value
|
_kwargs[key] = value
|
||||||
|
|
||||||
data = trio.run(
|
data = trio.run(
|
||||||
partial(core.api, brokermod, meth, **_kwargs)
|
partial(core.api, broker, meth, **_kwargs)
|
||||||
)
|
)
|
||||||
|
|
||||||
if keys:
|
if keys:
|
||||||
|
@ -89,18 +109,17 @@ def api(meth, kwargs, loglevel, broker, keys):
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--broker', '-b', default=DEFAULT_BROKER,
|
|
||||||
help='Broker backend to use')
|
|
||||||
@click.option('--loglevel', '-l', default='warning', help='Logging level')
|
|
||||||
@click.option('--df-output', '-df', flag_value=True,
|
@click.option('--df-output', '-df', flag_value=True,
|
||||||
help='Output in `pandas.DataFrame` format')
|
help='Output in `pandas.DataFrame` format')
|
||||||
@click.argument('tickers', nargs=-1, required=True)
|
@click.argument('tickers', nargs=-1, required=True)
|
||||||
def quote(loglevel, broker, tickers, df_output):
|
@click.pass_obj
|
||||||
|
def quote(config, tickers, df_output):
|
||||||
"""Retreive symbol quotes on the console in either json or dataframe
|
"""Retreive symbol quotes on the console in either json or dataframe
|
||||||
format.
|
format.
|
||||||
"""
|
"""
|
||||||
brokermod = get_brokermod(broker)
|
# global opts
|
||||||
get_console_log(loglevel)
|
brokermod = config['brokermod']
|
||||||
|
|
||||||
quotes = trio.run(partial(core.stocks_quote, brokermod, tickers))
|
quotes = trio.run(partial(core.stocks_quote, brokermod, tickers))
|
||||||
if not quotes:
|
if not quotes:
|
||||||
log.error(f"No quotes could be found for {tickers}?")
|
log.error(f"No quotes could be found for {tickers}?")
|
||||||
|
@ -125,40 +144,22 @@ def quote(loglevel, broker, tickers, df_output):
|
||||||
click.echo(colorize_json(quotes))
|
click.echo(colorize_json(quotes))
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def maybe_spawn_brokerd_as_subactor(sleep=0.5, tries=10, loglevel=None):
|
|
||||||
"""If no ``brokerd`` daemon-actor can be found spawn one in a
|
|
||||||
local subactor.
|
|
||||||
"""
|
|
||||||
async with tractor.open_nursery() as nursery:
|
|
||||||
async with tractor.find_actor('brokerd') as portal:
|
|
||||||
if not portal:
|
|
||||||
log.info(
|
|
||||||
"No broker daemon could be found, spawning brokerd..")
|
|
||||||
portal = await nursery.start_actor(
|
|
||||||
'brokerd',
|
|
||||||
rpc_module_paths=_data_mods,
|
|
||||||
loglevel=loglevel,
|
|
||||||
)
|
|
||||||
yield portal
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--broker', '-b', default=DEFAULT_BROKER,
|
|
||||||
help='Broker backend to use')
|
|
||||||
@click.option('--loglevel', '-l', default='warning', help='Logging level')
|
|
||||||
@click.option('--tl', is_flag=True, help='Enable tractor logging')
|
@click.option('--tl', is_flag=True, help='Enable tractor logging')
|
||||||
@click.option('--rate', '-r', default=3, help='Quote rate limit')
|
@click.option('--rate', '-r', default=3, help='Quote rate limit')
|
||||||
@click.option('--test', '-t', help='Test quote stream file')
|
@click.option('--test', '-t', help='Test quote stream file')
|
||||||
@click.option('--dhost', '-dh', default='127.0.0.1',
|
@click.option('--dhost', '-dh', default='127.0.0.1',
|
||||||
help='Daemon host address to connect to')
|
help='Daemon host address to connect to')
|
||||||
@click.argument('name', nargs=1, required=True)
|
@click.argument('name', nargs=1, required=True)
|
||||||
def monitor(loglevel, broker, rate, name, dhost, test, tl):
|
@click.pass_obj
|
||||||
|
def monitor(config, rate, name, dhost, test, tl):
|
||||||
"""Spawn a real-time watchlist.
|
"""Spawn a real-time watchlist.
|
||||||
"""
|
"""
|
||||||
from .ui.monitor import _async_main
|
# global opts
|
||||||
log = get_console_log(loglevel) # activate console logging
|
brokermod = config['brokermod']
|
||||||
brokermod = get_brokermod(broker)
|
loglevel = config['loglevel']
|
||||||
|
log = config['log']
|
||||||
|
|
||||||
watchlist_from_file = wl.ensure_watchlists(_watchlists_data_path)
|
watchlist_from_file = wl.ensure_watchlists(_watchlists_data_path)
|
||||||
watchlists = wl.merge_watchlist(watchlist_from_file, wl._builtins)
|
watchlists = wl.merge_watchlist(watchlist_from_file, wl._builtins)
|
||||||
tickers = watchlists[name]
|
tickers = watchlists[name]
|
||||||
|
@ -166,6 +167,8 @@ def monitor(loglevel, broker, rate, name, dhost, test, tl):
|
||||||
log.error(f"No symbols found for watchlist `{name}`?")
|
log.error(f"No symbols found for watchlist `{name}`?")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
from .ui.monitor import _async_main
|
||||||
|
|
||||||
async def main(tries):
|
async def main(tries):
|
||||||
async with maybe_spawn_brokerd_as_subactor(
|
async with maybe_spawn_brokerd_as_subactor(
|
||||||
tries=tries, loglevel=loglevel
|
tries=tries, loglevel=loglevel
|
||||||
|
@ -185,20 +188,21 @@ def monitor(loglevel, broker, rate, name, dhost, test, tl):
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--broker', '-b', default=DEFAULT_BROKER,
|
|
||||||
help='Broker backend to use')
|
|
||||||
@click.option('--loglevel', '-l', default='warning', help='Logging level')
|
|
||||||
@click.option('--rate', '-r', default=5, help='Logging level')
|
@click.option('--rate', '-r', default=5, help='Logging level')
|
||||||
@click.option('--filename', '-f', default='quotestream.jsonstream',
|
@click.option('--filename', '-f', default='quotestream.jsonstream',
|
||||||
help='Logging level')
|
help='Logging level')
|
||||||
@click.option('--dhost', '-dh', default='127.0.0.1',
|
@click.option('--dhost', '-dh', default='127.0.0.1',
|
||||||
help='Daemon host address to connect to')
|
help='Daemon host address to connect to')
|
||||||
@click.argument('name', nargs=1, required=True)
|
@click.argument('name', nargs=1, required=True)
|
||||||
def record(loglevel, broker, rate, name, dhost, filename):
|
@click.pass_obj
|
||||||
|
def record(config, rate, name, dhost, filename):
|
||||||
"""Record client side quotes to file
|
"""Record client side quotes to file
|
||||||
"""
|
"""
|
||||||
log = get_console_log(loglevel) # activate console logging
|
# global opts
|
||||||
brokermod = get_brokermod(broker)
|
brokermod = config['brokermod']
|
||||||
|
loglevel = config['loglevel']
|
||||||
|
log = config['log']
|
||||||
|
|
||||||
watchlist_from_file = wl.ensure_watchlists(_watchlists_data_path)
|
watchlist_from_file = wl.ensure_watchlists(_watchlists_data_path)
|
||||||
watchlists = wl.merge_watchlist(watchlist_from_file, wl._builtins)
|
watchlists = wl.merge_watchlist(watchlist_from_file, wl._builtins)
|
||||||
tickers = watchlists[name]
|
tickers = watchlists[name]
|
||||||
|
@ -222,15 +226,17 @@ def record(loglevel, broker, rate, name, dhost, filename):
|
||||||
|
|
||||||
|
|
||||||
@cli.group()
|
@cli.group()
|
||||||
@click.option('--loglevel', '-l', default='warning', help='Logging level')
|
|
||||||
@click.option('--config_dir', '-d', default=_watchlists_data_path,
|
@click.option('--config_dir', '-d', default=_watchlists_data_path,
|
||||||
help='Path to piker configuration directory')
|
help='Path to piker configuration directory')
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def watchlists(ctx, loglevel, config_dir):
|
def watchlists(ctx, config_dir):
|
||||||
"""Watchlists commands and operations
|
"""Watchlists commands and operations
|
||||||
"""
|
"""
|
||||||
|
loglevel = ctx.parent.params['loglevel']
|
||||||
get_console_log(loglevel) # activate console logging
|
get_console_log(loglevel) # activate console logging
|
||||||
|
|
||||||
wl.make_config_dir(_config_dir)
|
wl.make_config_dir(_config_dir)
|
||||||
|
ctx.ensure_object(dict)
|
||||||
ctx.obj = {'path': config_dir,
|
ctx.obj = {'path': config_dir,
|
||||||
'watchlist': wl.ensure_watchlists(config_dir)}
|
'watchlist': wl.ensure_watchlists(config_dir)}
|
||||||
|
|
||||||
|
@ -317,9 +323,12 @@ def dump(ctx, name):
|
||||||
@click.option('--loglevel', '-l', default='warning', help='Logging level')
|
@click.option('--loglevel', '-l', default='warning', help='Logging level')
|
||||||
@click.option('--ids', flag_value=True, help='Include numeric ids in output')
|
@click.option('--ids', flag_value=True, help='Include numeric ids in output')
|
||||||
@click.argument('symbol', required=True)
|
@click.argument('symbol', required=True)
|
||||||
def contracts(loglevel, broker, symbol, ids):
|
@click.pass_context
|
||||||
|
def contracts(ctx, loglevel, broker, symbol, ids):
|
||||||
|
|
||||||
brokermod = get_brokermod(broker)
|
brokermod = get_brokermod(broker)
|
||||||
get_console_log(loglevel)
|
get_console_log(loglevel)
|
||||||
|
|
||||||
contracts = trio.run(partial(core.contracts, brokermod, symbol))
|
contracts = trio.run(partial(core.contracts, brokermod, symbol))
|
||||||
if not ids:
|
if not ids:
|
||||||
# just print out expiry dates which can be used with
|
# just print out expiry dates which can be used with
|
||||||
|
@ -333,19 +342,18 @@ def contracts(loglevel, broker, symbol, ids):
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--broker', '-b', default=DEFAULT_BROKER,
|
|
||||||
help='Broker backend to use')
|
|
||||||
@click.option('--loglevel', '-l', default='warning', help='Logging level')
|
|
||||||
@click.option('--df-output', '-df', flag_value=True,
|
@click.option('--df-output', '-df', flag_value=True,
|
||||||
help='Output in `pandas.DataFrame` format')
|
help='Output in `pandas.DataFrame` format')
|
||||||
@click.option('--date', '-d', help='Contracts expiry date')
|
@click.option('--date', '-d', help='Contracts expiry date')
|
||||||
@click.argument('symbol', required=True)
|
@click.argument('symbol', required=True)
|
||||||
def optsquote(loglevel, broker, symbol, df_output, date):
|
@click.pass_obj
|
||||||
|
def optsquote(config, symbol, df_output, date):
|
||||||
"""Retreive symbol quotes on the console in either
|
"""Retreive symbol quotes on the console in either
|
||||||
json or dataframe format.
|
json or dataframe format.
|
||||||
"""
|
"""
|
||||||
brokermod = get_brokermod(broker)
|
# global opts
|
||||||
get_console_log(loglevel)
|
brokermod = config['brokermod']
|
||||||
|
|
||||||
quotes = trio.run(
|
quotes = trio.run(
|
||||||
partial(
|
partial(
|
||||||
core.option_chain, brokermod, symbol, date
|
core.option_chain, brokermod, symbol, date
|
||||||
|
@ -366,20 +374,20 @@ def optsquote(loglevel, broker, symbol, df_output, date):
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--broker', '-b', default=DEFAULT_BROKER,
|
|
||||||
help='Broker backend to use')
|
|
||||||
@click.option('--loglevel', '-l', default='warning', help='Logging level')
|
|
||||||
@click.option('--tl', is_flag=True, help='Enable tractor logging')
|
@click.option('--tl', is_flag=True, help='Enable tractor logging')
|
||||||
@click.option('--date', '-d', help='Contracts expiry date')
|
@click.option('--date', '-d', help='Contracts expiry date')
|
||||||
@click.option('--test', '-t', help='Test quote stream file')
|
@click.option('--test', '-t', help='Test quote stream file')
|
||||||
@click.option('--rate', '-r', default=1, help='Logging level')
|
@click.option('--rate', '-r', default=1, help='Logging level')
|
||||||
@click.argument('symbol', required=True)
|
@click.argument('symbol', required=True)
|
||||||
def optschain(loglevel, broker, symbol, date, tl, rate, test):
|
@click.pass_obj
|
||||||
|
def optschain(config, symbol, date, tl, rate, test):
|
||||||
"""Start the real-time option chain UI.
|
"""Start the real-time option chain UI.
|
||||||
"""
|
"""
|
||||||
|
# global opts
|
||||||
|
loglevel = config['loglevel']
|
||||||
|
brokermod = config['brokermod']
|
||||||
|
|
||||||
from .ui.option_chain import _async_main
|
from .ui.option_chain import _async_main
|
||||||
log = get_console_log(loglevel) # activate console logging
|
|
||||||
brokermod = get_brokermod(broker)
|
|
||||||
|
|
||||||
async def main(tries):
|
async def main(tries):
|
||||||
async with maybe_spawn_brokerd_as_subactor(
|
async with maybe_spawn_brokerd_as_subactor(
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
"""
|
"""
|
||||||
Stuff for you eyes.
|
Stuff for your eyes.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# XXX clear all flags at import to avoid upsetting
|
||||||
|
# ol' kivy see: https://github.com/kivy/kivy/issues/4225
|
||||||
|
# though this is likely a ``click`` problem
|
||||||
|
sys.argv[1:] = []
|
||||||
|
|
||||||
# use the trio async loop
|
# use the trio async loop
|
||||||
os.environ['KIVY_EVENTLOOP'] = 'trio'
|
os.environ['KIVY_EVENTLOOP'] = 'trio'
|
||||||
|
|
|
@ -148,13 +148,14 @@ async def stream_symbol_selection():
|
||||||
"""
|
"""
|
||||||
widgets = tractor.current_actor().statespace['widgets']
|
widgets = tractor.current_actor().statespace['widgets']
|
||||||
table = widgets['table']
|
table = widgets['table']
|
||||||
q = trio.Queue(1)
|
send_chan, recv_chan = trio.open_memory_channel(0)
|
||||||
table._click_queues.append(q)
|
table._click_queues.append(send_chan)
|
||||||
try:
|
try:
|
||||||
async for symbol in q:
|
async with recv_chan:
|
||||||
yield symbol
|
async for symbol in recv_chan:
|
||||||
|
yield symbol
|
||||||
finally:
|
finally:
|
||||||
table._click_queues.remove(q)
|
table._click_queues.remove(send_chan)
|
||||||
|
|
||||||
|
|
||||||
async def _async_main(
|
async def _async_main(
|
||||||
|
@ -251,8 +252,7 @@ async def _async_main(
|
||||||
quotes
|
quotes
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
# Trio-kivy entry point.
|
await async_runTouchApp(widgets['root'])
|
||||||
await async_runTouchApp(widgets['root']) # run kivy
|
|
||||||
finally:
|
finally:
|
||||||
# cancel remote data feed task
|
# cancel remote data feed task
|
||||||
await quote_gen.aclose()
|
await quote_gen.aclose()
|
||||||
|
|
|
@ -16,7 +16,6 @@ from kivy.core.window import Window
|
||||||
from kivy.uix.label import Label
|
from kivy.uix.label import Label
|
||||||
|
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from ..brokers.core import contracts
|
|
||||||
from ..brokers.data import DataFeed
|
from ..brokers.data import DataFeed
|
||||||
from .pager import PagerView
|
from .pager import PagerView
|
||||||
|
|
||||||
|
@ -155,6 +154,8 @@ async def find_local_monitor():
|
||||||
if not portal:
|
if not portal:
|
||||||
log.warn(
|
log.warn(
|
||||||
"No monitor app could be found, no symbol link established..")
|
"No monitor app could be found, no symbol link established..")
|
||||||
|
else:
|
||||||
|
log.info(f"Found {portal.channel.uid}")
|
||||||
yield portal
|
yield portal
|
||||||
|
|
||||||
|
|
||||||
|
@ -172,6 +173,7 @@ class OptionChain(object):
|
||||||
):
|
):
|
||||||
self.symbol = None
|
self.symbol = None
|
||||||
self.expiry = None
|
self.expiry = None
|
||||||
|
self.rate = rate
|
||||||
self.widgets = widgets
|
self.widgets = widgets
|
||||||
self.bidasks = bidasks
|
self.bidasks = bidasks
|
||||||
self._strikes2rows = {}
|
self._strikes2rows = {}
|
||||||
|
@ -212,17 +214,14 @@ class OptionChain(object):
|
||||||
"""Open an internal update task scope required to allow
|
"""Open an internal update task scope required to allow
|
||||||
for dynamic real-time operation.
|
for dynamic real-time operation.
|
||||||
"""
|
"""
|
||||||
self._parent_nursery = nursery
|
n = self._nursery = nursery
|
||||||
async with trio.open_nursery() as n:
|
# fill out and start updating strike table
|
||||||
self._nursery = n
|
n.start_soon(
|
||||||
# fill out and start updating strike table
|
partial(self._start_displaying, symbol, expiry=expiry)
|
||||||
n.start_soon(
|
)
|
||||||
partial(self._start_displaying, symbol, expiry=expiry)
|
# listen for undlerlying symbol changes from a local monitor app
|
||||||
)
|
n.start_soon(self._rx_symbols)
|
||||||
# listen for undlerlying symbol changes from a local monitor app
|
yield self
|
||||||
n.start_soon(self._rx_symbols)
|
|
||||||
yield self
|
|
||||||
n.cancel_scope.cancel()
|
|
||||||
|
|
||||||
self._nursery = None
|
self._nursery = None
|
||||||
# make sure we always tear down our existing data feed
|
# make sure we always tear down our existing data feed
|
||||||
|
@ -346,9 +345,6 @@ class OptionChain(object):
|
||||||
self._update_cs.cancel()
|
self._update_cs.cancel()
|
||||||
await trio.sleep(0)
|
await trio.sleep(0)
|
||||||
|
|
||||||
if self._quote_gen:
|
|
||||||
await self._quote_gen.aclose()
|
|
||||||
|
|
||||||
# redraw any symbol specific UI components
|
# redraw any symbol specific UI components
|
||||||
if self.symbol != symbol or expiry is None:
|
if self.symbol != symbol or expiry is None:
|
||||||
# set window title
|
# set window title
|
||||||
|
@ -359,7 +355,6 @@ class OptionChain(object):
|
||||||
# retreive all contracts to populate expiry row
|
# retreive all contracts to populate expiry row
|
||||||
all_contracts = await self.feed.call_client(
|
all_contracts = await self.feed.call_client(
|
||||||
'get_all_contracts', symbols=[symbol])
|
'get_all_contracts', symbols=[symbol])
|
||||||
# all_contracts = await contracts(self.feed.brokermod, symbol)
|
|
||||||
|
|
||||||
if not all_contracts:
|
if not all_contracts:
|
||||||
label = self.no_opts_label
|
label = self.no_opts_label
|
||||||
|
@ -374,7 +369,7 @@ class OptionChain(object):
|
||||||
# msgpack... The expiry index is 2, see the ``ContractsKey`` named
|
# msgpack... The expiry index is 2, see the ``ContractsKey`` named
|
||||||
# tuple in the questrade broker mod. It would normally look
|
# tuple in the questrade broker mod. It would normally look
|
||||||
# something like:
|
# something like:
|
||||||
# expiry = next(iter(all_contracts)).expiry if not expiry else expiry
|
# exp = next(iter(all_contracts)).expiry if not exp else exp
|
||||||
ei = 2
|
ei = 2
|
||||||
# start streaming soonest contract by default if not provided
|
# start streaming soonest contract by default if not provided
|
||||||
expiry = next(iter(all_contracts))[ei] if not expiry else expiry
|
expiry = next(iter(all_contracts))[ei] if not expiry else expiry
|
||||||
|
@ -401,6 +396,7 @@ class OptionChain(object):
|
||||||
self._quote_gen, first_quotes = await self.feed.open_stream(
|
self._quote_gen, first_quotes = await self.feed.open_stream(
|
||||||
[(symbol, expiry)],
|
[(symbol, expiry)],
|
||||||
'option',
|
'option',
|
||||||
|
rate=self.rate,
|
||||||
)
|
)
|
||||||
log.debug(f"Got first_quotes for {symbol}:{expiry}")
|
log.debug(f"Got first_quotes for {symbol}:{expiry}")
|
||||||
records, displayables = self.feed.format_quotes(first_quotes)
|
records, displayables = self.feed.format_quotes(first_quotes)
|
||||||
|
@ -443,7 +439,6 @@ async def new_chain_ui(
|
||||||
portal: tractor._portal.Portal,
|
portal: tractor._portal.Portal,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
brokermod: types.ModuleType,
|
brokermod: types.ModuleType,
|
||||||
nursery: trio._core._run.Nursery,
|
|
||||||
rate: int = 1,
|
rate: int = 1,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create and return a new option chain UI.
|
"""Create and return a new option chain UI.
|
||||||
|
@ -499,13 +494,11 @@ async def _async_main(
|
||||||
portal,
|
portal,
|
||||||
symbol,
|
symbol,
|
||||||
brokermod,
|
brokermod,
|
||||||
nursery,
|
|
||||||
rate=rate,
|
rate=rate,
|
||||||
)
|
)
|
||||||
async with chain.open_rt_display(nursery, symbol):
|
async with chain.open_rt_display(nursery, symbol):
|
||||||
try:
|
try:
|
||||||
# trio-kivy entry point.
|
await async_runTouchApp(chain.widgets['root'])
|
||||||
await async_runTouchApp(chain.widgets['root']) # run kivy
|
|
||||||
finally:
|
finally:
|
||||||
if chain._quote_gen:
|
if chain._quote_gen:
|
||||||
await chain._quote_gen.aclose()
|
await chain._quote_gen.aclose()
|
||||||
|
|
|
@ -383,8 +383,8 @@ class Row(HoverBehavior, GridLayout):
|
||||||
def on_press(self, value=None):
|
def on_press(self, value=None):
|
||||||
log.info(f"Pressed row for {self._last_record['symbol']}")
|
log.info(f"Pressed row for {self._last_record['symbol']}")
|
||||||
if self.table and not self.is_header:
|
if self.table and not self.is_header:
|
||||||
for q in self.table._click_queues:
|
for sendchan in self.table._click_queues:
|
||||||
q.put_nowait(self._last_record['symbol'])
|
sendchan.send_nowait(self._last_record['symbol'])
|
||||||
|
|
||||||
|
|
||||||
class TickerTable(GridLayout):
|
class TickerTable(GridLayout):
|
||||||
|
@ -399,7 +399,7 @@ class TickerTable(GridLayout):
|
||||||
self._auto_sort = auto_sort
|
self._auto_sort = auto_sort
|
||||||
self._symbols2index = {}
|
self._symbols2index = {}
|
||||||
self._sorted = []
|
self._sorted = []
|
||||||
self._click_queues: List[trio.Queue] = []
|
self._click_queues: List[trio.abc.SendChannel[str]] = []
|
||||||
|
|
||||||
def append_row(self, key, row):
|
def append_row(self, key, row):
|
||||||
"""Append a `Row` of `Cell` objects to this table.
|
"""Append a `Row` of `Cell` objects to this table.
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import tractor
|
import tractor
|
||||||
|
import trio
|
||||||
from piker import log
|
from piker import log
|
||||||
|
from piker.brokers import questrade, config
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
parser.addoption("--ll", action="store", dest='loglevel',
|
parser.addoption("--ll", action="store", dest='loglevel',
|
||||||
default=None, help="logging level to set when testing")
|
default=None, help="logging level to set when testing")
|
||||||
|
parser.addoption("--confdir", default=None,
|
||||||
|
help="Use a practice API account")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session', autouse=True)
|
@pytest.fixture(scope='session', autouse=True)
|
||||||
|
@ -17,9 +23,65 @@ def loglevel(request):
|
||||||
tractor.log._default_loglevel = orig
|
tractor.log._default_loglevel = orig
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture(scope='session')
|
||||||
def brokerconf():
|
def test_config():
|
||||||
from piker.brokers import config
|
dirname = os.path.dirname
|
||||||
|
dirpath = os.path.abspath(
|
||||||
|
os.path.join(
|
||||||
|
dirname(os.path.realpath(__file__)),
|
||||||
|
'data'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return dirpath
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def travis():
|
||||||
|
is_travis = os.environ.get('TRAVIS', False)
|
||||||
|
if is_travis:
|
||||||
|
# this directory is cached, see .travis.yaml
|
||||||
|
cache_dir = config.get_broker_conf_path()
|
||||||
|
refresh_token = os.environ['QT_REFRESH_TOKEN']
|
||||||
|
|
||||||
|
def write_with_token(token):
|
||||||
|
conf, path = config.load(cache_dir)
|
||||||
|
conf.setdefault('questrade', {}).update(
|
||||||
|
{'refresh_token': token,
|
||||||
|
'is_practice': 'True'}
|
||||||
|
)
|
||||||
|
config.write(conf, path)
|
||||||
|
|
||||||
|
async def ensure_config():
|
||||||
|
# try to refresh current token using cached brokers config
|
||||||
|
# if it fails fail try using the refresh token provided by the
|
||||||
|
# env var and if that fails stop the test run here.
|
||||||
|
try:
|
||||||
|
async with questrade.get_client(ask_user=False):
|
||||||
|
pass
|
||||||
|
|
||||||
|
except (KeyError, ValueError, questrade.BrokerError):
|
||||||
|
# 3 cases:
|
||||||
|
# - config doesn't have a ``refresh_token`` k/v
|
||||||
|
# - cache dir does not exist yet
|
||||||
|
# - current token is expired; take it form env var
|
||||||
|
write_with_token(refresh_token)
|
||||||
|
|
||||||
|
async with questrade.get_client(ask_user=False):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# XXX ``pytest_trio`` doesn't support scope or autouse
|
||||||
|
trio.run(ensure_config)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session', autouse=True)
|
||||||
|
def brokerconf(request, test_config, travis):
|
||||||
|
"""If the `--confdir` flag is not passed use the
|
||||||
|
broker config file found in that dir.
|
||||||
|
"""
|
||||||
|
confdir = request.config.option.confdir
|
||||||
|
if confdir is not None:
|
||||||
|
config._override_config_dir(confdir)
|
||||||
|
|
||||||
return config.load()[0]
|
return config.load()[0]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,27 +2,21 @@
|
||||||
Questrade broker testing
|
Questrade broker testing
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
import logging
|
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
from trio.testing import trio_test
|
from trio.testing import trio_test
|
||||||
import tractor
|
|
||||||
from tractor.testing import tractor_test
|
|
||||||
from piker.brokers import questrade as qt
|
from piker.brokers import questrade as qt
|
||||||
import pytest
|
import pytest
|
||||||
|
import tractor
|
||||||
|
from tractor.testing import tractor_test
|
||||||
|
|
||||||
|
from piker.brokers import get_brokermod
|
||||||
|
from piker.brokers.data import DataFeed
|
||||||
|
|
||||||
|
|
||||||
log = tractor.get_logger('tests')
|
log = tractor.get_logger('tests')
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
def check_qt_conf_section(brokerconf):
|
|
||||||
"""Skip this module's tests if we have not quetrade API creds.
|
|
||||||
"""
|
|
||||||
if not brokerconf.has_section('questrade'):
|
|
||||||
pytest.skip("No questrade API credentials available")
|
|
||||||
|
|
||||||
|
|
||||||
# stock quote
|
# stock quote
|
||||||
_ex_quotes = {
|
_ex_quotes = {
|
||||||
'stock': {
|
'stock': {
|
||||||
|
@ -126,15 +120,17 @@ async def test_concurrent_tokens_refresh(us_symbols, loglevel):
|
||||||
quoter = await qt.stock_quoter(client, us_symbols)
|
quoter = await qt.stock_quoter(client, us_symbols)
|
||||||
|
|
||||||
async def get_quotes():
|
async def get_quotes():
|
||||||
for tries in range(30):
|
for tries in range(15):
|
||||||
log.info(f"{tries}: GETTING QUOTES!")
|
log.info(f"{tries}: GETTING QUOTES!")
|
||||||
quotes = await quoter(us_symbols)
|
quotes = await quoter(us_symbols)
|
||||||
await trio.sleep(0.1)
|
assert quotes
|
||||||
|
await trio.sleep(0.2)
|
||||||
|
|
||||||
async def intermittently_refresh_tokens(client):
|
async def intermittently_refresh_tokens(client):
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
await client.ensure_access(force_refresh=True)
|
await client.ensure_access(
|
||||||
|
force_refresh=True, ask_user=False)
|
||||||
log.info(f"last token data is {client.access_data}")
|
log.info(f"last token data is {client.access_data}")
|
||||||
await trio.sleep(1)
|
await trio.sleep(1)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -197,7 +193,10 @@ async def test_option_chain(tmx_symbols):
|
||||||
quotes = await client.option_chains(contracts)
|
quotes = await client.option_chains(contracts)
|
||||||
# verify contents match what we expect
|
# verify contents match what we expect
|
||||||
for quote in quotes:
|
for quote in quotes:
|
||||||
assert quote['underlying'] in tmx_symbols
|
underlying = quote['underlying']
|
||||||
|
# XXX: sometimes it's '' for old expiries?
|
||||||
|
if underlying:
|
||||||
|
assert underlying in tmx_symbols
|
||||||
for key in _ex_quotes['option']:
|
for key in _ex_quotes['option']:
|
||||||
quote.pop(key)
|
quote.pop(key)
|
||||||
assert not quote
|
assert not quote
|
||||||
|
@ -230,43 +229,34 @@ async def test_option_quote_latency(tmx_symbols):
|
||||||
await trio.sleep(0.1)
|
await trio.sleep(0.1)
|
||||||
|
|
||||||
|
|
||||||
async def stream_option_chain(portal, symbols):
|
async def stream_option_chain(feed, symbols):
|
||||||
"""Start up an option quote stream.
|
"""Start up an option quote stream.
|
||||||
|
|
||||||
``symbols`` arg is ignored here.
|
``symbols`` arg is ignored here.
|
||||||
"""
|
"""
|
||||||
symbol = symbols[0]
|
symbol = symbols[0]
|
||||||
async with qt.get_client() as client:
|
contracts = await feed.call_client(
|
||||||
contracts = await client.get_all_contracts([symbol])
|
'get_all_contracts', symbols=[symbol])
|
||||||
|
|
||||||
contractkey = next(iter(contracts))
|
contractkey = next(iter(contracts))
|
||||||
subs_keys = list(
|
subs_keys = list(
|
||||||
map(lambda item: (item.symbol, item.expiry), contracts))
|
# map(lambda item: (item.symbol, item.expiry), contracts))
|
||||||
|
map(lambda item: (item[0], item[2]), contracts))
|
||||||
sub = subs_keys[0]
|
sub = subs_keys[0]
|
||||||
|
|
||||||
agen = await portal.run(
|
|
||||||
'piker.brokers.data',
|
|
||||||
'start_quote_stream',
|
|
||||||
broker='questrade',
|
|
||||||
symbols=[sub],
|
|
||||||
feed_type='option',
|
|
||||||
rate=3,
|
|
||||||
diff_cached=False,
|
|
||||||
)
|
|
||||||
# latency arithmetic
|
# latency arithmetic
|
||||||
loops = 8
|
loops = 8
|
||||||
period = 1/3. # 3 rps
|
period = 1/3. # 3 rps
|
||||||
timeout = loops / period
|
timeout = float('inf') #loops / period
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# wait on the data streamer to actually start
|
|
||||||
# delivering
|
|
||||||
await agen.__anext__()
|
|
||||||
|
|
||||||
# it'd sure be nice to have an asyncitertools here...
|
# it'd sure be nice to have an asyncitertools here...
|
||||||
with trio.fail_after(timeout):
|
with trio.fail_after(timeout):
|
||||||
|
stream, first_quotes = await feed.open_stream(
|
||||||
|
[sub], 'option', rate=4, diff_cached=False,
|
||||||
|
)
|
||||||
count = 0
|
count = 0
|
||||||
async for quotes in agen:
|
async for quotes in stream:
|
||||||
# print(f'got quotes for {quotes.keys()}')
|
# print(f'got quotes for {quotes.keys()}')
|
||||||
# we should receive all calls and puts
|
# we should receive all calls and puts
|
||||||
assert len(quotes) == len(contracts[contractkey]) * 2
|
assert len(quotes) == len(contracts[contractkey]) * 2
|
||||||
|
@ -282,21 +272,12 @@ async def stream_option_chain(portal, symbols):
|
||||||
# switch the subscription and make sure
|
# switch the subscription and make sure
|
||||||
# stream is still working
|
# stream is still working
|
||||||
sub = subs_keys[1]
|
sub = subs_keys[1]
|
||||||
await agen.aclose()
|
|
||||||
agen = await portal.run(
|
|
||||||
'piker.brokers.data',
|
|
||||||
'start_quote_stream',
|
|
||||||
broker='questrade',
|
|
||||||
symbols=[sub],
|
|
||||||
feed_type='option',
|
|
||||||
rate=4,
|
|
||||||
diff_cached=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
await agen.__anext__()
|
|
||||||
with trio.fail_after(timeout):
|
with trio.fail_after(timeout):
|
||||||
|
stream, first_quotes = await feed.open_stream(
|
||||||
|
[sub], 'option', rate=4, diff_cached=False,
|
||||||
|
)
|
||||||
count = 0
|
count = 0
|
||||||
async for quotes in agen:
|
async for quotes in stream:
|
||||||
for symbol, quote in quotes.items():
|
for symbol, quote in quotes.items():
|
||||||
assert quote['key'] == sub
|
assert quote['key'] == sub
|
||||||
count += 1
|
count += 1
|
||||||
|
@ -304,29 +285,32 @@ async def stream_option_chain(portal, symbols):
|
||||||
break
|
break
|
||||||
finally:
|
finally:
|
||||||
# unsub
|
# unsub
|
||||||
await agen.aclose()
|
await stream.aclose()
|
||||||
|
|
||||||
|
|
||||||
async def stream_stocks(portal, symbols):
|
async def stream_stocks(feed, symbols):
|
||||||
"""Start up a stock quote stream.
|
"""Start up a stock quote stream.
|
||||||
"""
|
"""
|
||||||
agen = await portal.run(
|
stream, first_quotes = await feed.open_stream(
|
||||||
'piker.brokers.data',
|
symbols, 'stock', rate=3, diff_cached=False)
|
||||||
'start_quote_stream',
|
# latency arithmetic
|
||||||
broker='questrade',
|
loops = 8
|
||||||
symbols=symbols,
|
period = 1/3. # 3 rps
|
||||||
diff_cached=False,
|
timeout = loops / period
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
# it'd sure be nice to have an asyncitertools here...
|
# it'd sure be nice to have an asyncitertools here...
|
||||||
async for quotes in agen:
|
count = 0
|
||||||
|
async for quotes in stream:
|
||||||
assert quotes
|
assert quotes
|
||||||
for key in quotes:
|
for key in quotes:
|
||||||
assert key in symbols
|
assert key in symbols
|
||||||
break
|
count += 1
|
||||||
|
if count == loops:
|
||||||
|
break
|
||||||
finally:
|
finally:
|
||||||
# unsub
|
# unsub
|
||||||
await agen.aclose()
|
await stream.aclose()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -339,8 +323,10 @@ async def stream_stocks(portal, symbols):
|
||||||
(stream_option_chain, stream_option_chain),
|
(stream_option_chain, stream_option_chain),
|
||||||
],
|
],
|
||||||
ids=[
|
ids=[
|
||||||
'stocks', 'options',
|
'stocks',
|
||||||
'stocks_and_options', 'stocks_and_stocks',
|
'options',
|
||||||
|
'stocks_and_options',
|
||||||
|
'stocks_and_stocks',
|
||||||
'options_and_options',
|
'options_and_options',
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -348,6 +334,7 @@ async def stream_stocks(portal, symbols):
|
||||||
async def test_quote_streaming(tmx_symbols, loglevel, stream_what):
|
async def test_quote_streaming(tmx_symbols, loglevel, stream_what):
|
||||||
"""Set up option streaming using the broker daemon.
|
"""Set up option streaming using the broker daemon.
|
||||||
"""
|
"""
|
||||||
|
brokermod = get_brokermod('questrade')
|
||||||
async with tractor.find_actor('brokerd') as portal:
|
async with tractor.find_actor('brokerd') as portal:
|
||||||
async with tractor.open_nursery() as nursery:
|
async with tractor.open_nursery() as nursery:
|
||||||
# only one per host address, spawns an actor if None
|
# only one per host address, spawns an actor if None
|
||||||
|
@ -360,6 +347,8 @@ async def test_quote_streaming(tmx_symbols, loglevel, stream_what):
|
||||||
'piker.brokers.core'
|
'piker.brokers.core'
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
feed = DataFeed(portal, brokermod)
|
||||||
|
|
||||||
if len(stream_what) > 1:
|
if len(stream_what) > 1:
|
||||||
# stream disparate symbol sets per task
|
# stream disparate symbol sets per task
|
||||||
first, *tail = tmx_symbols
|
first, *tail = tmx_symbols
|
||||||
|
@ -369,7 +358,7 @@ async def test_quote_streaming(tmx_symbols, loglevel, stream_what):
|
||||||
|
|
||||||
async with trio.open_nursery() as n:
|
async with trio.open_nursery() as n:
|
||||||
for syms, func in zip(symbols, stream_what):
|
for syms, func in zip(symbols, stream_what):
|
||||||
n.start_soon(func, portal, syms)
|
n.start_soon(func, feed, syms)
|
||||||
|
|
||||||
# stop all spawned subactors
|
# stop all spawned subactors
|
||||||
await nursery.cancel()
|
await nursery.cancel()
|
||||||
|
|
Loading…
Reference in New Issue