Initial commit... about time.
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
[[source]]
|
||||||
|
name = "pypi"
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
ipython = "*"
|
||||||
|
ipdb = "*"
|
||||||
|
pyparsing = "*"
|
||||||
|
jinja2 = "*"
|
||||||
|
aiohttp = "*"
|
||||||
|
click = "*"
|
||||||
|
aiofiles = "*"
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.7"
|
||||||
Generated
+300
@@ -0,0 +1,300 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "5229faf1ac969cd54e8b83f073f080f632df013f9449ef21124bc67eeaa10f79"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.7"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"aiofiles": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee",
|
||||||
|
"sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.4.0"
|
||||||
|
},
|
||||||
|
"aiohttp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55",
|
||||||
|
"sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed",
|
||||||
|
"sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10",
|
||||||
|
"sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5",
|
||||||
|
"sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1",
|
||||||
|
"sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939",
|
||||||
|
"sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390",
|
||||||
|
"sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa",
|
||||||
|
"sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc",
|
||||||
|
"sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5",
|
||||||
|
"sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d",
|
||||||
|
"sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf",
|
||||||
|
"sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6",
|
||||||
|
"sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72",
|
||||||
|
"sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12",
|
||||||
|
"sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366",
|
||||||
|
"sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4",
|
||||||
|
"sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300",
|
||||||
|
"sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d",
|
||||||
|
"sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303",
|
||||||
|
"sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6",
|
||||||
|
"sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.5.4"
|
||||||
|
},
|
||||||
|
"async-timeout": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||||
|
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
||||||
|
],
|
||||||
|
"version": "==3.0.1"
|
||||||
|
},
|
||||||
|
"attrs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
|
||||||
|
"sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
|
||||||
|
],
|
||||||
|
"version": "==19.1.0"
|
||||||
|
},
|
||||||
|
"backcall": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4",
|
||||||
|
"sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"
|
||||||
|
],
|
||||||
|
"version": "==0.1.0"
|
||||||
|
},
|
||||||
|
"chardet": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||||
|
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||||
|
],
|
||||||
|
"version": "==3.0.4"
|
||||||
|
},
|
||||||
|
"click": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||||
|
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==7.0"
|
||||||
|
},
|
||||||
|
"decorator": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de",
|
||||||
|
"sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"
|
||||||
|
],
|
||||||
|
"version": "==4.4.0"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||||
|
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||||
|
],
|
||||||
|
"version": "==2.8"
|
||||||
|
},
|
||||||
|
"ipdb": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:dce2112557edfe759742ca2d0fee35c59c97b0cc7a05398b791079d78f1519ce"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.12"
|
||||||
|
},
|
||||||
|
"ipython": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b038baa489c38f6d853a3cfc4c635b0cda66f2864d136fe8f40c1a6e334e2a6b",
|
||||||
|
"sha256:f5102c1cd67e399ec8ea66bcebe6e3968ea25a8977e53f012963e5affeb1fe38"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==7.4.0"
|
||||||
|
},
|
||||||
|
"ipython-genutils": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8",
|
||||||
|
"sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"
|
||||||
|
],
|
||||||
|
"version": "==0.2.0"
|
||||||
|
},
|
||||||
|
"jedi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2bb0603e3506f708e792c7f4ad8fc2a7a9d9c2d292a358fbbd58da531695595b",
|
||||||
|
"sha256:2c6bcd9545c7d6440951b12b44d373479bf18123a401a52025cf98563fbd826c"
|
||||||
|
],
|
||||||
|
"version": "==0.13.3"
|
||||||
|
},
|
||||||
|
"jinja2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
||||||
|
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.10"
|
||||||
|
},
|
||||||
|
"markupsafe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
|
||||||
|
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
||||||
|
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
||||||
|
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
||||||
|
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
||||||
|
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
||||||
|
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
||||||
|
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
||||||
|
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
||||||
|
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
||||||
|
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
||||||
|
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
||||||
|
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
||||||
|
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
||||||
|
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
|
||||||
|
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
|
||||||
|
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
|
||||||
|
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
|
||||||
|
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
|
||||||
|
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
|
||||||
|
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
|
||||||
|
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
|
||||||
|
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
|
||||||
|
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
|
||||||
|
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
||||||
|
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
||||||
|
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
||||||
|
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
|
||||||
|
],
|
||||||
|
"version": "==1.1.1"
|
||||||
|
},
|
||||||
|
"multidict": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f",
|
||||||
|
"sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3",
|
||||||
|
"sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef",
|
||||||
|
"sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b",
|
||||||
|
"sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73",
|
||||||
|
"sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc",
|
||||||
|
"sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3",
|
||||||
|
"sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd",
|
||||||
|
"sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351",
|
||||||
|
"sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941",
|
||||||
|
"sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d",
|
||||||
|
"sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1",
|
||||||
|
"sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b",
|
||||||
|
"sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a",
|
||||||
|
"sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3",
|
||||||
|
"sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7",
|
||||||
|
"sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0",
|
||||||
|
"sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0",
|
||||||
|
"sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014",
|
||||||
|
"sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5",
|
||||||
|
"sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036",
|
||||||
|
"sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d",
|
||||||
|
"sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a",
|
||||||
|
"sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce",
|
||||||
|
"sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1",
|
||||||
|
"sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a",
|
||||||
|
"sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9",
|
||||||
|
"sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7",
|
||||||
|
"sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b"
|
||||||
|
],
|
||||||
|
"version": "==4.5.2"
|
||||||
|
},
|
||||||
|
"parso": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4580328ae3f548b358f4901e38c0578229186835f0fa0846e47369796dd5bcc9",
|
||||||
|
"sha256:68406ebd7eafe17f8e40e15a84b56848eccbf27d7c1feb89e93d8fca395706db"
|
||||||
|
],
|
||||||
|
"version": "==0.3.4"
|
||||||
|
},
|
||||||
|
"pexpect": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba",
|
||||||
|
"sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b"
|
||||||
|
],
|
||||||
|
"markers": "sys_platform != 'win32'",
|
||||||
|
"version": "==4.6.0"
|
||||||
|
},
|
||||||
|
"pickleshare": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca",
|
||||||
|
"sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"
|
||||||
|
],
|
||||||
|
"version": "==0.7.5"
|
||||||
|
},
|
||||||
|
"prompt-toolkit": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780",
|
||||||
|
"sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1",
|
||||||
|
"sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55"
|
||||||
|
],
|
||||||
|
"version": "==2.0.9"
|
||||||
|
},
|
||||||
|
"ptyprocess": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0",
|
||||||
|
"sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"
|
||||||
|
],
|
||||||
|
"version": "==0.6.0"
|
||||||
|
},
|
||||||
|
"pygments": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a",
|
||||||
|
"sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"
|
||||||
|
],
|
||||||
|
"version": "==2.3.1"
|
||||||
|
},
|
||||||
|
"pyparsing": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a",
|
||||||
|
"sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.3.1"
|
||||||
|
},
|
||||||
|
"six": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
||||||
|
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
||||||
|
],
|
||||||
|
"version": "==1.12.0"
|
||||||
|
},
|
||||||
|
"traitlets": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
|
||||||
|
"sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"
|
||||||
|
],
|
||||||
|
"version": "==4.3.2"
|
||||||
|
},
|
||||||
|
"wcwidth": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
|
||||||
|
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
|
||||||
|
],
|
||||||
|
"version": "==0.1.7"
|
||||||
|
},
|
||||||
|
"yarl": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9",
|
||||||
|
"sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f",
|
||||||
|
"sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb",
|
||||||
|
"sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320",
|
||||||
|
"sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842",
|
||||||
|
"sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0",
|
||||||
|
"sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829",
|
||||||
|
"sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310",
|
||||||
|
"sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4",
|
||||||
|
"sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8",
|
||||||
|
"sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1"
|
||||||
|
],
|
||||||
|
"version": "==1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
from aiohttp import web
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from shlex import quote as shell_quote
|
||||||
|
from string import Template
|
||||||
|
import asyncio
|
||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from pyparsing import *
|
||||||
|
import aiofiles
|
||||||
|
import click
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
# Parser #
|
||||||
|
########################################################################
|
||||||
|
|
||||||
|
#
|
||||||
|
# Endpoint Definition
|
||||||
|
#
|
||||||
|
|
||||||
|
# Method
|
||||||
|
method = ( Literal('GET')
|
||||||
|
| Literal('POST')
|
||||||
|
| Literal('PUT')
|
||||||
|
| Literal('DELETE')
|
||||||
|
| Literal('PATCH') )
|
||||||
|
multi_method = delimitedList(method, delim="|", combine=True)
|
||||||
|
method_spec = Combine(Literal('*') | multi_method)
|
||||||
|
|
||||||
|
# Pattern
|
||||||
|
regex = Word(alphas + nums + '\\+*,[]-_')(name="regex")
|
||||||
|
symbol = Word(alphas)(name="symbol")
|
||||||
|
p_pattern = Combine('/{' + symbol + Optional(':' + regex) + '}')
|
||||||
|
p_path = Word('/', alphas + nums + '$-_.+!*\'(),')
|
||||||
|
urlpattern = Combine(OneOrMore(p_pattern | p_path))(name="urlpattern")
|
||||||
|
|
||||||
|
# Body
|
||||||
|
body = (Suppress('{') + SkipTo(Combine(LineStart() + '}' + LineEnd()))(name="body"))
|
||||||
|
|
||||||
|
# Endpoint
|
||||||
|
endpoint = (Optional(method_spec + Suppress(White()),
|
||||||
|
default='*')(name="method")
|
||||||
|
+ urlpattern
|
||||||
|
+ Suppress(White())
|
||||||
|
+ body)(name="endpoint")
|
||||||
|
|
||||||
|
kapow_program = OneOrMore(endpoint)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ResourceManager:
|
||||||
|
shell_repr: str
|
||||||
|
coro: object
|
||||||
|
|
||||||
|
|
||||||
|
async def get_value(context, path):
|
||||||
|
if path == 'request/method':
|
||||||
|
return context['request'].method.encode('utf-8')
|
||||||
|
elif path == 'request/path':
|
||||||
|
return context['request'].path.encode('utf-8')
|
||||||
|
elif path.startswith('request/match'):
|
||||||
|
key = path.split('/', 2)[-1]
|
||||||
|
return context['request'].match_info[key].encode('utf-8')
|
||||||
|
elif path.startswith('request/param'):
|
||||||
|
key = path.split('/', 2)[-1]
|
||||||
|
return context['request'].rel_url.query[key].encode('utf-8')
|
||||||
|
elif path.startswith('request/header'):
|
||||||
|
key = path.split('/', 2)[-1]
|
||||||
|
return context['request'].headers[key].encode('utf-8')
|
||||||
|
elif path.startswith('request/form'):
|
||||||
|
key = path.split('/', 2)[-1]
|
||||||
|
return (await context['request'].post())[key].encode('utf-8')
|
||||||
|
elif path == 'request/body':
|
||||||
|
return await context['request'].read()
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Unknown path {path!r}')
|
||||||
|
|
||||||
|
async def set_value(context, path, value):
|
||||||
|
if path == 'response/status':
|
||||||
|
context['response_status'] = int(value.decode('utf-8'))
|
||||||
|
elif path == 'response/body':
|
||||||
|
context['response_body'].write(value)
|
||||||
|
elif path.startswith('response/header/'):
|
||||||
|
key = path.split('/', 2)[-1]
|
||||||
|
context['response_headers'][key] = value.rstrip(b'\n').decode('utf-8')
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Unknown path {path!r}')
|
||||||
|
|
||||||
|
def is_readable(path):
|
||||||
|
return path.startswith('request/')
|
||||||
|
|
||||||
|
|
||||||
|
def is_writable(path):
|
||||||
|
return path.startswith('response/')
|
||||||
|
|
||||||
|
|
||||||
|
def get_manager(resource, context):
|
||||||
|
view, path = resource.split(':')
|
||||||
|
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def manager():
|
||||||
|
if view == 'value':
|
||||||
|
if not is_readable(path):
|
||||||
|
raise ValueError(f'Non-readable path "{path}".')
|
||||||
|
else:
|
||||||
|
value = await get_value(context, path)
|
||||||
|
yield ResourceManager(
|
||||||
|
shell_repr=shell_quote(value.decode('utf-8')),
|
||||||
|
coro=asyncio.sleep(0))
|
||||||
|
elif view == 'fifo':
|
||||||
|
# No race condition here? Shut your ass!!
|
||||||
|
# https://stackoverflow.com/a/1430566
|
||||||
|
filename = tempfile.mktemp()
|
||||||
|
os.mkfifo(filename)
|
||||||
|
|
||||||
|
async def manage_fifo():
|
||||||
|
try:
|
||||||
|
if is_readable(path):
|
||||||
|
async with aiofiles.open(filename, 'wb') as fifo:
|
||||||
|
await fifo.write(await get_value(context, path))
|
||||||
|
elif is_writable(path):
|
||||||
|
async with aiofiles.open(filename, 'rb') as fifo:
|
||||||
|
await set_value(context, path, await fifo.read())
|
||||||
|
else:
|
||||||
|
raise RuntimeError('WTF!')
|
||||||
|
finally:
|
||||||
|
os.unlink(filename)
|
||||||
|
|
||||||
|
yield ResourceManager(
|
||||||
|
shell_repr=shell_quote(filename),
|
||||||
|
coro=manage_fifo())
|
||||||
|
|
||||||
|
elif view == 'file':
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w+b', buffering=0) as tmp:
|
||||||
|
if is_readable(path):
|
||||||
|
value = await get_value(context, path)
|
||||||
|
tmp.write(value)
|
||||||
|
tmp.flush()
|
||||||
|
|
||||||
|
yield ResourceManager(
|
||||||
|
shell_repr=shell_quote(tmp.name),
|
||||||
|
coro=asyncio.sleep(0))
|
||||||
|
|
||||||
|
if is_writable(path):
|
||||||
|
tmp.seek(0)
|
||||||
|
await set_value(context, path, tmp.read())
|
||||||
|
elif view == 'source':
|
||||||
|
raise NotImplementedError('source view not implemented')
|
||||||
|
elif view == 'sink':
|
||||||
|
raise NotImplementedError('sink view not implemented')
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Unknown view type {view}')
|
||||||
|
|
||||||
|
return manager
|
||||||
|
|
||||||
|
|
||||||
|
class KapowTemplate(Template):
|
||||||
|
delimiter = '@'
|
||||||
|
idpattern = r'(?a:[_a-z][_a-z0-9]*:[_a-z][-_a-z0-9/]*)'
|
||||||
|
|
||||||
|
async def run(self, context):
|
||||||
|
async with contextlib.AsyncExitStack() as stack:
|
||||||
|
# Initialize all resources creating a mapping
|
||||||
|
resources = dict() # resource: (shell_repr, manager)
|
||||||
|
for match in self.pattern.findall(self.template):
|
||||||
|
_, resource, *_ = match
|
||||||
|
if resource not in resources:
|
||||||
|
manager = get_manager(resource, context)
|
||||||
|
resources[resource] = await stack.enter_async_context(manager())
|
||||||
|
|
||||||
|
code = self.substitute(**{k: v.shell_repr
|
||||||
|
for k, v in resources.items()})
|
||||||
|
|
||||||
|
if False:
|
||||||
|
print('-'*80)
|
||||||
|
print(code)
|
||||||
|
print('-'*80)
|
||||||
|
|
||||||
|
shell_task = await asyncio.create_subprocess_shell(code)
|
||||||
|
manager_tasks = [asyncio.create_task(v.coro)
|
||||||
|
for v in resources.values()]
|
||||||
|
|
||||||
|
await shell_task.wait() # Run the subshell process
|
||||||
|
done, pending = await asyncio.wait(manager_tasks, timeout=0) # Managers commit changes
|
||||||
|
if pending:
|
||||||
|
# print(f"Warning: Resources not consumed ({len(pending)})")
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
def create_runner(code):
|
||||||
|
return KapowTemplate(code).run
|
||||||
|
|
||||||
|
|
||||||
|
def create_context(request):
|
||||||
|
context = dict()
|
||||||
|
context["request"] = request
|
||||||
|
context["response_body"] = io.BytesIO()
|
||||||
|
context["response_status"] = 200
|
||||||
|
context["response_headers"] = dict()
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
def response_from_context(context):
|
||||||
|
return web.Response(
|
||||||
|
body=context["response_body"].getbuffer(),
|
||||||
|
status=context["response_status"],
|
||||||
|
headers=context["response_headers"])
|
||||||
|
|
||||||
|
|
||||||
|
def generate_endpoint(code):
|
||||||
|
async def endpoint(request):
|
||||||
|
context = create_context(request)
|
||||||
|
runner = create_runner(code)
|
||||||
|
await runner(context) # Will change context
|
||||||
|
return response_from_context(context)
|
||||||
|
return endpoint
|
||||||
|
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
# Webserver #
|
||||||
|
########################################################################
|
||||||
|
|
||||||
|
def register_endpoint(app, methods, pattern, code):
|
||||||
|
print(f"Registering methods={methods!r} pattern={pattern!r}")
|
||||||
|
endpoint = generate_endpoint(code)
|
||||||
|
for method in methods: # May be '*'
|
||||||
|
app.add_routes([web.route(method, pattern, endpoint)])
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.argument('program', type=click.File())
|
||||||
|
def main(program):
|
||||||
|
app = web.Application()
|
||||||
|
for ep, _, _ in kapow_program.scanString(program.read()):
|
||||||
|
register_endpoint(app,
|
||||||
|
ep.method.asList()[0].split('|'),
|
||||||
|
''.join(ep.urlpattern),
|
||||||
|
ep.body)
|
||||||
|
web.run_app(app)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
10-SECOND PROPOSAL
|
||||||
|
===================
|
||||||
|
|
||||||
|
*kapow* is a specialized language for marrying the **web** and the **shell**.
|
||||||
|
|
||||||
|
|
||||||
|
DESCRIPTION
|
||||||
|
===========
|
||||||
|
|
||||||
|
The **web** and the **shell** are two different beasts, both packed with history.
|
||||||
|
|
||||||
|
There are some concepts in HTTP and the shell that **resemble each other**.
|
||||||
|
|
||||||
|
```
|
||||||
|
+------------------------+-------------------------+
|
||||||
|
| HTTP | SHELL |
|
||||||
|
+--------------+------------------------+-------------------------+
|
||||||
|
| Input | POST form-encoding | Command line parameters |
|
||||||
|
| Parameters | GET parameters | Environment variables |
|
||||||
|
| | Headers | |
|
||||||
|
| | Serialized body (JSON) | |
|
||||||
|
+--------------+------------------------+-------------------------+
|
||||||
|
| Data Streams | Response/Request Body | Stdin/Stdout/Stderr |
|
||||||
|
| | Websocket | Input/Output files |
|
||||||
|
| | Uploaded files | |
|
||||||
|
+--------------+------------------------+-------------------------+
|
||||||
|
| Control | Status codes | Signals |
|
||||||
|
| | HTTP Methods | |
|
||||||
|
+--------------+------------------------+-------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
Any tool designed to give an HTTP interface to an existing shell command **must map concepts of boths**. For example:
|
||||||
|
|
||||||
|
- "GET parameters" to "Command line parameters"
|
||||||
|
- "Headers" to "Environment variables"
|
||||||
|
- "Stdout" to "Response body"
|
||||||
|
|
||||||
|
*kapow* is not opinionated about the different ways you can map both worlds. Instead it provides a concise language used to express the mapping and a set of common defaults.
|
||||||
|
|
||||||
|
|
||||||
|
Why not tool...?
|
||||||
|
----------------
|
||||||
|
|
||||||
|
All the alternatives we found are **rigid** about how they match between HTTP and shell concepts.
|
||||||
|
|
||||||
|
* [shell2http](https://github.com/msoap/shell2http): HTTP-server to execute shell commands. Designed for development, prototyping or remote control. Settings through two command line arguments, path and shell command.
|
||||||
|
* [websocketd](https://github.com/joewalnes/websocketd): Turn any program that uses STDIN/STDOUT into a WebSocket server. Like inetd, but for WebSockets.
|
||||||
|
* [webhook](https://github.com/adnanh/webhook): webhook is a lightweight incoming webhook server to run shell commands.
|
||||||
|
* [gotty](https://github.com/yudai/gotty): GoTTY is a simple command line tool that turns your CLI tools into web applications. (For interactive commands only)
|
||||||
|
|
||||||
|
Tools with a rigid matching **can't evade** *[impedance mismatch](https://haacked.com/archive/2004/06/15/impedance-mismatch.aspx/)*. Resulting is an easy-to-use software, convenient in some scenarios but incapable in others.
|
||||||
|
|
||||||
|
|
||||||
|
Why not my good-old programming language...?
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
* Boilerplate
|
||||||
|
* Custom code = More bugs
|
||||||
|
* Security issues (Command injection, etc)
|
||||||
|
* Dependency on developers
|
||||||
|
* "A programming language is low level when its programs require attention to the irrelevant" Alan Perlis
|
||||||
|
|
||||||
|
*kapow* aims to be halfway from one of the mentioned tools and a general programming language. A limited scripting language. Think of *awk*, 20 years later, for HTTP.
|
||||||
|
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
Imagine you want to improve your monitoring system to be able to check if a WiFi in a remote building is working. Suppose you already have a wireless capable linux server on that building.
|
||||||
|
|
||||||
|
What about exposing a wifi scan as a service?
|
||||||
|
|
||||||
|
```
|
||||||
|
$ iwlist scan | grep 'ESSID:"BBVA"'
|
||||||
|
```
|
||||||
|
|
||||||
|
The above command will list the visible WiFI networks and filter for the one we are interested in (**BBVA**); exiting with code **0** when found and **1** otherwise.
|
||||||
|
|
||||||
|
We want to signal the WiFI status with a meaningful HTTP status code: 200 for UP and 503 for DOWN.
|
||||||
|
|
||||||
|
So the only thing our service has to do is translate from the **command's exit code** to a meaningful **HTTP status code**.
|
||||||
|
|
||||||
|
Our *kapow* program (monitor.pow) should look **something like** this:
|
||||||
|
|
||||||
|
```
|
||||||
|
/monitor/wifi =
|
||||||
|
$(iwlist scan | grep 'ESSID:"BBVA')
|
||||||
|
| 0 = status 200 # OK
|
||||||
|
| 1 = status 503 # Service Unavailable
|
||||||
|
```
|
||||||
|
|
||||||
|
With this code, the user can run the service with:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ kapow monitor.pow
|
||||||
|
```
|
||||||
|
And then:
|
||||||
|
|
||||||
|
1. *kapow* will open a port with an HTTP server serving the URI **/monitor/wifi**.
|
||||||
|
2. When requested `curl http://<server>/monitor/wifi`. The command is executed resulting in a WiFI scan.
|
||||||
|
3. The command's **exit code** is translated to an HTTP **status code** and returned.
|
||||||
|
|
||||||
|
USE CASES OR VALUE
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Reuse 40+ years of existing computer programs as microservices (nanoservices?).
|
||||||
|
* Expose command line only tools as services. Eliminating the need of interactive SSH sessions.
|
||||||
|
* Fine-grained access control to CLI based on options/parameters
|
||||||
|
|
||||||
|
|
||||||
|
REQUIRED SKILLS
|
||||||
|
===============
|
||||||
|
|
||||||
|
- Knowledge of HTTP and the command line.
|
||||||
|
- Programming.
|
||||||
|
- Low-level stuff...
|
||||||
|
|
||||||
|
|
||||||
|
ESTIMATION
|
||||||
|
==========
|
||||||
|
|
||||||
|
- 2 weeks
|
||||||
|
|
||||||
|
|
||||||
|
OUTPUT
|
||||||
|
======
|
||||||
|
|
||||||
|
- A collection of **concrete** use cases, 5 to 10.
|
||||||
|
- A DSL design (over paper) to implement the use cases.
|
||||||
|
- A working **dirty** proof of concept.
|
||||||
Reference in New Issue
Block a user