http: Add base HTTP server implementation.

* Add HTTP server base implementation with some basic pages
* Add Jinja2 dependency
* Things are mostly working, except for static resources. Those will
  have to come next.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
2023-07-31 18:04:53 -07:00
parent 5f34b353bb
commit c5e7080663
8 changed files with 562 additions and 10 deletions

View File

@@ -3,11 +3,16 @@ import argparse
import logging import logging
from .pull import pull from .pull import pull
from .http import run_app
def parse_args(): def parse_args():
parser = argparse.ArgumentParser(description="Run 4chan bans archiver") parser = argparse.ArgumentParser(description="Run 4chan bans archiver")
parser.add_argument('--log-level', choices=['debug', 'info', 'warning', 'error', 'critical'], default='info') parser.add_argument(
"--log-level",
choices=["debug", "info", "warning", "error", "critical"],
default="info",
)
subparsers = parser.add_subparsers(title="Commands", dest="command") subparsers = parser.add_subparsers(title="Commands", dest="command")
@@ -36,7 +41,7 @@ def parse_args():
return args return args
async def main(): def main():
args = parse_args() args = parse_args()
try: try:
level = getattr(logging, args.log_level.upper()) level = getattr(logging, args.log_level.upper())
@@ -44,13 +49,18 @@ async def main():
print(f"ERROR: no such logging level {args.log_level}. Exiting") print(f"ERROR: no such logging level {args.log_level}. Exiting")
raise SystemExit() raise SystemExit()
logging.basicConfig(level=level, format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s') logging.basicConfig(
level=level, format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s"
)
match args.command: match args.command:
case "pull": case "pull":
await pull() asyncio.run(pull())
case "serve": case "serve":
print("TODO: HTTP server") run_app()
case command: case command:
assert False, f"unknown command {command} that was not caught in add_subcommand" assert (
False
), f"unknown command {command} that was not caught in add_subcommand"
asyncio.run(main())
main()

View File

@@ -54,6 +54,7 @@ def search_db(
board: str = "", board: str = "",
reason: str = "", reason: str = "",
name: str = "", name: str = "",
trip: str = "",
com: str = "", com: str = "",
sub: str = "", sub: str = "",
time_before: int = sys.maxsize, time_before: int = sys.maxsize,
@@ -68,9 +69,10 @@ def search_db(
select * select *
from bans from bans
where where
(:board is null or board like :board_like) (:board = "" or board = :board)
and (:reason = "" or reason like :reason_like) and (:reason = "" or reason like :reason_like)
and (:name = "" or name like :name_like) and (:name = "" or name like :name_like)
and (:trip = "" or trip like :trip_like)
and (:com = "" or com like :com_like) and (:com = "" or com like :com_like)
and (:sub = "" or sub like :sub_like) and (:sub = "" or sub like :sub_like)
and (time <= :time_before) and (time <= :time_before)
@@ -82,11 +84,12 @@ def search_db(
""", """,
{ {
"board": board, "board": board,
"board_like": f"%{board}%",
"reason": reason, "reason": reason,
"reason_like": f"%{reason}%", "reason_like": f"%{reason}%",
"name": name, "name": name,
"name_like": f"%{name}%", "name_like": f"%{name}%",
"trip": trip,
"trip_like": f"%{trip}%",
"com": com, "com": com,
"com_like": f"%{com}%", "com_like": f"%{com}%",
"sub": sub, "sub": sub,

174
chanbans/http.py Normal file
View File

@@ -0,0 +1,174 @@
from collections import defaultdict
import functools
import json
from typing import Any, MutableMapping, Optional, Sequence
from aiohttp import web
from jinja2 import Environment, PackageLoader, select_autoescape
from .db import get_db, search_db
# 2023-07-24 - eating stirfry tonight. It's pretty bad.
# 2023-07-31 - In a meeting. Hope I don't get caught
_env = Environment(
loader=PackageLoader("chanbans"),
autoescape=select_autoescape(),
)
TemplateContext = MutableMapping[str, Any]
class HtmlResponse(web.Response):
def __init__(self, *args, **kwargs):
headers = kwargs.get(
"headers",
{
"Content-Type": "text/html",
},
)
kwargs["headers"] = headers
super().__init__(*args, **kwargs)
class TemplateView(web.View):
template_path: str
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
assert not hasattr(self, "_env")
global _env
self._env = _env
self.template = self.env.get_template(self.template_path)
@property
def env(self) -> Environment:
return self._env
def get_context(self) -> TemplateContext:
"Gets the context for this template"
return dict()
def template_render(self) -> str:
"Renders the template on this object."
context = self.get_context()
return self.template.render(**context)
################################################################################
# Template views
################################################################################
def template_view_factory(path: str) -> type:
class FactoryView(TemplateView):
template_path = path
async def get(self):
html = self.template_render()
return HtmlResponse(text=html)
return FactoryView
class IndexView(TemplateView):
template_path = "index.html"
@functools.cache
def get_context(self) -> TemplateContext:
ctx = super().get_context()
query = self.get_search_query()
ctx["posts"] = search_db(**query)
ctx["query"] = query
return ctx
# Query parameters:
# ?board=pol
# &reason=evasion
# &name=Anonymous
# &trip=asdfbgh
# &com=comment%20search
# &sub=subject%20search
# &time_before=123456
# &time_after=123456
# &md5=md5_sum
# &page=0
def get_search_query(self):
allowed_keys = (
"board",
"reason",
"name",
"trip",
"com",
"sub",
"time_before",
"time_after",
"md5",
"page",
)
query = {
key: value
for key, value in self.request.query.items()
if key in allowed_keys and key in self.request.query
}
if "time_before" in query:
try:
query["time_before"] = int(query["time_before"])
except ValueError:
query.pop("time_before")
if "time_after" in query:
try:
query["time_after"] = int(query["time_after"])
except ValueError:
query.pop("time_after")
return query
async def get(self):
html = self.template_render()
return HtmlResponse(text=html)
################################################################################
# API views
################################################################################
class BansJson(web.View):
async def get(self):
db = get_db()
page = self.request.match_info["page"]
# TODO - configure the number of bans per page in settings
BANS_PER_PAGE = 10
result = db.execute(
"""
SELECT *
FROM bans
ORDER BY id
DESC LIMIT :limit
""",
{"limit": BANS_PER_PAGE},
)
return web.json_response(list(result.fetchall()))
################################################################################
# Routes
################################################################################
app = web.Application()
app.add_routes(
[
web.get(r"/bans", IndexView),
web.get(r"/bans/faq", template_view_factory("faq.html")),
#web.get(r"/api/v1/bans/{page:\d+}", BansJson),
]
)
def run_app():
web.run_app(app)

View File

@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="en">
<head>
{% block head %}
{% endblock head %}
<title>
{% block title %}
{% endblock title %}
</title>
<style>
body {
background: #ffe linear-gradient(180deg, rgba(254,214,175,1) 0%, rgba(255,255,238,1) 200px) repeat-x;
font-family:helvetica neue,arial,sans-serif;
margin:5px 0;
padding:0 5px;
font-size:13px;
color: maroon;
text-align: center;
}
nav {
position: fixed;
top: 0;
width: 100%;
margin: 5px 0;
text-align: left;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
.reply a {
text-decoration: none;
}
header {
text-align: center;
}
#search {
margin: auto;
width: 300px;
margin-bottom: 5px;
}
#search input {
width: 100%;
}
#bans-table {
border-top: 1px solid maroon;
border-left: 1px solid maroon;
border-right: 1px solid maroon;
vertical-align: top;
text-align: center;
margin: auto;
marin-top: 30px;
background-color: #fff;
color: black;
}
#bans-table td {
border-left: 1px solid maroon;
padding: 5px;
vertical-align: top;
}
#bans-table th {
width: auto;
border-left: 1px solid maroon;
background-color: #fca;
font-weight: 700;
padding: 2px;
text-align: center;
}
#bans-table tr {
border-bottom: 1px solid maroon;
}
.constructed-post td:nth-child(1) {
word-break: normal;
width: 50%;
}
.subject {
font-weight: 700;
}
.ws {
color: #000;
}
.ws.reply {
background-color: #d6daf0;
}
.nws.reply {
color: maroon;
background-color: #f0e0d6;
}
.reply {
padding: 5px;
}
.ws .subject {
color: #0f0c5d;
}
.nws .subject {
color: #cc1105;
}
.name-block {
display: inline-block;
}
.name {
color: #117743;
font-weight: 700;
}
.poster-trip {
color: #117743;
font-weight: 400 !important;
}
.file-info {
margin-left: 20px;
}
.file-thumb {
margin: 3px 20px 5px;
}
.file-thumb img {
border: none;
}
.post-message {
margin: 13px 40px;
}
.quote {
color: #789922;
}
</style>
{% block style %}
{% endblock style %}
</head>
<body>
{% block body %}
<header>
{% block header %}
<div id="header">
<h1>4chan bans archive</h1>
<p>
All data is retrieved from <a href="https://www.4chan.org/bans">https://www.4chan.org/bans</a>.
</p>
<p>
This is not a comprehensive list of all bans - only what the moderation team makes public.
</p>
</div>
{% endblock header %}
</header>
<nav>
[ <a href="/bans">Home</a> / <a href="/bans/faq">FAQ</a> ]
</nav>
<main>
{% block main %}
{% endblock main %}
</main>
{% endblock body %}
</body>
</html>

View File

@@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block title %}4chan bans archive FAQ - hiddenservice.cc{% endblock title %}
{% block style %}
<style>
.infobox {
background-color: #fff;
width: 33%;
margin: auto;
padding: 5px;
bottom: 0;
border: 1px solid maroon;
text-align: left;
}
</style>
{% endblock style %}
{% block main %}
<div class="infobox">
<h4>What is this place?</h4>
<p>
<a href="https://www.4chan.org">4chan</a> will periodically (every 15-60
minutes) publish a small list of bans that have been handed out on the
website for transparency. This website exists to archive those bans and
uphold that transparency.
</p>
<h4>What happened to 4bans?</h4>
<p>
4bans is still around. I'm not affiliated with them. Their DNS provider was
having "major issues" - I don't know the full situation. They are still
accessible through their IP address directly:
<a href="http://185.10.68.107:1776">http://185.10.68.107:1776</a>
</p>
</div>
{% endblock %}

View File

@@ -0,0 +1,69 @@
{% extends "base.html" %}
{% block title %}4chan bans archive - hiddenservice.cc{% endblock title %}
{% block main %}
<div id="search">
<form action="" method="get">
<input type="text" name="board" placeholder="Board" value="{{query['board']}}" />
<input type="text" name="reason" placeholder="Reason/rule" value="{{query['reason']}}" />
<input type="text" name="name" placeholder="Name" value="{{query['name']}}" />
<input type="text" name="trip" placeholder="Trip" value="{{query['trip']}}" />
<input type="text" name="com" placeholder="Comment" value="{{query['com']}}" />
<input type="text" name="sub" placeholder="Subject" value="{{query['sub']}}" />
<input type="text" name="md5" placeholder="MD5 Sum" value="{{query['md5']}}" />
<input type="submit" value="Search" />
</form>
</div>
<table id="bans-table">
<thead>
<tr>
<th>Post</th>
<th>Action</th>
<th>Length</th>
<th>Reason</th>
</tr>
</thead>
{% for post in posts %}
<tr class="constructed-post">
<td align="left">
<div class="post reply {% if post['nsfw'] %}nws{% else %}ws{% endif %}">
<div class="post-info">
<input type="checkbox" disabled>
<span class="board"><a href="?board={{post['board']}}">/{{post['board']}}/</a></span>
<span class="subject">{{post['sub']|safe}}</span>
<span class="name-block">
<span class="name">{{post['name']}}</span>
{% if post['trip'] %}
<span class="poster-trip">{{post['trip']}}</span>
{% endif %}
</span>
<span class="date-time">{{post['now']}}</span>
<span class="post-num">No. XXX</span>
</div>
{% if post['thumb_path'] %}
<div class="file">
<div class="file-info">
<span class="file-text">
File: {{post['tim']}}{{post['ext']}}
({{post['fsize']}}, {{post['w']}}x{{post['h']}}, {{post['filename']}}{{post['ext']}})
</span>
</div>
<span class="file-thumb">
<img src="{{post['thumb_path']}}" data-md5="{{post['md5']}}" loading="lazy">
</span>
</div>
{% endif %}
<blockquote class="post-message">
{{post['com']|safe}}
</blockquote>
</div>
</td>
<td>{{post['action']}}</td>
<td>{{post['length']}}</td>
<td>{{post['reason']}}</td>
</tr>
{% endfor %}
</table>
{# pagination #}
{% endblock main %}

80
poetry.lock generated
View File

@@ -485,6 +485,24 @@ pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"
plugins = ["setuptools"] plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"] requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "jinja2"
version = "3.1.2"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
]
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]] [[package]]
name = "lazy-object-proxy" name = "lazy-object-proxy"
version = "1.9.0" version = "1.9.0"
@@ -531,6 +549,66 @@ files = [
{file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"},
] ]
[[package]]
name = "markupsafe"
version = "2.1.3"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"},
{file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"},
{file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"},
{file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"},
{file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"},
{file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"},
{file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"},
{file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"},
{file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"},
{file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"},
{file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"},
{file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"},
{file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"},
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
]
[[package]] [[package]]
name = "mccabe" name = "mccabe"
version = "0.7.0" version = "0.7.0"
@@ -1007,4 +1085,4 @@ multidict = ">=4.0"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "e73a1335e7f7de402b4ed8fa81acc6580b7c53cf938e17724837ab1dc31ac6de" content-hash = "40f027ee1da4d79b385c85556d6ff43600364194c9352a273c419b96f313a868"

View File

@@ -10,6 +10,7 @@ readme = "README.md"
python = "^3.9" python = "^3.9"
beautifulsoup4 = "^4.12.2" beautifulsoup4 = "^4.12.2"
aiohttp = "^3.8.4" aiohttp = "^3.8.4"
jinja2 = "^3.1.2"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]