Merge pull request #71 from pikers/fix_ci

Fix ci
kivy_mainline_and_py3.8
goodboy 2019-02-26 00:53:04 -05:00 committed by GitHub
commit dfacf8d338
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 621 additions and 467 deletions

View File

@ -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/

370
Pipfile.lock generated
View File

@ -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": [

View File

@ -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',

View File

@ -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

View File

@ -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)

View File

@ -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]

View File

@ -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)

View File

@ -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

View File

@ -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'

View File

@ -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(

View File

@ -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'

View File

@ -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()

View File

@ -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()

View File

@ -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.

View File

@ -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]

View File

@ -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()