mirror of
https://git.sr.ht/~tsileo/microblog.pub
synced 2025-06-05 21:59:23 +02:00
Compare commits
59 Commits
2.0.0-rc.1
...
2.0.0-rc.6
Author | SHA1 | Date | |
---|---|---|---|
2853bf2a28 | |||
0144a1c0d4 | |||
d93bcf6128 | |||
647add2bab | |||
f50a233ce9 | |||
d909bf93a0 | |||
8e7fbcc501 | |||
7a665df2b5 | |||
b5b56e9ed5 | |||
9a36b0edf5 | |||
20f996d165 | |||
602da69083 | |||
f6cfe06f66 | |||
c8a9793638 | |||
5eaa0f291b | |||
881d0ad899 | |||
5a20b9d23a | |||
919a61f75d | |||
7faa4655f8 | |||
cf6a891349 | |||
58b383ba4e | |||
57fc5ef913 | |||
5348398b23 | |||
572a84b4bd | |||
992cd55d7b | |||
6216b316e8 | |||
96eae971b8 | |||
928bdafeea | |||
dc89aeb70b | |||
25d3daa6d2 | |||
715df3c563 | |||
cb5d21baeb | |||
8d0b5d1114 | |||
4fcf585c23 | |||
6873ede288 | |||
e0ad21f335 | |||
b3f25e7da1 | |||
d44c8a58aa | |||
54aa2f51f4 | |||
3305d489ec | |||
e19c623c71 | |||
5905ad96b4 | |||
9093659b0a | |||
b99552384c | |||
949365d8ba | |||
a55b06b252 | |||
c30033c19e | |||
a6321f52d8 | |||
4e1e4d0ea8 | |||
110f7df962 | |||
4c86cd4be3 | |||
df06defbef | |||
b2f268682c | |||
567595bb4b | |||
91b8bb26b7 | |||
bd4d5a004a | |||
04da8725ed | |||
0c7a19749d | |||
2a37034775 |
@ -10,13 +10,18 @@ ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
|
||||
|
||||
FROM python-base as builder-base
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --no-install-recommends curl build-essential gcc
|
||||
RUN apt-get install -y --no-install-recommends curl build-essential gcc libffi-dev libssl-dev libxml2-dev libxslt1-dev zlib1g-dev libxslt-dev gcc libjpeg-dev zlib1g-dev libwebp-dev
|
||||
# rustc is needed to compile Python packages
|
||||
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
WORKDIR $PYSETUP_PATH
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
RUN poetry install --no-dev
|
||||
RUN poetry install --only main
|
||||
|
||||
FROM python-base as production
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --no-install-recommends libjpeg-dev libxslt1-dev libxml2-dev libxslt-dev
|
||||
RUN groupadd --gid 1000 microblogpub \
|
||||
&& useradd --uid 1000 --gid microblogpub --shell /bin/bash microblogpub
|
||||
COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
|
||||
|
14
Makefile
14
Makefile
@ -27,5 +27,17 @@ move-to:
|
||||
-docker run --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv move-to $(account)
|
||||
|
||||
.PHONY: self-destruct
|
||||
move-to:
|
||||
self-destruct:
|
||||
-docker run --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv self-destruct
|
||||
|
||||
.PHONY: reset-password
|
||||
reset-password:
|
||||
-docker run --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv reset-password
|
||||
|
||||
.PHONY: check-config
|
||||
check-config:
|
||||
-docker run --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv check-config
|
||||
|
||||
.PHONY: compile-scss
|
||||
compile-scss:
|
||||
-docker run --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv compile-scss
|
||||
|
@ -22,7 +22,7 @@ There are still some rough edges, but the server is mostly functional.
|
||||
- Author notes in Markdown, with code highlighting support
|
||||
- Dedicated section for articles/blog posts (enabled when the first article is posted)
|
||||
- Lightweight
|
||||
- Uses SQLite, and no external dependencies except Python 3.10+
|
||||
- Uses SQLite, and Python 3.10+
|
||||
- Can be deployed on small VPS
|
||||
- Privacy-aware
|
||||
- EXIF metadata (like GPS location) are stripped before storage
|
||||
|
@ -6,7 +6,6 @@ from typing import Any
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from markdown import markdown
|
||||
|
||||
from app import config
|
||||
from app.config import ALSO_KNOWN_AS
|
||||
@ -14,6 +13,7 @@ from app.config import AP_CONTENT_TYPE # noqa: F401
|
||||
from app.config import MOVED_TO
|
||||
from app.httpsig import auth
|
||||
from app.key import get_pubkey_as_pem
|
||||
from app.source import dedup_tags
|
||||
from app.source import hashtagify
|
||||
from app.utils.url import check_url
|
||||
|
||||
@ -53,11 +53,26 @@ AS_EXTENDED_CTX = [
|
||||
]
|
||||
|
||||
|
||||
class ObjectIsGoneError(Exception):
|
||||
class FetchError(Exception):
|
||||
def __init__(self, url: str, resp: httpx.Response | None = None) -> None:
|
||||
resp_part = ""
|
||||
if resp:
|
||||
resp_part = f", got HTTP {resp.status_code}: {resp.text}"
|
||||
message = f"Failed to fetch {url}{resp_part}"
|
||||
super().__init__(message)
|
||||
self.resp = resp
|
||||
self.url = url
|
||||
|
||||
|
||||
class ObjectIsGoneError(FetchError):
|
||||
pass
|
||||
|
||||
|
||||
class ObjectNotFoundError(Exception):
|
||||
class ObjectNotFoundError(FetchError):
|
||||
pass
|
||||
|
||||
|
||||
class ObjectUnavailableError(FetchError):
|
||||
pass
|
||||
|
||||
|
||||
@ -86,6 +101,19 @@ class VisibilityEnum(str, enum.Enum):
|
||||
|
||||
|
||||
_LOCAL_ACTOR_SUMMARY, _LOCAL_ACTOR_TAGS = hashtagify(config.CONFIG.summary)
|
||||
_LOCAL_ACTOR_METADATA = []
|
||||
if config.CONFIG.metadata:
|
||||
for kv in config.CONFIG.metadata:
|
||||
kv_value, kv_tags = hashtagify(kv.value)
|
||||
_LOCAL_ACTOR_METADATA.append(
|
||||
{
|
||||
"name": kv.key,
|
||||
"type": "PropertyValue",
|
||||
"value": kv_value,
|
||||
}
|
||||
)
|
||||
_LOCAL_ACTOR_TAGS.extend(kv_tags)
|
||||
|
||||
|
||||
ME = {
|
||||
"@context": AS_EXTENDED_CTX,
|
||||
@ -98,7 +126,7 @@ ME = {
|
||||
"outbox": config.BASE_URL + "/outbox",
|
||||
"preferredUsername": config.USERNAME,
|
||||
"name": config.CONFIG.name,
|
||||
"summary": markdown(_LOCAL_ACTOR_SUMMARY, extensions=["mdx_linkify"]),
|
||||
"summary": _LOCAL_ACTOR_SUMMARY,
|
||||
"endpoints": {
|
||||
# For compat with servers expecting a sharedInbox...
|
||||
"sharedInbox": config.BASE_URL
|
||||
@ -106,16 +134,7 @@ ME = {
|
||||
},
|
||||
"url": config.ID + "/", # XXX: the path is important for Mastodon compat
|
||||
"manuallyApprovesFollowers": config.CONFIG.manually_approves_followers,
|
||||
"attachment": [
|
||||
{
|
||||
"name": kv.key,
|
||||
"type": "PropertyValue",
|
||||
"value": markdown(kv.value, extensions=["mdx_linkify", "fenced_code"]),
|
||||
}
|
||||
for kv in config.CONFIG.metadata
|
||||
]
|
||||
if config.CONFIG.metadata
|
||||
else [],
|
||||
"attachment": _LOCAL_ACTOR_METADATA,
|
||||
"icon": {
|
||||
"mediaType": mimetypes.guess_type(config.CONFIG.icon_url)[0],
|
||||
"type": "Image",
|
||||
@ -126,7 +145,7 @@ ME = {
|
||||
"owner": config.ID,
|
||||
"publicKeyPem": get_pubkey_as_pem(config.KEY_PATH),
|
||||
},
|
||||
"tag": _LOCAL_ACTOR_TAGS,
|
||||
"tag": dedup_tags(_LOCAL_ACTOR_TAGS),
|
||||
}
|
||||
|
||||
if ALSO_KNOWN_AS:
|
||||
@ -166,11 +185,17 @@ async def fetch(
|
||||
|
||||
# Special handling for deleted object
|
||||
if resp.status_code == 410:
|
||||
raise ObjectIsGoneError(f"{url} is gone")
|
||||
raise ObjectIsGoneError(url, resp)
|
||||
elif resp.status_code in [401, 403]:
|
||||
raise ObjectUnavailableError(url, resp)
|
||||
elif resp.status_code == 404:
|
||||
raise ObjectNotFoundError(f"{url} not found")
|
||||
raise ObjectNotFoundError(url, resp)
|
||||
|
||||
try:
|
||||
resp.raise_for_status()
|
||||
except httpx.HTTPError as http_error:
|
||||
raise FetchError(url, resp) from http_error
|
||||
|
||||
try:
|
||||
return resp.json()
|
||||
except json.JSONDecodeError:
|
||||
|
74
app/actor.py
74
app/actor.py
@ -1,6 +1,7 @@
|
||||
import hashlib
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from functools import cached_property
|
||||
from typing import Union
|
||||
from urllib.parse import urlparse
|
||||
@ -12,6 +13,8 @@ from sqlalchemy.orm import joinedload
|
||||
from app import activitypub as ap
|
||||
from app import media
|
||||
from app.database import AsyncSession
|
||||
from app.utils.datetime import as_utc
|
||||
from app.utils.datetime import now
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from app.models import Actor as ActorModel
|
||||
@ -109,7 +112,7 @@ class Actor:
|
||||
|
||||
@property
|
||||
def tags(self) -> list[ap.RawObject]:
|
||||
return self.ap_actor.get("tag", [])
|
||||
return ap.as_list(self.ap_actor.get("tag", []))
|
||||
|
||||
@property
|
||||
def followers_collection_id(self) -> str | None:
|
||||
@ -189,8 +192,26 @@ async def fetch_actor(
|
||||
if existing_actor:
|
||||
if existing_actor.is_deleted:
|
||||
raise ap.ObjectNotFoundError(f"{actor_id} was deleted")
|
||||
|
||||
if now() - as_utc(existing_actor.updated_at) > timedelta(hours=24):
|
||||
logger.info(
|
||||
f"Refreshing {actor_id=} last updated {existing_actor.updated_at}"
|
||||
)
|
||||
try:
|
||||
ap_actor = await ap.fetch(actor_id)
|
||||
await update_actor_if_needed(
|
||||
db_session,
|
||||
existing_actor,
|
||||
RemoteActor(ap_actor),
|
||||
)
|
||||
return existing_actor
|
||||
except Exception:
|
||||
logger.exception(f"Failed to refresh {actor_id}")
|
||||
# If we fail to refresh the actor, return the cached one
|
||||
return existing_actor
|
||||
else:
|
||||
return existing_actor
|
||||
|
||||
if save_if_not_found:
|
||||
ap_actor = await ap.fetch(actor_id)
|
||||
# Some softwares uses URL when we expect ID
|
||||
@ -204,11 +225,32 @@ async def fetch_actor(
|
||||
)
|
||||
).one_or_none()
|
||||
if existing_actor_by_url:
|
||||
# Update the actor as we had to fetch it anyway
|
||||
await update_actor_if_needed(
|
||||
db_session,
|
||||
existing_actor_by_url,
|
||||
RemoteActor(ap_actor),
|
||||
)
|
||||
return existing_actor_by_url
|
||||
|
||||
return await save_actor(db_session, ap_actor)
|
||||
else:
|
||||
raise ap.ObjectNotFoundError
|
||||
raise ap.ObjectNotFoundError(actor_id)
|
||||
|
||||
|
||||
async def update_actor_if_needed(
|
||||
db_session: AsyncSession,
|
||||
actor_in_db: "ActorModel",
|
||||
ra: RemoteActor,
|
||||
) -> None:
|
||||
# Check if we actually need to udpte the actor in DB
|
||||
if _actor_hash(ra) != _actor_hash(actor_in_db):
|
||||
actor_in_db.ap_actor = ra.ap_actor
|
||||
actor_in_db.handle = ra.handle
|
||||
actor_in_db.ap_type = ra.ap_type
|
||||
|
||||
actor_in_db.updated_at = now()
|
||||
await db_session.flush()
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -217,9 +259,11 @@ class ActorMetadata:
|
||||
is_following: bool
|
||||
is_follower: bool
|
||||
is_follow_request_sent: bool
|
||||
is_follow_request_rejected: bool
|
||||
outbox_follow_ap_id: str | None
|
||||
inbox_follow_ap_id: str | None
|
||||
moved_to: typing.Optional["ActorModel"]
|
||||
has_blocked_local_actor: bool
|
||||
|
||||
|
||||
ActorsMetadata = dict[str, ActorMetadata]
|
||||
@ -262,6 +306,26 @@ async def get_actors_metadata(
|
||||
)
|
||||
)
|
||||
}
|
||||
rejected_follow_requests = {
|
||||
reject.activity_object_ap_id
|
||||
for reject in await db_session.execute(
|
||||
select(models.InboxObject.activity_object_ap_id).where(
|
||||
models.InboxObject.ap_type == "Reject",
|
||||
models.InboxObject.ap_actor_id.in_(ap_actor_ids),
|
||||
)
|
||||
)
|
||||
}
|
||||
blocks = {
|
||||
block.ap_actor_id
|
||||
for block in await db_session.execute(
|
||||
select(models.InboxObject.ap_actor_id).where(
|
||||
models.InboxObject.ap_type == "Block",
|
||||
models.InboxObject.undone_by_inbox_object_id.is_(None),
|
||||
models.InboxObject.ap_actor_id.in_(ap_actor_ids),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
idx: ActorsMetadata = {}
|
||||
for actor in actors:
|
||||
if not actor.ap_id:
|
||||
@ -284,9 +348,15 @@ async def get_actors_metadata(
|
||||
is_following=actor.ap_id in following,
|
||||
is_follower=actor.ap_id in followers,
|
||||
is_follow_request_sent=actor.ap_id in sent_follow_requests,
|
||||
is_follow_request_rejected=bool(
|
||||
sent_follow_requests[actor.ap_id] in rejected_follow_requests
|
||||
)
|
||||
if actor.ap_id in sent_follow_requests
|
||||
else False,
|
||||
outbox_follow_ap_id=sent_follow_requests.get(actor.ap_id),
|
||||
inbox_follow_ap_id=followers.get(actor.ap_id),
|
||||
moved_to=moved_to,
|
||||
has_blocked_local_actor=actor.ap_id in blocks,
|
||||
)
|
||||
return idx
|
||||
|
||||
|
15
app/admin.py
15
app/admin.py
@ -40,13 +40,22 @@ from app.utils import pagination
|
||||
from app.utils.emoji import EMOJIS_BY_NAME
|
||||
|
||||
|
||||
def user_session_or_redirect(
|
||||
async def user_session_or_redirect(
|
||||
request: Request,
|
||||
session: str | None = Cookie(default=None),
|
||||
) -> None:
|
||||
if request.method == "POST":
|
||||
form_data = await request.form()
|
||||
if "redirect_url" in form_data:
|
||||
redirect_url = form_data["redirect_url"]
|
||||
else:
|
||||
redirect_url = request.url_for("admin_stream")
|
||||
else:
|
||||
redirect_url = str(request.url)
|
||||
|
||||
_RedirectToLoginPage = HTTPException(
|
||||
status_code=302,
|
||||
headers={"Location": request.url_for("login") + f"?redirect={request.url}"},
|
||||
headers={"Location": request.url_for("login") + f"?redirect={redirect_url}"},
|
||||
)
|
||||
|
||||
if not session:
|
||||
@ -85,6 +94,8 @@ async def get_lookup(
|
||||
error = ap.FetchErrorTypeEnum.TIMEOUT
|
||||
except (ap.ObjectNotFoundError, ap.ObjectIsGoneError):
|
||||
error = ap.FetchErrorTypeEnum.NOT_FOUND
|
||||
except (ap.ObjectUnavailableError):
|
||||
error = ap.FetchErrorTypeEnum.UNAUHTORIZED
|
||||
except Exception:
|
||||
logger.exception(f"Failed to lookup {query}")
|
||||
error = ap.FetchErrorTypeEnum.INTERNAL_ERROR
|
||||
|
@ -1,11 +1,12 @@
|
||||
import hashlib
|
||||
import mimetypes
|
||||
from datetime import datetime
|
||||
from functools import cached_property
|
||||
from typing import Any
|
||||
|
||||
import pydantic
|
||||
from bs4 import BeautifulSoup # type: ignore
|
||||
from markdown import markdown
|
||||
from mistletoe import markdown # type: ignore
|
||||
|
||||
from app import activitypub as ap
|
||||
from app.actor import LOCAL_ACTOR
|
||||
@ -155,7 +156,7 @@ class Object:
|
||||
@cached_property
|
||||
def url(self) -> str | None:
|
||||
obj_url = self.ap_object.get("url")
|
||||
if isinstance(obj_url, str):
|
||||
if isinstance(obj_url, str) and obj_url:
|
||||
return obj_url
|
||||
elif obj_url:
|
||||
for u in ap.as_list(obj_url):
|
||||
@ -175,7 +176,7 @@ class Object:
|
||||
|
||||
# PeerTube returns the content as markdown
|
||||
if self.ap_object.get("mediaType") == "text/markdown":
|
||||
content = markdown(content, extensions=["mdx_linkify"])
|
||||
content = markdown(content)
|
||||
|
||||
return content
|
||||
|
||||
@ -276,6 +277,17 @@ class Attachment(BaseModel):
|
||||
proxied_url: str | None = None
|
||||
resized_url: str | None = None
|
||||
|
||||
@property
|
||||
def mimetype(self) -> str:
|
||||
mimetype = self.media_type
|
||||
if not mimetype:
|
||||
mimetype, _ = mimetypes.guess_type(self.url)
|
||||
|
||||
if not mimetype:
|
||||
return "unknown"
|
||||
|
||||
return mimetype.split("/")[-1]
|
||||
|
||||
|
||||
class RemoteObject(Object):
|
||||
def __init__(self, raw_object: ap.RawObject, actor: Actor):
|
||||
|
252
app/boxes.py
252
app/boxes.py
@ -24,6 +24,7 @@ from app.actor import Actor
|
||||
from app.actor import RemoteActor
|
||||
from app.actor import fetch_actor
|
||||
from app.actor import save_actor
|
||||
from app.actor import update_actor_if_needed
|
||||
from app.ap_object import RemoteObject
|
||||
from app.config import BASE_URL
|
||||
from app.config import BLOCKED_SERVERS
|
||||
@ -32,6 +33,7 @@ from app.config import MANUALLY_APPROVES_FOLLOWERS
|
||||
from app.config import set_moved_to
|
||||
from app.database import AsyncSession
|
||||
from app.outgoing_activities import new_outgoing_activity
|
||||
from app.source import dedup_tags
|
||||
from app.source import markdownify
|
||||
from app.uploads import upload_to_attachment
|
||||
from app.utils import opengraph
|
||||
@ -130,7 +132,11 @@ async def send_delete(db_session: AsyncSession, ap_object_id: str) -> None:
|
||||
db_session, outbox_object_to_delete.in_reply_to
|
||||
)
|
||||
if replied_object:
|
||||
replied_object.replies_count = replied_object.replies_count - 1
|
||||
new_replies_count = await _get_replies_count(
|
||||
db_session, replied_object.ap_id
|
||||
)
|
||||
|
||||
replied_object.replies_count = new_replies_count
|
||||
if replied_object.replies_count < 0:
|
||||
logger.warning("negative replies count for {replied_object.ap_id}")
|
||||
replied_object.replies_count = 0
|
||||
@ -284,6 +290,7 @@ async def _send_undo(db_session: AsyncSession, ap_object_id: str) -> None:
|
||||
raise ValueError("Should never happen")
|
||||
|
||||
outbox_object_to_undo.undone_by_outbox_object_id = outbox_object.id
|
||||
outbox_object_to_undo.is_deleted = True
|
||||
|
||||
if outbox_object_to_undo.ap_type == "Follow":
|
||||
if not outbox_object_to_undo.activity_object_ap_id:
|
||||
@ -342,17 +349,19 @@ async def fetch_conversation_root(
|
||||
db_session: AsyncSession,
|
||||
obj: AnyboxObject | RemoteObject,
|
||||
is_root: bool = False,
|
||||
depth: int = 0,
|
||||
) -> str:
|
||||
"""Some softwares do not set the context/conversation field (like Misskey).
|
||||
This means we have to track conversation ourselves. To do set, we fetch
|
||||
This means we have to track conversation ourselves. To do so, we fetch
|
||||
the root of the conversation and either:
|
||||
- use the context field if set
|
||||
- or build a custom conversation ID
|
||||
"""
|
||||
if not obj.in_reply_to or is_root:
|
||||
logger.info(f"Fetching convo root for ap_id={obj.ap_id}/{depth=}")
|
||||
if obj.ap_context:
|
||||
return obj.ap_context
|
||||
else:
|
||||
|
||||
if not obj.in_reply_to or is_root or depth > 10:
|
||||
# Use the root AP ID if there'no context
|
||||
return f"microblogpub:root:{obj.ap_id}"
|
||||
else:
|
||||
@ -366,16 +375,25 @@ async def fetch_conversation_root(
|
||||
db_session, ap.get_actor_id(raw_reply)
|
||||
)
|
||||
in_reply_to_object = RemoteObject(raw_reply, actor=raw_reply_actor)
|
||||
except (ap.ObjectNotFoundError, ap.ObjectIsGoneError, ap.NotAnObjectError):
|
||||
return await fetch_conversation_root(db_session, obj, is_root=True)
|
||||
except (
|
||||
ap.FetchError,
|
||||
ap.NotAnObjectError,
|
||||
):
|
||||
return await fetch_conversation_root(
|
||||
db_session, obj, is_root=True, depth=depth + 1
|
||||
)
|
||||
except httpx.HTTPStatusError as http_status_error:
|
||||
if 400 <= http_status_error.response.status_code < 500:
|
||||
# We may not have access, in this case consider if root
|
||||
return await fetch_conversation_root(db_session, obj, is_root=True)
|
||||
return await fetch_conversation_root(
|
||||
db_session, obj, is_root=True, depth=depth + 1
|
||||
)
|
||||
else:
|
||||
raise
|
||||
|
||||
return await fetch_conversation_root(db_session, in_reply_to_object)
|
||||
return await fetch_conversation_root(
|
||||
db_session, in_reply_to_object, depth=depth + 1
|
||||
)
|
||||
|
||||
|
||||
async def send_move(
|
||||
@ -452,6 +470,7 @@ async def send_create(
|
||||
content, tags, mentioned_actors = await markdownify(db_session, source)
|
||||
attachments = []
|
||||
|
||||
in_reply_to_object: AnyboxObject | None = None
|
||||
if in_reply_to:
|
||||
in_reply_to_object = await get_anybox_object_by_ap_id(db_session, in_reply_to)
|
||||
if not in_reply_to_object:
|
||||
@ -469,23 +488,6 @@ async def send_create(
|
||||
context = in_reply_to_object.ap_context
|
||||
conversation = in_reply_to_object.ap_context
|
||||
|
||||
if in_reply_to_object.is_from_outbox:
|
||||
await db_session.execute(
|
||||
update(models.OutboxObject)
|
||||
.where(
|
||||
models.OutboxObject.ap_id == in_reply_to,
|
||||
)
|
||||
.values(replies_count=models.OutboxObject.replies_count + 1)
|
||||
)
|
||||
elif in_reply_to_object.is_from_inbox:
|
||||
await db_session.execute(
|
||||
update(models.InboxObject)
|
||||
.where(
|
||||
models.InboxObject.ap_id == in_reply_to,
|
||||
)
|
||||
.values(replies_count=models.InboxObject.replies_count + 1)
|
||||
)
|
||||
|
||||
for (upload, filename, alt_text) in uploads:
|
||||
attachments.append(upload_to_attachment(upload, filename, alt_text))
|
||||
|
||||
@ -550,7 +552,7 @@ async def send_create(
|
||||
"context": context,
|
||||
"conversation": context,
|
||||
"url": outbox_object_id(note_id),
|
||||
"tag": tags,
|
||||
"tag": dedup_tags(tags),
|
||||
"summary": content_warning,
|
||||
"inReplyTo": in_reply_to,
|
||||
"sensitive": is_sensitive,
|
||||
@ -570,7 +572,7 @@ async def send_create(
|
||||
for tag in tags:
|
||||
if tag["type"] == "Hashtag":
|
||||
tagged_object = models.TaggedOutboxObject(
|
||||
tag=tag["name"][1:],
|
||||
tag=tag["name"][1:].lower(),
|
||||
outbox_object_id=outbox_object.id,
|
||||
)
|
||||
db_session.add(tagged_object)
|
||||
@ -604,6 +606,31 @@ async def send_create(
|
||||
)
|
||||
|
||||
await db_session.commit()
|
||||
|
||||
# Refresh the replies counter if needed
|
||||
if in_reply_to_object:
|
||||
new_replies_count = await _get_replies_count(
|
||||
db_session, in_reply_to_object.ap_id
|
||||
)
|
||||
if in_reply_to_object.is_from_outbox:
|
||||
await db_session.execute(
|
||||
update(models.OutboxObject)
|
||||
.where(
|
||||
models.OutboxObject.ap_id == in_reply_to_object.ap_id,
|
||||
)
|
||||
.values(replies_count=new_replies_count)
|
||||
)
|
||||
elif in_reply_to_object.is_from_inbox:
|
||||
await db_session.execute(
|
||||
update(models.InboxObject)
|
||||
.where(
|
||||
models.InboxObject.ap_id == in_reply_to_object.ap_id,
|
||||
)
|
||||
.values(replies_count=new_replies_count)
|
||||
)
|
||||
|
||||
await db_session.commit()
|
||||
|
||||
return note_id
|
||||
|
||||
|
||||
@ -1020,6 +1047,29 @@ async def _handle_delete_activity(
|
||||
await db_session.flush()
|
||||
|
||||
|
||||
async def _get_replies_count(
|
||||
db_session: AsyncSession,
|
||||
replied_object_ap_id: str,
|
||||
) -> int:
|
||||
return (
|
||||
await db_session.scalar(
|
||||
select(func.count(models.InboxObject.id)).where(
|
||||
func.json_extract(models.InboxObject.ap_object, "$.inReplyTo")
|
||||
== replied_object_ap_id,
|
||||
models.InboxObject.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
) + (
|
||||
await db_session.scalar(
|
||||
select(func.count(models.OutboxObject.id)).where(
|
||||
func.json_extract(models.OutboxObject.ap_object, "$.inReplyTo")
|
||||
== replied_object_ap_id,
|
||||
models.OutboxObject.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def _revert_side_effect_for_deleted_object(
|
||||
db_session: AsyncSession,
|
||||
delete_activity: models.InboxObject,
|
||||
@ -1028,7 +1078,17 @@ async def _revert_side_effect_for_deleted_object(
|
||||
) -> None:
|
||||
is_delete_needs_to_be_forwarded = False
|
||||
|
||||
# Decrement the replies counter if needed
|
||||
# Delete related notifications
|
||||
notif_deletion_result = await db_session.execute(
|
||||
delete(models.Notification)
|
||||
.where(models.Notification.inbox_object_id == deleted_ap_object.id)
|
||||
.execution_options(synchronize_session=False)
|
||||
)
|
||||
logger.info(
|
||||
f"Deleted {notif_deletion_result.rowcount} notifications" # type: ignore
|
||||
)
|
||||
|
||||
# Decrement/refresh the replies counter if needed
|
||||
if deleted_ap_object.in_reply_to:
|
||||
replied_object = await get_anybox_object_by_ap_id(
|
||||
db_session,
|
||||
@ -1040,20 +1100,28 @@ async def _revert_side_effect_for_deleted_object(
|
||||
# also needs to be forwarded
|
||||
is_delete_needs_to_be_forwarded = True
|
||||
|
||||
new_replies_count = await _get_replies_count(
|
||||
db_session, replied_object.ap_id
|
||||
)
|
||||
|
||||
await db_session.execute(
|
||||
update(models.OutboxObject)
|
||||
.where(
|
||||
models.OutboxObject.id == replied_object.id,
|
||||
)
|
||||
.values(replies_count=models.OutboxObject.replies_count - 1)
|
||||
.values(replies_count=new_replies_count)
|
||||
)
|
||||
else:
|
||||
new_replies_count = await _get_replies_count(
|
||||
db_session, replied_object.ap_id
|
||||
)
|
||||
|
||||
await db_session.execute(
|
||||
update(models.InboxObject)
|
||||
.where(
|
||||
models.InboxObject.id == replied_object.id,
|
||||
)
|
||||
.values(replies_count=models.InboxObject.replies_count - 1)
|
||||
.values(replies_count=new_replies_count)
|
||||
)
|
||||
|
||||
if deleted_ap_object.ap_type == "Like" and deleted_ap_object.activity_object_ap_id:
|
||||
@ -1338,6 +1406,13 @@ async def _handle_undo_activity(
|
||||
inbox_object_id=ap_activity_to_undo.id,
|
||||
)
|
||||
db_session.add(notif)
|
||||
elif ap_activity_to_undo.ap_type == "Block":
|
||||
notif = models.Notification(
|
||||
notification_type=models.NotificationType.UNBLOCKED,
|
||||
actor_id=from_actor.id,
|
||||
inbox_object_id=ap_activity_to_undo.id,
|
||||
)
|
||||
db_session.add(notif)
|
||||
else:
|
||||
logger.warning(f"Don't know how to undo {ap_activity_to_undo.ap_type} activity")
|
||||
|
||||
@ -1422,7 +1497,8 @@ async def _handle_update_activity(
|
||||
updated_actor = RemoteActor(wrapped_object)
|
||||
if (
|
||||
from_actor.ap_id != updated_actor.ap_id
|
||||
or from_actor.ap_type != updated_actor.ap_type
|
||||
or ap.as_list(from_actor.ap_type)[0] not in ap.ACTOR_TYPES
|
||||
or ap.as_list(updated_actor.ap_type)[0] not in ap.ACTOR_TYPES
|
||||
or from_actor.handle != updated_actor.handle
|
||||
):
|
||||
raise ValueError(
|
||||
@ -1431,7 +1507,7 @@ async def _handle_update_activity(
|
||||
)
|
||||
|
||||
# Update the actor
|
||||
from_actor.ap_actor = updated_actor.ap_actor
|
||||
await update_actor_if_needed(db_session, from_actor, updated_actor)
|
||||
elif (ap_type := wrapped_object["type"]) in [
|
||||
"Question",
|
||||
"Note",
|
||||
@ -1454,6 +1530,7 @@ async def _handle_update_activity(
|
||||
# Everything looks correct, update the object in the inbox
|
||||
logger.info(f"Updating {existing_object.ap_id}")
|
||||
existing_object.ap_object = wrapped_object
|
||||
existing_object.updated_at = now()
|
||||
else:
|
||||
# TODO(ts): support updating objects
|
||||
logger.info(f'Cannot update {wrapped_object["type"]}')
|
||||
@ -1464,8 +1541,24 @@ async def _handle_create_activity(
|
||||
from_actor: models.Actor,
|
||||
create_activity: models.InboxObject,
|
||||
forwarded_by_actor: models.Actor | None = None,
|
||||
relates_to_inbox_object: models.InboxObject | None = None,
|
||||
) -> None:
|
||||
logger.info("Processing Create activity")
|
||||
|
||||
# Some PeerTube activities make no sense to process
|
||||
if (
|
||||
ap_object_type := ap.as_list(
|
||||
(await ap.get_object(create_activity.ap_object))["type"]
|
||||
)[0]
|
||||
) in ["CacheFile"]:
|
||||
logger.info(f"Dropping Create activity for {ap_object_type} object")
|
||||
await db_session.delete(create_activity)
|
||||
return None
|
||||
|
||||
if relates_to_inbox_object:
|
||||
logger.warning(f"{relates_to_inbox_object.ap_id} is already in the inbox")
|
||||
return None
|
||||
|
||||
wrapped_object = ap.unwrap_activity(create_activity.ap_object)
|
||||
if create_activity.actor.ap_id != ap.get_actor_id(wrapped_object):
|
||||
raise ValueError("Object actor does not match activity")
|
||||
@ -1483,8 +1576,9 @@ async def _handle_create_activity(
|
||||
logger.warning(
|
||||
f"Got a Delete for {ro.ap_id} from {delete_object.actor.ap_id}??"
|
||||
)
|
||||
return None
|
||||
else:
|
||||
logger.info("Got a Delete for this object, deleting activity")
|
||||
logger.info("Already received a Delete for this object, deleting activity")
|
||||
create_activity.is_deleted = True
|
||||
await db_session.flush()
|
||||
return None
|
||||
@ -1515,6 +1609,14 @@ async def _handle_read_activity(
|
||||
if not wrapped_object_actor.is_blocked:
|
||||
ro = RemoteObject(wrapped_object, actor=wrapped_object_actor)
|
||||
|
||||
# Check if we already know about this object
|
||||
if await get_inbox_object_by_ap_id(
|
||||
db_session,
|
||||
ro.ap_id,
|
||||
):
|
||||
logger.info(f"{ro.ap_id} is already in the inbox, skipping processing")
|
||||
return None
|
||||
|
||||
# Then process it likes it's coming from a forwarded activity
|
||||
await _process_note_object(db_session, read_activity, wrapped_object_actor, ro)
|
||||
|
||||
@ -1567,6 +1669,8 @@ async def _process_note_object(
|
||||
is_hidden_from_stream=not (
|
||||
(not is_reply and is_from_following) or is_mention or is_local_reply
|
||||
),
|
||||
# We may already have some replies in DB
|
||||
replies_count=await _get_replies_count(db_session, ro.ap_id),
|
||||
)
|
||||
|
||||
db_session.add(inbox_object)
|
||||
@ -1590,20 +1694,28 @@ async def _process_note_object(
|
||||
replied_object, # type: ignore # outbox check below
|
||||
)
|
||||
else:
|
||||
new_replies_count = await _get_replies_count(
|
||||
db_session, replied_object.ap_id
|
||||
)
|
||||
|
||||
await db_session.execute(
|
||||
update(models.OutboxObject)
|
||||
.where(
|
||||
models.OutboxObject.id == replied_object.id,
|
||||
)
|
||||
.values(replies_count=models.OutboxObject.replies_count + 1)
|
||||
.values(replies_count=new_replies_count)
|
||||
)
|
||||
else:
|
||||
new_replies_count = await _get_replies_count(
|
||||
db_session, replied_object.ap_id
|
||||
)
|
||||
|
||||
await db_session.execute(
|
||||
update(models.InboxObject)
|
||||
.where(
|
||||
models.InboxObject.id == replied_object.id,
|
||||
)
|
||||
.values(replies_count=models.InboxObject.replies_count + 1)
|
||||
.values(replies_count=new_replies_count)
|
||||
)
|
||||
|
||||
# This object is a reply of a local object, we may need to forward it
|
||||
@ -1783,6 +1895,12 @@ async def _handle_announce_activity(
|
||||
announced_raw_object = await ap.fetch(
|
||||
announce_activity.activity_object_ap_id
|
||||
)
|
||||
|
||||
# Some software return objects wrapped in a Create activity (like
|
||||
# python-federation)
|
||||
if ap.as_list(announced_raw_object["type"])[0] == "Create":
|
||||
announced_raw_object = await ap.get_object(announced_raw_object)
|
||||
|
||||
announced_actor = await fetch_actor(
|
||||
db_session, ap.get_actor_id(announced_raw_object)
|
||||
)
|
||||
@ -1834,6 +1952,28 @@ async def _handle_like_activity(
|
||||
db_session.add(notif)
|
||||
|
||||
|
||||
async def _handle_block_activity(
|
||||
db_session: AsyncSession,
|
||||
actor: models.Actor,
|
||||
block_activity: models.InboxObject,
|
||||
):
|
||||
if block_activity.activity_object_ap_id != LOCAL_ACTOR.ap_id:
|
||||
logger.warning(
|
||||
"Received invalid Block activity "
|
||||
f"{block_activity.activity_object_ap_id=}"
|
||||
)
|
||||
await db_session.delete(block_activity)
|
||||
return
|
||||
|
||||
# Create a notification
|
||||
notif = models.Notification(
|
||||
notification_type=models.NotificationType.BLOCKED,
|
||||
actor_id=actor.id,
|
||||
inbox_object_id=block_activity.id,
|
||||
)
|
||||
db_session.add(notif)
|
||||
|
||||
|
||||
async def _process_transient_object(
|
||||
db_session: AsyncSession,
|
||||
raw_object: ap.RawObject,
|
||||
@ -1844,6 +1984,7 @@ async def _process_transient_object(
|
||||
if ap_type in ["Add", "Remove"]:
|
||||
logger.info(f"Dropping unsupported {ap_type} object")
|
||||
else:
|
||||
# FIXME(ts): handle transient create
|
||||
logger.warning(f"Received unknown {ap_type} object")
|
||||
|
||||
return None
|
||||
@ -1854,12 +1995,34 @@ async def save_to_inbox(
|
||||
raw_object: ap.RawObject,
|
||||
sent_by_ap_actor_id: str,
|
||||
) -> None:
|
||||
# Special case for server sending the actor as a payload (like python-federation)
|
||||
if ap.as_list(raw_object["type"])[0] in ap.ACTOR_TYPES:
|
||||
if ap.get_id(raw_object) == sent_by_ap_actor_id:
|
||||
updated_actor = RemoteActor(raw_object)
|
||||
|
||||
try:
|
||||
actor = await fetch_actor(db_session, sent_by_ap_actor_id)
|
||||
except ap.ObjectNotFoundError:
|
||||
logger.warning("Actor not found")
|
||||
return
|
||||
|
||||
# Update the actor
|
||||
actor.ap_actor = updated_actor.ap_actor
|
||||
await db_session.commit()
|
||||
return
|
||||
|
||||
else:
|
||||
logger.warning(
|
||||
f"Reveived an actor payload {raw_object} from " f"{sent_by_ap_actor_id}"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
actor = await fetch_actor(db_session, ap.get_id(raw_object["actor"]))
|
||||
except ap.ObjectNotFoundError:
|
||||
logger.warning("Actor not found")
|
||||
return
|
||||
except httpx.HTTPStatusError:
|
||||
except ap.FetchError:
|
||||
logger.exception("Failed to fetch actor")
|
||||
return
|
||||
|
||||
@ -1867,7 +2030,7 @@ async def save_to_inbox(
|
||||
logger.warning(f"Server {actor.server} is blocked")
|
||||
return
|
||||
|
||||
if "id" not in raw_object:
|
||||
if "id" not in raw_object or not raw_object["id"]:
|
||||
await _process_transient_object(db_session, raw_object, actor)
|
||||
return None
|
||||
|
||||
@ -1967,7 +2130,11 @@ async def save_to_inbox(
|
||||
|
||||
if activity_ro.ap_type == "Create":
|
||||
await _handle_create_activity(
|
||||
db_session, actor, inbox_object, forwarded_by_actor=forwarded_by_actor
|
||||
db_session,
|
||||
actor,
|
||||
inbox_object,
|
||||
forwarded_by_actor=forwarded_by_actor,
|
||||
relates_to_inbox_object=relates_to_inbox_object,
|
||||
)
|
||||
elif activity_ro.ap_type == "Read":
|
||||
await _handle_read_activity(db_session, actor, inbox_object)
|
||||
@ -2068,6 +2235,15 @@ async def save_to_inbox(
|
||||
relates_to_outbox_object,
|
||||
relates_to_inbox_object,
|
||||
)
|
||||
elif activity_ro.ap_type == "View":
|
||||
# View is used by Peertube, there's nothing useful we can do with it
|
||||
await db_session.delete(inbox_object)
|
||||
elif activity_ro.ap_type == "Block":
|
||||
await _handle_block_activity(
|
||||
db_session,
|
||||
actor,
|
||||
inbox_object,
|
||||
)
|
||||
else:
|
||||
logger.warning(f"Received an unknown {inbox_object.ap_type} object")
|
||||
|
||||
|
@ -12,7 +12,7 @@ from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
from loguru import logger
|
||||
from markdown import markdown
|
||||
from mistletoe import markdown # type: ignore
|
||||
|
||||
from app.utils.emoji import _load_emojis
|
||||
from app.utils.version import get_version_commit
|
||||
@ -102,6 +102,9 @@ class Config(pydantic.BaseModel):
|
||||
emoji: str | None = None
|
||||
also_known_as: str | None = None
|
||||
|
||||
hides_followers: bool = False
|
||||
hides_following: bool = False
|
||||
|
||||
inbox_retention_days: int = 15
|
||||
|
||||
# Config items to make tests easier
|
||||
@ -144,6 +147,8 @@ _SCHEME = "https" if CONFIG.https else "http"
|
||||
ID = f"{_SCHEME}://{DOMAIN}"
|
||||
USERNAME = CONFIG.username
|
||||
MANUALLY_APPROVES_FOLLOWERS = CONFIG.manually_approves_followers
|
||||
HIDES_FOLLOWERS = CONFIG.hides_followers
|
||||
HIDES_FOLLOWING = CONFIG.hides_following
|
||||
PRIVACY_REPLACE = None
|
||||
if CONFIG.privacy_replace:
|
||||
PRIVACY_REPLACE = {pr.domain: pr.replace_by for pr in CONFIG.privacy_replace}
|
||||
@ -153,9 +158,7 @@ ALSO_KNOWN_AS = CONFIG.also_known_as
|
||||
|
||||
INBOX_RETENTION_DAYS = CONFIG.inbox_retention_days
|
||||
CUSTOM_FOOTER = (
|
||||
markdown(
|
||||
CONFIG.custom_footer.replace("{version}", VERSION), extensions=["mdx_linkify"]
|
||||
)
|
||||
markdown(CONFIG.custom_footer.replace("{version}", VERSION))
|
||||
if CONFIG.custom_footer
|
||||
else None
|
||||
)
|
||||
@ -195,10 +198,19 @@ def generate_csrf_token() -> str:
|
||||
return csrf_serializer.dumps(secrets.token_hex(16)) # type: ignore
|
||||
|
||||
|
||||
def verify_csrf_token(csrf_token: str = Form()) -> None:
|
||||
def verify_csrf_token(
|
||||
csrf_token: str = Form(),
|
||||
redirect_url: str | None = Form(None),
|
||||
) -> None:
|
||||
please_try_again = "please try again"
|
||||
if redirect_url:
|
||||
please_try_again = f'<a href="{redirect_url}">please try again</a>'
|
||||
try:
|
||||
csrf_serializer.loads(csrf_token, max_age=1800)
|
||||
except (itsdangerous.BadData, itsdangerous.SignatureExpired):
|
||||
logger.exception("Failed to verify CSRF token")
|
||||
raise HTTPException(status_code=403, detail="CSRF error")
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"The security token has expired, {please_try_again}",
|
||||
)
|
||||
return None
|
||||
|
@ -88,8 +88,12 @@ def _body_digest(body: bytes) -> str:
|
||||
return "SHA-256=" + base64.b64encode(h.digest()).decode("utf-8")
|
||||
|
||||
|
||||
async def _get_public_key(db_session: AsyncSession, key_id: str) -> Key:
|
||||
if cached_key := _KEY_CACHE.get(key_id):
|
||||
async def _get_public_key(
|
||||
db_session: AsyncSession,
|
||||
key_id: str,
|
||||
should_skip_cache: bool = False,
|
||||
) -> Key:
|
||||
if not should_skip_cache and (cached_key := _KEY_CACHE.get(key_id)):
|
||||
logger.info(f"Key {key_id} found in cache")
|
||||
return cached_key
|
||||
|
||||
@ -101,6 +105,7 @@ async def _get_public_key(db_session: AsyncSession, key_id: str) -> Key:
|
||||
select(models.Actor).where(models.Actor.ap_id == key_id.split("#")[0])
|
||||
)
|
||||
).one_or_none()
|
||||
if not should_skip_cache:
|
||||
if existing_actor and existing_actor.public_key_id == key_id:
|
||||
k = Key(existing_actor.ap_id, key_id)
|
||||
k.load_pub(existing_actor.public_key_as_pem)
|
||||
@ -110,16 +115,15 @@ async def _get_public_key(db_session: AsyncSession, key_id: str) -> Key:
|
||||
|
||||
# Fetch it
|
||||
from app import activitypub as ap
|
||||
from app.actor import RemoteActor
|
||||
from app.actor import update_actor_if_needed
|
||||
|
||||
# Without signing the request as if it's the first contact, the 2 servers
|
||||
# might race to fetch each other key
|
||||
try:
|
||||
actor = await ap.fetch(key_id, disable_httpsig=True)
|
||||
except httpx.HTTPStatusError as http_err:
|
||||
if http_err.response.status_code in [401, 403]:
|
||||
except ap.ObjectUnavailableError:
|
||||
actor = await ap.fetch(key_id, disable_httpsig=False)
|
||||
else:
|
||||
raise
|
||||
|
||||
if actor["type"] == "Key":
|
||||
# The Key is not embedded in the Person
|
||||
@ -136,6 +140,12 @@ async def _get_public_key(db_session: AsyncSession, key_id: str) -> Key:
|
||||
f"failed to fetch requested key {key_id}: got {actor['publicKey']}"
|
||||
)
|
||||
|
||||
if should_skip_cache and actor["type"] != "Key" and existing_actor:
|
||||
# We had to skip the cache, which means the actor key probably changed
|
||||
# and we want to update our cached version
|
||||
await update_actor_if_needed(db_session, existing_actor, RemoteActor(actor))
|
||||
await db_session.commit()
|
||||
|
||||
_KEY_CACHE[key_id] = k
|
||||
return k
|
||||
|
||||
@ -219,7 +229,17 @@ async def httpsig_checker(
|
||||
has_valid_signature = _verify_h(
|
||||
signed_string, base64.b64decode(hsig["signature"]), k.pubkey
|
||||
)
|
||||
# FIXME: fetch/update the user if the signature is wrong
|
||||
|
||||
# If the signature is not valid, we may have to update the cached actor
|
||||
if not has_valid_signature:
|
||||
logger.info("Invalid signature, trying to refresh actor")
|
||||
try:
|
||||
k = await _get_public_key(db_session, hsig["keyId"], should_skip_cache=True)
|
||||
has_valid_signature = _verify_h(
|
||||
signed_string, base64.b64decode(hsig["signature"]), k.pubkey
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to refresh actor")
|
||||
|
||||
httpsig_info = HTTPSigInfo(
|
||||
has_valid_signature=has_valid_signature,
|
||||
|
@ -26,7 +26,7 @@ async def new_ap_incoming_activity(
|
||||
raw_object: ap.RawObject,
|
||||
) -> models.IncomingActivity | None:
|
||||
ap_id: str
|
||||
if "id" not in raw_object:
|
||||
if "id" not in raw_object or ap.as_list(raw_object["type"])[0] in ap.ACTOR_TYPES:
|
||||
if "@context" not in raw_object:
|
||||
logger.warning(f"Dropping invalid object: {raw_object}")
|
||||
return None
|
||||
|
@ -276,7 +276,7 @@ async def _check_access_token(
|
||||
if now() > access_token_info.created_at.replace(tzinfo=timezone.utc) + timedelta(
|
||||
seconds=access_token_info.expires_in
|
||||
):
|
||||
logger.info("Access token is expired")
|
||||
logger.info("Access token has expired")
|
||||
return False, None
|
||||
|
||||
return True, access_token_info
|
||||
|
@ -38,4 +38,9 @@ async def lookup(db_session: AsyncSession, query: str) -> Actor | RemoteObject:
|
||||
if ap.as_list(ap_obj["type"])[0] in ap.ACTOR_TYPES:
|
||||
return RemoteActor(ap_obj)
|
||||
else:
|
||||
# Some software return objects wrapped in a Create activity (like
|
||||
# python-federation)
|
||||
if ap.as_list(ap_obj["type"])[0] == "Create":
|
||||
ap_obj = await ap.get_object(ap_obj)
|
||||
|
||||
return await RemoteObject.from_raw_object(ap_obj)
|
||||
|
85
app/main.py
85
app/main.py
@ -403,6 +403,20 @@ async def _build_followx_collection(
|
||||
return collection_page
|
||||
|
||||
|
||||
async def _empty_followx_collection(
|
||||
db_session: AsyncSession,
|
||||
model_cls: Type[models.Following | models.Follower],
|
||||
path: str,
|
||||
) -> ap.RawObject:
|
||||
total_items = await db_session.scalar(select(func.count(model_cls.id)))
|
||||
return {
|
||||
"@context": ap.AS_CTX,
|
||||
"id": ID + path,
|
||||
"type": "OrderedCollection",
|
||||
"totalItems": total_items,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/followers")
|
||||
async def followers(
|
||||
request: Request,
|
||||
@ -413,6 +427,15 @@ async def followers(
|
||||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> ActivityPubResponse | templates.TemplateResponse:
|
||||
if is_activitypub_requested(request):
|
||||
if config.HIDES_FOLLOWERS:
|
||||
return ActivityPubResponse(
|
||||
await _empty_followx_collection(
|
||||
db_session=db_session,
|
||||
model_cls=models.Follower,
|
||||
path="/followers",
|
||||
)
|
||||
)
|
||||
else:
|
||||
return ActivityPubResponse(
|
||||
await _build_followx_collection(
|
||||
db_session=db_session,
|
||||
@ -423,6 +446,9 @@ async def followers(
|
||||
)
|
||||
)
|
||||
|
||||
if config.HIDES_FOLLOWERS and not is_current_user_admin(request):
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
# We only show the most recent 20 followers on the public website
|
||||
followers_result = await db_session.scalars(
|
||||
select(models.Follower)
|
||||
@ -460,6 +486,15 @@ async def following(
|
||||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> ActivityPubResponse | templates.TemplateResponse:
|
||||
if is_activitypub_requested(request):
|
||||
if config.HIDES_FOLLOWING:
|
||||
return ActivityPubResponse(
|
||||
await _empty_followx_collection(
|
||||
db_session=db_session,
|
||||
model_cls=models.Following,
|
||||
path="/following",
|
||||
)
|
||||
)
|
||||
else:
|
||||
return ActivityPubResponse(
|
||||
await _build_followx_collection(
|
||||
db_session=db_session,
|
||||
@ -470,6 +505,9 @@ async def following(
|
||||
)
|
||||
)
|
||||
|
||||
if config.HIDES_FOLLOWING and not is_current_user_admin(request):
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
# We only show the most recent 20 follows on the public website
|
||||
following = (
|
||||
(
|
||||
@ -725,7 +763,7 @@ async def tag_by_name(
|
||||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> ActivityPubResponse | templates.TemplateResponse:
|
||||
where = [
|
||||
models.TaggedOutboxObject.tag == tag,
|
||||
models.TaggedOutboxObject.tag == tag.lower(),
|
||||
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
|
||||
models.OutboxObject.is_deleted.is_(False),
|
||||
]
|
||||
@ -751,7 +789,7 @@ async def tag_by_name(
|
||||
return ActivityPubResponse(
|
||||
{
|
||||
"@context": ap.AS_CTX,
|
||||
"id": BASE_URL + f"/t/{tag}",
|
||||
"id": BASE_URL + f"/t/{tag.lower()}",
|
||||
"type": "OrderedCollection",
|
||||
"totalItems": tagged_count,
|
||||
"orderedItems": [
|
||||
@ -845,6 +883,48 @@ async def post_remote_follow(
|
||||
)
|
||||
|
||||
|
||||
@app.get("/remote_interaction")
|
||||
async def remote_interaction(
|
||||
request: Request,
|
||||
ap_id: str,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> templates.TemplateResponse:
|
||||
outbox_object = await boxes.get_outbox_object_by_ap_id(
|
||||
db_session,
|
||||
ap_id,
|
||||
)
|
||||
if not outbox_object:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"remote_interact.html",
|
||||
{"outbox_object": outbox_object},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/remote_interaction")
|
||||
async def post_remote_interaction(
|
||||
request: Request,
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
profile: str = Form(),
|
||||
ap_id: str = Form(),
|
||||
) -> RedirectResponse:
|
||||
if not profile.startswith("@"):
|
||||
profile = f"@{profile}"
|
||||
|
||||
remote_follow_template = await get_remote_follow_template(profile)
|
||||
if not remote_follow_template:
|
||||
# TODO(ts): error message to user
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
return RedirectResponse(
|
||||
remote_follow_template.format(uri=ap_id),
|
||||
status_code=302,
|
||||
)
|
||||
|
||||
|
||||
@app.get("/.well-known/webfinger")
|
||||
async def wellknown_webfinger(resource: str) -> JSONResponse:
|
||||
"""Exposes/servers WebFinger data."""
|
||||
@ -1141,6 +1221,7 @@ async def robots_file():
|
||||
Disallow: /followers
|
||||
Disallow: /following
|
||||
Disallow: /admin
|
||||
Disallow: /remote_interaction
|
||||
Disallow: /remote_follow"""
|
||||
|
||||
|
||||
|
@ -75,7 +75,7 @@ class InboxObject(Base, BaseObject):
|
||||
|
||||
ap_actor_id = Column(String, nullable=False)
|
||||
ap_type = Column(String, nullable=False, index=True)
|
||||
ap_id = Column(String, nullable=False, unique=True, index=True)
|
||||
ap_id: Mapped[str] = Column(String, nullable=False, unique=True, index=True)
|
||||
ap_context = Column(String, nullable=True)
|
||||
ap_published_at = Column(DateTime(timezone=True), nullable=False)
|
||||
ap_object: Mapped[ap.RawObject] = Column(JSON, nullable=False)
|
||||
@ -160,7 +160,7 @@ class OutboxObject(Base, BaseObject):
|
||||
public_id = Column(String, nullable=False, index=True)
|
||||
|
||||
ap_type = Column(String, nullable=False, index=True)
|
||||
ap_id = Column(String, nullable=False, unique=True, index=True)
|
||||
ap_id: Mapped[str] = Column(String, nullable=False, unique=True, index=True)
|
||||
ap_context = Column(String, nullable=True)
|
||||
ap_object: Mapped[ap.RawObject] = Column(JSON, nullable=False)
|
||||
|
||||
@ -551,6 +551,9 @@ class NotificationType(str, enum.Enum):
|
||||
UPDATED_WEBMENTION = "updated_webmention"
|
||||
DELETED_WEBMENTION = "deleted_webmention"
|
||||
|
||||
BLOCKED = "blocked"
|
||||
UNBLOCKED = "unblocked"
|
||||
|
||||
|
||||
class Notification(Base):
|
||||
__tablename__ = "notifications"
|
||||
|
@ -212,6 +212,7 @@ a {
|
||||
}
|
||||
}
|
||||
#main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
main {
|
||||
@ -220,10 +221,19 @@ main {
|
||||
margin: 30px auto;
|
||||
}
|
||||
|
||||
.main-flex {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
div {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
@ -378,7 +388,7 @@ nav.flexbox {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
a {
|
||||
a:not(.label-btn) {
|
||||
color: $primary-color;
|
||||
text-decoration: none;
|
||||
&:hover, &:active {
|
||||
@ -386,25 +396,31 @@ nav.flexbox {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
a.active {
|
||||
a.active:not(.label-btn) {
|
||||
color: $secondary-color;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
// after nav.flexbox to override default behavior
|
||||
a.label-btn {
|
||||
color: $form-text-color;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: $form-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.ap-object {
|
||||
margin: 15px 0;
|
||||
padding: 20px;
|
||||
.in-reply-to {
|
||||
color: $muted-color;
|
||||
&:hover {
|
||||
color: $secondary-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
nav {
|
||||
color: $muted-color;
|
||||
}
|
||||
.in-reply-to {
|
||||
display: inline;
|
||||
color: $muted-color;
|
||||
}
|
||||
.e-content, .activity-og-meta {
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
@ -509,3 +525,9 @@ nav.flexbox {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-title {
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
178
app/source.py
178
app/source.py
@ -1,52 +1,123 @@
|
||||
import re
|
||||
import typing
|
||||
|
||||
from markdown import markdown
|
||||
from mistletoe import Document # type: ignore
|
||||
from mistletoe.html_renderer import HTMLRenderer # type: ignore
|
||||
from mistletoe.span_token import SpanToken # type: ignore
|
||||
from pygments import highlight # type: ignore
|
||||
from pygments.formatters import HtmlFormatter # type: ignore
|
||||
from pygments.lexers import get_lexer_by_name as get_lexer # type: ignore
|
||||
from pygments.lexers import guess_lexer # type: ignore
|
||||
from sqlalchemy import select
|
||||
|
||||
from app import webfinger
|
||||
from app.config import BASE_URL
|
||||
from app.config import CODE_HIGHLIGHTING_THEME
|
||||
from app.database import AsyncSession
|
||||
from app.utils import emoji
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from app.actor import Actor
|
||||
|
||||
|
||||
def _set_a_attrs(attrs, new=False):
|
||||
attrs[(None, "target")] = "_blank"
|
||||
attrs[(None, "class")] = "external"
|
||||
attrs[(None, "rel")] = "noopener"
|
||||
attrs[(None, "title")] = attrs[(None, "href")]
|
||||
return attrs
|
||||
|
||||
|
||||
_FORMATTER = HtmlFormatter(style=CODE_HIGHLIGHTING_THEME)
|
||||
_HASHTAG_REGEX = re.compile(r"(#[\d\w]+)")
|
||||
_MENTION_REGEX = re.compile(r"@[\d\w_.+-]+@[\d\w-]+\.[\d\w\-.]+")
|
||||
_MENTION_REGEX = re.compile(r"(@[\d\w_.+-]+@[\d\w-]+\.[\d\w\-.]+)")
|
||||
_URL_REGEX = re.compile(
|
||||
"(https?:\\/\\/(?:www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*))" # noqa: E501
|
||||
)
|
||||
|
||||
|
||||
def hashtagify(content: str) -> tuple[str, list[dict[str, str]]]:
|
||||
tags = []
|
||||
hashtags = re.findall(_HASHTAG_REGEX, content)
|
||||
hashtags = sorted(set(hashtags), reverse=True) # unique tags, longest first
|
||||
for hashtag in hashtags:
|
||||
tag = hashtag[1:]
|
||||
link = f'<a href="{BASE_URL}/t/{tag}" class="mention hashtag" rel="tag">#<span>{tag}</span></a>' # noqa: E501
|
||||
tags.append(dict(href=f"{BASE_URL}/t/{tag}", name=hashtag, type="Hashtag"))
|
||||
content = content.replace(hashtag, link)
|
||||
return content, tags
|
||||
class AutoLink(SpanToken):
|
||||
parse_inner = False
|
||||
precedence = 1
|
||||
pattern = _URL_REGEX
|
||||
|
||||
def __init__(self, match_obj: re.Match) -> None:
|
||||
self.target = match_obj.group()
|
||||
|
||||
|
||||
async def _mentionify(
|
||||
class Mention(SpanToken):
|
||||
parse_inner = False
|
||||
precedence = 10
|
||||
pattern = _MENTION_REGEX
|
||||
|
||||
def __init__(self, match_obj: re.Match) -> None:
|
||||
self.target = match_obj.group()
|
||||
|
||||
|
||||
class Hashtag(SpanToken):
|
||||
parse_inner = False
|
||||
precedence = 10
|
||||
pattern = _HASHTAG_REGEX
|
||||
|
||||
def __init__(self, match_obj: re.Match) -> None:
|
||||
self.target = match_obj.group()
|
||||
|
||||
|
||||
class CustomRenderer(HTMLRenderer):
|
||||
def __init__(
|
||||
self,
|
||||
mentioned_actors: dict[str, "Actor"] = {},
|
||||
enable_mentionify: bool = True,
|
||||
enable_hashtagify: bool = True,
|
||||
) -> None:
|
||||
extra_tokens = []
|
||||
if enable_mentionify:
|
||||
extra_tokens.append(Mention)
|
||||
if enable_hashtagify:
|
||||
extra_tokens.append(Hashtag)
|
||||
super().__init__(AutoLink, *extra_tokens)
|
||||
|
||||
self.tags: list[dict[str, str]] = []
|
||||
self.mentioned_actors = mentioned_actors
|
||||
|
||||
def render_auto_link(self, token: AutoLink) -> str:
|
||||
template = '<a href="{target}" rel="noopener">{inner}</a>'
|
||||
target = self.escape_url(token.target)
|
||||
return template.format(target=target, inner=target)
|
||||
|
||||
def render_mention(self, token: Mention) -> str:
|
||||
mention = token.target
|
||||
actor = self.mentioned_actors.get(mention)
|
||||
if not actor:
|
||||
return mention
|
||||
|
||||
self.tags.append(dict(type="Mention", href=actor.ap_id, name=mention))
|
||||
|
||||
link = f'<span class="h-card"><a href="{actor.url}" class="u-url mention">{actor.handle}</a></span>' # noqa: E501
|
||||
return link
|
||||
|
||||
def render_hashtag(self, token: Hashtag) -> str:
|
||||
tag = token.target[1:]
|
||||
link = f'<a href="{BASE_URL}/t/{tag.lower()}" class="mention hashtag" rel="tag">#<span>{tag}</span></a>' # noqa: E501
|
||||
self.tags.append(
|
||||
dict(
|
||||
href=f"{BASE_URL}/t/{tag.lower()}",
|
||||
name=token.target.lower(),
|
||||
type="Hashtag",
|
||||
)
|
||||
)
|
||||
return link
|
||||
|
||||
def render_block_code(self, token: typing.Any) -> str:
|
||||
code = token.children[0].content
|
||||
lexer = get_lexer(token.language) if token.language else guess_lexer(code)
|
||||
return highlight(code, lexer, _FORMATTER)
|
||||
|
||||
|
||||
async def _prefetch_mentioned_actors(
|
||||
db_session: AsyncSession,
|
||||
content: str,
|
||||
) -> tuple[str, list[dict[str, str]], list["Actor"]]:
|
||||
) -> dict[str, "Actor"]:
|
||||
from app import models
|
||||
from app.actor import fetch_actor
|
||||
|
||||
tags = []
|
||||
mentioned_actors = []
|
||||
actors = {}
|
||||
|
||||
for mention in re.findall(_MENTION_REGEX, content):
|
||||
if mention in actors:
|
||||
continue
|
||||
|
||||
_, username, domain = mention.split("@")
|
||||
actor = (
|
||||
await db_session.execute(
|
||||
@ -63,12 +134,27 @@ async def _mentionify(
|
||||
continue
|
||||
actor = await fetch_actor(db_session, actor_url)
|
||||
|
||||
mentioned_actors.append(actor)
|
||||
tags.append(dict(type="Mention", href=actor.ap_id, name=mention))
|
||||
actors[mention] = actor
|
||||
|
||||
link = f'<span class="h-card"><a href="{actor.url}" class="u-url mention">{actor.handle}</a></span>' # noqa: E501
|
||||
content = content.replace(mention, link)
|
||||
return content, tags, mentioned_actors
|
||||
return actors
|
||||
|
||||
|
||||
def hashtagify(
|
||||
content: str,
|
||||
) -> tuple[str, list[dict[str, str]]]:
|
||||
tags = []
|
||||
with CustomRenderer(
|
||||
mentioned_actors={},
|
||||
enable_mentionify=False,
|
||||
enable_hashtagify=True,
|
||||
) as renderer:
|
||||
rendered_content = renderer.render(Document(content))
|
||||
tags.extend(renderer.tags)
|
||||
|
||||
# Handle custom emoji
|
||||
tags.extend(emoji.tags(content))
|
||||
|
||||
return rendered_content, tags
|
||||
|
||||
|
||||
async def markdownify(
|
||||
@ -82,17 +168,33 @@ async def markdownify(
|
||||
|
||||
"""
|
||||
tags = []
|
||||
mentioned_actors: list["Actor"] = []
|
||||
if enable_hashtagify:
|
||||
content, hashtag_tags = hashtagify(content)
|
||||
tags.extend(hashtag_tags)
|
||||
mentioned_actors: dict[str, "Actor"] = {}
|
||||
if enable_mentionify:
|
||||
content, mention_tags, mentioned_actors = await _mentionify(db_session, content)
|
||||
tags.extend(mention_tags)
|
||||
mentioned_actors = await _prefetch_mentioned_actors(db_session, content)
|
||||
|
||||
with CustomRenderer(
|
||||
mentioned_actors=mentioned_actors,
|
||||
enable_mentionify=enable_mentionify,
|
||||
enable_hashtagify=enable_hashtagify,
|
||||
) as renderer:
|
||||
rendered_content = renderer.render(Document(content))
|
||||
tags.extend(renderer.tags)
|
||||
|
||||
# Handle custom emoji
|
||||
tags.extend(emoji.tags(content))
|
||||
|
||||
content = markdown(content, extensions=["mdx_linkify", "fenced_code"])
|
||||
return rendered_content, dedup_tags(tags), list(mentioned_actors.values())
|
||||
|
||||
return content, tags, mentioned_actors
|
||||
|
||||
def dedup_tags(tags: list[dict[str, str]]) -> list[dict[str, str]]:
|
||||
idx = set()
|
||||
deduped_tags = []
|
||||
for tag in tags:
|
||||
tag_idx = (tag["type"], tag["name"])
|
||||
if tag_idx in idx:
|
||||
continue
|
||||
|
||||
idx.add(tag_idx)
|
||||
deduped_tags.append(tag)
|
||||
|
||||
return deduped_tags
|
||||
|
@ -419,3 +419,5 @@ _templates.env.filters["privacy_replace_url"] = privacy_replace.replace_url
|
||||
_templates.env.globals["JS_HASH"] = config.JS_HASH
|
||||
_templates.env.globals["CSS_HASH"] = config.CSS_HASH
|
||||
_templates.env.globals["BASE_URL"] = config.BASE_URL
|
||||
_templates.env.globals["HIDES_FOLLOWERS"] = config.HIDES_FOLLOWERS
|
||||
_templates.env.globals["HIDES_FOLLOWING"] = config.HIDES_FOLLOWING
|
||||
|
@ -27,7 +27,7 @@
|
||||
{{ utils.actor_action(inbox_object, "followed you") }}
|
||||
{{ utils.display_actor(inbox_object.actor, actors_metadata) }}
|
||||
{% elif inbox_object.ap_type == "Like" %}
|
||||
{{ utils.actor_action(inbox_object, "liked one of your post", with_icon=True) }}
|
||||
{{ utils.actor_action(inbox_object, "liked one of your posts", with_icon=True) }}
|
||||
{{ utils.display_object(inbox_object.relates_to_anybox_object) }}
|
||||
{% else %}
|
||||
<p>
|
||||
|
@ -12,18 +12,16 @@
|
||||
{% for outbox_object in outbox %}
|
||||
|
||||
{% if outbox_object.ap_type == "Announce" %}
|
||||
<div class="actor-action">You shared</div>
|
||||
<div class="actor-action">You shared <span title="{{ outbox_object.ap_published_at.isoformat() }}">{{ outbox_object.ap_published_at | timeago }}</span></div>
|
||||
{{ utils.display_object(outbox_object.relates_to_anybox_object) }}
|
||||
{% elif outbox_object.ap_type == "Like" %}
|
||||
<div class="actor-action">You liked</div>
|
||||
<div class="actor-action">You liked <span title="{{ outbox_object.ap_published_at.isoformat() }}">{{ outbox_object.ap_published_at | timeago }}</span></div>
|
||||
{{ utils.display_object(outbox_object.relates_to_anybox_object) }}
|
||||
{% elif outbox_object.ap_type == "Follow" %}
|
||||
<div class="actor-action">You followed</div>
|
||||
<div class="actor-action">You followed <span title="{{ outbox_object.ap_published_at.isoformat() }}">{{ outbox_object.ap_published_at | timeago }}</span></div>
|
||||
{{ utils.display_actor(outbox_object.relates_to_actor, actors_metadata) }}
|
||||
{% elif outbox_object.ap_type in ["Article", "Note", "Video", "Question"] %}
|
||||
{{ utils.display_object(outbox_object) }}
|
||||
{% else %}
|
||||
Implement {{ outbox_object.ap_type }}
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
|
@ -1,12 +1,12 @@
|
||||
{%- import "utils.html" as utils with context -%}
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block main_tag %} class="main-flex"{% endblock %}
|
||||
{% block head %}
|
||||
<title>{{ title }}</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="centered primary-color">
|
||||
<h1>{{ title }}</h1>
|
||||
<div class="centered primary-color box">
|
||||
<h1 class="error-title">{{ title | safe }}</h1>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -36,8 +36,12 @@
|
||||
{% if articles_count %}
|
||||
<li>{{ header_link("articles", "Articles") }}</li>
|
||||
{% endif %}
|
||||
{% if not HIDES_FOLLOWERS or is_admin %}
|
||||
<li>{{ header_link("followers", "Followers") }} <span class="counter">{{ followers_count }}</span></li>
|
||||
{% endif %}
|
||||
{% if not HIDES_FOLLOWING or is_admin %}
|
||||
<li>{{ header_link("following", "Following") }} <span class="counter">{{ following_count }}</span></li>
|
||||
{% endif %}
|
||||
<li>{{ header_link("get_remote_follow", "Remote follow") }}</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
@ -14,7 +14,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="main">
|
||||
<main>
|
||||
<main{%- block main_tag %}{%- endblock %}>
|
||||
{% if is_admin %}
|
||||
<div id="admin">
|
||||
{% macro admin_link(url, text) %}
|
||||
|
@ -1,15 +1,18 @@
|
||||
{%- import "utils.html" as utils with context -%}
|
||||
{% extends "layout.html" %}
|
||||
{% block main_tag %} class="main-flex"{% endblock %}
|
||||
{% block content %}
|
||||
<div class="centered">
|
||||
{% if error %}
|
||||
<p class="primary-color">Invalid password.</p>
|
||||
{% endif %}
|
||||
<form class="form" action="/admin/login" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="redirect" value="{{ redirect }}">
|
||||
<input type="password" placeholder="password" name="password" autofocus>
|
||||
<input type="submit" value="login">
|
||||
</form>
|
||||
<div>
|
||||
{% if error %}
|
||||
<p class="primary-color">Invalid password.</p>
|
||||
{% endif %}
|
||||
<form class="form" action="/admin/login" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="redirect" value="{{ redirect }}">
|
||||
<input type="password" placeholder="password" name="password" autofocus>
|
||||
<input type="submit" value="login">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -20,6 +20,8 @@
|
||||
<div class="box error-box">
|
||||
{% if error.value == "NOT_FOUND" %}
|
||||
<p>The remote object is unavailable.</p>
|
||||
{% elif error.value == "UNAUTHORIZED" %}
|
||||
<p>Missing permissions to fetch the remote object.</p>
|
||||
{% elif error.value == "TIMEOUT" %}
|
||||
<p>Lookup timed out, please try refreshing the page.</p>
|
||||
{% else %}
|
||||
|
@ -36,6 +36,12 @@
|
||||
{%- elif notif.notification_type.value == "follow_request_rejected" %}
|
||||
{{ notif_actor_action(notif, "rejected your follow request") }}
|
||||
{{ utils.display_actor(notif.actor, actors_metadata) }}
|
||||
{% elif notif.notification_type.value == "blocked" %}
|
||||
{{ notif_actor_action(notif, "blocked you") }}
|
||||
{{ utils.display_actor(notif.actor, actors_metadata) }}
|
||||
{% elif notif.notification_type.value == "unblocked" %}
|
||||
{{ notif_actor_action(notif, "unblocked you") }}
|
||||
{{ utils.display_actor(notif.actor, actors_metadata) }}
|
||||
{%- elif notif.notification_type.value == "move" %}
|
||||
{# for move notif, the actor is the target and the inbox object the Move activity #}
|
||||
<div class="actor-action">
|
||||
|
26
app/templates/remote_interact.html
Normal file
26
app/templates/remote_interact.html
Normal file
@ -0,0 +1,26 @@
|
||||
{%- import "utils.html" as utils with context -%}
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block head %}
|
||||
<title>Interact from your instance</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "header.html" %}
|
||||
|
||||
<div class="box">
|
||||
<h2>Interact with this object</h2>
|
||||
</div>
|
||||
|
||||
{{ utils.display_object(outbox_object) }}
|
||||
|
||||
<div class="box">
|
||||
<form class="form" action="{{ url_for("post_remote_interaction") }}" method="POST">
|
||||
{{ utils.embed_csrf_token() }}
|
||||
<input type="text" name="profile" placeholder="you@instance.tld" autofocus>
|
||||
<input type="hidden" name="ap_id" value="{{ outbox_object.ap_id }}">
|
||||
<input type="submit" value="interact from your instance">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -216,13 +216,25 @@
|
||||
<div>
|
||||
<nav class="flexbox actor-metadata">
|
||||
<ul>
|
||||
{% if metadata.has_blocked_local_actor %}
|
||||
<li>blocked you</li>
|
||||
{% endif %}
|
||||
{% if metadata.is_following %}
|
||||
<li>already following</li>
|
||||
<li>{{ admin_undo_button(metadata.outbox_follow_ap_id, "unfollow")}}</li>
|
||||
{% if not with_details %}
|
||||
<li>{{ admin_profile_button(actor.ap_id) }}</li>
|
||||
{% endif %}
|
||||
{% elif metadata.is_follow_request_sent %}
|
||||
{% if metadata.is_follow_request_rejected %}
|
||||
<li>follow request rejected</li>
|
||||
{% if not metadata.has_blocked_local_actor %}
|
||||
<li>{{ admin_follow_button(actor) }}</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li>follow request sent</li>
|
||||
<li>{{ admin_undo_button(metadata.outbox_follow_ap_id, "undo follow") }}</li>
|
||||
{% endif %}
|
||||
{% elif not actor.moved_to %}
|
||||
<li>{{ admin_follow_button(actor) }}</li>
|
||||
{% endif %}
|
||||
@ -231,7 +243,7 @@
|
||||
{% if not metadata.is_following and not with_details %}
|
||||
<li>{{ admin_profile_button(actor.ap_id) }}</li>
|
||||
{% endif %}
|
||||
{% elif actor.is_from_db and not with_details %}
|
||||
{% elif actor.is_from_db and not with_details and not metadata.is_following %}
|
||||
<li>{{ admin_profile_button(actor.ap_id) }}</li>
|
||||
{% endif %}
|
||||
{% if actor.moved_to %}
|
||||
@ -261,6 +273,9 @@
|
||||
<li>rejected</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if with_details %}
|
||||
<li><a href="{{ actor.url }}" class="label-btn">remote profile</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@ -295,7 +310,7 @@
|
||||
|
||||
{% macro display_og_meta(object) %}
|
||||
{% if object.og_meta %}
|
||||
{% for og_meta in object.og_meta %}
|
||||
{% for og_meta in object.og_meta[:1] %}
|
||||
<div class="activity-og-meta">
|
||||
{% if og_meta.image %}
|
||||
<div>
|
||||
@ -338,11 +353,13 @@
|
||||
{% elif attachment.type == "Audio" or (attachment | has_media_type("audio")) %}
|
||||
<audio controls preload="metadata" src="{{ attachment.url | media_proxy_url }}"{% if attachment.name%} title="{{ attachment.name }}"{% endif %} class="attachment"></audio>
|
||||
{% elif attachment.type == "Link" %}
|
||||
<a href="{{ attachment.url }}" class="attachment">{{ attachment.url }}</a>
|
||||
<a href="{{ attachment.url }}" class="attachment">{{ attachment.url | truncate(64, True) }}</a> ({{ attachment.mimetype}})
|
||||
{% else %}
|
||||
<a href="{{ attachment.url | media_proxy_url }}"{% if attachment.name %} title="{{ attachment.name }}"{% endif %} class="attachment">{{ attachment.url }}</a>
|
||||
<a href="{{ attachment.url | media_proxy_url }}"{% if attachment.name %} title="{{ attachment.url }}"{% endif %} class="attachment">
|
||||
{% if attachment.name %}{{ attachment.name }}{% else %}{{ attachment.url | truncate(64, True) }}{% endif %}
|
||||
</a> ({{ attachment.mimetype }})
|
||||
{% endif %}
|
||||
{% if object.sensitive %}
|
||||
{% if object.sensitive and (attachment.type == "Image" or (attachment | has_media_type("image")) or attachment.type == "Video" or (attachment | has_media_type("video"))) %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -369,9 +386,9 @@
|
||||
{% endif %}
|
||||
|
||||
{% if object.in_reply_to %}
|
||||
<a href="{% if is_admin and object.is_in_reply_to_from_inbox %}{{ url_for("get_lookup") }}?query={% endif %}{{ object.in_reply_to }}" title="{{ object.in_reply_to }}" class="in-reply-to" rel="nofollow">
|
||||
in reply to {{ object.in_reply_to|truncate(64, True) }}
|
||||
</a>
|
||||
<p class="in-reply-to">in reply to <a href="{% if is_admin and object.is_in_reply_to_from_inbox %}{{ url_for("get_lookup") }}?query={% endif %}{{ object.in_reply_to }}" title="{{ object.in_reply_to }}" rel="nofollow">
|
||||
this {{ object.ap_type|lower }}
|
||||
</a></p>
|
||||
{% endif %}
|
||||
|
||||
{% if object.ap_type == "Article" %}
|
||||
@ -442,10 +459,11 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{{ display_og_meta(object) }}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{{ display_og_meta(object) }}
|
||||
|
||||
</div>
|
||||
{% if object.summary %}
|
||||
</div>
|
||||
@ -460,6 +478,16 @@
|
||||
<li>
|
||||
<div><a href="{{ object.url }}"{% if object.is_from_inbox %} rel="nofollow"{% endif %} class="object-permalink u-url u-uid">permalink</a></div>
|
||||
</li>
|
||||
|
||||
{% if object.is_from_outbox and is_object_page and not is_admin and not request.url.path.startswith("/remote_interaction") %}
|
||||
<li>
|
||||
<a class="label-btn" href="{{ request.url_for("remote_interaction") }}?ap_id={{ object.ap_id }}">
|
||||
interact from your instance
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if not is_article_mode %}
|
||||
<li>
|
||||
<time class="dt-published" datetime="{{ object.ap_published_at.replace(microsecond=0).isoformat() }}" title="{{ object.ap_published_at.replace(microsecond=0).isoformat() }}">{{ object.ap_published_at | timeago }}</time>
|
||||
@ -555,7 +583,7 @@
|
||||
{% if object.visibility in [visibility_enum.PUBLIC, visibility_enum.UNLISTED] %}
|
||||
<li>
|
||||
{% if object.announced_via_outbox_object_ap_id %}
|
||||
{{ admin_undo_button(object.liked_via_outbox_object_ap_id, "unshare") }}
|
||||
{{ admin_undo_button(object.announced_via_outbox_object_ap_id, "unshare") }}
|
||||
{% else %}
|
||||
{{ admin_announce_button(object.ap_id, permalink_id=object.permalink_id) }}
|
||||
{% endif %}
|
||||
|
@ -46,7 +46,7 @@ async def save_upload(db_session: AsyncSession, f: UploadFile) -> models.Upload:
|
||||
width = None
|
||||
height = None
|
||||
|
||||
if f.content_type.startswith("image"):
|
||||
if f.content_type.startswith("image") and not f.content_type == "image/gif":
|
||||
with Image.open(f.file) as _original_image:
|
||||
# Fix image orientation (as we will remove the info from the EXIF
|
||||
# metadata)
|
||||
|
@ -9,6 +9,7 @@ from bs4 import BeautifulSoup # type: ignore
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app import activitypub as ap
|
||||
from app import ap_object
|
||||
from app import config
|
||||
from app.actor import LOCAL_ACTOR
|
||||
@ -69,7 +70,12 @@ async def external_urls(
|
||||
tags_hrefs.add(tag_href)
|
||||
if tag.get("type") == "Mention":
|
||||
if tag["href"] != LOCAL_ACTOR.ap_id:
|
||||
try:
|
||||
mentioned_actor = await fetch_actor(db_session, tag["href"])
|
||||
except (ap.FetchError, ap.NotAnObjectError):
|
||||
tags_hrefs.add(tag["href"])
|
||||
continue
|
||||
|
||||
tags_hrefs.add(mentioned_actor.url)
|
||||
tags_hrefs.add(mentioned_actor.ap_id)
|
||||
else:
|
||||
@ -81,6 +87,10 @@ async def external_urls(
|
||||
soup = BeautifulSoup(ro.content, "html5lib")
|
||||
for link in soup.find_all("a"):
|
||||
h = link.get("href")
|
||||
if not h:
|
||||
continue
|
||||
|
||||
try:
|
||||
ph = urlparse(h)
|
||||
mimetype, _ = mimetypes.guess_type(h)
|
||||
if (
|
||||
@ -93,6 +103,9 @@ async def external_urls(
|
||||
)
|
||||
):
|
||||
urls.add(h)
|
||||
except Exception:
|
||||
logger.exception(f"Failed to check {h}")
|
||||
continue
|
||||
|
||||
return urls - tags_hrefs
|
||||
|
||||
|
@ -58,6 +58,10 @@ def is_url_valid(url: str) -> bool:
|
||||
logger.warning(f"{parsed.hostname} is blocked")
|
||||
return False
|
||||
|
||||
if parsed.hostname.endswith(".onion"):
|
||||
logger.warning(f"{url} is an onion service")
|
||||
return False
|
||||
|
||||
ip_address = _getaddrinfo(
|
||||
parsed.hostname, parsed.port or (80 if parsed.scheme == "http" else 443)
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Developer's guide
|
||||
|
||||
This guide assume you have some knoweldge of [ActivityPub](https://activitypub.rocks/).
|
||||
This guide assumes you have some knowledge of [ActivityPub](https://activitypub.rocks/).
|
||||
|
||||
[TOC]
|
||||
|
||||
|
@ -11,7 +11,7 @@ For now, there's no image published on Docker Hub, this means you will have to b
|
||||
Clone the repository, replace `you-domain.tld` by your own domain.
|
||||
|
||||
Note that if you want to serve static assets via your reverse proxy (like nginx), clone it in a place
|
||||
where accessible by your reverse proxy user.
|
||||
where it is accessible by your reverse proxy user.
|
||||
|
||||
```bash
|
||||
git clone https://git.sr.ht/~tsileo/microblog.pub your-domain.tld
|
||||
@ -89,6 +89,12 @@ Setup config.
|
||||
poetry run inv configuration-wizard
|
||||
```
|
||||
|
||||
Setup the database.
|
||||
|
||||
```bash
|
||||
poetry run inv migrate-db
|
||||
```
|
||||
|
||||
Grab your virtualenv path.
|
||||
|
||||
```bash
|
||||
|
2
docs/templates/layout.html
vendored
2
docs/templates/layout.html
vendored
@ -63,7 +63,7 @@ nav a:hover, main a:hover, header p a:hover {
|
||||
max-width: 960px;
|
||||
margin: 50px auto;
|
||||
}
|
||||
pre code {
|
||||
pre {
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
display: block;
|
||||
|
@ -29,17 +29,20 @@ You can tweak your profile by tweaking these items:
|
||||
- `summary` (using Markdown)
|
||||
- `icon_url`
|
||||
|
||||
Whenever one of these config items is updated, an `Update` activity will be sent to all know server to update your remote profile.
|
||||
Whenever one of these config items is updated, an `Update` activity will be sent to all known servers to update your remote profile.
|
||||
|
||||
The server will need to be restarted for taking changes into account.
|
||||
|
||||
Before restarting the server, you can ensure you haven't made any mistakes by running the [configuration checking task](/user_guide.html#configuration-checking).
|
||||
|
||||
|
||||
### Profile metadata
|
||||
|
||||
You can add metadata to your profile with the `metadata` config item.
|
||||
|
||||
Markdown is supported in the `value` field.
|
||||
|
||||
Be aware that most other softwares like Mastodon will limit the number of key/value to 4.
|
||||
Be aware that most other software like Mastodon will limit the number of key/value to 4.
|
||||
|
||||
```toml
|
||||
metadata = [
|
||||
@ -58,12 +61,32 @@ manually_approves_followers = true
|
||||
|
||||
The default value is `false`.
|
||||
|
||||
### Hiding followers
|
||||
|
||||
If you wish to hide your followers, add this config item to `profile.toml`:
|
||||
|
||||
```toml
|
||||
hides_followers = true
|
||||
```
|
||||
|
||||
The default value is `false`.
|
||||
|
||||
### Hiding who you are following
|
||||
|
||||
If you wish to hide who you are following, add this config item to `profile.toml`:
|
||||
|
||||
```toml
|
||||
hides_following = true
|
||||
```
|
||||
|
||||
The default value is `false`.
|
||||
|
||||
### Privacy replace
|
||||
|
||||
You can define domain to be rewrited to more "privacy friendly" alternatives, like [Invidious](https://invidious.io/)
|
||||
You can define domains to be rewritten to more "privacy friendly" alternatives, like [Invidious](https://invidious.io/)
|
||||
or [Nitter](https://nitter.net/about).
|
||||
|
||||
To do so, just add as these extra config items, this is a sample config that rewrite URLs for Twitter, Youtube, Reddit and Medium:
|
||||
To do so, add these extra config items. This is a sample config that rewrite URLs for Twitter, Youtube, Reddit and Medium:
|
||||
|
||||
```toml
|
||||
privacy_replace = [
|
||||
@ -102,7 +125,7 @@ $primary-color: #e14eea;
|
||||
$secondary-color: #32cd32;
|
||||
```
|
||||
|
||||
See `app/scss/main.scss` to see what variables can be overidden.
|
||||
See `app/scss/main.scss` to see what variables can be overridden.
|
||||
|
||||
#### Code highlighting theme
|
||||
|
||||
@ -114,7 +137,7 @@ code_highlighting_theme = "solarized-dark"
|
||||
|
||||
### Blocking servers
|
||||
|
||||
In addition to blocking "single actors" via the admin interface, you can also prevent any communications with whole servers.
|
||||
In addition to blocking "single actors" via the admin interface, you can also prevent any communication with entire servers.
|
||||
|
||||
Add a `blocked_servers` config item into `profile.toml`.
|
||||
|
||||
@ -132,13 +155,13 @@ blocked_servers = [
|
||||
|
||||
Public notes will be visible on the homepage.
|
||||
|
||||
Only the last 20 followers/follows you be showing on the public website.
|
||||
Only the last 20 followers/follows you have will be shown on the public website.
|
||||
|
||||
And only the last 20 interactions (likes/shares/webmentions) will be displayed, to keep things simple/clean.
|
||||
|
||||
## Admin section
|
||||
|
||||
You can login to the admin section by clicking on the `Admin` link in the footer or by visiting `https://yourdomain.tld/admin`.
|
||||
You can login to the admin section by clicking on the `Admin` link in the footer or by visiting `https://yourdomain.tld/admin/login`.
|
||||
The password is the one set during the initial configuration.
|
||||
|
||||
### Lookup
|
||||
@ -202,7 +225,7 @@ Receiving a share will trigger a notification, increment the shares counter on t
|
||||
|
||||
Liking an object will notify the author.
|
||||
|
||||
Unlike sharing, liked object are not displayed on the homepage.
|
||||
Unlike sharing, liked objects are not displayed on the homepage.
|
||||
|
||||
Most receiving servers will increment the number of likes.
|
||||
|
||||
@ -212,13 +235,13 @@ Receiving a like will trigger a notification, increment the likes counter on the
|
||||
|
||||
Bookmarks allow you to like objects without notifying the author.
|
||||
|
||||
It is basically a "private like", and allow you to easily access them later.
|
||||
It is basically a "private like", and allows you to easily access them later.
|
||||
|
||||
It will also prevent objects to be pruned.
|
||||
|
||||
### Webmentions
|
||||
|
||||
Sending webmention to ping mentioned websites is done automatically once a public note is authored.
|
||||
Sending webmentions to ping mentioned websites is done automatically once a public note is authored.
|
||||
|
||||
Receiving a webmention will trigger a notification, increment the webmentions counter on the object and the source page will be displayed on the object permalink.
|
||||
|
||||
@ -241,6 +264,8 @@ If you want to move followers from your existing account, ensure it is supported
|
||||
|
||||
For [Mastodon you can look at Moving or leaving accounts](https://docs.joinmastodon.org/user/moving/).
|
||||
|
||||
If you wish to move **to** another instance, see [Moving to another instance](/user_guide.html#moving-to-another-instance).
|
||||
|
||||
First you need to grab the "ActivityPub actor URL" for your existing account:
|
||||
|
||||
### Python edition
|
||||
@ -261,7 +286,7 @@ make account=username@domain.tld webfinger
|
||||
|
||||
Edit the config.
|
||||
|
||||
#### Edit the config
|
||||
### Edit the config
|
||||
|
||||
And add a reference to your old/existing account in `profile.toml`:
|
||||
|
||||
@ -273,6 +298,61 @@ Restart the server, and you should be able to complete the move from your existi
|
||||
|
||||
## Tasks
|
||||
|
||||
### Configuration checking
|
||||
|
||||
You can confirm that your configuration file (`data/profile.toml`) is valid using the `check-config`
|
||||
|
||||
#### Python edition
|
||||
|
||||
```bash
|
||||
poetry run inv check-config
|
||||
```
|
||||
|
||||
#### Docker edition
|
||||
|
||||
```bash
|
||||
make check-config
|
||||
```
|
||||
|
||||
### Recompiling CSS files
|
||||
|
||||
You can ensure your custom theme is valid by recompiling the CSS manually using the `compile-scss` task.
|
||||
|
||||
#### Python edition
|
||||
|
||||
```bash
|
||||
poetry run inv compile-scss
|
||||
```
|
||||
|
||||
#### Docker edition
|
||||
|
||||
```bash
|
||||
make compile-scss
|
||||
```
|
||||
|
||||
|
||||
### Password reset
|
||||
|
||||
If have lost your password, you can generate a new one using the `password-reset` task.
|
||||
|
||||
#### Python edition
|
||||
|
||||
```bash
|
||||
# shutdown supervisord
|
||||
poetry run inv password-reset
|
||||
# edit data/profile.toml
|
||||
# restart supervisord
|
||||
```
|
||||
|
||||
#### Docker edition
|
||||
|
||||
```bash
|
||||
docker compose stop
|
||||
make password-reset
|
||||
# edit data/profile.toml
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Pruning old data
|
||||
|
||||
You should prune old data from time to time to free disk space.
|
||||
@ -323,6 +403,8 @@ If you want to migrate to another instance, you have the ability to move your ex
|
||||
|
||||
Your new account should reference the existing one, refer to your software configuration (for example [Moving or leaving accounts from the Mastodon doc](https://docs.joinmastodon.org/user/moving/)).
|
||||
|
||||
If you wish to move **from** another instance, see [Moving from another instance](/user_guide.html#moving-from-another-instance).
|
||||
|
||||
Execute the Move task:
|
||||
|
||||
#### Python edition
|
||||
@ -364,3 +446,11 @@ poetry run inv self-destruct
|
||||
# For a Docker install
|
||||
make self-destruct
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the server is not (re)starting, you can:
|
||||
|
||||
- [Ensure that the configuration is valid](/user_guide.html#configuration-checking)
|
||||
- [Verify if you haven't any syntax error in the custom theme by recompiling the CSS](/user_guide.html#recompiling-css-files)
|
||||
- Look at the log files
|
||||
|
559
poetry.lock
generated
559
poetry.lock
generated
@ -26,7 +26,7 @@ tz = ["python-dateutil"]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "3.6.1"
|
||||
version = "3.6.2"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -39,7 +39,7 @@ sniffio = ">=1.1"
|
||||
[package.extras]
|
||||
doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
|
||||
test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"]
|
||||
trio = ["trio (>=0.16)"]
|
||||
trio = ["trio (>=0.16,<0.22)"]
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
@ -98,11 +98,11 @@ lxml = ["lxml"]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "22.8.0"
|
||||
version = "22.10.0"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.2"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0.0"
|
||||
@ -197,7 +197,7 @@ python-versions = "~=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2022.6.15"
|
||||
version = "2022.9.24"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -286,11 +286,11 @@ doc = ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"]
|
||||
|
||||
[[package]]
|
||||
name = "faker"
|
||||
version = "14.2.0"
|
||||
version = "15.1.1"
|
||||
description = "Faker is a Python package that generates fake data for you."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
python-dateutil = ">=2.4"
|
||||
@ -348,7 +348,7 @@ python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "1.1.3"
|
||||
version = "1.1.3.post0"
|
||||
description = "Lightweight in-process concurrent programming"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -431,7 +431,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httptools"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
description = "A collection of framework independent HTTP protocol utils."
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -463,7 +463,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "humanize"
|
||||
version = "4.3.0"
|
||||
version = "4.4.0"
|
||||
description = "Python humanize utilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -482,7 +482,7 @@ python-versions = ">=3.6.1"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.3"
|
||||
version = "3.4"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -498,7 +498,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "invoke"
|
||||
version = "1.7.1"
|
||||
version = "1.7.3"
|
||||
description = "Pythonic task execution"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -582,7 +582,7 @@ source = ["Cython (>=0.29.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "mako"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -596,17 +596,6 @@ babel = ["babel"]
|
||||
lingua = ["lingua"]
|
||||
testing = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "3.4.1"
|
||||
description = "Python implementation of Markdown."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
testing = ["coverage", "pyyaml"]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.1"
|
||||
@ -623,18 +612,6 @@ category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "mdx-linkify"
|
||||
version = "2.1"
|
||||
description = "Link recognition for Python Markdown"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
bleach = ">=3.1.0"
|
||||
Markdown = ">=3.0"
|
||||
|
||||
[[package]]
|
||||
name = "mf2py"
|
||||
version = "1.1.2"
|
||||
@ -648,6 +625,14 @@ BeautifulSoup4 = ">=4.6.0"
|
||||
html5lib = ">=1.0.1"
|
||||
requests = ">=2.18.4"
|
||||
|
||||
[[package]]
|
||||
name = "mistletoe"
|
||||
version = "0.9.0"
|
||||
description = "A fast, extensible Markdown parser in pure Python."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "~=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "0.960"
|
||||
@ -942,7 +927,7 @@ use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "respx"
|
||||
version = "0.19.2"
|
||||
version = "0.19.3"
|
||||
description = "A utility for mocking out the Python HTTPX and HTTP Core libraries."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -991,7 +976,7 @@ python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "1.4.40"
|
||||
version = "1.4.42"
|
||||
description = "Database Abstraction Library"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -1023,7 +1008,7 @@ sqlcipher = ["sqlcipher3-binary"]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy2-stubs"
|
||||
version = "0.0.2a27"
|
||||
version = "0.0.2a29"
|
||||
description = "Typing Stubs for SQLAlchemy 1.4"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -1086,7 +1071,7 @@ python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "types-bleach"
|
||||
version = "5.0.3"
|
||||
version = "5.0.3.1"
|
||||
description = "Typing stubs for bleach"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -1110,7 +1095,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "types-markdown"
|
||||
version = "3.4.1"
|
||||
version = "3.4.2.1"
|
||||
description = "Typing stubs for Markdown"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -1118,7 +1103,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "types-pillow"
|
||||
version = "9.2.1"
|
||||
version = "9.2.2.2"
|
||||
description = "Typing stubs for Pillow"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -1126,7 +1111,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "types-python-dateutil"
|
||||
version = "2.8.19"
|
||||
version = "2.8.19.2"
|
||||
description = "Typing stubs for python-dateutil"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -1134,7 +1119,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "types-requests"
|
||||
version = "2.28.9"
|
||||
version = "2.28.11.2"
|
||||
description = "Typing stubs for requests"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -1153,7 +1138,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "types-urllib3"
|
||||
version = "1.26.23"
|
||||
version = "1.26.25.1"
|
||||
description = "Typing stubs for urllib3"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -1161,7 +1146,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.3.0"
|
||||
version = "4.4.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -1204,16 +1189,16 @@ standard = ["colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)",
|
||||
|
||||
[[package]]
|
||||
name = "uvloop"
|
||||
version = "0.16.0"
|
||||
version = "0.17.0"
|
||||
description = "Fast implementation of asyncio event loop on top of libuv"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"]
|
||||
dev = ["Cython (>=0.29.32,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=22.0.0,<22.1.0)", "mypy (>=0.800)", "aiohttp"]
|
||||
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"]
|
||||
test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"]
|
||||
test = ["flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=22.0.0,<22.1.0)", "mypy (>=0.800)", "Cython (>=0.29.32,<0.30.0)", "aiohttp"]
|
||||
|
||||
[[package]]
|
||||
name = "watchdog"
|
||||
@ -1228,7 +1213,7 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "watchfiles"
|
||||
version = "0.16.1"
|
||||
version = "0.18.0"
|
||||
description = "Simple, modern and high performance file watching and code reload in python."
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -1275,7 +1260,7 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "be26936c88285524ab781d4130c8a97fc940d70dd0a88bf37c61d7db5bdbd232"
|
||||
content-hash = "89df524a545a19a20440d1872c93151bbf3f68d3b3d20cc50bc9049dd0e6d25f"
|
||||
|
||||
[metadata.files]
|
||||
aiosqlite = [
|
||||
@ -1284,8 +1269,8 @@ aiosqlite = [
|
||||
]
|
||||
alembic = []
|
||||
anyio = [
|
||||
{file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"},
|
||||
{file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"},
|
||||
{file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"},
|
||||
{file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
|
||||
]
|
||||
asgiref = [
|
||||
{file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"},
|
||||
@ -1313,29 +1298,27 @@ beautifulsoup4 = [
|
||||
{file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"},
|
||||
{file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"},
|
||||
{file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"},
|
||||
{file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"},
|
||||
{file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"},
|
||||
{file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"},
|
||||
{file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"},
|
||||
{file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"},
|
||||
{file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"},
|
||||
{file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"},
|
||||
{file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"},
|
||||
{file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"},
|
||||
{file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"},
|
||||
{file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"},
|
||||
{file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"},
|
||||
{file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"},
|
||||
{file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"},
|
||||
{file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"},
|
||||
{file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"},
|
||||
{file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"},
|
||||
{file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"},
|
||||
{file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"},
|
||||
{file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"},
|
||||
{file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"},
|
||||
{file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"},
|
||||
{file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"},
|
||||
{file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"},
|
||||
{file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"},
|
||||
{file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"},
|
||||
{file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"},
|
||||
{file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"},
|
||||
{file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"},
|
||||
{file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"},
|
||||
{file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"},
|
||||
{file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"},
|
||||
{file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"},
|
||||
{file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"},
|
||||
{file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"},
|
||||
{file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"},
|
||||
{file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"},
|
||||
{file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"},
|
||||
{file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"},
|
||||
{file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"},
|
||||
{file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
|
||||
]
|
||||
bleach = [
|
||||
{file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"},
|
||||
@ -1429,8 +1412,8 @@ cachetools = [
|
||||
{file = "cachetools-5.2.0.tar.gz", hash = "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
|
||||
{file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
|
||||
{file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
|
||||
{file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
|
||||
]
|
||||
cffi = [
|
||||
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
|
||||
@ -1522,8 +1505,8 @@ factory-boy = [
|
||||
{file = "factory_boy-3.2.1.tar.gz", hash = "sha256:a98d277b0c047c75eb6e4ab8508a7f81fb03d2cb21986f627913546ef7a2a55e"},
|
||||
]
|
||||
faker = [
|
||||
{file = "Faker-14.2.0-py3-none-any.whl", hash = "sha256:e02c55a5b0586caaf913cc6c254b3de178e08b031c5922e590fd033ebbdbfd02"},
|
||||
{file = "Faker-14.2.0.tar.gz", hash = "sha256:6db56e2c43a2b74250d1c332ef25fef7dc07dcb6c5fab5329dd7b4467b8ed7b9"},
|
||||
{file = "Faker-15.1.1-py3-none-any.whl", hash = "sha256:096c15e136adb365db24d8c3964fe26bfc68fe060c9385071a339f8c14e09c8a"},
|
||||
{file = "Faker-15.1.1.tar.gz", hash = "sha256:a741b77f484215c3aab2604100669657189548f440fcb2ed0f8b7ee21c385629"},
|
||||
]
|
||||
fastapi = [
|
||||
{file = "fastapi-0.78.0-py3-none-any.whl", hash = "sha256:15fcabd5c78c266fa7ae7d8de9b384bfc2375ee0503463a6febbe3bab69d6f65"},
|
||||
@ -1538,60 +1521,72 @@ flake8 = [
|
||||
]
|
||||
frozendict = []
|
||||
greenlet = [
|
||||
{file = "greenlet-1.1.3-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:8c287ae7ac921dfde88b1c125bd9590b7ec3c900c2d3db5197f1286e144e712b"},
|
||||
{file = "greenlet-1.1.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:870a48007872d12e95a996fca3c03a64290d3ea2e61076aa35d3b253cf34cd32"},
|
||||
{file = "greenlet-1.1.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:7c5227963409551ae4a6938beb70d56bf1918c554a287d3da6853526212fbe0a"},
|
||||
{file = "greenlet-1.1.3-cp27-cp27m-win32.whl", hash = "sha256:9fae214f6c43cd47f7bef98c56919b9222481e833be2915f6857a1e9e8a15318"},
|
||||
{file = "greenlet-1.1.3-cp27-cp27m-win_amd64.whl", hash = "sha256:de431765bd5fe62119e0bc6bc6e7b17ac53017ae1782acf88fcf6b7eae475a49"},
|
||||
{file = "greenlet-1.1.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:510c3b15587afce9800198b4b142202b323bf4b4b5f9d6c79cb9a35e5e3c30d2"},
|
||||
{file = "greenlet-1.1.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:9951dcbd37850da32b2cb6e391f621c1ee456191c6ae5528af4a34afe357c30e"},
|
||||
{file = "greenlet-1.1.3-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:07c58e169bbe1e87b8bbf15a5c1b779a7616df9fd3e61cadc9d691740015b4f8"},
|
||||
{file = "greenlet-1.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df02fdec0c533301497acb0bc0f27f479a3a63dcdc3a099ae33a902857f07477"},
|
||||
{file = "greenlet-1.1.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c88e134d51d5e82315a7c32b914a58751b7353eb5268dbd02eabf020b4c4700"},
|
||||
{file = "greenlet-1.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b41d19c0cfe5c259fe6c539fd75051cd39a5d33d05482f885faf43f7f5e7d26"},
|
||||
{file = "greenlet-1.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:6f5d4b2280ceea76c55c893827961ed0a6eadd5a584a7c4e6e6dd7bc10dfdd96"},
|
||||
{file = "greenlet-1.1.3-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:184416e481295832350a4bf731ba619a92f5689bf5d0fa4341e98b98b1265bd7"},
|
||||
{file = "greenlet-1.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd0404d154084a371e6d2bafc787201612a1359c2dee688ae334f9118aa0bf47"},
|
||||
{file = "greenlet-1.1.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a43bbfa9b6cfdfaeefbd91038dde65ea2c421dc387ed171613df340650874f2"},
|
||||
{file = "greenlet-1.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce5b64dfe8d0cca407d88b0ee619d80d4215a2612c1af8c98a92180e7109f4b5"},
|
||||
{file = "greenlet-1.1.3-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:903fa5716b8fbb21019268b44f73f3748c41d1a30d71b4a49c84b642c2fed5fa"},
|
||||
{file = "greenlet-1.1.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:0118817c9341ef2b0f75f5af79ac377e4da6ff637e5ee4ac91802c0e379dadb4"},
|
||||
{file = "greenlet-1.1.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:466ce0928e33421ee84ae04c4ac6f253a3a3e6b8d600a79bd43fd4403e0a7a76"},
|
||||
{file = "greenlet-1.1.3-cp35-cp35m-win32.whl", hash = "sha256:65ad1a7a463a2a6f863661329a944a5802c7129f7ad33583dcc11069c17e622c"},
|
||||
{file = "greenlet-1.1.3-cp35-cp35m-win_amd64.whl", hash = "sha256:7532a46505470be30cbf1dbadb20379fb481244f1ca54207d7df3bf0bbab6a20"},
|
||||
{file = "greenlet-1.1.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:caff52cb5cd7626872d9696aee5b794abe172804beb7db52eed1fd5824b63910"},
|
||||
{file = "greenlet-1.1.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:db41f3845eb579b544c962864cce2c2a0257fe30f0f1e18e51b1e8cbb4e0ac6d"},
|
||||
{file = "greenlet-1.1.3-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e8533f5111704d75de3139bf0b8136d3a6c1642c55c067866fa0a51c2155ee33"},
|
||||
{file = "greenlet-1.1.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537e4baf0db67f382eb29255a03154fcd4984638303ff9baaa738b10371fa57"},
|
||||
{file = "greenlet-1.1.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8bfd36f368efe0ab2a6aa3db7f14598aac454b06849fb633b762ddbede1db90"},
|
||||
{file = "greenlet-1.1.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0877a9a2129a2c56a2eae2da016743db7d9d6a05d5e1c198f1b7808c602a30e"},
|
||||
{file = "greenlet-1.1.3-cp36-cp36m-win32.whl", hash = "sha256:88b04e12c9b041a1e0bcb886fec709c488192638a9a7a3677513ac6ba81d8e79"},
|
||||
{file = "greenlet-1.1.3-cp36-cp36m-win_amd64.whl", hash = "sha256:4f166b4aca8d7d489e82d74627a7069ab34211ef5ebb57c300ec4b9337b60fc0"},
|
||||
{file = "greenlet-1.1.3-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:cd16a89efe3a003029c87ff19e9fba635864e064da646bc749fc1908a4af18f3"},
|
||||
{file = "greenlet-1.1.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5b756e6730ea59b2745072e28ad27f4c837084688e6a6b3633c8b1e509e6ae0e"},
|
||||
{file = "greenlet-1.1.3-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:9b2f7d0408ddeb8ea1fd43d3db79a8cefaccadd2a812f021333b338ed6b10aba"},
|
||||
{file = "greenlet-1.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44b4817c34c9272c65550b788913620f1fdc80362b209bc9d7dd2f40d8793080"},
|
||||
{file = "greenlet-1.1.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d58a5a71c4c37354f9e0c24c9c8321f0185f6945ef027460b809f4bb474bfe41"},
|
||||
{file = "greenlet-1.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dd51d2650e70c6c4af37f454737bf4a11e568945b27f74b471e8e2a9fd21268"},
|
||||
{file = "greenlet-1.1.3-cp37-cp37m-win32.whl", hash = "sha256:048d2bed76c2aa6de7af500ae0ea51dd2267aec0e0f2a436981159053d0bc7cc"},
|
||||
{file = "greenlet-1.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:77e41db75f9958f2083e03e9dd39da12247b3430c92267df3af77c83d8ff9eed"},
|
||||
{file = "greenlet-1.1.3-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:1626185d938d7381631e48e6f7713e8d4b964be246073e1a1d15c2f061ac9f08"},
|
||||
{file = "greenlet-1.1.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:1ec2779774d8e42ed0440cf8bc55540175187e8e934f2be25199bf4ed948cd9e"},
|
||||
{file = "greenlet-1.1.3-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f2f908239b7098799b8845e5936c2ccb91d8c2323be02e82f8dcb4a80dcf4a25"},
|
||||
{file = "greenlet-1.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b181e9aa6cb2f5ec0cacc8cee6e5a3093416c841ba32c185c30c160487f0380"},
|
||||
{file = "greenlet-1.1.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2cf45e339cabea16c07586306a31cfcc5a3b5e1626d365714d283732afed6809"},
|
||||
{file = "greenlet-1.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6200a11f003ec26815f7e3d2ded01b43a3810be3528dd760d2f1fa777490c3cd"},
|
||||
{file = "greenlet-1.1.3-cp38-cp38-win32.whl", hash = "sha256:db5b25265010a1b3dca6a174a443a0ed4c4ab12d5e2883a11c97d6e6d59b12f9"},
|
||||
{file = "greenlet-1.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:095a980288fe05adf3d002fbb180c99bdcf0f930e220aa66fcd56e7914a38202"},
|
||||
{file = "greenlet-1.1.3-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:cbc1eb55342cbac8f7ec159088d54e2cfdd5ddf61c87b8bbe682d113789331b2"},
|
||||
{file = "greenlet-1.1.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:694ffa7144fa5cc526c8f4512665003a39fa09ef00d19bbca5c8d3406db72fbe"},
|
||||
{file = "greenlet-1.1.3-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:aa741c1a8a8cc25eb3a3a01a62bdb5095a773d8c6a86470bde7f607a447e7905"},
|
||||
{file = "greenlet-1.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3a669f11289a8995d24fbfc0e63f8289dd03c9aaa0cc8f1eab31d18ca61a382"},
|
||||
{file = "greenlet-1.1.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76a53bfa10b367ee734b95988bd82a9a5f0038a25030f9f23bbbc005010ca600"},
|
||||
{file = "greenlet-1.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fb0aa7f6996879551fd67461d5d3ab0c3c0245da98be90c89fcb7a18d437403"},
|
||||
{file = "greenlet-1.1.3-cp39-cp39-win32.whl", hash = "sha256:5fbe1ab72b998ca77ceabbae63a9b2e2dc2d963f4299b9b278252ddba142d3f1"},
|
||||
{file = "greenlet-1.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:ffe73f9e7aea404722058405ff24041e59d31ca23d1da0895af48050a07b6932"},
|
||||
{file = "greenlet-1.1.3.tar.gz", hash = "sha256:bcb6c6dd1d6be6d38d6db283747d07fda089ff8c559a835236560a4410340455"},
|
||||
{file = "greenlet-1.1.3.post0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:949c9061b8c6d3e6e439466a9be1e787208dec6246f4ec5fffe9677b4c19fcc3"},
|
||||
{file = "greenlet-1.1.3.post0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d7815e1519a8361c5ea2a7a5864945906f8e386fa1bc26797b4d443ab11a4589"},
|
||||
{file = "greenlet-1.1.3.post0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9649891ab4153f217f319914455ccf0b86986b55fc0573ce803eb998ad7d6854"},
|
||||
{file = "greenlet-1.1.3.post0-cp27-cp27m-win32.whl", hash = "sha256:11fc7692d95cc7a6a8447bb160d98671ab291e0a8ea90572d582d57361360f05"},
|
||||
{file = "greenlet-1.1.3.post0-cp27-cp27m-win_amd64.whl", hash = "sha256:05ae7383f968bba4211b1fbfc90158f8e3da86804878442b4fb6c16ccbcaa519"},
|
||||
{file = "greenlet-1.1.3.post0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ccbe7129a282ec5797df0451ca1802f11578be018a32979131065565da89b392"},
|
||||
{file = "greenlet-1.1.3.post0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a8b58232f5b72973350c2b917ea3df0bebd07c3c82a0a0e34775fc2c1f857e9"},
|
||||
{file = "greenlet-1.1.3.post0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:f6661b58412879a2aa099abb26d3c93e91dedaba55a6394d1fb1512a77e85de9"},
|
||||
{file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c6e942ca9835c0b97814d14f78da453241837419e0d26f7403058e8db3e38f8"},
|
||||
{file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a812df7282a8fc717eafd487fccc5ba40ea83bb5b13eb3c90c446d88dbdfd2be"},
|
||||
{file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83a7a6560df073ec9de2b7cb685b199dfd12519bc0020c62db9d1bb522f989fa"},
|
||||
{file = "greenlet-1.1.3.post0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:17a69967561269b691747e7f436d75a4def47e5efcbc3c573180fc828e176d80"},
|
||||
{file = "greenlet-1.1.3.post0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:60839ab4ea7de6139a3be35b77e22e0398c270020050458b3d25db4c7c394df5"},
|
||||
{file = "greenlet-1.1.3.post0-cp310-cp310-win_amd64.whl", hash = "sha256:8926a78192b8b73c936f3e87929931455a6a6c6c385448a07b9f7d1072c19ff3"},
|
||||
{file = "greenlet-1.1.3.post0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c6f90234e4438062d6d09f7d667f79edcc7c5e354ba3a145ff98176f974b8132"},
|
||||
{file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814f26b864ed2230d3a7efe0336f5766ad012f94aad6ba43a7c54ca88dd77cba"},
|
||||
{file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fda1139d87ce5f7bd80e80e54f9f2c6fe2f47983f1a6f128c47bf310197deb6"},
|
||||
{file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0643250dd0756f4960633f5359884f609a234d4066686754e834073d84e9b51"},
|
||||
{file = "greenlet-1.1.3.post0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cb863057bed786f6622982fb8b2c122c68e6e9eddccaa9fa98fd937e45ee6c4f"},
|
||||
{file = "greenlet-1.1.3.post0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8c0581077cf2734569f3e500fab09c0ff6a2ab99b1afcacbad09b3c2843ae743"},
|
||||
{file = "greenlet-1.1.3.post0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:695d0d8b5ae42c800f1763c9fce9d7b94ae3b878919379150ee5ba458a460d57"},
|
||||
{file = "greenlet-1.1.3.post0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5662492df0588a51d5690f6578f3bbbd803e7f8d99a99f3bf6128a401be9c269"},
|
||||
{file = "greenlet-1.1.3.post0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:bffba15cff4802ff493d6edcf20d7f94ab1c2aee7cfc1e1c7627c05f1102eee8"},
|
||||
{file = "greenlet-1.1.3.post0-cp35-cp35m-win32.whl", hash = "sha256:7afa706510ab079fd6d039cc6e369d4535a48e202d042c32e2097f030a16450f"},
|
||||
{file = "greenlet-1.1.3.post0-cp35-cp35m-win_amd64.whl", hash = "sha256:3a24f3213579dc8459e485e333330a921f579543a5214dbc935bc0763474ece3"},
|
||||
{file = "greenlet-1.1.3.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:64e10f303ea354500c927da5b59c3802196a07468332d292aef9ddaca08d03dd"},
|
||||
{file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:eb6ac495dccb1520667cfea50d89e26f9ffb49fa28496dea2b95720d8b45eb54"},
|
||||
{file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:88720794390002b0c8fa29e9602b395093a9a766b229a847e8d88349e418b28a"},
|
||||
{file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39464518a2abe9c505a727af7c0b4efff2cf242aa168be5f0daa47649f4d7ca8"},
|
||||
{file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0914f02fcaa8f84f13b2df4a81645d9e82de21ed95633765dd5cc4d3af9d7403"},
|
||||
{file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96656c5f7c95fc02c36d4f6ef32f4e94bb0b6b36e6a002c21c39785a4eec5f5d"},
|
||||
{file = "greenlet-1.1.3.post0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4f74aa0092602da2069df0bc6553919a15169d77bcdab52a21f8c5242898f519"},
|
||||
{file = "greenlet-1.1.3.post0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:3aeac044c324c1a4027dca0cde550bd83a0c0fbff7ef2c98df9e718a5086c194"},
|
||||
{file = "greenlet-1.1.3.post0-cp36-cp36m-win32.whl", hash = "sha256:fe7c51f8a2ab616cb34bc33d810c887e89117771028e1e3d3b77ca25ddeace04"},
|
||||
{file = "greenlet-1.1.3.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:70048d7b2c07c5eadf8393e6398595591df5f59a2f26abc2f81abca09610492f"},
|
||||
{file = "greenlet-1.1.3.post0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:66aa4e9a726b70bcbfcc446b7ba89c8cec40f405e51422c39f42dfa206a96a05"},
|
||||
{file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:025b8de2273d2809f027d347aa2541651d2e15d593bbce0d5f502ca438c54136"},
|
||||
{file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:82a38d7d2077128a017094aff334e67e26194f46bd709f9dcdacbf3835d47ef5"},
|
||||
{file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7d20c3267385236b4ce54575cc8e9f43e7673fc761b069c820097092e318e3b"},
|
||||
{file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8ece5d1a99a2adcb38f69af2f07d96fb615415d32820108cd340361f590d128"},
|
||||
{file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2794eef1b04b5ba8948c72cc606aab62ac4b0c538b14806d9c0d88afd0576d6b"},
|
||||
{file = "greenlet-1.1.3.post0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a8d24eb5cb67996fb84633fdc96dbc04f2d8b12bfcb20ab3222d6be271616b67"},
|
||||
{file = "greenlet-1.1.3.post0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0120a879aa2b1ac5118bce959ea2492ba18783f65ea15821680a256dfad04754"},
|
||||
{file = "greenlet-1.1.3.post0-cp37-cp37m-win32.whl", hash = "sha256:bef49c07fcb411c942da6ee7d7ea37430f830c482bf6e4b72d92fd506dd3a427"},
|
||||
{file = "greenlet-1.1.3.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:62723e7eb85fa52e536e516ee2ac91433c7bb60d51099293671815ff49ed1c21"},
|
||||
{file = "greenlet-1.1.3.post0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d25cdedd72aa2271b984af54294e9527306966ec18963fd032cc851a725ddc1b"},
|
||||
{file = "greenlet-1.1.3.post0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:924df1e7e5db27d19b1359dc7d052a917529c95ba5b8b62f4af611176da7c8ad"},
|
||||
{file = "greenlet-1.1.3.post0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ec615d2912b9ad807afd3be80bf32711c0ff9c2b00aa004a45fd5d5dde7853d9"},
|
||||
{file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0971d37ae0eaf42344e8610d340aa0ad3d06cd2eee381891a10fe771879791f9"},
|
||||
{file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325f272eb997916b4a3fc1fea7313a8adb760934c2140ce13a2117e1b0a8095d"},
|
||||
{file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75afcbb214d429dacdf75e03a1d6d6c5bd1fa9c35e360df8ea5b6270fb2211c"},
|
||||
{file = "greenlet-1.1.3.post0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5c2d21c2b768d8c86ad935e404cc78c30d53dea009609c3ef3a9d49970c864b5"},
|
||||
{file = "greenlet-1.1.3.post0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:467b73ce5dcd89e381292fb4314aede9b12906c18fab903f995b86034d96d5c8"},
|
||||
{file = "greenlet-1.1.3.post0-cp38-cp38-win32.whl", hash = "sha256:8149a6865b14c33be7ae760bcdb73548bb01e8e47ae15e013bf7ef9290ca309a"},
|
||||
{file = "greenlet-1.1.3.post0-cp38-cp38-win_amd64.whl", hash = "sha256:104f29dd822be678ef6b16bf0035dcd43206a8a48668a6cae4d2fe9c7a7abdeb"},
|
||||
{file = "greenlet-1.1.3.post0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:c8c9301e3274276d3d20ab6335aa7c5d9e5da2009cccb01127bddb5c951f8870"},
|
||||
{file = "greenlet-1.1.3.post0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:8415239c68b2ec9de10a5adf1130ee9cb0ebd3e19573c55ba160ff0ca809e012"},
|
||||
{file = "greenlet-1.1.3.post0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:3c22998bfef3fcc1b15694818fc9b1b87c6cc8398198b96b6d355a7bcb8c934e"},
|
||||
{file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa1845944e62f358d63fcc911ad3b415f585612946b8edc824825929b40e59e"},
|
||||
{file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:890f633dc8cb307761ec566bc0b4e350a93ddd77dc172839be122be12bae3e10"},
|
||||
{file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cf37343e43404699d58808e51f347f57efd3010cc7cee134cdb9141bd1ad9ea"},
|
||||
{file = "greenlet-1.1.3.post0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5edf75e7fcfa9725064ae0d8407c849456553a181ebefedb7606bac19aa1478b"},
|
||||
{file = "greenlet-1.1.3.post0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a954002064ee919b444b19c1185e8cce307a1f20600f47d6f4b6d336972c809"},
|
||||
{file = "greenlet-1.1.3.post0-cp39-cp39-win32.whl", hash = "sha256:2ccdc818cc106cc238ff7eba0d71b9c77be868fdca31d6c3b1347a54c9b187b2"},
|
||||
{file = "greenlet-1.1.3.post0-cp39-cp39-win_amd64.whl", hash = "sha256:91a84faf718e6f8b888ca63d0b2d6d185c8e2a198d2a7322d75c303e7097c8b7"},
|
||||
{file = "greenlet-1.1.3.post0.tar.gz", hash = "sha256:f5e09dc5c6e1796969fd4b775ea1417d70e49a5df29aaa8e5d10675d9e11872c"},
|
||||
]
|
||||
h11 = [
|
||||
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
|
||||
@ -1612,61 +1607,68 @@ httpcore = [
|
||||
{file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"},
|
||||
]
|
||||
httptools = [
|
||||
{file = "httptools-0.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcddfe70553be717d9745990dfdb194e22ee0f60eb8f48c0794e7bfeda30d2d5"},
|
||||
{file = "httptools-0.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1ee0b459257e222b878a6c09ccf233957d3a4dcb883b0847640af98d2d9aac23"},
|
||||
{file = "httptools-0.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceafd5e960b39c7e0d160a1936b68eb87c5e79b3979d66e774f0c77d4d8faaed"},
|
||||
{file = "httptools-0.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fdb9f9ed79bc6f46b021b3319184699ba1a22410a82204e6e89c774530069683"},
|
||||
{file = "httptools-0.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:abe829275cdd4174b4c4e65ad718715d449e308d59793bf3a931ee1bf7e7b86c"},
|
||||
{file = "httptools-0.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7af6bdbd21a2a25d6784f6d67f44f5df33ef39b6159543b9f9064d365c01f919"},
|
||||
{file = "httptools-0.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5d1fe6b6661022fd6cac541f54a4237496b246e6f1c0a6b41998ee08a1135afe"},
|
||||
{file = "httptools-0.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:48e48530d9b995a84d1d89ae6b3ec4e59ea7d494b150ac3bbc5e2ac4acce92cd"},
|
||||
{file = "httptools-0.4.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a113789e53ac1fa26edf99856a61e4c493868e125ae0dd6354cf518948fbbd5c"},
|
||||
{file = "httptools-0.4.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e2eb957787cbb614a0f006bfc5798ff1d90ac7c4dd24854c84edbdc8c02369e"},
|
||||
{file = "httptools-0.4.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:7ee9f226acab9085037582c059d66769862706e8e8cd2340470ceb8b3850873d"},
|
||||
{file = "httptools-0.4.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:701e66b59dd21a32a274771238025d58db7e2b6ecebbab64ceff51b8e31527ae"},
|
||||
{file = "httptools-0.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6a1a7dfc1f9c78a833e2c4904757a0f47ce25d08634dd2a52af394eefe5f9777"},
|
||||
{file = "httptools-0.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:903f739c9fb78dab8970b0f3ea51f21955b24b45afa77b22ff0e172fc11ef111"},
|
||||
{file = "httptools-0.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54bbd295f031b866b9799dd39cb45deee81aca036c9bff9f58ca06726f6494f1"},
|
||||
{file = "httptools-0.4.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3194f6d6443befa8d4db16c1946b2fc428a3ceb8ab32eb6f09a59f86104dc1a0"},
|
||||
{file = "httptools-0.4.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cd1295f52971097f757edfbfce827b6dbbfb0f7a74901ee7d4933dff5ad4c9af"},
|
||||
{file = "httptools-0.4.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:20a45bcf22452a10fa8d58b7dbdb474381f6946bf5b8933e3662d572bc61bae4"},
|
||||
{file = "httptools-0.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d1f27bb0f75bef722d6e22dc609612bfa2f994541621cd2163f8c943b6463dfe"},
|
||||
{file = "httptools-0.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7f7bfb74718f52d5ed47d608d507bf66d3bc01d4a8b3e6dd7134daaae129357b"},
|
||||
{file = "httptools-0.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a522d12e2ddbc2e91842ffb454a1aeb0d47607972c7d8fc88bd0838d97fb8a2a"},
|
||||
{file = "httptools-0.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2db44a0b294d317199e9f80123e72c6b005c55b625b57fae36de68670090fa48"},
|
||||
{file = "httptools-0.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c286985b5e194ca0ebb2908d71464b9be8f17cc66d6d3e330e8d5407248f56ad"},
|
||||
{file = "httptools-0.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3a4e165ca6204f34856b765d515d558dc84f1352033b8721e8d06c3e44930c3"},
|
||||
{file = "httptools-0.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:72aa3fbe636b16d22e04b5a9d24711b043495e0ecfe58080addf23a1a37f3409"},
|
||||
{file = "httptools-0.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9967d9758df505975913304c434cb9ab21e2c609ad859eb921f2f615a038c8de"},
|
||||
{file = "httptools-0.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f72b5d24d6730035128b238decdc4c0f2104b7056a7ca55cf047c106842ec890"},
|
||||
{file = "httptools-0.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:29bf97a5c532da9c7a04de2c7a9c31d1d54f3abd65a464119b680206bbbb1055"},
|
||||
{file = "httptools-0.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98993805f1e3cdb53de4eed02b55dcc953cdf017ba7bbb2fd89226c086a6d855"},
|
||||
{file = "httptools-0.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d9b90bf58f3ba04e60321a23a8723a1ff2a9377502535e70495e5ada8e6e6722"},
|
||||
{file = "httptools-0.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a99346ebcb801b213c591540837340bdf6fd060a8687518d01c607d338b7424"},
|
||||
{file = "httptools-0.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:645373c070080e632480a3d251d892cb795be3d3a15f86975d0f1aca56fd230d"},
|
||||
{file = "httptools-0.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:34d2903dd2a3dd85d33705b6fde40bf91fc44411661283763fd0746723963c83"},
|
||||
{file = "httptools-0.4.0.tar.gz", hash = "sha256:2c9a930c378b3d15d6b695fb95ebcff81a7395b4f9775c4f10a076beb0b2c1ff"},
|
||||
{file = "httptools-0.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8f470c79061599a126d74385623ff4744c4e0f4a0997a353a44923c0b561ee51"},
|
||||
{file = "httptools-0.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e90491a4d77d0cb82e0e7a9cb35d86284c677402e4ce7ba6b448ccc7325c5421"},
|
||||
{file = "httptools-0.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1d2357f791b12d86faced7b5736dea9ef4f5ecdc6c3f253e445ee82da579449"},
|
||||
{file = "httptools-0.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f90cd6fd97c9a1b7fe9215e60c3bd97336742a0857f00a4cb31547bc22560c2"},
|
||||
{file = "httptools-0.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5230a99e724a1bdbbf236a1b58d6e8504b912b0552721c7c6b8570925ee0ccde"},
|
||||
{file = "httptools-0.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3a47a34f6015dd52c9eb629c0f5a8a5193e47bf2a12d9a3194d231eaf1bc451a"},
|
||||
{file = "httptools-0.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:24bb4bb8ac3882f90aa95403a1cb48465de877e2d5298ad6ddcfdebec060787d"},
|
||||
{file = "httptools-0.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e67d4f8734f8054d2c4858570cc4b233bf753f56e85217de4dfb2495904cf02e"},
|
||||
{file = "httptools-0.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7e5eefc58d20e4c2da82c78d91b2906f1a947ef42bd668db05f4ab4201a99f49"},
|
||||
{file = "httptools-0.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0297822cea9f90a38df29f48e40b42ac3d48a28637368f3ec6d15eebefd182f9"},
|
||||
{file = "httptools-0.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:557be7fbf2bfa4a2ec65192c254e151684545ebab45eca5d50477d562c40f986"},
|
||||
{file = "httptools-0.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:54465401dbbec9a6a42cf737627fb0f014d50dc7365a6b6cd57753f151a86ff0"},
|
||||
{file = "httptools-0.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4d9ebac23d2de960726ce45f49d70eb5466725c0087a078866043dad115f850f"},
|
||||
{file = "httptools-0.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:e8a34e4c0ab7b1ca17b8763613783e2458e77938092c18ac919420ab8655c8c1"},
|
||||
{file = "httptools-0.5.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f659d7a48401158c59933904040085c200b4be631cb5f23a7d561fbae593ec1f"},
|
||||
{file = "httptools-0.5.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1616b3ba965cd68e6f759eeb5d34fbf596a79e84215eeceebf34ba3f61fdc7"},
|
||||
{file = "httptools-0.5.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3625a55886257755cb15194efbf209584754e31d336e09e2ffe0685a76cb4b60"},
|
||||
{file = "httptools-0.5.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:72ad589ba5e4a87e1d404cc1cb1b5780bfcb16e2aec957b88ce15fe879cc08ca"},
|
||||
{file = "httptools-0.5.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:850fec36c48df5a790aa735417dca8ce7d4b48d59b3ebd6f83e88a8125cde324"},
|
||||
{file = "httptools-0.5.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f222e1e9d3f13b68ff8a835574eda02e67277d51631d69d7cf7f8e07df678c86"},
|
||||
{file = "httptools-0.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3cb8acf8f951363b617a8420768a9f249099b92e703c052f9a51b66342eea89b"},
|
||||
{file = "httptools-0.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550059885dc9c19a072ca6d6735739d879be3b5959ec218ba3e013fd2255a11b"},
|
||||
{file = "httptools-0.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a04fe458a4597aa559b79c7f48fe3dceabef0f69f562daf5c5e926b153817281"},
|
||||
{file = "httptools-0.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d0c1044bce274ec6711f0770fd2d5544fe392591d204c68328e60a46f88843b"},
|
||||
{file = "httptools-0.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c6eeefd4435055a8ebb6c5cc36111b8591c192c56a95b45fe2af22d9881eee25"},
|
||||
{file = "httptools-0.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:5b65be160adcd9de7a7e6413a4966665756e263f0d5ddeffde277ffeee0576a5"},
|
||||
{file = "httptools-0.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fe9c766a0c35b7e3d6b6939393c8dfdd5da3ac5dec7f971ec9134f284c6c36d6"},
|
||||
{file = "httptools-0.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:85b392aba273566c3d5596a0a490978c085b79700814fb22bfd537d381dd230c"},
|
||||
{file = "httptools-0.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5e3088f4ed33947e16fd865b8200f9cfae1144f41b64a8cf19b599508e096bc"},
|
||||
{file = "httptools-0.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c2a56b6aad7cc8f5551d8e04ff5a319d203f9d870398b94702300de50190f63"},
|
||||
{file = "httptools-0.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b571b281a19762adb3f48a7731f6842f920fa71108aff9be49888320ac3e24d"},
|
||||
{file = "httptools-0.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa47ffcf70ba6f7848349b8a6f9b481ee0f7637931d91a9860a1838bfc586901"},
|
||||
{file = "httptools-0.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:bede7ee075e54b9a5bde695b4fc8f569f30185891796b2e4e09e2226801d09bd"},
|
||||
{file = "httptools-0.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:64eba6f168803a7469866a9c9b5263a7463fa8b7a25b35e547492aa7322036b6"},
|
||||
{file = "httptools-0.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4b098e4bb1174096a93f48f6193e7d9aa7071506a5877da09a783509ca5fff42"},
|
||||
{file = "httptools-0.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9423a2de923820c7e82e18980b937893f4aa8251c43684fa1772e341f6e06887"},
|
||||
{file = "httptools-0.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca1b7becf7d9d3ccdbb2f038f665c0f4857e08e1d8481cbcc1a86a0afcfb62b2"},
|
||||
{file = "httptools-0.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:50d4613025f15f4b11f1c54bbed4761c0020f7f921b95143ad6d58c151198142"},
|
||||
{file = "httptools-0.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8ffce9d81c825ac1deaa13bc9694c0562e2840a48ba21cfc9f3b4c922c16f372"},
|
||||
{file = "httptools-0.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:1af91b3650ce518d226466f30bbba5b6376dbd3ddb1b2be8b0658c6799dd450b"},
|
||||
{file = "httptools-0.5.0.tar.gz", hash = "sha256:295874861c173f9101960bba332429bb77ed4dcd8cdf5cee9922eb00e4f6bc09"},
|
||||
]
|
||||
httpx = [
|
||||
{file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"},
|
||||
{file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"},
|
||||
]
|
||||
humanize = [
|
||||
{file = "humanize-4.3.0-py3-none-any.whl", hash = "sha256:5dd159c9910cd57b94072e4d7decae097f0eb84c4645153706929a7f127cb2ef"},
|
||||
{file = "humanize-4.3.0.tar.gz", hash = "sha256:0dfac79fe8c1c0c734c14177b07b857bad9ae30dd50daa0a14e2c3d8054ee0c4"},
|
||||
{file = "humanize-4.4.0-py3-none-any.whl", hash = "sha256:8830ebf2d65d0395c1bd4c79189ad71e023f277c2c7ae00f263124432e6f2ffa"},
|
||||
{file = "humanize-4.4.0.tar.gz", hash = "sha256:efb2584565cc86b7ea87a977a15066de34cdedaf341b11c851cfcfd2b964779c"},
|
||||
]
|
||||
hyperframe = []
|
||||
idna = [
|
||||
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
||||
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
||||
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
|
||||
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
||||
]
|
||||
iniconfig = [
|
||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||
]
|
||||
invoke = [
|
||||
{file = "invoke-1.7.1-py3-none-any.whl", hash = "sha256:2dc975b4f92be0c0a174ad2d063010c8a1fdb5e9389d69871001118b4fcac4fb"},
|
||||
{file = "invoke-1.7.1.tar.gz", hash = "sha256:7b6deaf585eee0a848205d0b8c0014b9bf6f287a8eb798818a642dff1df14b19"},
|
||||
{file = "invoke-1.7.3-py3-none-any.whl", hash = "sha256:d9694a865764dd3fd91f25f7e9a97fb41666e822bbb00e670091e3f43933574d"},
|
||||
{file = "invoke-1.7.3.tar.gz", hash = "sha256:41b428342d466a82135d5ab37119685a989713742be46e42a3a399d685579314"},
|
||||
]
|
||||
isort = [
|
||||
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
|
||||
@ -1769,10 +1771,9 @@ lxml = [
|
||||
{file = "lxml-4.9.1.tar.gz", hash = "sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f"},
|
||||
]
|
||||
mako = [
|
||||
{file = "Mako-1.2.2-py3-none-any.whl", hash = "sha256:8efcb8004681b5f71d09c983ad5a9e6f5c40601a6ec469148753292abc0da534"},
|
||||
{file = "Mako-1.2.2.tar.gz", hash = "sha256:3724869b363ba630a272a5f89f68c070352137b8fd1757650017b7e06fda163f"},
|
||||
{file = "Mako-1.2.3-py3-none-any.whl", hash = "sha256:c413a086e38cd885088d5e165305ee8eed04e8b3f8f62df343480da0a385735f"},
|
||||
{file = "Mako-1.2.3.tar.gz", hash = "sha256:7fde96466fcfeedb0eed94f187f20b23d85e4cb41444be0e542e2c8c65c396cd"},
|
||||
]
|
||||
markdown = []
|
||||
markupsafe = [
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
|
||||
@ -1819,12 +1820,13 @@ mccabe = [
|
||||
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
|
||||
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
|
||||
]
|
||||
mdx-linkify = [
|
||||
{file = "mdx_linkify-2.1.tar.gz", hash = "sha256:e09278e43e5076b63398238b069a361913779683183481e9206235667cd89f54"},
|
||||
]
|
||||
mf2py = [
|
||||
{file = "mf2py-1.1.2.tar.gz", hash = "sha256:84f1f8f2ff3f1deb1c30be497e7ccd805452996a662fd4a77f09e0105bede2c9"},
|
||||
]
|
||||
mistletoe = [
|
||||
{file = "mistletoe-0.9.0-py3-none-any.whl", hash = "sha256:11316e2fe0be422a8248293ad0efbee9ad0c6f3683b2f45bc6b989ea17a68c74"},
|
||||
{file = "mistletoe-0.9.0.tar.gz", hash = "sha256:3cb96d78226d08f0d3bf09efcaf330d23902492006e18b2c06558e8b86bf7faf"},
|
||||
]
|
||||
mypy = [
|
||||
{file = "mypy-0.960-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3a3e525cd76c2c4f90f1449fd034ba21fcca68050ff7c8397bb7dd25dd8b8248"},
|
||||
{file = "mypy-0.960-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7a76dc4f91e92db119b1be293892df8379b08fd31795bb44e0ff84256d34c251"},
|
||||
@ -2095,8 +2097,8 @@ requests = [
|
||||
{file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
|
||||
]
|
||||
respx = [
|
||||
{file = "respx-0.19.2-py2.py3-none-any.whl", hash = "sha256:417f986fec599b9cc6531e93e494b7a75d1cb7bccff9dde5b53edc51f7954494"},
|
||||
{file = "respx-0.19.2.tar.gz", hash = "sha256:f3d210bb4de0ccc4c5afabeb87c3c1b03b3765a9c1a73eb042a07bb18ac33705"},
|
||||
{file = "respx-0.19.3-py2.py3-none-any.whl", hash = "sha256:ea3049468bfcf95c7827436230e05b85a037050dc569dbeaecdaa08fa0de5750"},
|
||||
{file = "respx-0.19.3.tar.gz", hash = "sha256:6dab3a19dfb0ea07cef996198c12bf8a34e6fc3573a24b184d714f765e8b5e57"},
|
||||
]
|
||||
rfc3986 = [
|
||||
{file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
|
||||
@ -2115,46 +2117,51 @@ soupsieve = [
|
||||
{file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"},
|
||||
]
|
||||
sqlalchemy = [
|
||||
{file = "SQLAlchemy-1.4.40-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:b07fc38e6392a65935dc8b486229679142b2ea33c94059366b4d8b56f1e35a97"},
|
||||
{file = "SQLAlchemy-1.4.40-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fb4edb6c354eac0fcc07cb91797e142f702532dbb16c1d62839d6eec35f814cf"},
|
||||
{file = "SQLAlchemy-1.4.40-cp27-cp27m-win32.whl", hash = "sha256:2026632051a93997cf8f6fda14360f99230be1725b7ab2ef15be205a4b8a5430"},
|
||||
{file = "SQLAlchemy-1.4.40-cp27-cp27m-win_amd64.whl", hash = "sha256:f2aa85aebc0ef6b342d5d3542f969caa8c6a63c8d36cf5098769158a9fa2123c"},
|
||||
{file = "SQLAlchemy-1.4.40-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a0b9e3d81f86ba04007f0349e373a5b8c81ec2047aadb8d669caf8c54a092461"},
|
||||
{file = "SQLAlchemy-1.4.40-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:1ab08141d93de83559f6a7d9a962830f918623a885b3759ec2b9d1a531ff28fe"},
|
||||
{file = "SQLAlchemy-1.4.40-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00dd998b43b282c71de46b061627b5edb9332510eb1edfc5017b9e4356ed44ea"},
|
||||
{file = "SQLAlchemy-1.4.40-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb342c0e25cc8f78a0e7c692da3b984f072666b316fbbec2a0e371cb4dfef5f0"},
|
||||
{file = "SQLAlchemy-1.4.40-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23b693876ac7963b6bc7b1a5f3a2642f38d2624af834faad5933913928089d1b"},
|
||||
{file = "SQLAlchemy-1.4.40-cp310-cp310-win32.whl", hash = "sha256:2cf50611ef4221ad587fb7a1708e61ff72966f84330c6317642e08d6db4138fd"},
|
||||
{file = "SQLAlchemy-1.4.40-cp310-cp310-win_amd64.whl", hash = "sha256:26ee4dbac5dd7abf18bf3cd8f04e51f72c339caf702f68172d308888cd26c6c9"},
|
||||
{file = "SQLAlchemy-1.4.40-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b41b87b929118838bafc4bb18cf3c5cd1b3be4b61cd9042e75174df79e8ac7a2"},
|
||||
{file = "SQLAlchemy-1.4.40-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:885e11638946472b4a0a7db8e6df604b2cf64d23dc40eedc3806d869fcb18fae"},
|
||||
{file = "SQLAlchemy-1.4.40-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b7ff0a8bf0aec1908b92b8dfa1246128bf4f94adbdd3da6730e9c542e112542d"},
|
||||
{file = "SQLAlchemy-1.4.40-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfa8ab4ba0c97ab6bcae1f0948497d14c11b6c6ecd1b32b8a79546a0823d8211"},
|
||||
{file = "SQLAlchemy-1.4.40-cp36-cp36m-win32.whl", hash = "sha256:d259fa08e4b3ed952c01711268bcf6cd2442b0c54866d64aece122f83da77c6d"},
|
||||
{file = "SQLAlchemy-1.4.40-cp36-cp36m-win_amd64.whl", hash = "sha256:c8d974c991eef0cd29418a5957ae544559dc326685a6f26b3a914c87759bf2f4"},
|
||||
{file = "SQLAlchemy-1.4.40-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:28b1791a30d62fc104070965f1a2866699c45bbf5adc0be0cf5f22935edcac58"},
|
||||
{file = "SQLAlchemy-1.4.40-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7ccdca6cd167611f4a62a8c2c0c4285c2535640d77108f782ce3f3cccb70f3a"},
|
||||
{file = "SQLAlchemy-1.4.40-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:69deec3a94de10062080d91e1ba69595efeafeafe68b996426dec9720031fb25"},
|
||||
{file = "SQLAlchemy-1.4.40-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ad778f4e80913fb171247e4fa82123d0068615ae1d51a9791fc4284cb81748"},
|
||||
{file = "SQLAlchemy-1.4.40-cp37-cp37m-win32.whl", hash = "sha256:9ced2450c9fd016f9232d976661623e54c450679eeefc7aa48a3d29924a63189"},
|
||||
{file = "SQLAlchemy-1.4.40-cp37-cp37m-win_amd64.whl", hash = "sha256:cdee4d475e35684d210dc6b430ff8ca2ed0636378ac19b457e2f6f350d1f5acc"},
|
||||
{file = "SQLAlchemy-1.4.40-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:08b47c971327e733ffd6bae2d4f50a7b761793efe69d41067fcba86282819eea"},
|
||||
{file = "SQLAlchemy-1.4.40-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf03d37819dc17a388d313919daf32058d19ba1e592efdf14ce8cbd997e6023"},
|
||||
{file = "SQLAlchemy-1.4.40-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a62c0ecbb9976550f26f7bf75569f425e661e7249349487f1483115e5fc893a6"},
|
||||
{file = "SQLAlchemy-1.4.40-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ec440990ab00650d0c7ea2c75bc225087afdd7ddcb248e3d934def4dff62762"},
|
||||
{file = "SQLAlchemy-1.4.40-cp38-cp38-win32.whl", hash = "sha256:2b64955850a14b9d481c17becf0d3f62fb1bb31ac2c45c2caf5ad06d9e811187"},
|
||||
{file = "SQLAlchemy-1.4.40-cp38-cp38-win_amd64.whl", hash = "sha256:959bf4390766a8696aa01285016c766b4eb676f712878aac5fce956dd49695d9"},
|
||||
{file = "SQLAlchemy-1.4.40-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:0992f3cc640ec0f88f721e426da884c34ff0a60eb73d3d64172e23dfadfc8a0b"},
|
||||
{file = "SQLAlchemy-1.4.40-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa9e0d7832b7511b3b3fd0e67fac85ff11fd752834c143ca2364c9b778c0485a"},
|
||||
{file = "SQLAlchemy-1.4.40-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c9d0f1a9538cc5e75f2ea0cb6c3d70155a1b7f18092c052e0d84105622a41b63"},
|
||||
{file = "SQLAlchemy-1.4.40-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c956a5d1adb49a35d78ef0fae26717afc48a36262359bb5b0cbd7a3a247c26f"},
|
||||
{file = "SQLAlchemy-1.4.40-cp39-cp39-win32.whl", hash = "sha256:6b70d02bbe1adbbf715d2249cacf9ac17c6f8d22dfcb3f1a4fbc5bf64364da8a"},
|
||||
{file = "SQLAlchemy-1.4.40-cp39-cp39-win_amd64.whl", hash = "sha256:bf073c619b5a7f7cd731507d0fdc7329bee14b247a63b0419929e4acd24afea8"},
|
||||
{file = "SQLAlchemy-1.4.40.tar.gz", hash = "sha256:44a660506080cc975e1dfa5776fe5f6315ddc626a77b50bf0eee18b0389ea265"},
|
||||
{file = "SQLAlchemy-1.4.42-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:28e881266a172a4d3c5929182fde6bb6fba22ac93f137d5380cc78a11a9dd124"},
|
||||
{file = "SQLAlchemy-1.4.42-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ca9389a00f639383c93ed00333ed763812f80b5ae9e772ea32f627043f8c9c88"},
|
||||
{file = "SQLAlchemy-1.4.42-cp27-cp27m-win32.whl", hash = "sha256:1d0c23ecf7b3bc81e29459c34a3f4c68ca538de01254e24718a7926810dc39a6"},
|
||||
{file = "SQLAlchemy-1.4.42-cp27-cp27m-win_amd64.whl", hash = "sha256:6c9d004eb78c71dd4d3ce625b80c96a827d2e67af9c0d32b1c1e75992a7916cc"},
|
||||
{file = "SQLAlchemy-1.4.42-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9e3a65ce9ed250b2f096f7b559fe3ee92e6605fab3099b661f0397a9ac7c8d95"},
|
||||
{file = "SQLAlchemy-1.4.42-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:2e56dfed0cc3e57b2f5c35719d64f4682ef26836b81067ee6cfad062290fd9e2"},
|
||||
{file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b42c59ffd2d625b28cdb2ae4cde8488543d428cba17ff672a543062f7caee525"},
|
||||
{file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22459fc1718785d8a86171bbe7f01b5c9d7297301ac150f508d06e62a2b4e8d2"},
|
||||
{file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df76e9c60879fdc785a34a82bf1e8691716ffac32e7790d31a98d7dec6e81545"},
|
||||
{file = "SQLAlchemy-1.4.42-cp310-cp310-win32.whl", hash = "sha256:e7e740453f0149437c101ea4fdc7eea2689938c5760d7dcc436c863a12f1f565"},
|
||||
{file = "SQLAlchemy-1.4.42-cp310-cp310-win_amd64.whl", hash = "sha256:effc89e606165ca55f04f3f24b86d3e1c605e534bf1a96e4e077ce1b027d0b71"},
|
||||
{file = "SQLAlchemy-1.4.42-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:97ff50cd85bb907c2a14afb50157d0d5486a4b4639976b4a3346f34b6d1b5272"},
|
||||
{file = "SQLAlchemy-1.4.42-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12c6949bae10f1012ab5c0ea52ab8db99adcb8c7b717938252137cdf694c775"},
|
||||
{file = "SQLAlchemy-1.4.42-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11b2ec26c5d2eefbc3e6dca4ec3d3d95028be62320b96d687b6e740424f83b7d"},
|
||||
{file = "SQLAlchemy-1.4.42-cp311-cp311-win32.whl", hash = "sha256:6045b3089195bc008aee5c273ec3ba9a93f6a55bc1b288841bd4cfac729b6516"},
|
||||
{file = "SQLAlchemy-1.4.42-cp311-cp311-win_amd64.whl", hash = "sha256:0501f74dd2745ec38f44c3a3900fb38b9db1ce21586b691482a19134062bf049"},
|
||||
{file = "SQLAlchemy-1.4.42-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:6e39e97102f8e26c6c8550cb368c724028c575ec8bc71afbbf8faaffe2b2092a"},
|
||||
{file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15d878929c30e41fb3d757a5853b680a561974a0168cd33a750be4ab93181628"},
|
||||
{file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fa5b7eb2051e857bf83bade0641628efe5a88de189390725d3e6033a1fff4257"},
|
||||
{file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1c5f8182b4f89628d782a183d44db51b5af84abd6ce17ebb9804355c88a7b5"},
|
||||
{file = "SQLAlchemy-1.4.42-cp36-cp36m-win32.whl", hash = "sha256:a7dd5b7b34a8ba8d181402d824b87c5cee8963cb2e23aa03dbfe8b1f1e417cde"},
|
||||
{file = "SQLAlchemy-1.4.42-cp36-cp36m-win_amd64.whl", hash = "sha256:5ede1495174e69e273fad68ad45b6d25c135c1ce67723e40f6cf536cb515e20b"},
|
||||
{file = "SQLAlchemy-1.4.42-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:9256563506e040daddccaa948d055e006e971771768df3bb01feeb4386c242b0"},
|
||||
{file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4948b6c5f4e56693bbeff52f574279e4ff972ea3353f45967a14c30fb7ae2beb"},
|
||||
{file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1811a0b19a08af7750c0b69e38dec3d46e47c4ec1d74b6184d69f12e1c99a5e0"},
|
||||
{file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b01d9cd2f9096f688c71a3d0f33f3cd0af8549014e66a7a7dee6fc214a7277d"},
|
||||
{file = "SQLAlchemy-1.4.42-cp37-cp37m-win32.whl", hash = "sha256:bd448b262544b47a2766c34c0364de830f7fb0772d9959c1c42ad61d91ab6565"},
|
||||
{file = "SQLAlchemy-1.4.42-cp37-cp37m-win_amd64.whl", hash = "sha256:04f2598c70ea4a29b12d429a80fad3a5202d56dce19dd4916cc46a965a5ca2e9"},
|
||||
{file = "SQLAlchemy-1.4.42-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3ab7c158f98de6cb4f1faab2d12973b330c2878d0c6b689a8ca424c02d66e1b3"},
|
||||
{file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee377eb5c878f7cefd633ab23c09e99d97c449dd999df639600f49b74725b80"},
|
||||
{file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:934472bb7d8666727746a75670a1f8d91a9cae8c464bba79da30a0f6faccd9e1"},
|
||||
{file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb94a3d1ba77ff2ef11912192c066f01e68416f554c194d769391638c8ad09a"},
|
||||
{file = "SQLAlchemy-1.4.42-cp38-cp38-win32.whl", hash = "sha256:f0f574465b78f29f533976c06b913e54ab4980b9931b69aa9d306afff13a9471"},
|
||||
{file = "SQLAlchemy-1.4.42-cp38-cp38-win_amd64.whl", hash = "sha256:a85723c00a636eed863adb11f1e8aaa36ad1c10089537823b4540948a8429798"},
|
||||
{file = "SQLAlchemy-1.4.42-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5ce6929417d5dce5ad1d3f147db81735a4a0573b8fb36e3f95500a06eaddd93e"},
|
||||
{file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723e3b9374c1ce1b53564c863d1a6b2f1dc4e97b1c178d9b643b191d8b1be738"},
|
||||
{file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:876eb185911c8b95342b50a8c4435e1c625944b698a5b4a978ad2ffe74502908"},
|
||||
{file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd49af453e590884d9cdad3586415922a8e9bb669d874ee1dc55d2bc425aacd"},
|
||||
{file = "SQLAlchemy-1.4.42-cp39-cp39-win32.whl", hash = "sha256:e4ef8cb3c5b326f839bfeb6af5f406ba02ad69a78c7aac0fbeeba994ad9bb48a"},
|
||||
{file = "SQLAlchemy-1.4.42-cp39-cp39-win_amd64.whl", hash = "sha256:5f966b64c852592469a7eb759615bbd351571340b8b344f1d3fa2478b5a4c934"},
|
||||
{file = "SQLAlchemy-1.4.42.tar.gz", hash = "sha256:177e41914c476ed1e1b77fd05966ea88c094053e17a85303c4ce007f88eff363"},
|
||||
]
|
||||
sqlalchemy2-stubs = [
|
||||
{file = "sqlalchemy2-stubs-0.0.2a27.tar.gz", hash = "sha256:f79bce50b7837a2c2374ef4480b41e2b8a8226f313f347dc2a70526a4191db93"},
|
||||
{file = "sqlalchemy2_stubs-0.0.2a27-py3-none-any.whl", hash = "sha256:6cea12fec3c261f6e0e14a95d2cc4914e373095e68ec4fc2eb473183ac2b17a2"},
|
||||
{file = "sqlalchemy2-stubs-0.0.2a29.tar.gz", hash = "sha256:1bbc6aebd76db7c0351a9f45cc1c4e8ac335ba150094c2af091e8b87b9118419"},
|
||||
{file = "sqlalchemy2_stubs-0.0.2a29-py3-none-any.whl", hash = "sha256:ece266cdabf3797b13ddddba27561b67ae7dedc038942bf66e045e978a5e3a66"},
|
||||
]
|
||||
starlette = [
|
||||
{file = "starlette-0.19.1-py3-none-any.whl", hash = "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf"},
|
||||
@ -2170,33 +2177,39 @@ tomli-w = [
|
||||
{file = "tomli_w-1.0.0-py3-none-any.whl", hash = "sha256:9f2a07e8be30a0729e533ec968016807069991ae2fd921a78d42f429ae5f4463"},
|
||||
{file = "tomli_w-1.0.0.tar.gz", hash = "sha256:f463434305e0336248cac9c2dc8076b707d8a12d019dd349f5c1e382dd1ae1b9"},
|
||||
]
|
||||
types-bleach = []
|
||||
types-bleach = [
|
||||
{file = "types-bleach-5.0.3.1.tar.gz", hash = "sha256:ce8772ea5126dab1883851b41e3aeff229aa5213ced36096990344e632e92373"},
|
||||
{file = "types_bleach-5.0.3.1-py3-none-any.whl", hash = "sha256:af5f1b3a54ff279f54c29eccb2e6988ebb6718bc4061469588a5fd4880a79287"},
|
||||
]
|
||||
types-cachetools = [
|
||||
{file = "types-cachetools-5.2.1.tar.gz", hash = "sha256:069cfc825697cd51445c1feabbe4edc1fae2b2315870e7a9a179a7c4a5851bee"},
|
||||
{file = "types_cachetools-5.2.1-py3-none-any.whl", hash = "sha256:b496b7e364ba050c4eaadcc6582f2c9fbb04f8ee7141eb3b311a8589dbd4506a"},
|
||||
]
|
||||
types-emoji = []
|
||||
types-markdown = [
|
||||
{file = "types-Markdown-3.4.1.tar.gz", hash = "sha256:cda9bfd1fcb11e8133a037f8a184e3059ae7389a5f5cc0b53117bf2902aca10d"},
|
||||
{file = "types_Markdown-3.4.1-py3-none-any.whl", hash = "sha256:2d1e5bfff192c78d6644bc3820fea9c7c7cb42dc87558020728ec5b728448ce2"},
|
||||
{file = "types-Markdown-3.4.2.1.tar.gz", hash = "sha256:03c0904cf5886a7d8193e2f50bcf842afc89e0ab80f060f389f6c2635c65628f"},
|
||||
{file = "types_Markdown-3.4.2.1-py3-none-any.whl", hash = "sha256:b2333f6f4b8f69af83de359e10a097e4a3f14bbd6d2484e1829d9b0ec56fa0cb"},
|
||||
]
|
||||
types-pillow = [
|
||||
{file = "types-Pillow-9.2.1.tar.gz", hash = "sha256:9781104ee2176f680576523fa2a2b83b134957aec6f4d62582cc9e74c93a60b4"},
|
||||
{file = "types_Pillow-9.2.1-py3-none-any.whl", hash = "sha256:d63743ef631e47f8d8669590ea976162321a9a7604588b424b6306533453fb63"},
|
||||
{file = "types-Pillow-9.2.2.2.tar.gz", hash = "sha256:b88bd03d6b1d467d2dd1d54808b04631ca88941fe16f9eeb92bc114e5a145ec0"},
|
||||
{file = "types_Pillow-9.2.2.2-py3-none-any.whl", hash = "sha256:49a633ad811446efeb2abbfea4596cff470b1a48adba6c944fae57b3a667e5cb"},
|
||||
]
|
||||
types-python-dateutil = [
|
||||
{file = "types-python-dateutil-2.8.19.2.tar.gz", hash = "sha256:e6e32ce18f37765b08c46622287bc8d8136dc0c562d9ad5b8fd158c59963d7a7"},
|
||||
{file = "types_python_dateutil-2.8.19.2-py3-none-any.whl", hash = "sha256:3f4dbe465e7e0c6581db11fd7a4855d1355b78712b3f292bd399cd332247e9c0"},
|
||||
]
|
||||
types-python-dateutil = []
|
||||
types-requests = [
|
||||
{file = "types-requests-2.28.9.tar.gz", hash = "sha256:feaf581bd580497a47fe845d506fa3b91b484cf706ff27774e87659837de9962"},
|
||||
{file = "types_requests-2.28.9-py3-none-any.whl", hash = "sha256:86cb66d3de2f53eac5c09adc42cf6547eefbd0c7e1210beca1ee751c35d96083"},
|
||||
{file = "types-requests-2.28.11.2.tar.gz", hash = "sha256:fdcd7bd148139fb8eef72cf4a41ac7273872cad9e6ada14b11ff5dfdeee60ed3"},
|
||||
{file = "types_requests-2.28.11.2-py3-none-any.whl", hash = "sha256:14941f8023a80b16441b3b46caffcbfce5265fd14555844d6029697824b5a2ef"},
|
||||
]
|
||||
types-tabulate = []
|
||||
types-urllib3 = [
|
||||
{file = "types-urllib3-1.26.23.tar.gz", hash = "sha256:b78e819f0e350221d0689a5666162e467ba3910737bafda14b5c2c85e9bb1e56"},
|
||||
{file = "types_urllib3-1.26.23-py3-none-any.whl", hash = "sha256:333e675b188a1c1fd980b4b352f9e40572413a4c1ac689c23cd546e96310070a"},
|
||||
{file = "types-urllib3-1.26.25.1.tar.gz", hash = "sha256:a948584944b2412c9a74b9cf64f6c48caf8652cb88b38361316f6d15d8a184cd"},
|
||||
{file = "types_urllib3-1.26.25.1-py3-none-any.whl", hash = "sha256:f6422596cc9ee5fdf68f9d547f541096a20c2dcfd587e37c804c9ea720bf5cb2"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
|
||||
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
|
||||
{file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
|
||||
{file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
|
||||
@ -2207,22 +2220,36 @@ uvicorn = [
|
||||
{file = "uvicorn-0.18.3.tar.gz", hash = "sha256:9a66e7c42a2a95222f76ec24a4b754c158261c4696e683b9dadc72b590e0311b"},
|
||||
]
|
||||
uvloop = [
|
||||
{file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"},
|
||||
{file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c"},
|
||||
{file = "uvloop-0.16.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64"},
|
||||
{file = "uvloop-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9"},
|
||||
{file = "uvloop-0.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638"},
|
||||
{file = "uvloop-0.16.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450"},
|
||||
{file = "uvloop-0.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805"},
|
||||
{file = "uvloop-0.16.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382"},
|
||||
{file = "uvloop-0.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee"},
|
||||
{file = "uvloop-0.16.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464"},
|
||||
{file = "uvloop-0.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab"},
|
||||
{file = "uvloop-0.16.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f"},
|
||||
{file = "uvloop-0.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897"},
|
||||
{file = "uvloop-0.16.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f"},
|
||||
{file = "uvloop-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861"},
|
||||
{file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"},
|
||||
{file = "uvloop-0.17.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce9f61938d7155f79d3cb2ffa663147d4a76d16e08f65e2c66b77bd41b356718"},
|
||||
{file = "uvloop-0.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:68532f4349fd3900b839f588972b3392ee56042e440dd5873dfbbcd2cc67617c"},
|
||||
{file = "uvloop-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0949caf774b9fcefc7c5756bacbbbd3fc4c05a6b7eebc7c7ad6f825b23998d6d"},
|
||||
{file = "uvloop-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff3d00b70ce95adce264462c930fbaecb29718ba6563db354608f37e49e09024"},
|
||||
{file = "uvloop-0.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a5abddb3558d3f0a78949c750644a67be31e47936042d4f6c888dd6f3c95f4aa"},
|
||||
{file = "uvloop-0.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8efcadc5a0003d3a6e887ccc1fb44dec25594f117a94e3127954c05cf144d811"},
|
||||
{file = "uvloop-0.17.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3378eb62c63bf336ae2070599e49089005771cc651c8769aaad72d1bd9385a7c"},
|
||||
{file = "uvloop-0.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6aafa5a78b9e62493539456f8b646f85abc7093dd997f4976bb105537cf2635e"},
|
||||
{file = "uvloop-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c686a47d57ca910a2572fddfe9912819880b8765e2f01dc0dd12a9bf8573e539"},
|
||||
{file = "uvloop-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:864e1197139d651a76c81757db5eb199db8866e13acb0dfe96e6fc5d1cf45fc4"},
|
||||
{file = "uvloop-0.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2a6149e1defac0faf505406259561bc14b034cdf1d4711a3ddcdfbaa8d825a05"},
|
||||
{file = "uvloop-0.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6708f30db9117f115eadc4f125c2a10c1a50d711461699a0cbfaa45b9a78e376"},
|
||||
{file = "uvloop-0.17.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:23609ca361a7fc587031429fa25ad2ed7242941adec948f9d10c045bfecab06b"},
|
||||
{file = "uvloop-0.17.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2deae0b0fb00a6af41fe60a675cec079615b01d68beb4cc7b722424406b126a8"},
|
||||
{file = "uvloop-0.17.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45cea33b208971e87a31c17622e4b440cac231766ec11e5d22c76fab3bf9df62"},
|
||||
{file = "uvloop-0.17.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9b09e0f0ac29eee0451d71798878eae5a4e6a91aa275e114037b27f7db72702d"},
|
||||
{file = "uvloop-0.17.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dbbaf9da2ee98ee2531e0c780455f2841e4675ff580ecf93fe5c48fe733b5667"},
|
||||
{file = "uvloop-0.17.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a4aee22ece20958888eedbad20e4dbb03c37533e010fb824161b4f05e641f738"},
|
||||
{file = "uvloop-0.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:307958f9fc5c8bb01fad752d1345168c0abc5d62c1b72a4a8c6c06f042b45b20"},
|
||||
{file = "uvloop-0.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ebeeec6a6641d0adb2ea71dcfb76017602ee2bfd8213e3fcc18d8f699c5104f"},
|
||||
{file = "uvloop-0.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1436c8673c1563422213ac6907789ecb2b070f5939b9cbff9ef7113f2b531595"},
|
||||
{file = "uvloop-0.17.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8887d675a64cfc59f4ecd34382e5b4f0ef4ae1da37ed665adba0c2badf0d6578"},
|
||||
{file = "uvloop-0.17.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3db8de10ed684995a7f34a001f15b374c230f7655ae840964d51496e2f8a8474"},
|
||||
{file = "uvloop-0.17.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7d37dccc7ae63e61f7b96ee2e19c40f153ba6ce730d8ba4d3b4e9738c1dccc1b"},
|
||||
{file = "uvloop-0.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cbbe908fda687e39afd6ea2a2f14c2c3e43f2ca88e3a11964b297822358d0e6c"},
|
||||
{file = "uvloop-0.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d97672dc709fa4447ab83276f344a165075fd9f366a97b712bdd3fee05efae8"},
|
||||
{file = "uvloop-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1e507c9ee39c61bfddd79714e4f85900656db1aec4d40c6de55648e85c2799c"},
|
||||
{file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c092a2c1e736086d59ac8e41f9c98f26bbf9b9222a76f21af9dfe949b99b2eb9"},
|
||||
{file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:30babd84706115626ea78ea5dbc7dd8d0d01a2e9f9b306d24ca4ed5796c66ded"},
|
||||
{file = "uvloop-0.17.0.tar.gz", hash = "sha256:0ddf6baf9cf11a1a22c71487f39f15b2cf78eb5bde7e5b45fbb99e8a9d91b9e1"},
|
||||
]
|
||||
watchdog = [
|
||||
{file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"},
|
||||
@ -2252,24 +2279,24 @@ watchdog = [
|
||||
{file = "watchdog-2.1.9.tar.gz", hash = "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609"},
|
||||
]
|
||||
watchfiles = [
|
||||
{file = "watchfiles-0.16.1-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:1e41c8b4bf3e07c18aa51775b36b718830fa727929529a7d6e5b38cf845a06b4"},
|
||||
{file = "watchfiles-0.16.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b2c7ad91a867dd688b9a12097dd6a4f89397b43fccee871152aa67197cc94398"},
|
||||
{file = "watchfiles-0.16.1-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:75a4b9cec1b1c337ea77d4428b29861553d6bf8179923b1bc7e825e217460e2c"},
|
||||
{file = "watchfiles-0.16.1-cp37-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2a3debb19912072799d7ca53e99fc5f090f77948f5601392623b2a416b4c86be"},
|
||||
{file = "watchfiles-0.16.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35f3e411822e14a35f2ef656535aad4e6e79670d6b6ef8e53db958e28916b1fe"},
|
||||
{file = "watchfiles-0.16.1-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9a7a6dc63684ff5ba11f0be0e64f744112c3c7a0baf4ec8f6794f9a6257d21e"},
|
||||
{file = "watchfiles-0.16.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e939a2693404ac11e055f9d1237db8ad7635e2185a6143bde00116e691ea2983"},
|
||||
{file = "watchfiles-0.16.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cd7d2fd9a8f28066edc8db5278f3632eb94d10596af760fa0601631f32b1a41e"},
|
||||
{file = "watchfiles-0.16.1-cp37-abi3-win32.whl", hash = "sha256:f91035a273001390093a09e52274a34695b0d15ee8736183b640bbc3b8a432ab"},
|
||||
{file = "watchfiles-0.16.1-cp37-abi3-win_amd64.whl", hash = "sha256:a8a1809bf910672aa0b7ed6e6045d4fc2cf1e0718b99bc443ef17faa5697b68a"},
|
||||
{file = "watchfiles-0.16.1-cp37-abi3-win_arm64.whl", hash = "sha256:baa6d0c1c5140e1dcf6ff802dd7b09fcd95b358e50d42fabc83d83f719451c54"},
|
||||
{file = "watchfiles-0.16.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:5741246ae399a03395aa5ee35480083a4f29d58ffd41dd3395594f8805f8cdbc"},
|
||||
{file = "watchfiles-0.16.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44c6aff58b8a70a26431737e483a54e8e224279b21873388571ed184fe7c91a7"},
|
||||
{file = "watchfiles-0.16.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d1b2d0cf060e5222a930a3e2f40f6577da1d18c085c32741b98a128dc1e72c"},
|
||||
{file = "watchfiles-0.16.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:70159e759f52b65a50c498182dece80364bfd721e839c254c328cbc7a1716616"},
|
||||
{file = "watchfiles-0.16.1-pp39-pypy39_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22af3b915f928ef59d427d7228668f87ac8054ed8200808c73fbcaa4f82d5572"},
|
||||
{file = "watchfiles-0.16.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a6a1ac96edf5bc3f8e36f4462fc1daad0ec3769ff2adb920571e120e37c91c5"},
|
||||
{file = "watchfiles-0.16.1.tar.gz", hash = "sha256:aed7575e24434c8fec2f2bbb0cecb1521ea1240234d9108db7915a3424d92394"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:76a4c4a8e25a2c9a4f7fa3d373bbaf5558c17b97b4cf8411d33de368fe6b68a9"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:d5d799614d4c56d29c5ba56f4f619f967210dc10a0d6965b62d326b9e2f72c9e"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:39b932b044fef6c43e813e0bef908e0edf185bf7b5d8d53246651cb7ac9efe79"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1686bc4ac40ffde7256b6543b0f9a2cc8b531ae45243786f1d3f1dda2fe39e24"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:320bcde0adaa972403ed3b70f784409437325a1a4df2de54ba0672203d8847e5"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ba6d8c2f957cae3e888bc250bc60ed09fe869b3f55f09d020ed3fecbefb6a4c"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd4215badad1e3d1ad5fb79f21432dd5157e2e7b0765d27a19dc2a28580c6979"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cfdbfc4b6797c28dd1a8524581fed00ca333971b4111af8cd42fb7a92dcdc227"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-win32.whl", hash = "sha256:8eddc2d19bf6f49aee224072ec0f4f3258125a49f11b5dcff1448e68718a745e"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-win_amd64.whl", hash = "sha256:be87c9b1fe2b02105a9ac6d9df7500a110652bbd97cf46b13964eeaef9a6c89c"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-win_arm64.whl", hash = "sha256:184799818c4fa7dbc6a1e4ca20bcbc6b85e4e0db07ce4554ea2f29b75ccd0cdc"},
|
||||
{file = "watchfiles-0.18.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f39fcdac5d5b9815a0c2ab9005d39854296b11fa15386a9a69c09cbbc5dde2c"},
|
||||
{file = "watchfiles-0.18.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78b1e7c29b92dfc8fc32f15949019232b493767d236c2bff31848df13fdb9e8a"},
|
||||
{file = "watchfiles-0.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27d64a6ed5e0aebef97c70fa3899a6958d4f7f049effc659e7dc3e81f3170a7b"},
|
||||
{file = "watchfiles-0.18.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4bbc8bfa0f3871b1867af42837a5635a9c1cbb2b68d039754b4750642c34aaee"},
|
||||
{file = "watchfiles-0.18.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3a12e4de5446fb6e286b720d0cb3a080811caf0ef43e556c2db5fe10ef0342"},
|
||||
{file = "watchfiles-0.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e611a90482ac14ef3ec234c1604ed921d1b0c68970eba82f1cf0d59a3e4eb76"},
|
||||
{file = "watchfiles-0.18.0.tar.gz", hash = "sha256:bbe10d134eef1666451382015e48f092c941a6d4562a98ffa1a288f79a897c46"},
|
||||
]
|
||||
wcwidth = [
|
||||
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
|
||||
|
@ -18,7 +18,6 @@ httpx = {extras = ["http2"], version = "^0.23.0"}
|
||||
SQLAlchemy = {extras = ["asyncio"], version = "^1.4.39"}
|
||||
alembic = "^1.8.0"
|
||||
bleach = "^5.0.0"
|
||||
Markdown = "^3.3.7"
|
||||
prompt-toolkit = "^3.0.29"
|
||||
tomli-w = "^1.0.0"
|
||||
python-dateutil = "^2.8.2"
|
||||
@ -27,7 +26,6 @@ html5lib = "^1.1"
|
||||
mf2py = "^1.1.2"
|
||||
Pygments = "^2.12.0"
|
||||
loguru = "^0.6.0"
|
||||
mdx-linkify = "^2.1"
|
||||
Pillow = "^9.1.1"
|
||||
blurhash-python = "^1.1.3"
|
||||
html2text = "^2020.1.16"
|
||||
@ -44,6 +42,8 @@ invoke = "^1.7.1"
|
||||
boussole = "^2.0.0"
|
||||
uvicorn = {extras = ["standard"], version = "^0.18.3"}
|
||||
Brotli = "^1.0.9"
|
||||
greenlet = "^1.1.3"
|
||||
mistletoe = "^0.9.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "^22.3.0"
|
||||
|
@ -1,19 +1,115 @@
|
||||
import re
|
||||
import shutil
|
||||
import typing
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from jinja2 import Environment
|
||||
from jinja2 import FileSystemLoader
|
||||
from jinja2 import select_autoescape
|
||||
from markdown import markdown
|
||||
from mistletoe import Document # type: ignore
|
||||
from mistletoe import HTMLRenderer # type: ignore
|
||||
from mistletoe import block_token # type: ignore
|
||||
from pygments import highlight # type: ignore
|
||||
from pygments.formatters import HtmlFormatter # type: ignore
|
||||
from pygments.lexers import get_lexer_by_name as get_lexer # type: ignore
|
||||
from pygments.lexers import guess_lexer # type: ignore
|
||||
|
||||
from app.config import VERSION
|
||||
from app.source import CustomRenderer
|
||||
from app.utils.datetime import now
|
||||
|
||||
_FORMATTER = HtmlFormatter()
|
||||
_FORMATTER.noclasses = True
|
||||
|
||||
def markdownify(content: str) -> str:
|
||||
return markdown(
|
||||
content, extensions=["mdx_linkify", "fenced_code", "codehilite", "toc"]
|
||||
|
||||
class DocRenderer(CustomRenderer):
|
||||
def __init__(
|
||||
self,
|
||||
depth=5,
|
||||
omit_title=True,
|
||||
filter_conds=[],
|
||||
) -> None:
|
||||
super().__init__(
|
||||
enable_mentionify=False,
|
||||
enable_hashtagify=False,
|
||||
)
|
||||
self._headings: list[tuple[int, str, str]] = []
|
||||
self._ids: set[str] = set()
|
||||
self.depth = depth
|
||||
self.omit_title = omit_title
|
||||
self.filter_conds = filter_conds
|
||||
|
||||
@property
|
||||
def toc(self):
|
||||
"""
|
||||
Returns table of contents as a block_token.List instance.
|
||||
"""
|
||||
|
||||
def get_indent(level):
|
||||
if self.omit_title:
|
||||
level -= 1
|
||||
return " " * 4 * (level - 1)
|
||||
|
||||
def build_list_item(heading):
|
||||
level, content, title_id = heading
|
||||
template = '{indent}- <a href="#{id}" rel="nofollow">{content}</a>\n'
|
||||
return template.format(
|
||||
indent=get_indent(level), content=content, id=title_id
|
||||
)
|
||||
|
||||
lines = [build_list_item(heading) for heading in self._headings]
|
||||
items = block_token.tokenize(lines)
|
||||
return items[0]
|
||||
|
||||
def render_heading(self, token):
|
||||
"""
|
||||
Overrides super().render_heading; stores rendered heading first,
|
||||
then returns it.
|
||||
"""
|
||||
template = '<h{level} id="{id}">{inner}</h{level}>'
|
||||
inner = self.render_inner(token)
|
||||
title_id = inner.lower().replace(" ", "-")
|
||||
if title_id in self._ids:
|
||||
i = 1
|
||||
while 1:
|
||||
title_id = f"{title_id}_{i}"
|
||||
if title_id not in self._ids:
|
||||
break
|
||||
self._ids.add(title_id)
|
||||
rendered = template.format(level=token.level, inner=inner, id=title_id)
|
||||
content = self.parse_rendered_heading(rendered)
|
||||
|
||||
if not (
|
||||
self.omit_title
|
||||
and token.level == 1
|
||||
or token.level > self.depth
|
||||
or any(cond(content) for cond in self.filter_conds)
|
||||
):
|
||||
self._headings.append((token.level, content, title_id))
|
||||
return rendered
|
||||
|
||||
@staticmethod
|
||||
def parse_rendered_heading(rendered):
|
||||
"""
|
||||
Helper method; converts rendered heading to plain text.
|
||||
"""
|
||||
return re.sub(r"<.+?>", "", rendered)
|
||||
|
||||
def render_block_code(self, token: typing.Any) -> str:
|
||||
code = token.children[0].content
|
||||
lexer = get_lexer(token.language) if token.language else guess_lexer(code)
|
||||
return highlight(code, lexer, _FORMATTER)
|
||||
|
||||
|
||||
def markdownify(content: str) -> tuple[str, Any]:
|
||||
with DocRenderer() as renderer:
|
||||
rendered_content = renderer.render(Document(content))
|
||||
|
||||
with HTMLRenderer() as html_renderer:
|
||||
toc = html_renderer.render(renderer.toc)
|
||||
|
||||
return rendered_content, toc
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@ -30,32 +126,36 @@ def main() -> None:
|
||||
last_updated = now().replace(second=0, microsecond=0).isoformat()
|
||||
|
||||
readme = Path("README.md")
|
||||
content, toc = markdownify(readme.read_text().removeprefix("# microblog.pub"))
|
||||
template.stream(
|
||||
content=markdownify(readme.read_text().removeprefix("# microblog.pub")),
|
||||
content=content,
|
||||
version=VERSION,
|
||||
path="/",
|
||||
last_updated=last_updated,
|
||||
).dump("docs/dist/index.html")
|
||||
|
||||
install = Path("docs/install.md")
|
||||
content, toc = markdownify(install.read_text())
|
||||
template.stream(
|
||||
content=markdownify(install.read_text()),
|
||||
content=content.replace("[TOC]", toc),
|
||||
version=VERSION,
|
||||
path="/installing.html",
|
||||
last_updated=last_updated,
|
||||
).dump("docs/dist/installing.html")
|
||||
|
||||
user_guide = Path("docs/user_guide.md")
|
||||
content, toc = markdownify(user_guide.read_text())
|
||||
template.stream(
|
||||
content=markdownify(user_guide.read_text()),
|
||||
content=content.replace("[TOC]", toc),
|
||||
version=VERSION,
|
||||
path="/user_guide.html",
|
||||
last_updated=last_updated,
|
||||
).dump("docs/dist/user_guide.html")
|
||||
|
||||
developer_guide = Path("docs/developer_guide.md")
|
||||
content, toc = markdownify(developer_guide.read_text())
|
||||
template.stream(
|
||||
content=markdownify(developer_guide.read_text()),
|
||||
content=content.replace("[TOC]", toc),
|
||||
version=VERSION,
|
||||
path="/developer_guide.html",
|
||||
last_updated=last_updated,
|
||||
|
43
tasks.py
43
tasks.py
@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import io
|
||||
import shutil
|
||||
import tarfile
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
@ -45,7 +46,12 @@ def compile_scss(ctx, watch=False):
|
||||
# type: (Context, bool) -> None
|
||||
from app.utils.favicon import build_favicon
|
||||
|
||||
favicon_file = Path("data/favicon.ico")
|
||||
if not favicon_file.exists():
|
||||
build_favicon()
|
||||
else:
|
||||
shutil.copy2(favicon_file, "app/static/favicon.ico")
|
||||
|
||||
theme_file = Path("data/_theme.scss")
|
||||
if not theme_file.exists():
|
||||
theme_file.write_text("// override vars for theming here")
|
||||
@ -264,7 +270,7 @@ def move_to(ctx, moved_to):
|
||||
)
|
||||
return
|
||||
|
||||
await send_move(db_session, moved_to)
|
||||
await send_move(db_session, new_actor.ap_id)
|
||||
|
||||
print("Done")
|
||||
|
||||
@ -312,3 +318,38 @@ def yunohost_config(
|
||||
summary=summary,
|
||||
password=password,
|
||||
)
|
||||
|
||||
|
||||
@task
|
||||
def reset_password(ctx):
|
||||
# type: (Context) -> None
|
||||
import bcrypt
|
||||
from prompt_toolkit import prompt
|
||||
|
||||
new_password = bcrypt.hashpw(
|
||||
prompt("New admin password: ", is_password=True).encode(), bcrypt.gensalt()
|
||||
).decode()
|
||||
|
||||
print()
|
||||
print("Update data/profile.toml with:")
|
||||
print(f'admin_password = "{new_password}"')
|
||||
|
||||
|
||||
@task
|
||||
def check_config(ctx):
|
||||
# type: (Context) -> None
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from loguru import logger
|
||||
|
||||
logger.disable("app")
|
||||
|
||||
try:
|
||||
from app import config # noqa: F401
|
||||
except Exception as exc:
|
||||
print("Config error, please fix data/profile.toml:\n")
|
||||
print("".join(traceback.format_exception(exc)))
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("Config is OK")
|
||||
|
@ -68,6 +68,20 @@ def build_accept_activity(
|
||||
}
|
||||
|
||||
|
||||
def build_block_activity(
|
||||
from_remote_actor: actor.RemoteActor,
|
||||
for_remote_actor: actor.RemoteActor,
|
||||
outbox_public_id: str | None = None,
|
||||
) -> ap.RawObject:
|
||||
return {
|
||||
"@context": ap.AS_CTX,
|
||||
"type": "Block",
|
||||
"id": from_remote_actor.ap_id + "/block/" + (outbox_public_id or uuid4().hex),
|
||||
"actor": from_remote_actor.ap_id,
|
||||
"object": for_remote_actor.ap_id,
|
||||
}
|
||||
|
||||
|
||||
def build_move_activity(
|
||||
from_remote_actor: actor.RemoteActor,
|
||||
for_remote_object: actor.RemoteActor,
|
||||
|
@ -423,3 +423,53 @@ def test_inbox__move_activity(
|
||||
).scalar_one()
|
||||
assert notif.actor.ap_id == new_ra.ap_id
|
||||
assert notif.inbox_object_id == inbox_activity.id
|
||||
|
||||
|
||||
def test_inbox__block_activity(
|
||||
db: Session,
|
||||
client: TestClient,
|
||||
respx_mock: respx.MockRouter,
|
||||
) -> None:
|
||||
# Given a remote actor
|
||||
ra = setup_remote_actor(respx_mock)
|
||||
|
||||
# Which is followed by the local actor
|
||||
setup_remote_actor_as_following(ra)
|
||||
|
||||
# When receiving a Block activity
|
||||
follow_activity = RemoteObject(
|
||||
factories.build_block_activity(
|
||||
from_remote_actor=ra,
|
||||
for_remote_actor=LOCAL_ACTOR,
|
||||
),
|
||||
ra,
|
||||
)
|
||||
with mock_httpsig_checker(ra):
|
||||
response = client.post(
|
||||
"/inbox",
|
||||
headers={"Content-Type": ap.AS_CTX},
|
||||
json=follow_activity.ap_object,
|
||||
)
|
||||
|
||||
# Then the server returns a 202
|
||||
assert response.status_code == 202
|
||||
|
||||
run_process_next_incoming_activity()
|
||||
|
||||
# And the actor was saved in DB
|
||||
saved_actor = db.execute(select(models.Actor)).scalar_one()
|
||||
assert saved_actor.ap_id == ra.ap_id
|
||||
|
||||
# And the Block activity was saved in the inbox
|
||||
inbox_activity = db.execute(
|
||||
select(models.InboxObject).where(models.InboxObject.ap_type == "Block")
|
||||
).scalar_one()
|
||||
|
||||
# And a notification was created
|
||||
notif = db.execute(
|
||||
select(models.Notification).where(
|
||||
models.Notification.notification_type == models.NotificationType.BLOCKED
|
||||
)
|
||||
).scalar_one()
|
||||
assert notif.actor.ap_id == ra.ap_id
|
||||
assert notif.inbox_object_id == inbox_activity.id
|
||||
|
@ -77,23 +77,29 @@ def test_send_delete__reverts_side_effects(
|
||||
|
||||
# with a note that has existing replies
|
||||
inbox_note = setup_inbox_note(actor)
|
||||
inbox_note.replies_count = 1
|
||||
# with a bogus counter
|
||||
inbox_note.replies_count = 5
|
||||
db.commit()
|
||||
|
||||
# and a local reply
|
||||
outbox_note = setup_outbox_note(
|
||||
# and 2 local replies
|
||||
setup_outbox_note(
|
||||
to=[ap.AS_PUBLIC],
|
||||
cc=[LOCAL_ACTOR.followers_collection_id], # type: ignore
|
||||
in_reply_to=inbox_note.ap_id,
|
||||
)
|
||||
outbox_note2 = setup_outbox_note(
|
||||
to=[ap.AS_PUBLIC],
|
||||
cc=[LOCAL_ACTOR.followers_collection_id], # type: ignore
|
||||
in_reply_to=inbox_note.ap_id,
|
||||
)
|
||||
inbox_note.replies_count = inbox_note.replies_count + 1
|
||||
db.commit()
|
||||
|
||||
# When deleting one of the replies
|
||||
response = client.post(
|
||||
"/admin/actions/delete",
|
||||
data={
|
||||
"redirect_url": "http://testserver/",
|
||||
"ap_object_id": outbox_note.ap_id,
|
||||
"ap_object_id": outbox_note2.ap_id,
|
||||
"csrf_token": generate_csrf_token(),
|
||||
},
|
||||
cookies=generate_admin_session_cookies(),
|
||||
@ -108,14 +114,14 @@ def test_send_delete__reverts_side_effects(
|
||||
select(models.OutboxObject).where(models.OutboxObject.ap_type == "Delete")
|
||||
).scalar_one()
|
||||
assert outbox_object.ap_type == "Delete"
|
||||
assert outbox_object.activity_object_ap_id == outbox_note.ap_id
|
||||
assert outbox_object.activity_object_ap_id == outbox_note2.ap_id
|
||||
|
||||
# And an outgoing activity was queued
|
||||
outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one()
|
||||
assert outgoing_activity.outbox_object_id == outbox_object.id
|
||||
assert outgoing_activity.recipient == ra.inbox_url
|
||||
|
||||
# And the replies count of the replied object was decremented
|
||||
# And the replies count of the replied object was refreshed correctly
|
||||
db.refresh(inbox_note)
|
||||
assert inbox_note.replies_count == 1
|
||||
|
||||
@ -173,7 +179,7 @@ def test_send_create_activity__with_attachment(
|
||||
outbox_object = db.execute(select(models.OutboxObject)).scalar_one()
|
||||
assert outbox_object.ap_type == "Note"
|
||||
assert outbox_object.summary is None
|
||||
assert outbox_object.content == "<p>hello</p>"
|
||||
assert outbox_object.content == "<p>hello</p>\n"
|
||||
assert len(outbox_object.attachments) == 1
|
||||
attachment = outbox_object.attachments[0]
|
||||
assert attachment.type == "Document"
|
||||
@ -221,7 +227,7 @@ def test_send_create_activity__no_content_with_cw_and_attachments(
|
||||
outbox_object = db.execute(select(models.OutboxObject)).scalar_one()
|
||||
assert outbox_object.ap_type == "Note"
|
||||
assert outbox_object.summary is None
|
||||
assert outbox_object.content == "<p>cw</p>"
|
||||
assert outbox_object.content == "<p>cw</p>\n"
|
||||
assert len(outbox_object.attachments) == 1
|
||||
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
@ -31,7 +33,19 @@ def test_followers__ap(client, db) -> None:
|
||||
response = client.get("/followers", headers={"Accept": ap.AP_CONTENT_TYPE})
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"] == ap.AP_CONTENT_TYPE
|
||||
assert response.json()["id"].endswith("/followers")
|
||||
json_resp = response.json()
|
||||
assert json_resp["id"].endswith("/followers")
|
||||
assert "first" in json_resp
|
||||
|
||||
|
||||
def test_followers__ap_hides_followers(client, db) -> None:
|
||||
with mock.patch("app.main.config.HIDES_FOLLOWERS", True):
|
||||
response = client.get("/followers", headers={"Accept": ap.AP_CONTENT_TYPE})
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"] == ap.AP_CONTENT_TYPE
|
||||
json_resp = response.json()
|
||||
assert json_resp["id"].endswith("/followers")
|
||||
assert "first" not in json_resp
|
||||
|
||||
|
||||
def test_followers__html(client, db) -> None:
|
||||
@ -40,14 +54,40 @@ def test_followers__html(client, db) -> None:
|
||||
assert response.headers["content-type"].startswith("text/html")
|
||||
|
||||
|
||||
def test_followers__html_hides_followers(client, db) -> None:
|
||||
with mock.patch("app.main.config.HIDES_FOLLOWERS", True):
|
||||
response = client.get("/followers", headers={"Accept": "text/html"})
|
||||
assert response.status_code == 404
|
||||
assert response.headers["content-type"].startswith("text/html")
|
||||
|
||||
|
||||
def test_following__ap(client, db) -> None:
|
||||
response = client.get("/following", headers={"Accept": ap.AP_CONTENT_TYPE})
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"] == ap.AP_CONTENT_TYPE
|
||||
assert response.json()["id"].endswith("/following")
|
||||
json_resp = response.json()
|
||||
assert json_resp["id"].endswith("/following")
|
||||
assert "first" in json_resp
|
||||
|
||||
|
||||
def test_following__ap_hides_following(client, db) -> None:
|
||||
with mock.patch("app.main.config.HIDES_FOLLOWING", True):
|
||||
response = client.get("/following", headers={"Accept": ap.AP_CONTENT_TYPE})
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"] == ap.AP_CONTENT_TYPE
|
||||
json_resp = response.json()
|
||||
assert json_resp["id"].endswith("/following")
|
||||
assert "first" not in json_resp
|
||||
|
||||
|
||||
def test_following__html(client, db) -> None:
|
||||
response = client.get("/following")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"].startswith("text/html")
|
||||
|
||||
|
||||
def test_following__html_hides_following(client, db) -> None:
|
||||
with mock.patch("app.main.config.HIDES_FOLLOWING", True):
|
||||
response = client.get("/following", headers={"Accept": "text/html"})
|
||||
assert response.status_code == 404
|
||||
assert response.headers["content-type"].startswith("text/html")
|
||||
|
Reference in New Issue
Block a user