Last active
December 5, 2018 11:45
-
-
Save akiross/c5cd4485e6abbb5e6b7dd09df6cfb6c1 to your computer and use it in GitHub Desktop.
Basic json-log viewer with filtering
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Copyright (c) 2018 Alessandro "AkiRoss" Re | |
# | |
# Permission is hereby granted, free of charge, to any person obtaining a copy | |
# of this software and associated documentation files (the "Software"), to deal | |
# in the Software without restriction, including without limitation the rights | |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
# copies of the Software, and to permit persons to whom the Software is | |
# furnished to do so, subject to the following conditions: | |
# | |
# The above copyright notice and this permission notice shall be included in all | |
# copies or substantial portions of the Software. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
# SOFTWARE. | |
"""Visualize log files by filtering and comparing events. | |
Requires flask, jinja2 and gevent.""" | |
import os | |
import sys | |
import json | |
import jinja2 | |
import argparse | |
from itertools import zip_longest | |
from gevent.pywsgi import WSGIServer | |
from flask import Flask, Response, render_template_string | |
log_view_template = r''' | |
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | |
<title>Logview {{ fname }}</title> | |
<style> | |
div.views { | |
display: flex; | |
} | |
ul.view { | |
display: flex; | |
flex-direction: column; | |
flex-grow: 1; | |
} | |
.w-40 { width: 40% !important; } | |
.view.left { | |
background-color: #eeeeff; | |
} | |
.view.right { | |
background-color: #eeffee; | |
} | |
</style> | |
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> | |
<script> | |
function loadURL() { | |
var lsel = document.getElementById("leftSel").value; | |
var lloc = document.getElementById("leftSelSub").value; | |
var rsel = document.getElementById("rightSel").value; | |
var rloc = document.getElementById("rightSelSub").value; | |
var url = new URL(window.location); | |
window.location = new URL("/"+lsel+"/"+lloc+"/"+rsel+"/"+rloc, url.origin); | |
} | |
</script> | |
</head> | |
<body> | |
<table class="table table-striped"> | |
<thead class="thead-dark"> | |
<tr> | |
<th colspan="3" class="text-center">Inspecting {{ fname }}</th> | |
</tr> | |
{% if tag_keys is not none %} | |
<tr> | |
<th class="w-50 text-center"> | |
<select id="leftSel" class="custom-select mr-sm-2 w-40"> | |
{% for key in tag_keys %} | |
<option value="{{ key }}">{{ key }}</option> | |
{% endfor %} | |
</select> | |
<select id="leftSelSub" class="custom-select mr-sm-2 w-40"> | |
{% for key in location_keys %} | |
<option value="{{ key }}">{{ key }}</option> | |
{% endfor %} | |
</select> | |
</th> | |
<th class="text-center"> | |
<button type="button" class="btn btn-primary" onclick="loadURL();">Reload</button> | |
</th> | |
<th class="text-center"> | |
<select id="rightSel" class="custom-select mr-sm-2 w-40"> | |
{% for key in tag_keys %} | |
<option value="{{ key }}">{{ key }}</option> | |
{% endfor %} | |
</select> | |
<select id="rightSelSub" class="custom-select mr-sm-2 w-40"> | |
{% for key in location_keys %} | |
<option value="{{ key }}">{{ key }}</option> | |
{% endfor %} | |
</select> | |
</th> | |
</tr> | |
{% endif %} | |
</thead> | |
{% for litem, citem, ritem in items %} | |
<tr> | |
<td>{{ litem }}</td> | |
<td>{{ citem }}</td> | |
<td>{{ ritem }}</td> | |
</tr> | |
{% endfor %} | |
</table> | |
</body> | |
</html> | |
''' | |
app = Flask(__name__) | |
def get_first(l, default=None): | |
"""Return item at position 0 or default if not present.""" | |
try: | |
return l[0] | |
except IndexError: | |
return default | |
def dget(d, key, sep='/'): | |
"""d is a dict, key is foo/bar/baz, return d['foo']['bar']['baz'].""" | |
def _rget(d, k): | |
if len(k) == 1: | |
return d[k[0]] | |
return _rget(d[k[0]], k[1:]) | |
return _rget(d, key.split(sep)) | |
def lr_selector(iterable, left_cond, right_cond): | |
left, right = [], [] | |
# Use a single iterator | |
iterator = iter(iterable) | |
def _next_heads(): | |
"""Extract at least one item for each list.""" | |
for item in iterator: | |
if left_cond(item): | |
left.append(item) | |
if right_cond(item): | |
right.append(item) | |
if left and right: | |
break # We got at least one item per list, stop here | |
def _make_picker(target): | |
"""Pick the next element from a list, filling the list first.""" | |
def _next(default=None, peek=False): | |
if not target: | |
_next_heads() # Try to pop the next element | |
if not target: | |
return default # Nothing left, return default | |
if peek: | |
return target[0] # Just peek the value | |
else: | |
v, target[:] = target[0], target[1:] # Pop head | |
return v | |
return _next | |
return _make_picker(left), _make_picker(right) | |
def stream_template(template_code, **context): | |
app.update_template_context(context) | |
# t = app.jinja_env.Template(template_code) | |
t = jinja2.Template(template_code) | |
rv = t.stream(context) | |
rv.enable_buffering(5) | |
return rv | |
def make_viewer(args): | |
"""Build a log viewer.""" | |
print("Building logview with arguments:", args) | |
@app.route("/", defaults={'ltag': None, 'rtag': None, 'lloc': None, 'rloc': None}) | |
@app.route("/<ltag>/<rtag>/", strict_slashes=False, defaults={'lloc': None, 'rloc': None}) | |
@app.route("/<ltag>/<lloc>/<rtag>/<rloc>/", strict_slashes=False) | |
def logview(ltag, lloc, rtag, rloc): | |
fname = args.log_file | |
# TODO use generator for data streaming | |
# see http://flask.pocoo.org/docs/1.0/patterns/streaming/ | |
# Get timestamps | |
res = args.resolution | |
# Merge by timestamps | |
fill_value = '' | |
def left_cond(row): | |
row_tags = dget(row, args.first_path) | |
row_loc = dget(row, args.second_path) | |
tag_cond = ltag is not None and ltag in row_tags | |
loc_cond = lloc is None or lloc == row_loc | |
return tag_cond and loc_cond | |
def right_cond(row): | |
row_tags = dget(row, args.first_path) | |
row_loc = dget(row, args.second_path) | |
tag_cond = rtag is not None and rtag in row_tags | |
loc_cond = lloc is None or rloc == row_loc | |
return tag_cond and loc_cond | |
def extract_data(item): | |
if item is None: | |
return None | |
time = dget(item, args.time_path) | |
message = dget(item, args.message_path) | |
return (time, message) | |
fp = open(fname, 'rt') | |
js = map(json.loads, fp) # Convert each line to json object | |
next_left, next_right = lr_selector(js, left_cond, right_cond) | |
# Merge lines by time | |
def _generate_rows(): | |
while True: | |
l = extract_data(next_left(peek=True)) | |
r = extract_data(next_right(peek=True)) | |
if r is None and l is None: | |
fp.close() | |
break | |
elif r is None or l is not None and l[0]//res < r[0]//res: | |
yield (l[1], l[0], fill_value) | |
next_left() | |
elif l is None or r is not None and l[0]//res > r[0]//res: | |
yield (fill_value, r[0], r[1]) | |
next_right() | |
else: | |
yield (l[1], l[0], r[1]) | |
next_left() | |
next_right() | |
# Stream response | |
return Response(stream_template(log_view_template, | |
fname=fname, | |
items=_generate_rows(), | |
ltag=ltag, | |
rtag=rtag)) | |
# Render template with rows | |
return render_template_string(log_view_template, | |
fname=fname, | |
items=merged_rows, | |
ltag=ltag, | |
rtag=rtag) | |
return logview | |
if __name__ == '__main__': | |
parser = argparse.ArgumentParser() | |
parser.add_argument('--resolution', type=int, default=1000, | |
help='Timestamp resolution to match columns') | |
parser.add_argument('--time-path', default='context/time') | |
parser.add_argument('--message-path', default='message') | |
# TODO: arbitrary length | |
parser.add_argument('--first-path', default='context/tags') | |
parser.add_argument('--second-path', default='context/location/filename') | |
parser.add_argument('log_file', help='Log file to process') | |
args = parser.parse_args() | |
make_viewer(args) | |
server = WSGIServer(('', 5000), app) | |
server.serve_forever() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[[source]] | |
url = "https://pypi.org/simple" | |
verify_ssl = true | |
name = "pypi" | |
[dev-packages] | |
neovim = "*" | |
[packages] | |
flask = "*" | |
gevent = "*" | |
"jinja2" = "*" | |
greenlet = "*" | |
[requires] | |
python_version = "3.7" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"_meta": { | |
"hash": { | |
"sha256": "8f0f48695208bbce1b66edbcaa5f7a788f02305da313ded19ca04ac9b271f87f" | |
}, | |
"pipfile-spec": 6, | |
"requires": { | |
"python_version": "3.7" | |
}, | |
"sources": [ | |
{ | |
"name": "pypi", | |
"url": "https://pypi.org/simple", | |
"verify_ssl": true | |
} | |
] | |
}, | |
"default": { | |
"click": { | |
"hashes": [ | |
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", | |
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" | |
], | |
"version": "==7.0" | |
}, | |
"flask": { | |
"hashes": [ | |
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", | |
"sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" | |
], | |
"index": "pypi", | |
"version": "==1.0.2" | |
}, | |
"gevent": { | |
"hashes": [ | |
"sha256:1f277c5cf060b30313c5f9b91588f4c645e11839e14a63c83fcf6f24b1bc9b95", | |
"sha256:298a04a334fb5e3dcd6f89d063866a09155da56041bc94756da59db412cb45b1", | |
"sha256:30e9b2878d5b57c68a40b3a08d496bcdaefc79893948989bb9b9fab087b3f3c0", | |
"sha256:33533bc5c6522883e4437e901059fe5afa3ea74287eeea27a130494ff130e731", | |
"sha256:3f06f4802824c577272960df003a304ce95b3e82eea01dad2637cc8609c80e2c", | |
"sha256:419fd562e4b94b91b58cccb3bd3f17e1a11f6162fca6c591a7822bc8a68f023d", | |
"sha256:4ea938f44b882e02cca9583069d38eb5f257cc15a03e918980c307e7739b1038", | |
"sha256:51143a479965e3e634252a0f4a1ea07e5307cf8dc773ef6bf9dfe6741785fb4c", | |
"sha256:5bf9bd1dd4951552d9207af3168f420575e3049016b9c10fe0c96760ce3555b7", | |
"sha256:6004512833707a1877cc1a5aea90fd182f569e089bc9ab22a81d480dad018f1b", | |
"sha256:640b3b52121ab519e0980cb38b572df0d2bc76941103a697e828c13d76ac8836", | |
"sha256:6951655cc18b8371d823e81c700883debb0f633aae76f425dfeb240f76b95a67", | |
"sha256:71eeb8d9146e8131b65c3364bb760b097c21b7b9fdbec91bf120685a510f997a", | |
"sha256:7c899e5a6f94d6310352716740f05e41eb8c52d995f27fc01e90380913aa8f22", | |
"sha256:8465f84ba31aaf52a080837e9c5ddd592ab0a21dfda3212239ce1e1796f4d503", | |
"sha256:99de2e38dde8669dd30a8a1261bdb39caee6bd00a5f928d01dfdb85ab0502562", | |
"sha256:9fa4284b44bc42bef6e437488d000ae37499ccee0d239013465638504c4565b7", | |
"sha256:a1beea0443d3404c03e069d4c4d9fc13d8ec001771c77f9a23f01911a41f0e49", | |
"sha256:a66a26b78d90d7c4e9bf9efb2b2bd0af49234604ac52eaca03ea79ac411e3f6d", | |
"sha256:a94e197bd9667834f7bb6bd8dff1736fab68619d0f8cd78a9c1cebe3c4944677", | |
"sha256:ac0331d3a3289a3d16627742be9c8969f293740a31efdedcd9087dadd6b2da57", | |
"sha256:d26b57c50bf52fb38dadf3df5bbecd2236f49d7ac98f3cf32d6b8a2d25afc27f", | |
"sha256:fd23b27387d76410eb6a01ea13efc7d8b4b95974ba212c336e8b1d6ab45a9578" | |
], | |
"index": "pypi", | |
"version": "==1.3.7" | |
}, | |
"greenlet": { | |
"hashes": [ | |
"sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0", | |
"sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28", | |
"sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8", | |
"sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304", | |
"sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0", | |
"sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214", | |
"sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043", | |
"sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6", | |
"sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625", | |
"sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc", | |
"sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638", | |
"sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163", | |
"sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4", | |
"sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490", | |
"sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248", | |
"sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939", | |
"sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87", | |
"sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720", | |
"sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656" | |
], | |
"index": "pypi", | |
"version": "==0.4.15" | |
}, | |
"itsdangerous": { | |
"hashes": [ | |
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", | |
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" | |
], | |
"version": "==1.1.0" | |
}, | |
"jinja2": { | |
"hashes": [ | |
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", | |
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" | |
], | |
"index": "pypi", | |
"version": "==2.10" | |
}, | |
"markupsafe": { | |
"hashes": [ | |
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", | |
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", | |
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", | |
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", | |
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", | |
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", | |
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", | |
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", | |
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", | |
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", | |
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", | |
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", | |
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", | |
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", | |
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", | |
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", | |
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", | |
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", | |
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", | |
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", | |
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", | |
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", | |
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", | |
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", | |
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", | |
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", | |
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", | |
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" | |
], | |
"version": "==1.1.0" | |
}, | |
"werkzeug": { | |
"hashes": [ | |
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", | |
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" | |
], | |
"version": "==0.14.1" | |
} | |
}, | |
"develop": { | |
"greenlet": { | |
"hashes": [ | |
"sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0", | |
"sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28", | |
"sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8", | |
"sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304", | |
"sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0", | |
"sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214", | |
"sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043", | |
"sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6", | |
"sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625", | |
"sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc", | |
"sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638", | |
"sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163", | |
"sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4", | |
"sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490", | |
"sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248", | |
"sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939", | |
"sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87", | |
"sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720", | |
"sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656" | |
], | |
"index": "pypi", | |
"version": "==0.4.15" | |
}, | |
"msgpack": { | |
"hashes": [ | |
"sha256:102802a9433dcf36f939b632cce9dea87310b2f163bb37ffc8bc343677726e88", | |
"sha256:3055c44f39833b6edb27fd48028dc7822d1fd75bfeef8a2434caed8d62bb24ee", | |
"sha256:3b7fd45c8e9e537640f541d3699b1773cf5cb9345d4a75f93baa8f055084e59c", | |
"sha256:64abc6bf3a2ac301702f5760f4e6e227d0fd4d84d9014ef9a40faa9d43365259", | |
"sha256:6e962c4adc7970af5a3d6a4f9bb87c617b1bd041fd9ab42355a263d421017ed9", | |
"sha256:72259661a83f8b08ef6ee83927ce4937f841226735824af5b10a536d886eeb36", | |
"sha256:78e297c3996fd9f35090fbddd1c148c2a71e0d6024500bcf3af90a4b9698bc19", | |
"sha256:85f1342b9d7549dd3daf494100d47a3dc7daae703cdbfc2c9ee7bbdc8a492cba", | |
"sha256:8ce9f88b6cb75d74eda2a5522e5c2e5ec0f17fd78605d6502abb61f46b306865", | |
"sha256:8d0af8d64198e4b4f942a15ea9cb0dd9c4a0bd3e4e2ba57425e108bdbd4c3a0f", | |
"sha256:9936ce3a530ca78db60b6631003b5f4ba383cfb1d9830a27d1b5c61857226e2f", | |
"sha256:b688721df31c4bad6f508fb262719eb7e4a3532024c66d3c44ad6a4704519dda", | |
"sha256:c28478328e9cd868ce54e8465eae9fa3605790450c66cc7e8bc416526917ef6e", | |
"sha256:cb4e228f3d93779a1d77a1e9d72759b79dfa2975c1a5bd2a090eaa98239fa4b1", | |
"sha256:d03d0b6e4adf5bd1cbf7a81a20a56c883351947a57b7b85235181b057adf1120", | |
"sha256:d2b179faebd278e5f4e255a6bbc7ccb467f02ed5c4c00c8a68dc926002223a20", | |
"sha256:f1a8f7bd84be103979a73da57be3cb929d702a656162ee466597b816fa9eec97" | |
], | |
"version": "==0.6.0" | |
}, | |
"neovim": { | |
"hashes": [ | |
"sha256:a6a0e7a5b4433bf4e6ddcbc5c5ff44170be7d84259d002b8e8d8fb4ee78af60f" | |
], | |
"index": "pypi", | |
"version": "==0.3.1" | |
}, | |
"pynvim": { | |
"hashes": [ | |
"sha256:dd881595055869c2de770517d403faf40d31aa991db2472a1843ff17db47b0fb" | |
], | |
"version": "==0.3.1" | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment