mirror of
https://github.com/TeamNewPipe/CrashReportImporter
synced 2025-10-06 00:22:39 +02:00
Rewrite most of the project as an LMTP server
This huge commit turns this project into an LMTP server to which Postfix can forward all mails directly. This is a lot more efficient than having the MTA send them to the MDA, save them, then fetch them via IMAP. It's a form of stream processing. It uses asyncio wherever possible to improve the parallel handling of mails and make the control flow easier than, say, having a message queue. We also have a relatively low mail volume (at most 2-3 mails per minute to this address) at the moment, so this would be overkill as well. Adding some scalability by spawning additional workers would not be too hard, though. This project now also targets GlitchTip instead of Sentry, as Sentry is non-free software now. GlitchTip is not very well documented at this point, but works quite well once you know where you need to fiddle. Unfortunately, it hardly reports errors to the user, both on the API as well as in the frontend, so one has to watch the logs a bit. Also, the Sentry docs, especially the SDK docs, don't apply 100% to GlitchTip. The rewrite further slightly improves the parsing of the stacktraces, making them look a lot more useful in GlitchTip compared to the old Sentry setup, and improving the bug aggregation a lot. A lot of research and trial and error went into this, but after importing the last 30 days worth of mail into the system with a little Python script, it seems a lot better than before. A lot of the parsing is still basically the old code, maybe with a little fixes and tweaks. The data representation in the GlitchTip storage has been rewritten entirely. The package was restructured to fix some of the mess, but there's still potential for improvements.
This commit is contained in:
@@ -4,3 +4,8 @@ Newpipe Crash Report Importer
|
||||
|
||||
See README.md for more information.
|
||||
"""
|
||||
|
||||
from .database_entry import DatabaseEntry
|
||||
from .lmtp_server import LmtpController, CrashReportHandler
|
||||
from .message import Message
|
||||
from .storage import DirectoryStorage, GlitchtipStorage
|
||||
|
51
newpipe_crash_report_importer/database_entry.py
Normal file
51
newpipe_crash_report_importer/database_entry.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from datetime import datetime
|
||||
from email.utils import parsedate_to_datetime
|
||||
from hashlib import sha256
|
||||
|
||||
from .message import Message
|
||||
|
||||
|
||||
class DatabaseEntry:
|
||||
def __init__(self, rfc822_message):
|
||||
self.message = Message(rfc822_message)
|
||||
|
||||
# from is just needed for calculating the SHA256 hash below
|
||||
self.from_ = rfc822_message["from"]
|
||||
self.to = rfc822_message["to"]
|
||||
|
||||
self.plaintext = self.message.plaintext
|
||||
self.newpipe_exception_info = self.message.embedded_json
|
||||
|
||||
try:
|
||||
# try to use the date given by the crash report
|
||||
self.date = datetime.strptime(
|
||||
self.newpipe_exception_info["time"], "%Y-%m-%d %H:%M"
|
||||
)
|
||||
if self.date.year < 2010:
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
# try to use the date from the mail header
|
||||
self.date = parsedate_to_datetime(rfc822_message["date"])
|
||||
if self.date.year < 2010:
|
||||
self.date = self.message.date_from_received_headers()
|
||||
print(self.date)
|
||||
|
||||
def to_dict(self):
|
||||
# we don't store the From header, as it's not needed for potential re-imports of the database, but could be
|
||||
# used to identify the senders after a long time
|
||||
# in fact, senders weren't stored in the production system either, but this never got committed to the
|
||||
# repository... D'oh!
|
||||
return {
|
||||
"to": self.to,
|
||||
"timestamp": int(self.date.timestamp()),
|
||||
"plaintext": self.plaintext,
|
||||
"newpipe-exception-info": self.newpipe_exception_info,
|
||||
}
|
||||
|
||||
def hash_id(self):
|
||||
hash = sha256((str(self.from_) + str(self.to)).encode())
|
||||
hash.update(self.date.strftime("%Y%m%d%H%M%S").encode())
|
||||
return hash.hexdigest()
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.from_, self.to, self.date))
|
10
newpipe_crash_report_importer/exceptions.py
Normal file
10
newpipe_crash_report_importer/exceptions.py
Normal file
@@ -0,0 +1,10 @@
|
||||
class ParserError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class StorageError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoPlaintextMessageFoundError(Exception):
|
||||
pass
|
61
newpipe_crash_report_importer/lmtp_server.py
Normal file
61
newpipe_crash_report_importer/lmtp_server.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import traceback
|
||||
from email.parser import Parser
|
||||
|
||||
import sentry_sdk
|
||||
from aiosmtpd.controller import Controller
|
||||
from aiosmtpd.lmtp import LMTP
|
||||
from aiosmtpd.smtp import Envelope
|
||||
|
||||
|
||||
class LmtpController(Controller):
|
||||
"""
|
||||
A custom controller implementation, return LMTP instances instead of SMTP ones.
|
||||
Inspired by GNU Mailman 3"s LMTPController.
|
||||
"""
|
||||
|
||||
def factory(self):
|
||||
return LMTP(self.handler, ident="NewPipe crash report importer")
|
||||
|
||||
|
||||
class CrashReportHandler:
|
||||
"""
|
||||
Very simple handler which only accepts mail for allowed addresses and stores them into the Sentry database.
|
||||
"""
|
||||
|
||||
def __init__(self, callback: callable):
|
||||
self.callback = callback
|
||||
|
||||
async def handle_RCPT(
|
||||
self, server, session, envelope: Envelope, address: str, rcpt_options
|
||||
):
|
||||
if address not in ["crashreport@newpipe.net", "crashreport@newpipe.schabi.org"]:
|
||||
return f"550 not handling mail for address {address}"
|
||||
|
||||
envelope.rcpt_tos.append(address)
|
||||
return "250 OK"
|
||||
|
||||
@staticmethod
|
||||
def convert_to_rfc822_message(envelope: Envelope):
|
||||
return Parser().parsestr(envelope.content.decode())
|
||||
|
||||
async def handle_DATA(self, server, session, envelope: Envelope):
|
||||
try:
|
||||
message = self.convert_to_rfc822_message(envelope)
|
||||
|
||||
# as the volume of incoming mails is relatively low (< 3 per minute usually) and reporting doesn't take
|
||||
# very long, we can just do it here and don't require some message queue/worker setup
|
||||
# the callback is defined as async, but can, due to the low volume, be implemented synchronously, too
|
||||
await self.callback(message)
|
||||
|
||||
except:
|
||||
# in case an exception happens in the callback (e.g., the message can't be parsed correctly), we don't
|
||||
# want to notify the sending MTA, but have them report success of delivery
|
||||
# it's after all not their problem: if they got so far, the message was indeed delivered to our LMTP server
|
||||
# however, we want the exception to show up in the log
|
||||
traceback.print_exc()
|
||||
|
||||
# also, we want to report all kinds of issues to GlitchTip
|
||||
sentry_sdk.capture_exception()
|
||||
|
||||
# make sure all control flow paths return a string reply!
|
||||
return "250 Message accepted for delivery"
|
@@ -1,63 +0,0 @@
|
||||
import email
|
||||
import imaplib
|
||||
|
||||
|
||||
class IMAPClientError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def fetch_messages_from_imap(host, port, username, password):
|
||||
"""
|
||||
Opens a connection to an IMAP server, downloads the relevant messages and
|
||||
archives processed mails in `Archives/Crashreport` directory on the
|
||||
IMAP server.
|
||||
|
||||
:param host: IMAP server hostname
|
||||
:param port: IMAP server port
|
||||
:param username: IMAP login username
|
||||
:param password: IMAP login password
|
||||
|
||||
:return: a generator iterating over all mails.
|
||||
"""
|
||||
|
||||
with imaplib.IMAP4(host, port=port) as client:
|
||||
client.starttls()
|
||||
client.login(username, password)
|
||||
client.select("INBOX", readonly=False)
|
||||
|
||||
client.create("Archives")
|
||||
client.create("Archives/Crashreport")
|
||||
|
||||
sorted_reply = client.uid("SORT", "(DATE)", "UTF7", "ALL")
|
||||
|
||||
if not sorted_reply[0] == "OK":
|
||||
raise IMAPClientError()
|
||||
|
||||
sorted_messages = sorted_reply[1][0].split()
|
||||
|
||||
for msg_uid in sorted_messages:
|
||||
reply = client.uid("FETCH", msg_uid, "(RFC822)")
|
||||
|
||||
if reply[0] != "OK":
|
||||
raise IMAPClientError()
|
||||
|
||||
message = email.message_from_bytes(reply[1][0][1])
|
||||
|
||||
yield message
|
||||
|
||||
# mark message as read and move to archives
|
||||
mark_read_reply = client.uid("STORE", msg_uid, "+FLAGS", "(\\Seen)")
|
||||
if mark_read_reply[0] != "OK":
|
||||
raise IMAPClientError()
|
||||
|
||||
# moving messages in IMAP unfortunately means copy and delete
|
||||
copy_reply = client.uid("COPY", msg_uid, "Archives/Crashreport")
|
||||
if copy_reply[0] != "OK":
|
||||
raise IMAPClientError()
|
||||
|
||||
delete_reply = client.uid("STORE", msg_uid, "+FLAGS", "(\\Deleted)")
|
||||
if delete_reply[0] != "OK":
|
||||
raise IMAPClientError()
|
||||
|
||||
# delete the message immediately
|
||||
client.expunge()
|
100
newpipe_crash_report_importer/message.py
Normal file
100
newpipe_crash_report_importer/message.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
import unicodedata
|
||||
from email.message import EmailMessage
|
||||
from email.utils import parsedate_to_datetime
|
||||
|
||||
import bleach
|
||||
|
||||
from .exceptions import ParserError
|
||||
|
||||
|
||||
class Message:
|
||||
"""
|
||||
Represents an incoming mail fetched from the IMAP server.
|
||||
"""
|
||||
|
||||
possible_charsets = [
|
||||
"ascii",
|
||||
"utf-8",
|
||||
"windows-1252",
|
||||
]
|
||||
|
||||
def __init__(self, rfc822_message: EmailMessage):
|
||||
self.rfc822_message = rfc822_message
|
||||
self.plaintext_or_html_part = self.get_plaintext_or_html_part()
|
||||
payload = self.plaintext_or_html_part.get_payload(decode=True)
|
||||
|
||||
for charset in self.possible_charsets:
|
||||
try:
|
||||
decoded_payload = payload.decode(charset)
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
else:
|
||||
break
|
||||
else:
|
||||
raise ParserError("Could not decode message payload")
|
||||
|
||||
self.plaintext = self.sanitize_message(decoded_payload)
|
||||
self.embedded_json = self.extract_json_from_string(self.plaintext)
|
||||
|
||||
def get_plaintext_or_html_part(self):
|
||||
"""
|
||||
Searches for the first part in the multipart RFC822 message whose
|
||||
content type is text/plain or text/html.
|
||||
|
||||
:return: The part or None
|
||||
:rtype: class:`email.message.Message`
|
||||
"""
|
||||
for part in self.rfc822_message.walk():
|
||||
if part.get_content_type() in ("text/plain", "text/html"):
|
||||
return part
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def sanitize_message(original_data):
|
||||
normalized = unicodedata.normalize("NFKD", original_data)
|
||||
decoded = html.unescape(normalized)
|
||||
sanitized = bleach.clean(decoded, tags=[], attributes={}, styles=[], strip=True)
|
||||
return unicodedata.normalize("NFKD", sanitized)
|
||||
|
||||
@staticmethod
|
||||
def extract_json_from_string(json_string):
|
||||
"""
|
||||
Attemt to fix all the shit "intelligent" mail clients do to the plain
|
||||
# text data NewPipe gives them.
|
||||
Although it's a really BAD idea to sanitize untrusted data, we'll give it
|
||||
a try - as long as there's no bugs in the JSON parser, it should be safe
|
||||
to do this.
|
||||
"""
|
||||
|
||||
match = re.search("({.*})", json_string, re.MULTILINE + re.DOTALL)
|
||||
|
||||
if match:
|
||||
try:
|
||||
data = match.group(1)
|
||||
data = unicodedata.normalize("NFKD", data)
|
||||
return json.loads(data, strict=False)
|
||||
except json.JSONDecodeError:
|
||||
raise ParserError("Could not parse JSON in given data")
|
||||
else:
|
||||
raise ParserError("Could not find JSON in given data")
|
||||
|
||||
def date_from_received_headers(self):
|
||||
headers = self.rfc822_message.get_all("received")
|
||||
header = None
|
||||
|
||||
for _h in headers:
|
||||
for domain in ["mail.orange-it.de", "mail.commandnotfound.org"]:
|
||||
if "by %s (Dovecot) with LMTP id" % domain in _h:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
header = _h
|
||||
break
|
||||
|
||||
date = parsedate_to_datetime(header.split(";")[-1].strip())
|
||||
|
||||
return date
|
@@ -37,7 +37,7 @@ class Message:
|
||||
|
||||
def __init__(self, rfc822_message):
|
||||
self.rfc822_message = rfc822_message
|
||||
self. plaintext_or_html_part = self.get_plaintext_or_html_part()
|
||||
self.plaintext_or_html_part = self.get_plaintext_or_html_part()
|
||||
payload = self.plaintext_or_html_part.get_payload(decode=True)
|
||||
|
||||
for charset in self.possible_charsets:
|
||||
@@ -71,8 +71,7 @@ class Message:
|
||||
def sanitize_message(original_data):
|
||||
normalized = unicodedata.normalize("NFKD", original_data)
|
||||
decoded = html.unescape(normalized)
|
||||
sanitized = bleach.clean(decoded,
|
||||
tags=[], attributes={}, styles=[], strip=True)
|
||||
sanitized = bleach.clean(decoded, tags=[], attributes={}, styles=[], strip=True)
|
||||
return unicodedata.normalize("NFKD", sanitized)
|
||||
|
||||
@staticmethod
|
||||
@@ -97,7 +96,6 @@ class Message:
|
||||
else:
|
||||
raise ParserError("Could not find JSON in given data")
|
||||
|
||||
|
||||
def date_from_received_headers(self):
|
||||
headers = self.rfc822_message.get_all("received")
|
||||
header = None
|
||||
@@ -128,8 +126,9 @@ class DatabaseEntry:
|
||||
|
||||
try:
|
||||
# try to use the date given by the crash report
|
||||
self.date = datetime.strptime(self.newpipe_exception_info["time"],
|
||||
"%Y-%m-%d %H:%M")
|
||||
self.date = datetime.strptime(
|
||||
self.newpipe_exception_info["time"], "%Y-%m-%d %H:%M"
|
||||
)
|
||||
if self.date.year < 2010:
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
@@ -178,7 +177,9 @@ class DirectoryStorage(Storage):
|
||||
|
||||
def save(self, entry: DatabaseEntry):
|
||||
message_id = entry.hash_id() + ".json"
|
||||
subdir = os.path.join(self.directory, message_id[0], message_id[:3], message_id[:5])
|
||||
subdir = os.path.join(
|
||||
self.directory, message_id[0], message_id[:3], message_id[:5]
|
||||
)
|
||||
os.makedirs(subdir, exist_ok=True)
|
||||
path = os.path.join(subdir, message_id)
|
||||
if not os.path.isfile(path):
|
||||
@@ -200,9 +201,10 @@ class SentryStorage(Storage):
|
||||
def __init__(self, dsn: str, package: str):
|
||||
self.dsn = raven.conf.remote.RemoteConfig.from_string(dsn)
|
||||
valid_chars = string.ascii_letters + string.digits
|
||||
self.db_fname = "".join(filter(lambda s: s in valid_chars,
|
||||
self.dsn.store_endpoint)) + \
|
||||
".stored.txt"
|
||||
self.db_fname = (
|
||||
"".join(filter(lambda s: s in valid_chars, self.dsn.store_endpoint))
|
||||
+ ".stored.txt"
|
||||
)
|
||||
self.package = package
|
||||
|
||||
def make_sentry_exception(self, entry: DatabaseEntry):
|
||||
@@ -295,7 +297,13 @@ class SentryStorage(Storage):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
for key in ["os", "service", "content_language", "content_country", "app_language"]:
|
||||
for key in [
|
||||
"os",
|
||||
"service",
|
||||
"content_language",
|
||||
"content_country",
|
||||
"app_language",
|
||||
]:
|
||||
try:
|
||||
rv["tags"][key] = newpipe_exc_info[key]
|
||||
except KeyError:
|
||||
@@ -330,21 +338,29 @@ class SentryStorage(Storage):
|
||||
|
||||
data = self.make_sentry_exception(entry)
|
||||
|
||||
auth_header = """
|
||||
auth_header = (
|
||||
"""
|
||||
Sentry sentry_version=5,
|
||||
sentry_client=newpipe-mail-reporter_0.0.1,
|
||||
sentry_key={pubkey},
|
||||
sentry_secret={privkey}
|
||||
""".replace("\n", "").format(
|
||||
timestamp=str(entry.date.timestamp()),
|
||||
pubkey=self.dsn.public_key,
|
||||
privkey=self.dsn.secret_key
|
||||
).strip()
|
||||
""".replace(
|
||||
"\n", ""
|
||||
)
|
||||
.format(
|
||||
timestamp=str(entry.date.timestamp()),
|
||||
pubkey=self.dsn.public_key,
|
||||
privkey=self.dsn.secret_key,
|
||||
)
|
||||
.strip()
|
||||
)
|
||||
|
||||
request = requests.Request("POST",
|
||||
self.dsn.store_endpoint,
|
||||
headers={"X-Sentry-Auth": auth_header},
|
||||
data=json.dumps(data))
|
||||
request = requests.Request(
|
||||
"POST",
|
||||
self.dsn.store_endpoint,
|
||||
headers={"X-Sentry-Auth": auth_header},
|
||||
data=json.dumps(data),
|
||||
)
|
||||
response = requests.Session().send(request.prepare())
|
||||
response.raise_for_status()
|
||||
|
||||
|
3
newpipe_crash_report_importer/storage/__init__.py
Normal file
3
newpipe_crash_report_importer/storage/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .base import Storage
|
||||
from .directory_storage import DirectoryStorage
|
||||
from .glitchtip_storage import GlitchtipStorage
|
10
newpipe_crash_report_importer/storage/base.py
Normal file
10
newpipe_crash_report_importer/storage/base.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from newpipe_crash_report_importer.database_entry import DatabaseEntry
|
||||
|
||||
|
||||
class Storage:
|
||||
"""
|
||||
Storage base class. Uses async I/O if possible
|
||||
"""
|
||||
|
||||
async def save(self, entry: DatabaseEntry) -> None:
|
||||
raise NotImplementedError()
|
29
newpipe_crash_report_importer/storage/directory_storage.py
Normal file
29
newpipe_crash_report_importer/storage/directory_storage.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from ..database_entry import DatabaseEntry
|
||||
from ..storage.base import Storage
|
||||
|
||||
|
||||
class DirectoryStorage(Storage):
|
||||
"""
|
||||
Local storage implementation. Puts every database entry in a file named
|
||||
by their hash ID in a directory.
|
||||
"""
|
||||
|
||||
def __init__(self, directory: str):
|
||||
self.directory = os.path.abspath(directory)
|
||||
os.makedirs(self.directory, exist_ok=True)
|
||||
|
||||
async def save(self, entry: DatabaseEntry):
|
||||
message_id = entry.hash_id() + ".json"
|
||||
subdir = os.path.join(
|
||||
self.directory, message_id[0], message_id[:3], message_id[:5]
|
||||
)
|
||||
os.makedirs(subdir, exist_ok=True)
|
||||
path = os.path.join(subdir, message_id)
|
||||
if not os.path.isfile(path):
|
||||
with open(path, "w") as f:
|
||||
json.dump(entry.to_dict(), f, indent=2)
|
||||
else:
|
||||
print("Entry already stored in directory -> skipped")
|
343
newpipe_crash_report_importer/storage/glitchtip_storage.py
Normal file
343
newpipe_crash_report_importer/storage/glitchtip_storage.py
Normal file
@@ -0,0 +1,343 @@
|
||||
import hashlib
|
||||
import re
|
||||
from typing import List, Union, Optional
|
||||
|
||||
import aiohttp
|
||||
from sentry_sdk.utils import Dsn
|
||||
|
||||
from . import Storage
|
||||
from ..database_entry import DatabaseEntry
|
||||
from ..exceptions import StorageError, ParserError
|
||||
|
||||
|
||||
class SentryFrame:
|
||||
"""
|
||||
Represents a Sentry stack frame payload.
|
||||
|
||||
Mostly based on a mix of reading the Sentry SDK docs, GlitchTip example data and a lot of trial-and-error.
|
||||
|
||||
Implements the value object pattern.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, filename: str, function: str, package: str, lineno: Optional[int] = None
|
||||
):
|
||||
# all the attributes in stack frames are optional
|
||||
|
||||
# in case of NewPipe, we require filename and function and package
|
||||
self.filename = filename
|
||||
self.function = function
|
||||
self.package = package
|
||||
|
||||
# line number is optional, as for builtins (java.*), we don't have any
|
||||
self.lineno = lineno
|
||||
|
||||
def to_dict(self):
|
||||
# GlitchTip doesn't care if optional data is set to null, so we don't even have to implement checks for that
|
||||
rv = {
|
||||
"filename": self.filename,
|
||||
"function": self.function,
|
||||
"package": self.package,
|
||||
"lineno": self.lineno,
|
||||
# for the sake of simplicity, we just say "every frame belongs to the app"
|
||||
"in_app": True,
|
||||
}
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
class SentryStacktrace:
|
||||
"""
|
||||
Represents a Sentry stacktrace payload.
|
||||
|
||||
Mostly based on a mix of reading the Sentry SDK docs, GlitchTip example data and a lot of trial-and-error.
|
||||
|
||||
Implements the value object pattern.
|
||||
"""
|
||||
|
||||
def __init__(self, frames: List[SentryFrame]):
|
||||
# the only mandatory element is the stack frames
|
||||
# we don't require any register values
|
||||
self.frames = frames
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"frames": [f.to_dict() for f in self.frames],
|
||||
}
|
||||
|
||||
|
||||
class SentryException:
|
||||
"""
|
||||
Represents a Sentry exception payload.
|
||||
|
||||
Mostly based on a mix of reading the Sentry SDK docs, GlitchTip example data and a lot of trial-and-error.
|
||||
|
||||
Implements the value object pattern.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, type: str, value: str, module: str, stacktrace: SentryStacktrace
|
||||
):
|
||||
# these are mandatory per the format description
|
||||
self.type = type
|
||||
# value appears to be the exception's message
|
||||
self.value = value
|
||||
|
||||
# the fields module, thread_id, mechanism and stacktrace are optional
|
||||
# we send the java package name as module, and a parsed stacktrace via stacktrace
|
||||
self.module = module
|
||||
self.stacktrace = stacktrace
|
||||
|
||||
def to_json(self) -> dict:
|
||||
# format description: https://develop.sentry.dev/sdk/event-payloads/exception/
|
||||
return {
|
||||
"type": self.type,
|
||||
"value": self.value,
|
||||
"stacktrace": self.stacktrace.to_dict(),
|
||||
}
|
||||
|
||||
|
||||
class SentryPayload:
|
||||
"""
|
||||
Represents a Sentry event payload, sent to the GlitchTip instance.
|
||||
|
||||
Mostly based on a mix of reading the Sentry SDK docs, GlitchTip example data and a lot of trial-and-error.
|
||||
|
||||
This class doesn't strictly implement the value object, as some attributes are optional and can and shall be
|
||||
mutated by the caller. The list of attributes initialized below, however, is constant.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
event_id: str,
|
||||
timestamp: Union[str, int],
|
||||
message: str,
|
||||
exception: SentryException,
|
||||
):
|
||||
# as we calculate hashes anyway for the directory storage, we probably should just use those as IDs here, too
|
||||
# this allows cross-referencing events in both storage implementations, which might be important for re-imports
|
||||
# of the database
|
||||
# first, try to make sure we receive an actual SHA256 hash
|
||||
assert len(event_id) == 64
|
||||
|
||||
# this is supposed to be a UUID4 (i.e., random) identifier, hence the limit to 32 characters (without dashes)
|
||||
# however, those are SHA256 hashes, which means their hex digests have a length of 64 characters
|
||||
# therefore, we derive a 32-character size MD5 hash from the SHA256 one
|
||||
self.event_id = hashlib.md5(event_id.encode()).hexdigest()
|
||||
assert len(self.event_id) == 32
|
||||
|
||||
# this could either be implemented as a RFC3339 string, or some numeric UNIX epoch style timestamp
|
||||
self.timestamp = timestamp
|
||||
|
||||
# will be used as the value for the "formatted" key in the message interface
|
||||
self.message = message
|
||||
|
||||
#
|
||||
self.exception = exception
|
||||
|
||||
# these are optional attributes according to the format description
|
||||
# IIRC, we had to explicitly these to null in order to avoid Sentry from guesstimating their values
|
||||
# some of the values may be populated by users after initializing the object
|
||||
self.extra = {
|
||||
"user_comment": None,
|
||||
"request": None,
|
||||
"user_action": None,
|
||||
}
|
||||
self.tags = {
|
||||
"os": None,
|
||||
"service": None,
|
||||
"content_language": None,
|
||||
}
|
||||
self.release: Optional[str] = None
|
||||
|
||||
@staticmethod
|
||||
def _render_sdk():
|
||||
# format description: https://develop.sentry.dev/sdk/event-payloads/sdk/
|
||||
return {
|
||||
"name": "newpipe.crashreportimporter",
|
||||
# we don't really care at all about the version, but it's supposed to be semver
|
||||
"version": "0.0.1",
|
||||
}
|
||||
|
||||
def _render_exceptions(self):
|
||||
return {"values": [self.exception.to_json()]}
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
# the Sentry API requires the keys event_id, timestamp and platform to be set
|
||||
# optional keys we want to use for some additional convenience are release, tags, and extra
|
||||
# future versions might use fingerprint as well to help with the deduplication of the events
|
||||
rv = {
|
||||
"event_id": self.event_id,
|
||||
"timestamp": self.timestamp,
|
||||
# setting the right platform apparently enables some convenience functionality in Sentry
|
||||
# Java seems the most suitable for Android stuff
|
||||
"platform": "java",
|
||||
# doesn't seem to be contained in any of the examples in glitchtip-backend/events/test_data any more
|
||||
# but still works, apparently (and is required by GlitchTip)
|
||||
"message": self.message,
|
||||
# Sentry apparently now allows for more than one exception to be passed (i.e., when an exception is
|
||||
# caused by another exception)
|
||||
# GlitchTip seems to support that, too, looking at their example data
|
||||
# therefore, the singular is not really appropriate and misleading
|
||||
"exception": self._render_exceptions(),
|
||||
"extra": self.extra,
|
||||
"tags": self.tags,
|
||||
# sending None/null in case this won't cause any issues, so we can be lazy here
|
||||
"release": self.release,
|
||||
# for some annoying reason, GlitchTip insists on us specifying an SDK
|
||||
"sdk": self._render_sdk(),
|
||||
# we only report errors to GlitchTip (it's also the default value)
|
||||
"level": "error",
|
||||
}
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
class GlitchtipStorage(Storage):
|
||||
"""
|
||||
Used to store incoming mails on a GlitchTip server.
|
||||
https://app.glitchtip.com/docs/
|
||||
|
||||
Remembers already sent mail reports by putting their hash IDs in a file
|
||||
in the application's working directory.
|
||||
"""
|
||||
|
||||
def __init__(self, dsn: str, package: str):
|
||||
self.sentry_auth = Dsn(dsn).to_auth()
|
||||
self.package = package
|
||||
|
||||
def make_sentry_payload(self, entry: DatabaseEntry):
|
||||
newpipe_exc_info = entry.newpipe_exception_info
|
||||
|
||||
frames: List[SentryFrame] = []
|
||||
|
||||
try:
|
||||
raw_data = "".join(newpipe_exc_info["exceptions"])
|
||||
except KeyError:
|
||||
raise StorageError("'exceptions' key missing in JSON body")
|
||||
|
||||
raw_frames = raw_data.replace("\n", " ").replace("\r", " ").split("\tat")
|
||||
|
||||
# pretty ugly, but that's what we receive from NewPipe
|
||||
# both message and exception name are contained in the first item in the frames
|
||||
message = raw_frames[0]
|
||||
|
||||
for raw_frame in raw_frames[1:]:
|
||||
# some very basic sanitation, as e-mail clients all suck
|
||||
raw_frame = raw_frame.strip()
|
||||
|
||||
# _very_ basic but gets the job done well enough
|
||||
frame_match = re.search(r"(.+)\(([a-zA-Z0-9:.\s]+)\)", raw_frame)
|
||||
|
||||
if frame_match:
|
||||
module_path = frame_match.group(1).split(".")
|
||||
filename_and_lineno = frame_match.group(2)
|
||||
|
||||
if ":" in filename_and_lineno:
|
||||
# "unknown source" is shown for lambda functions
|
||||
filename_and_lineno_match = re.search(
|
||||
r"(Unknown\s+Source|(?:[a-zA-Z]+\.(?:kt|java)+)):([0-9]+)",
|
||||
filename_and_lineno,
|
||||
)
|
||||
|
||||
if not filename_and_lineno_match:
|
||||
raise ValueError(
|
||||
f"could not find filename and line number in string {frame_match.group(2)}"
|
||||
)
|
||||
|
||||
# we want just two matches, anything else would be an error in the regex
|
||||
assert len(filename_and_lineno_match.groups()) == 2
|
||||
|
||||
frame = SentryFrame(
|
||||
filename_and_lineno_match.group(1),
|
||||
module_path[-1],
|
||||
".".join(module_path[:-1]),
|
||||
lineno=int(filename_and_lineno_match.group(2)),
|
||||
)
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
else:
|
||||
# apparently a native exception, so we don't have a line number
|
||||
frame = SentryFrame(
|
||||
frame_match.group(2),
|
||||
module_path[-1],
|
||||
".".join(module_path[:-1]),
|
||||
)
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
else:
|
||||
raise ParserError("Could not parse frame: '{}'".format(raw_frame))
|
||||
|
||||
try:
|
||||
type = message.split(":")[0].split(".")[-1]
|
||||
value = message.split(":")[1]
|
||||
module = ".".join(message.split(":")[0].split(".")[:-1])
|
||||
|
||||
except IndexError:
|
||||
type = value = module = "<none>"
|
||||
|
||||
timestamp = entry.date.timestamp()
|
||||
|
||||
# set up the payload, with all intermediary value objects
|
||||
stacktrace = SentryStacktrace(frames)
|
||||
exception = SentryException(type, value, module, stacktrace)
|
||||
|
||||
# TODO: support multiple exceptions to support "Caused by:"
|
||||
payload = SentryPayload(entry.hash_id(), timestamp, message, exception)
|
||||
|
||||
# try to fill in as much optional data as possible
|
||||
|
||||
try:
|
||||
# in Sentry, releases are now supposed to be unique organization wide
|
||||
# in GlitchTip, however, they seem to be regarded as tags, so this should work well enough
|
||||
payload.release = entry.newpipe_exception_info["version"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
for key in ["user_comment", "request", "user_action"]:
|
||||
try:
|
||||
payload.extra[key] = newpipe_exc_info[key]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
for key in ["os", "service", "content_language"]:
|
||||
try:
|
||||
payload.tags[key] = newpipe_exc_info[key]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
package = newpipe_exc_info["package"]
|
||||
except KeyError:
|
||||
package = None
|
||||
|
||||
if package is not None:
|
||||
if package != self.package:
|
||||
raise ValueError("Package name not allowed: %s" % package)
|
||||
else:
|
||||
payload.tags["package"] = newpipe_exc_info["package"]
|
||||
|
||||
return payload
|
||||
|
||||
async def save(self, entry: DatabaseEntry):
|
||||
exception = self.make_sentry_payload(entry)
|
||||
data = exception.to_dict()
|
||||
|
||||
# we use Sentry SDK's auth helper object to calculate both the required auth header as well as the URL from the
|
||||
# DSN string we already created a Dsn object for
|
||||
url = self.sentry_auth.store_api_url
|
||||
|
||||
# it would be great if the Auth object just had a method to create/update a headers dict
|
||||
headers = {
|
||||
"X-Sentry-Auth": str(self.sentry_auth.to_header()),
|
||||
# user agent isn't really necessary, but sentry-sdk sets it, too, so... why not
|
||||
"User-Agent": "NewPipe Crash Report Importer",
|
||||
# it's recommended by the Sentry docs to send a valid MIME type
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, data=data, headers=headers) as response:
|
||||
response.raise_for_status()
|
839
poetry.lock
generated
Normal file
839
poetry.lock
generated
Normal file
@@ -0,0 +1,839 @@
|
||||
[[package]]
|
||||
name = "aiodns"
|
||||
version = "2.0.0"
|
||||
description = "Simple DNS resolver for asyncio"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
pycares = ">=3.0.0"
|
||||
typing = {version = "*", markers = "python_version < \"3.7\""}
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.7.3"
|
||||
description = "Async http client/server framework (asyncio)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
aiodns = {version = "*", optional = true, markers = "extra == \"speedups\""}
|
||||
async-timeout = ">=3.0,<4.0"
|
||||
attrs = ">=17.3.0"
|
||||
brotlipy = {version = "*", optional = true, markers = "extra == \"speedups\""}
|
||||
cchardet = {version = "*", optional = true, markers = "extra == \"speedups\""}
|
||||
chardet = ">=2.0,<4.0"
|
||||
idna-ssl = {version = ">=1.0", markers = "python_version < \"3.7\""}
|
||||
multidict = ">=4.5,<7.0"
|
||||
typing-extensions = ">=3.6.5"
|
||||
yarl = ">=1.0,<2.0"
|
||||
|
||||
[package.extras]
|
||||
speedups = ["aiodns", "brotlipy", "cchardet"]
|
||||
|
||||
[[package]]
|
||||
name = "aiosmtpd"
|
||||
version = "1.2.2"
|
||||
description = "aiosmtpd - asyncio based SMTP server"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
atpublic = "*"
|
||||
|
||||
[[package]]
|
||||
name = "appdirs"
|
||||
version = "1.4.4"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "async-timeout"
|
||||
version = "3.0.1"
|
||||
description = "Timeout context manager for asyncio programs"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5.3"
|
||||
|
||||
[[package]]
|
||||
name = "atpublic"
|
||||
version = "2.1.2"
|
||||
description = "public -- @public for populating __all__"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
typing_extensions = {version = "*", markers = "python_version < \"3.8\""}
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "20.3.0"
|
||||
description = "Classes Without Boilerplate"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[package.extras]
|
||||
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"]
|
||||
docs = ["furo", "sphinx", "zope.interface"]
|
||||
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
|
||||
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "20.8b1"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
appdirs = "*"
|
||||
click = ">=7.1.2"
|
||||
dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""}
|
||||
mypy-extensions = ">=0.4.3"
|
||||
pathspec = ">=0.6,<1"
|
||||
regex = ">=2020.1.8"
|
||||
toml = ">=0.10.1"
|
||||
typed-ast = ">=1.4.0"
|
||||
typing-extensions = ">=3.7.4"
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
|
||||
|
||||
[[package]]
|
||||
name = "bleach"
|
||||
version = "3.2.3"
|
||||
description = "An easy safelist-based HTML-sanitizing tool."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.dependencies]
|
||||
packaging = "*"
|
||||
six = ">=1.9.0"
|
||||
webencodings = "*"
|
||||
|
||||
[[package]]
|
||||
name = "brotlipy"
|
||||
version = "0.7.0"
|
||||
description = "Python binding to the Brotli library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
cffi = ">=1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "cchardet"
|
||||
version = "2.1.7"
|
||||
description = "cChardet is high speed universal character encoding detector."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2020.12.5"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.14.4"
|
||||
description = "Foreign Function Interface for Python calling C code."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
pycparser = "*"
|
||||
|
||||
[[package]]
|
||||
name = "chardet"
|
||||
version = "3.0.4"
|
||||
description = "Universal encoding detector for Python 2 and 3"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "7.1.2"
|
||||
description = "Composable command line interface toolkit"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "dataclasses"
|
||||
version = "0.8"
|
||||
description = "A backport of the dataclasses module for Python 3.6"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6, <3.7"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "2.10"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "idna-ssl"
|
||||
version = "1.1.0"
|
||||
description = "Patch ssl.match_hostname for Unicode(idna) domains support"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
idna = ">=2.0"
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "5.1.0"
|
||||
description = "multidict implementation"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "0.4.3"
|
||||
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "20.8"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[package.dependencies]
|
||||
pyparsing = ">=2.0.2"
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.8.1"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "pycares"
|
||||
version = "3.1.1"
|
||||
description = "Python interface for c-ares"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
cffi = ">=1.5.0"
|
||||
|
||||
[package.extras]
|
||||
idna = ["idna (>=2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.20"
|
||||
description = "C parser in Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "2.4.7"
|
||||
description = "Python parsing module"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2020.11.13"
|
||||
description = "Alternative regular expression module, to replace re."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.25.1"
|
||||
description = "Python HTTP for Humans."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
chardet = ">=3.0.2,<5"
|
||||
idna = ">=2.5,<3"
|
||||
urllib3 = ">=1.21.1,<1.27"
|
||||
|
||||
[package.extras]
|
||||
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "0.19.5"
|
||||
description = "Python client for Sentry (https://sentry.io)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
urllib3 = ">=1.10.0"
|
||||
|
||||
[package.extras]
|
||||
aiohttp = ["aiohttp (>=3.5)"]
|
||||
beam = ["apache-beam (>=2.12)"]
|
||||
bottle = ["bottle (>=0.12.13)"]
|
||||
celery = ["celery (>=3)"]
|
||||
chalice = ["chalice (>=1.16.0)"]
|
||||
django = ["django (>=1.8)"]
|
||||
falcon = ["falcon (>=1.4)"]
|
||||
flask = ["flask (>=0.11)", "blinker (>=1.1)"]
|
||||
pure_eval = ["pure-eval", "executing", "asttokens"]
|
||||
pyspark = ["pyspark (>=2.4.4)"]
|
||||
rq = ["rq (>=0.6)"]
|
||||
sanic = ["sanic (>=0.8)"]
|
||||
sqlalchemy = ["sqlalchemy (>=1.2)"]
|
||||
tornado = ["tornado (>=5)"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.15.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.10.2"
|
||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "typed-ast"
|
||||
version = "1.4.2"
|
||||
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "typing"
|
||||
version = "3.7.4.3"
|
||||
description = "Type Hints for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "3.7.4.3"
|
||||
description = "Backported and Experimental Type Hints for Python 3.5+"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.2"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotlipy (>=0.6.0)"]
|
||||
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "webencodings"
|
||||
version = "0.5.1"
|
||||
description = "Character encoding aliases for legacy web content"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.6.3"
|
||||
description = "Yet another URL library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
idna = ">=2.0"
|
||||
multidict = ">=4.0"
|
||||
typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.6"
|
||||
content-hash = "50a1e4a263c215a4ef7db4795c4bfe8f4ee7f2acdb2e03d1cbbb5b8fe637937c"
|
||||
|
||||
[metadata.files]
|
||||
aiodns = [
|
||||
{file = "aiodns-2.0.0-py2.py3-none-any.whl", hash = "sha256:aaa5ac584f40fe778013df0aa6544bf157799bd3f608364b451840ed2c8688de"},
|
||||
{file = "aiodns-2.0.0.tar.gz", hash = "sha256:815fdef4607474295d68da46978a54481dd1e7be153c7d60f9e72773cd38d77d"},
|
||||
]
|
||||
aiohttp = [
|
||||
{file = "aiohttp-3.7.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:328b552513d4f95b0a2eea4c8573e112866107227661834652a8984766aa7656"},
|
||||
{file = "aiohttp-3.7.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c733ef3bdcfe52a1a75564389bad4064352274036e7e234730526d155f04d914"},
|
||||
{file = "aiohttp-3.7.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:2858b2504c8697beb9357be01dc47ef86438cc1cb36ecb6991796d19475faa3e"},
|
||||
{file = "aiohttp-3.7.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:d2cfac21e31e841d60dc28c0ec7d4ec47a35c608cb8906435d47ef83ffb22150"},
|
||||
{file = "aiohttp-3.7.3-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:3228b7a51e3ed533f5472f54f70fd0b0a64c48dc1649a0f0e809bec312934d7a"},
|
||||
{file = "aiohttp-3.7.3-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:dcc119db14757b0c7bce64042158307b9b1c76471e655751a61b57f5a0e4d78e"},
|
||||
{file = "aiohttp-3.7.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:7d9b42127a6c0bdcc25c3dcf252bb3ddc70454fac593b1b6933ae091396deb13"},
|
||||
{file = "aiohttp-3.7.3-cp36-cp36m-win32.whl", hash = "sha256:df48a623c58180874d7407b4d9ec06a19b84ed47f60a3884345b1a5099c1818b"},
|
||||
{file = "aiohttp-3.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:0b795072bb1bf87b8620120a6373a3c61bfcb8da7e5c2377f4bb23ff4f0b62c9"},
|
||||
{file = "aiohttp-3.7.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:0d438c8ca703b1b714e82ed5b7a4412c82577040dadff479c08405e2a715564f"},
|
||||
{file = "aiohttp-3.7.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8389d6044ee4e2037dca83e3f6994738550f6ee8cfb746762283fad9b932868f"},
|
||||
{file = "aiohttp-3.7.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3ea8c252d8df5e9166bcf3d9edced2af132f4ead8ac422eac723c5781063709a"},
|
||||
{file = "aiohttp-3.7.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:78e2f18a82b88cbc37d22365cf8d2b879a492faedb3f2975adb4ed8dfe994d3a"},
|
||||
{file = "aiohttp-3.7.3-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:df3a7b258cc230a65245167a202dd07320a5af05f3d41da1488ba0fa05bc9347"},
|
||||
{file = "aiohttp-3.7.3-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:f326b3c1bbfda5b9308252ee0dcb30b612ee92b0e105d4abec70335fab5b1245"},
|
||||
{file = "aiohttp-3.7.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:5e479df4b2d0f8f02133b7e4430098699450e1b2a826438af6bec9a400530957"},
|
||||
{file = "aiohttp-3.7.3-cp37-cp37m-win32.whl", hash = "sha256:6d42debaf55450643146fabe4b6817bb2a55b23698b0434107e892a43117285e"},
|
||||
{file = "aiohttp-3.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9c58b0b84055d8bc27b7df5a9d141df4ee6ff59821f922dd73155861282f6a3"},
|
||||
{file = "aiohttp-3.7.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:f411cb22115cb15452d099fec0ee636b06cf81bfb40ed9c02d30c8dc2bc2e3d1"},
|
||||
{file = "aiohttp-3.7.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c1e0920909d916d3375c7a1fdb0b1c78e46170e8bb42792312b6eb6676b2f87f"},
|
||||
{file = "aiohttp-3.7.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:59d11674964b74a81b149d4ceaff2b674b3b0e4d0f10f0be1533e49c4a28408b"},
|
||||
{file = "aiohttp-3.7.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:41608c0acbe0899c852281978492f9ce2c6fbfaf60aff0cefc54a7c4516b822c"},
|
||||
{file = "aiohttp-3.7.3-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:16a3cb5df5c56f696234ea9e65e227d1ebe9c18aa774d36ff42f532139066a5f"},
|
||||
{file = "aiohttp-3.7.3-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:6ccc43d68b81c424e46192a778f97da94ee0630337c9bbe5b2ecc9b0c1c59001"},
|
||||
{file = "aiohttp-3.7.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:d03abec50df423b026a5aa09656bd9d37f1e6a49271f123f31f9b8aed5dc3ea3"},
|
||||
{file = "aiohttp-3.7.3-cp38-cp38-win32.whl", hash = "sha256:39f4b0a6ae22a1c567cb0630c30dd082481f95c13ca528dc501a7766b9c718c0"},
|
||||
{file = "aiohttp-3.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:c68fdf21c6f3573ae19c7ee65f9ff185649a060c9a06535e9c3a0ee0bbac9235"},
|
||||
{file = "aiohttp-3.7.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:710376bf67d8ff4500a31d0c207b8941ff4fba5de6890a701d71680474fe2a60"},
|
||||
{file = "aiohttp-3.7.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2406dc1dda01c7f6060ab586e4601f18affb7a6b965c50a8c90ff07569cf782a"},
|
||||
{file = "aiohttp-3.7.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:2a7b7640167ab536c3cb90cfc3977c7094f1c5890d7eeede8b273c175c3910fd"},
|
||||
{file = "aiohttp-3.7.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:684850fb1e3e55c9220aad007f8386d8e3e477c4ec9211ae54d968ecdca8c6f9"},
|
||||
{file = "aiohttp-3.7.3-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:1edfd82a98c5161497bbb111b2b70c0813102ad7e0aa81cbeb34e64c93863005"},
|
||||
{file = "aiohttp-3.7.3-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:77149002d9386fae303a4a162e6bce75cc2161347ad2ba06c2f0182561875d45"},
|
||||
{file = "aiohttp-3.7.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:756ae7efddd68d4ea7d89c636b703e14a0c686688d42f588b90778a3c2fc0564"},
|
||||
{file = "aiohttp-3.7.3-cp39-cp39-win32.whl", hash = "sha256:3b0036c978cbcc4a4512278e98e3e6d9e6b834dc973206162eddf98b586ef1c6"},
|
||||
{file = "aiohttp-3.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:e1b95972a0ae3f248a899cdbac92ba2e01d731225f566569311043ce2226f5e7"},
|
||||
{file = "aiohttp-3.7.3.tar.gz", hash = "sha256:9c1a81af067e72261c9cbe33ea792893e83bc6aa987bfbd6fdc1e5e7b22777c4"},
|
||||
]
|
||||
aiosmtpd = [
|
||||
{file = "aiosmtpd-1.2.2.tar.gz", hash = "sha256:47fb2a64495e51301af390b6d2d887a16ad01d1df40b0de7ccf456046c52357d"},
|
||||
]
|
||||
appdirs = [
|
||||
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
|
||||
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
|
||||
]
|
||||
async-timeout = [
|
||||
{file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
|
||||
{file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"},
|
||||
]
|
||||
atpublic = [
|
||||
{file = "atpublic-2.1.2.tar.gz", hash = "sha256:82a2f2c0343ac67913f67cdee8fa4da294a4d6b863111527a459c8e4d1a646c8"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
|
||||
{file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
|
||||
]
|
||||
bleach = [
|
||||
{file = "bleach-3.2.3-py2.py3-none-any.whl", hash = "sha256:2d3b3f7e7d69148bb683b26a3f21eabcf62fa8fb7bc75d0e7a13bcecd9568d4d"},
|
||||
{file = "bleach-3.2.3.tar.gz", hash = "sha256:c6ad42174219b64848e2e2cd434e44f56cd24a93a9b4f8bc52cfed55a1cd5aad"},
|
||||
]
|
||||
brotlipy = [
|
||||
{file = "brotlipy-0.7.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:af65d2699cb9f13b26ec3ba09e75e80d31ff422c03675fcb36ee4dabe588fdc2"},
|
||||
{file = "brotlipy-0.7.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:50ca336374131cfad20612f26cc43c637ac0bfd2be3361495e99270883b52962"},
|
||||
{file = "brotlipy-0.7.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:fd1d1c64214af5d90014d82cee5d8141b13d44c92ada7a0c0ec0679c6f15a471"},
|
||||
{file = "brotlipy-0.7.0-cp27-cp27m-win32.whl", hash = "sha256:5de6f7d010b7558f72f4b061a07395c5c3fd57f0285c5af7f126a677b976a868"},
|
||||
{file = "brotlipy-0.7.0-cp27-cp27m-win_amd64.whl", hash = "sha256:637847560d671657f993313ecc6c6c6666a936b7a925779fd044065c7bc035b9"},
|
||||
{file = "brotlipy-0.7.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:b4c98b0d2c9c7020a524ca5bbff42027db1004c6571f8bc7b747f2b843128e7a"},
|
||||
{file = "brotlipy-0.7.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8b39abc3256c978f575df5cd7893153277216474f303e26f0e43ba3d3969ef96"},
|
||||
{file = "brotlipy-0.7.0-cp33-cp33m-macosx_10_6_intel.whl", hash = "sha256:96bc59ff9b5b5552843dc67999486a220e07a0522dddd3935da05dc194fa485c"},
|
||||
{file = "brotlipy-0.7.0-cp33-cp33m-manylinux1_i686.whl", hash = "sha256:091b299bf36dd6ef7a06570dbc98c0f80a504a56c5b797f31934d2ad01ae7d17"},
|
||||
{file = "brotlipy-0.7.0-cp33-cp33m-manylinux1_x86_64.whl", hash = "sha256:0be698678a114addcf87a4b9496c552c68a2c99bf93cf8e08f5738b392e82057"},
|
||||
{file = "brotlipy-0.7.0-cp33-cp33m-win32.whl", hash = "sha256:d2c1c724c4ac375feb2110f1af98ecdc0e5a8ea79d068efb5891f621a5b235cb"},
|
||||
{file = "brotlipy-0.7.0-cp33-cp33m-win_amd64.whl", hash = "sha256:3a3e56ced8b15fbbd363380344f70f3b438e0fd1fcf27b7526b6172ea950e867"},
|
||||
{file = "brotlipy-0.7.0-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:653faef61241bf8bf99d73ca7ec4baa63401ba7b2a2aa88958394869379d67c7"},
|
||||
{file = "brotlipy-0.7.0-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:0fa6088a9a87645d43d7e21e32b4a6bf8f7c3939015a50158c10972aa7f425b7"},
|
||||
{file = "brotlipy-0.7.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:79aaf217072840f3e9a3b641cccc51f7fc23037496bd71e26211856b93f4b4cb"},
|
||||
{file = "brotlipy-0.7.0-cp34-cp34m-win32.whl", hash = "sha256:a07647886e24e2fb2d68ca8bf3ada398eb56fd8eac46c733d4d95c64d17f743b"},
|
||||
{file = "brotlipy-0.7.0-cp34-cp34m-win_amd64.whl", hash = "sha256:c6cc0036b1304dd0073eec416cb2f6b9e37ac8296afd9e481cac3b1f07f9db25"},
|
||||
{file = "brotlipy-0.7.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:07194f4768eb62a4f4ea76b6d0df6ade185e24ebd85877c351daa0a069f1111a"},
|
||||
{file = "brotlipy-0.7.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7e31f7adcc5851ca06134705fcf3478210da45d35ad75ec181e1ce9ce345bb38"},
|
||||
{file = "brotlipy-0.7.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9448227b0df082e574c45c983fa5cd4bda7bfb11ea6b59def0940c1647be0c3c"},
|
||||
{file = "brotlipy-0.7.0-cp35-cp35m-win32.whl", hash = "sha256:dc6c5ee0df9732a44d08edab32f8a616b769cc5a4155a12d2d010d248eb3fb07"},
|
||||
{file = "brotlipy-0.7.0-cp35-cp35m-win_amd64.whl", hash = "sha256:3c1d5e2cf945a46975bdb11a19257fa057b67591eb232f393d260e7246d9e571"},
|
||||
{file = "brotlipy-0.7.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:2a80319ae13ea8dd60ecdc4f5ccf6da3ae64787765923256b62c598c5bba4121"},
|
||||
{file = "brotlipy-0.7.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:2699945a0a992c04fc7dc7fa2f1d0575a2c8b4b769f2874a08e8eae46bef36ae"},
|
||||
{file = "brotlipy-0.7.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1ea4e578241504b58f2456a6c69952c88866c794648bdc74baee74839da61d44"},
|
||||
{file = "brotlipy-0.7.0-cp36-cp36m-win32.whl", hash = "sha256:2e5c64522364a9ebcdf47c5744a5ddeb3f934742d31e61ebfbbc095460b47162"},
|
||||
{file = "brotlipy-0.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:09ec3e125d16749b31c74f021aba809541b3564e5359f8c265cbae442810b41a"},
|
||||
{file = "brotlipy-0.7.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:4e4638b49835d567d447a2cfacec109f9a777f219f071312268b351b6839436d"},
|
||||
{file = "brotlipy-0.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1379347337dc3d20b2d61456d44ccce13e0625db2611c368023b4194d5e2477f"},
|
||||
{file = "brotlipy-0.7.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:22a53ccebcce2425e19f99682c12be510bf27bd75c9b77a1720db63047a77554"},
|
||||
{file = "brotlipy-0.7.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4bac11c1ffba9eaa2894ec958a44e7f17778b3303c2ee9f99c39fcc511c26668"},
|
||||
{file = "brotlipy-0.7.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:08a16ebe2ffc52f645c076f96b138f185e74e5d59b4a65e84af17d5997d82890"},
|
||||
{file = "brotlipy-0.7.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7b21341eab7c939214e457e24b265594067a6ad268305289148ebaf2dacef325"},
|
||||
{file = "brotlipy-0.7.0-pp226-pp226u-macosx_10_10_x86_64.whl", hash = "sha256:786afc8c9bd67de8d31f46e408a3386331e126829114e4db034f91eacb05396d"},
|
||||
{file = "brotlipy-0.7.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:890b973039ba26c3ad2e86e8908ab527ed64f9b1357f81a676604da8088e4bf9"},
|
||||
{file = "brotlipy-0.7.0-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:4864ac52c116ea3e3a844248a9c9fbebb8797891cbca55484ecb6eed3ebeba24"},
|
||||
{file = "brotlipy-0.7.0.tar.gz", hash = "sha256:36def0b859beaf21910157b4c33eb3b06d8ce459c942102f16988cca6ea164df"},
|
||||
]
|
||||
cchardet = [
|
||||
{file = "cchardet-2.1.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6f70139aaf47ffb94d89db603af849b82efdf756f187cdd3e566e30976c519f"},
|
||||
{file = "cchardet-2.1.7-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5a25f9577e9bebe1a085eec2d6fdd72b7a9dd680811bba652ea6090fb2ff472f"},
|
||||
{file = "cchardet-2.1.7-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6b6397d8a32b976a333bdae060febd39ad5479817fabf489e5596a588ad05133"},
|
||||
{file = "cchardet-2.1.7-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:228d2533987c450f39acf7548f474dd6814c446e9d6bd228e8f1d9a2d210f10b"},
|
||||
{file = "cchardet-2.1.7-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:54341e7e1ba9dc0add4c9d23b48d3a94e2733065c13920e85895f944596f6150"},
|
||||
{file = "cchardet-2.1.7-cp36-cp36m-win32.whl", hash = "sha256:eee4f5403dc3a37a1ca9ab87db32b48dc7e190ef84601068f45397144427cc5e"},
|
||||
{file = "cchardet-2.1.7-cp36-cp36m-win_amd64.whl", hash = "sha256:f86e0566cb61dc4397297696a4a1b30f6391b50bc52b4f073507a48466b6255a"},
|
||||
{file = "cchardet-2.1.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:302aa443ae2526755d412c9631136bdcd1374acd08e34f527447f06f3c2ddb98"},
|
||||
{file = "cchardet-2.1.7-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:70eeae8aaf61192e9b247cf28969faef00578becd2602526ecd8ae7600d25e0e"},
|
||||
{file = "cchardet-2.1.7-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a39526c1c526843965cec589a6f6b7c2ab07e3e56dc09a7f77a2be6a6afa4636"},
|
||||
{file = "cchardet-2.1.7-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:b154effa12886e9c18555dfc41a110f601f08d69a71809c8d908be4b1ab7314f"},
|
||||
{file = "cchardet-2.1.7-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ec3eb5a9c475208cf52423524dcaf713c394393e18902e861f983c38eeb77f18"},
|
||||
{file = "cchardet-2.1.7-cp37-cp37m-win32.whl", hash = "sha256:50ad671e8d6c886496db62c3bd68b8d55060688c655873aa4ce25ca6105409a1"},
|
||||
{file = "cchardet-2.1.7-cp37-cp37m-win_amd64.whl", hash = "sha256:54d0b26fd0cd4099f08fb9c167600f3e83619abefeaa68ad823cc8ac1f7bcc0c"},
|
||||
{file = "cchardet-2.1.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b59ddc615883835e03c26f81d5fc3671fab2d32035c87f50862de0da7d7db535"},
|
||||
{file = "cchardet-2.1.7-cp38-cp38-manylinux1_i686.whl", hash = "sha256:27a9ba87c9f99e0618e1d3081189b1217a7d110e5c5597b0b7b7c3fedd1c340a"},
|
||||
{file = "cchardet-2.1.7-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:90086e5645f8a1801350f4cc6cb5d5bf12d3fa943811bb08667744ec1ecc9ccd"},
|
||||
{file = "cchardet-2.1.7-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:45456c59ec349b29628a3c6bfb86d818ec3a6fbb7eb72de4ff3bd4713681c0e3"},
|
||||
{file = "cchardet-2.1.7-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f16517f3697569822c6d09671217fdeab61dfebc7acb5068634d6b0728b86c0b"},
|
||||
{file = "cchardet-2.1.7-cp38-cp38-win32.whl", hash = "sha256:0b859069bbb9d27c78a2c9eb997e6f4b738db2d7039a03f8792b4058d61d1109"},
|
||||
{file = "cchardet-2.1.7-cp38-cp38-win_amd64.whl", hash = "sha256:273699c4e5cd75377776501b72a7b291a988c6eec259c29505094553ee505597"},
|
||||
{file = "cchardet-2.1.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:48ba829badef61441e08805cfa474ccd2774be2ff44b34898f5854168c596d4d"},
|
||||
{file = "cchardet-2.1.7-cp39-cp39-manylinux1_i686.whl", hash = "sha256:bd7f262f41fd9caf5a5f09207a55861a67af6ad5c66612043ed0f81c58cdf376"},
|
||||
{file = "cchardet-2.1.7-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fdac1e4366d0579fff056d1280b8dc6348be964fda8ebb627c0269e097ab37fa"},
|
||||
{file = "cchardet-2.1.7-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:80e6faae75ecb9be04a7b258dc4750d459529debb6b8dee024745b7b5a949a34"},
|
||||
{file = "cchardet-2.1.7-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c96aee9ebd1147400e608a3eff97c44f49811f8904e5a43069d55603ac4d8c97"},
|
||||
{file = "cchardet-2.1.7-cp39-cp39-win32.whl", hash = "sha256:2309ff8fc652b0fc3c0cff5dbb172530c7abb92fe9ba2417c9c0bcf688463c1c"},
|
||||
{file = "cchardet-2.1.7-cp39-cp39-win_amd64.whl", hash = "sha256:24974b3e40fee9e7557bb352be625c39ec6f50bc2053f44a3d1191db70b51675"},
|
||||
{file = "cchardet-2.1.7.tar.gz", hash = "sha256:c428b6336545053c2589f6caf24ea32276c6664cb86db817e03a94c60afa0eaf"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
|
||||
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
|
||||
]
|
||||
cffi = [
|
||||
{file = "cffi-1.14.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775"},
|
||||
{file = "cffi-1.14.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06"},
|
||||
{file = "cffi-1.14.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26"},
|
||||
{file = "cffi-1.14.4-cp27-cp27m-win32.whl", hash = "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c"},
|
||||
{file = "cffi-1.14.4-cp27-cp27m-win_amd64.whl", hash = "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b"},
|
||||
{file = "cffi-1.14.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d"},
|
||||
{file = "cffi-1.14.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca"},
|
||||
{file = "cffi-1.14.4-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698"},
|
||||
{file = "cffi-1.14.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b"},
|
||||
{file = "cffi-1.14.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293"},
|
||||
{file = "cffi-1.14.4-cp35-cp35m-win32.whl", hash = "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2"},
|
||||
{file = "cffi-1.14.4-cp35-cp35m-win_amd64.whl", hash = "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7"},
|
||||
{file = "cffi-1.14.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"},
|
||||
{file = "cffi-1.14.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362"},
|
||||
{file = "cffi-1.14.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec"},
|
||||
{file = "cffi-1.14.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b"},
|
||||
{file = "cffi-1.14.4-cp36-cp36m-win32.whl", hash = "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668"},
|
||||
{file = "cffi-1.14.4-cp36-cp36m-win_amd64.whl", hash = "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009"},
|
||||
{file = "cffi-1.14.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb"},
|
||||
{file = "cffi-1.14.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d"},
|
||||
{file = "cffi-1.14.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03"},
|
||||
{file = "cffi-1.14.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01"},
|
||||
{file = "cffi-1.14.4-cp37-cp37m-win32.whl", hash = "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e"},
|
||||
{file = "cffi-1.14.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35"},
|
||||
{file = "cffi-1.14.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d"},
|
||||
{file = "cffi-1.14.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b"},
|
||||
{file = "cffi-1.14.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53"},
|
||||
{file = "cffi-1.14.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e"},
|
||||
{file = "cffi-1.14.4-cp38-cp38-win32.whl", hash = "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d"},
|
||||
{file = "cffi-1.14.4-cp38-cp38-win_amd64.whl", hash = "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375"},
|
||||
{file = "cffi-1.14.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909"},
|
||||
{file = "cffi-1.14.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd"},
|
||||
{file = "cffi-1.14.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a"},
|
||||
{file = "cffi-1.14.4-cp39-cp39-win32.whl", hash = "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3"},
|
||||
{file = "cffi-1.14.4-cp39-cp39-win_amd64.whl", hash = "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b"},
|
||||
{file = "cffi-1.14.4.tar.gz", hash = "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c"},
|
||||
]
|
||||
chardet = [
|
||||
{file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
|
||||
{file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
|
||||
]
|
||||
click = [
|
||||
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
|
||||
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
|
||||
]
|
||||
dataclasses = [
|
||||
{file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
|
||||
{file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
|
||||
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
|
||||
]
|
||||
idna-ssl = [
|
||||
{file = "idna-ssl-1.1.0.tar.gz", hash = "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"},
|
||||
]
|
||||
multidict = [
|
||||
{file = "multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f"},
|
||||
{file = "multidict-5.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf"},
|
||||
{file = "multidict-5.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281"},
|
||||
{file = "multidict-5.1.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d"},
|
||||
{file = "multidict-5.1.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d"},
|
||||
{file = "multidict-5.1.0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da"},
|
||||
{file = "multidict-5.1.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224"},
|
||||
{file = "multidict-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26"},
|
||||
{file = "multidict-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6"},
|
||||
{file = "multidict-5.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76"},
|
||||
{file = "multidict-5.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a"},
|
||||
{file = "multidict-5.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f"},
|
||||
{file = "multidict-5.1.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348"},
|
||||
{file = "multidict-5.1.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93"},
|
||||
{file = "multidict-5.1.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9"},
|
||||
{file = "multidict-5.1.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37"},
|
||||
{file = "multidict-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5"},
|
||||
{file = "multidict-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632"},
|
||||
{file = "multidict-5.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952"},
|
||||
{file = "multidict-5.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79"},
|
||||
{file = "multidict-5.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456"},
|
||||
{file = "multidict-5.1.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7"},
|
||||
{file = "multidict-5.1.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635"},
|
||||
{file = "multidict-5.1.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a"},
|
||||
{file = "multidict-5.1.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea"},
|
||||
{file = "multidict-5.1.0-cp38-cp38-win32.whl", hash = "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656"},
|
||||
{file = "multidict-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3"},
|
||||
{file = "multidict-5.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93"},
|
||||
{file = "multidict-5.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647"},
|
||||
{file = "multidict-5.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d"},
|
||||
{file = "multidict-5.1.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8"},
|
||||
{file = "multidict-5.1.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1"},
|
||||
{file = "multidict-5.1.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841"},
|
||||
{file = "multidict-5.1.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda"},
|
||||
{file = "multidict-5.1.0-cp39-cp39-win32.whl", hash = "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"},
|
||||
{file = "multidict-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359"},
|
||||
{file = "multidict-5.1.0.tar.gz", hash = "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5"},
|
||||
]
|
||||
mypy-extensions = [
|
||||
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||
]
|
||||
packaging = [
|
||||
{file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"},
|
||||
{file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"},
|
||||
]
|
||||
pathspec = [
|
||||
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
|
||||
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
|
||||
]
|
||||
pycares = [
|
||||
{file = "pycares-3.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:81edb016d9e43dde7473bc3999c29cdfee3a6b67308fed1ea21049f458e83ae0"},
|
||||
{file = "pycares-3.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1917b82494907a4a342db420bc4dd5bac355a5fa3984c35ba9bf51422b020b48"},
|
||||
{file = "pycares-3.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a5089fd660f0b0d228b14cdaa110d0d311edfa5a63f800618dbf1321dcaef66b"},
|
||||
{file = "pycares-3.1.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:48a7750f04e69e1f304f4332b755728067e7c4b1abe2760bba1cacd9ff7a847a"},
|
||||
{file = "pycares-3.1.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d88a279cbc5af613f73e86e19b3f63850f7a2e2736e249c51995dedcc830b1bb"},
|
||||
{file = "pycares-3.1.1-cp35-cp35m-win32.whl", hash = "sha256:96c90e11b4a4c7c0b8ff5aaaae969c5035493136586043ff301979aae0623941"},
|
||||
{file = "pycares-3.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:eee7b6a5f5b5af050cb7d66ab28179287b416f06d15a8974ac831437fec51336"},
|
||||
{file = "pycares-3.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:050f00b39ed77ea8a4e555f09417d4b1a6b5baa24bb9531a3e15d003d2319b3f"},
|
||||
{file = "pycares-3.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:2e4f74677542737fb5af4ea9a2e415ec5ab31aa67e7b8c3c969fdb15c069f679"},
|
||||
{file = "pycares-3.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f8deaefefc3a589058df1b177275f79233e8b0eeee6734cf4336d80164ecd022"},
|
||||
{file = "pycares-3.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c5cb72644b04e5e5abfb1e10a0e7eb75da6684ea0e60871652f348e412cf3b11"},
|
||||
{file = "pycares-3.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:c457a709e6f2befea7e2996c991eda6d79705dd075f6521593ba6ebc1485b811"},
|
||||
{file = "pycares-3.1.1-cp36-cp36m-win32.whl", hash = "sha256:1d8d177c40567de78108a7835170f570ab04f09084bfd32df9919c0eaec47aa1"},
|
||||
{file = "pycares-3.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f41ac1c858687e53242828c9f59c2e7b0b95dbcd5bdd09c7e5d3c48b0f89a25a"},
|
||||
{file = "pycares-3.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:9a0a1845f8cb2e62332bca0aaa9ad5494603ac43fb60d510a61d5b5b170d7216"},
|
||||
{file = "pycares-3.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:eba9a9227438da5e78fc8eee32f32eb35d9a50cf0a0bd937eb6275c7cc3015fe"},
|
||||
{file = "pycares-3.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0c5bd1f6f885a219d5e972788d6eef7b8043b55c3375a845e5399638436e0bba"},
|
||||
{file = "pycares-3.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:a05bbfdfd41f8410a905a818f329afe7510cbd9ee65c60f8860a72b6c64ce5dc"},
|
||||
{file = "pycares-3.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:11c628402cc8fc8ef461076d4e47f88afc1f8609989ebbff0dbffcd54c97239f"},
|
||||
{file = "pycares-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:fadb97d2e02dabdc15a0091591a972a938850d79ddde23d385d813c1731983f0"},
|
||||
{file = "pycares-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cce46dd4717debfd2aab79d6d7f0cbdf6b1e982dc4d9bebad81658d59ede07c2"},
|
||||
{file = "pycares-3.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0a24d2e580a8eb567140d7b69f12cb7de90c836bd7b6488ec69394d308605ac3"},
|
||||
{file = "pycares-3.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:fa78e919f3bd7d6d075db262aa41079b4c02da315c6043c6f43881e2ebcdd623"},
|
||||
{file = "pycares-3.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:236286f81664658b32c141c8e79d20afc3d54f6e2e49dfc8b702026be7265855"},
|
||||
{file = "pycares-3.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:7d86e62b700b21401ffe7fd1bbfe91e08489416fecae99c6570ab023c6896022"},
|
||||
{file = "pycares-3.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1b90fa00a89564df059fb18e796458864cc4e00cb55e364dbf921997266b7c55"},
|
||||
{file = "pycares-3.1.1-cp38-cp38-win32.whl", hash = "sha256:cfdd1f90bcf373b00f4b2c55ea47868616fe2f779f792fc913fa82a3d64ffe43"},
|
||||
{file = "pycares-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7e2d7effd08d2e5a3cb95d98a7286ebab71ab2fbce84fa93cc2dd56caf7240dd"},
|
||||
{file = "pycares-3.1.1.tar.gz", hash = "sha256:18dfd4fd300f570d6c4536c1d987b7b7673b2a9d14346592c5d6ed716df0d104"},
|
||||
]
|
||||
pycparser = [
|
||||
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
|
||||
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
|
||||
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
|
||||
]
|
||||
regex = [
|
||||
{file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"},
|
||||
{file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"},
|
||||
]
|
||||
requests = [
|
||||
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
|
||||
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
|
||||
]
|
||||
sentry-sdk = [
|
||||
{file = "sentry-sdk-0.19.5.tar.gz", hash = "sha256:737a094e49a529dd0fdcaafa9e97cf7c3d5eb964bd229821d640bc77f3502b3f"},
|
||||
{file = "sentry_sdk-0.19.5-py2.py3-none-any.whl", hash = "sha256:0a711ec952441c2ec89b8f5d226c33bc697914f46e876b44a4edd3e7864cf4d0"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
||||
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
|
||||
]
|
||||
toml = [
|
||||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||
]
|
||||
typed-ast = [
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"},
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"},
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"},
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"},
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"},
|
||||
{file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"},
|
||||
]
|
||||
typing = [
|
||||
{file = "typing-3.7.4.3-py2-none-any.whl", hash = "sha256:283d868f5071ab9ad873e5e52268d611e851c870a2ba354193026f2dfb29d8b5"},
|
||||
{file = "typing-3.7.4.3.tar.gz", hash = "sha256:1187fb9c82fd670d10aa07bbb6cfcfe4bdda42d6fab8d5134f04e8c4d0b71cc9"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
|
||||
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
|
||||
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"},
|
||||
{file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"},
|
||||
]
|
||||
webencodings = [
|
||||
{file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
|
||||
{file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
|
||||
]
|
||||
yarl = [
|
||||
{file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"},
|
||||
{file = "yarl-1.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478"},
|
||||
{file = "yarl-1.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6"},
|
||||
{file = "yarl-1.6.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e"},
|
||||
{file = "yarl-1.6.3-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406"},
|
||||
{file = "yarl-1.6.3-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76"},
|
||||
{file = "yarl-1.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366"},
|
||||
{file = "yarl-1.6.3-cp36-cp36m-win32.whl", hash = "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721"},
|
||||
{file = "yarl-1.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643"},
|
||||
{file = "yarl-1.6.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e"},
|
||||
{file = "yarl-1.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3"},
|
||||
{file = "yarl-1.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8"},
|
||||
{file = "yarl-1.6.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a"},
|
||||
{file = "yarl-1.6.3-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c"},
|
||||
{file = "yarl-1.6.3-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f"},
|
||||
{file = "yarl-1.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970"},
|
||||
{file = "yarl-1.6.3-cp37-cp37m-win32.whl", hash = "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e"},
|
||||
{file = "yarl-1.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50"},
|
||||
{file = "yarl-1.6.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2"},
|
||||
{file = "yarl-1.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec"},
|
||||
{file = "yarl-1.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"},
|
||||
{file = "yarl-1.6.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc"},
|
||||
{file = "yarl-1.6.3-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959"},
|
||||
{file = "yarl-1.6.3-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2"},
|
||||
{file = "yarl-1.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2"},
|
||||
{file = "yarl-1.6.3-cp38-cp38-win32.whl", hash = "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896"},
|
||||
{file = "yarl-1.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a"},
|
||||
{file = "yarl-1.6.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e"},
|
||||
{file = "yarl-1.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724"},
|
||||
{file = "yarl-1.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c"},
|
||||
{file = "yarl-1.6.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25"},
|
||||
{file = "yarl-1.6.3-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96"},
|
||||
{file = "yarl-1.6.3-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0"},
|
||||
{file = "yarl-1.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4"},
|
||||
{file = "yarl-1.6.3-cp39-cp39-win32.whl", hash = "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424"},
|
||||
{file = "yarl-1.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6"},
|
||||
{file = "yarl-1.6.3.tar.gz", hash = "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10"},
|
||||
]
|
22
pyproject.toml
Normal file
22
pyproject.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[tool.poetry]
|
||||
name = "crashreportimporter"
|
||||
version = "0.0.1"
|
||||
description = "NewPipe crash report importer"
|
||||
authors = ["TheAssassin <theassassin@assassinate-you.net>"]
|
||||
license = "MIT"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.6"
|
||||
aiosmtpd = "^1.2.2"
|
||||
bleach = "^3.2.3"
|
||||
requests = "^2.25.1"
|
||||
sentry-sdk = "^0.19.5"
|
||||
aiohttp = {extras = ["speedups"], version = "^3.7.3"}
|
||||
aiodns = "^2.0.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "^20.8b1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
@@ -1,3 +0,0 @@
|
||||
bleach>=1.4
|
||||
raven>=5.19
|
||||
requests>=2.10
|
114
run.py
114
run.py
@@ -8,80 +8,74 @@ and in a local directory.
|
||||
See README.md for more information.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from newpipe_crash_report_importer.mail_client import fetch_messages_from_imap
|
||||
from newpipe_crash_report_importer.storage import DatabaseEntry, \
|
||||
DirectoryStorage, SentryStorage
|
||||
|
||||
import traceback
|
||||
import sentry_sdk
|
||||
|
||||
from newpipe_crash_report_importer import (
|
||||
DatabaseEntry,
|
||||
DirectoryStorage,
|
||||
GlitchtipStorage,
|
||||
LmtpController,
|
||||
CrashReportHandler,
|
||||
Message,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# read e-mail credentials
|
||||
with open("mail-credentials.txt") as f:
|
||||
lines = [l.strip(" \n") for l in f.readlines()]
|
||||
|
||||
client = fetch_messages_from_imap(*lines[:4])
|
||||
# report errors in the importer to GlitchTip, too
|
||||
sentry_sdk.init(dsn=os.environ["OWN_DSN"])
|
||||
|
||||
# initialize storages
|
||||
directory_storage = DirectoryStorage("mails")
|
||||
|
||||
with open("sentry-dsn.txt") as f:
|
||||
sentry_dsn = f.read().strip(" \n\r")
|
||||
with open("legacy-dsn.txt") as f:
|
||||
legacy_dsn = f.read().strip(" \n\r")
|
||||
newpipe_dsn = os.environ["NEWPIPE_DSN"]
|
||||
newpipe_legacy_dsn = os.environ["NEWPIPE_LEGACY_DSN"]
|
||||
|
||||
sentry_storage = SentryStorage(sentry_dsn, "org.schabi.newpipe")
|
||||
legacy_storage = SentryStorage(legacy_dsn, "org.schabi.newpipelegacy")
|
||||
sentry_storage = GlitchtipStorage(newpipe_dsn, "org.schabi.newpipe")
|
||||
legacy_storage = GlitchtipStorage(newpipe_legacy_dsn, "org.schabi.newpipelegacy")
|
||||
|
||||
errors_count = 0
|
||||
mails_count = 0
|
||||
|
||||
for i, m in enumerate(client):
|
||||
print("\rWriting mail {}".format(i), end="")
|
||||
# define handler code as closure
|
||||
# TODO: this is not very elegant, should be refactored
|
||||
async def handle_received_mail(message: Message):
|
||||
print(f"Handling mail")
|
||||
|
||||
try:
|
||||
entry = DatabaseEntry(m)
|
||||
entry = DatabaseEntry(message)
|
||||
except Exception as e:
|
||||
errors_count += 1
|
||||
print()
|
||||
print("Error while parsing the message: %s" % repr(e))
|
||||
return
|
||||
|
||||
if (
|
||||
entry.date.timestamp()
|
||||
< (datetime.now() - timedelta(days=29, hours=23)).timestamp()
|
||||
):
|
||||
print("Exception older than 29 days and 23 hours, discarding...")
|
||||
return
|
||||
|
||||
if entry.date.timestamp() > datetime.now().timestamp():
|
||||
print("Exception occured in the future... How could that happen?")
|
||||
return
|
||||
|
||||
await directory_storage.save(entry)
|
||||
|
||||
package = entry.newpipe_exception_info["package"]
|
||||
|
||||
if package == "org.schabi.newpipe":
|
||||
await sentry_storage.save(entry)
|
||||
elif package == "org.schabi.newpipelegacy":
|
||||
await legacy_storage.save(entry)
|
||||
else:
|
||||
raise RuntimeError("Unknown package: " + package)
|
||||
|
||||
if entry.date.timestamp() < (datetime.now() - timedelta(days=29, hours=23)).timestamp():
|
||||
print()
|
||||
print("Exception older than 29 days and 23 hours, discarding...")
|
||||
continue
|
||||
if entry.date.timestamp() > datetime.now().timestamp():
|
||||
errors_count += 1
|
||||
print()
|
||||
print("Exception occured in the future... How could that happen?")
|
||||
continue
|
||||
# set up LMTP server
|
||||
controller = LmtpController(
|
||||
CrashReportHandler(handle_received_mail), enable_SMTPUTF8=True
|
||||
)
|
||||
controller.start()
|
||||
print(controller.hostname, controller.port)
|
||||
|
||||
try:
|
||||
directory_storage.save(entry)
|
||||
except Exception as e:
|
||||
errors_count += 1
|
||||
print()
|
||||
print("Error while writing the message: %s" % repr(e))
|
||||
traceback.print_exc()
|
||||
|
||||
try:
|
||||
package = entry.newpipe_exception_info["package"]
|
||||
if package == "org.schabi.newpipe":
|
||||
sentry_storage.save(entry)
|
||||
elif package == "org.schabi.newpipelegacy":
|
||||
legacy_storage.save(entry)
|
||||
else:
|
||||
raise Exception("Unknown package: " + package)
|
||||
except Exception as e:
|
||||
errors_count += 1
|
||||
print()
|
||||
print("Error while writing the message: %s" % repr(e))
|
||||
traceback.print_exc()
|
||||
|
||||
mails_count = i
|
||||
|
||||
print()
|
||||
print("Total message count: %s" % str(mails_count))
|
||||
if errors_count > 0:
|
||||
print("Total error count: %s" % str(errors_count))
|
||||
# run server forever
|
||||
asyncio.get_event_loop().run_forever()
|
||||
|
Reference in New Issue
Block a user