1
0
mirror of https://git.sr.ht/~tsileo/microblog.pub synced 2025-06-05 21:59:23 +02:00

28 Commits

Author SHA1 Message Date
0f1fdd3944 Add missing migration 2022-09-21 19:19:12 +02:00
254588f7c0 Boostrap support for quote URL 2022-09-21 19:18:44 +02:00
4fcf585c23 Fix OG meta display 2022-09-20 20:15:59 +02:00
6873ede288 Tweak CSS 2022-09-20 20:00:35 +02:00
e0ad21f335 Drop View activities 2022-09-20 12:22:00 +02:00
b3f25e7da1 Improve replies counter for out-of-order replies 2022-09-19 21:16:09 +02:00
d44c8a58aa More improvements for the replies counter 2022-09-19 20:46:05 +02:00
54aa2f51f4 Improve replies counter handling 2022-09-19 20:31:54 +02:00
3305d489ec Fix tag parsing for actors 2022-09-19 19:33:44 +02:00
e19c623c71 Tweak Dockerfile 2022-09-18 21:33:50 +02:00
5905ad96b4 Tweak Dockerfile 2022-09-18 20:54:25 +02:00
9093659b0a Tweak error wording 2022-09-16 18:37:09 +02:00
b99552384c Improve expired session and CSRF error handling 2022-09-16 18:14:50 +02:00
949365d8ba Add more tasks and tweak docs 2022-09-16 17:38:19 +02:00
a55b06b252 knoweldge -> knowledge 2022-09-16 08:58:22 +02:00
c30033c19e Fix minor grammatical issues, mostly in docs 2022-09-16 08:52:43 +02:00
a6321f52d8 Add task to reset password 2022-09-15 22:47:36 +02:00
4e1e4d0ea8 Tweak actor update 2022-09-15 22:19:01 +02:00
110f7df962 Fix GIF upload handling 2022-09-14 08:38:54 +02:00
4c86cd4be3 Always show followers/following page when admin 2022-09-13 22:33:20 +02:00
df06defbef Tweak docs 2022-09-13 21:23:32 +02:00
b2f268682c New config item to hide followers/following 2022-09-13 21:03:35 +02:00
567595bb4b Tweak inbox processing 2022-09-13 21:03:11 +02:00
91b8bb26b7 Bugfixes 2022-09-13 21:02:47 +02:00
bd4d5a004a Improve Announce handling 2022-09-13 07:59:35 +02:00
04da8725ed Improve fetch 2022-09-12 08:04:16 +02:00
0c7a19749d Tweak docs about moving 2022-09-11 19:37:35 +02:00
2a37034775 Fix move task 2022-09-11 19:26:41 +02:00
34 changed files with 764 additions and 235 deletions

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,34 @@
"""Add support for quote URL
Revision ID: c3027d0e18dc
Revises: 604d125ea2fb
Create Date: 2022-09-21 07:08:24.568124+00:00
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = 'c3027d0e18dc'
down_revision = '604d125ea2fb'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('inbox', schema=None) as batch_op:
batch_op.add_column(sa.Column('quoted_inbox_object_id', sa.Integer(), nullable=True))
batch_op.create_foreign_key('fk_quoted_inbox_object_id', 'inbox', ['quoted_inbox_object_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('inbox', schema=None) as batch_op:
batch_op.drop_constraint('fk_quoted_inbox_object_id', type_='foreignkey')
batch_op.drop_column('quoted_inbox_object_id')
# ### end Alembic commands ###

View File

@ -61,6 +61,10 @@ class ObjectNotFoundError(Exception):
pass
class ObjectUnavailableError(Exception):
pass
class FetchErrorTypeEnum(str, enum.Enum):
TIMEOUT = "TIMEOUT"
NOT_FOUND = "NOT_FOUND"
@ -167,6 +171,8 @@ async def fetch(
# Special handling for deleted object
if resp.status_code == 410:
raise ObjectIsGoneError(f"{url} is gone")
elif resp.status_code in [401, 403]:
raise ObjectUnavailableError(f"not allowed to fetch {url}")
elif resp.status_code == 404:
raise ObjectNotFoundError(f"{url} not found")

View File

@ -109,7 +109,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:

View File

@ -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

View File

@ -2,9 +2,11 @@ import hashlib
from datetime import datetime
from functools import cached_property
from typing import Any
from typing import Optional
import pydantic
from bs4 import BeautifulSoup # type: ignore
from loguru import logger
from markdown import markdown
from app import activitypub as ap
@ -74,6 +76,10 @@ class Object:
def tags(self) -> list[ap.RawObject]:
return ap.as_list(self.ap_object.get("tag", []))
@property
def quote_url(self) -> str | None:
return self.ap_object.get("quoteUrl")
@cached_property
def inlined_images(self) -> set[str]:
image_urls: set[str] = set()
@ -278,9 +284,15 @@ class Attachment(BaseModel):
class RemoteObject(Object):
def __init__(self, raw_object: ap.RawObject, actor: Actor):
def __init__(
self,
raw_object: ap.RawObject,
actor: Actor,
quoted_object: Object | None = None,
):
self._raw_object = raw_object
self._actor = actor
self._quoted_object = quoted_object
if self._actor.ap_id != ap.get_actor_id(self._raw_object):
raise ValueError(f"Invalid actor {self._actor.ap_id}")
@ -290,6 +302,7 @@ class RemoteObject(Object):
cls,
raw_object: ap.RawObject,
actor: Actor | None = None,
fetch_quoted_url: bool = True,
):
# Pre-fetch the actor
actor_id = ap.get_actor_id(raw_object)
@ -306,7 +319,17 @@ class RemoteObject(Object):
ap_actor=await ap.fetch(ap.get_actor_id(raw_object)),
)
return cls(raw_object, _actor)
quoted_object: Object | None = None
if quote_url := raw_object.get("quoteUrl"):
try:
quoted_object = await RemoteObject.from_raw_object(
await ap.fetch(quote_url),
fetch_quoted_url=fetch_quoted_url,
)
except Exception:
logger.exception(f"Failed to fetch {quote_url=}")
return cls(raw_object, _actor, quoted_object=quoted_object)
@property
def og_meta(self) -> list[dict[str, Any]] | None:
@ -319,3 +342,9 @@ class RemoteObject(Object):
@property
def actor(self) -> Actor:
return self._actor
@property
def quoted_object(self) -> Optional["RemoteObject"]:
if self._quoted_object:
return self._quoted_object
return None

View File

@ -130,7 +130,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
@ -344,7 +348,7 @@ async def fetch_conversation_root(
is_root: bool = False,
) -> 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
@ -366,7 +370,12 @@ 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):
except (
ap.ObjectNotFoundError,
ap.ObjectIsGoneError,
ap.NotAnObjectError,
ap.ObjectUnavailableError,
):
return await fetch_conversation_root(db_session, obj, is_root=True)
except httpx.HTTPStatusError as http_status_error:
if 400 <= http_status_error.response.status_code < 500:
@ -452,6 +461,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 +479,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))
@ -604,6 +597,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 +1038,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,
@ -1040,20 +1081,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:
@ -1422,7 +1471,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(
@ -1483,8 +1533,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
@ -1525,8 +1576,11 @@ async def _process_note_object(
from_actor: models.Actor,
ro: RemoteObject,
forwarded_by_actor: models.Actor | None = None,
) -> None:
if parent_activity.ap_type not in ["Create", "Read"]:
process_quoted_url: bool = True,
) -> models.InboxObject:
if process_quoted_url and parent_activity.quote_url == ro.ap_id:
logger.info(f"Processing quoted URL for {parent_activity.ap_id}")
elif parent_activity.ap_type not in ["Create", "Read"]:
raise ValueError(f"Unexpected parent activity {parent_activity.ap_id}")
ap_published_at = now()
@ -1567,6 +1621,9 @@ 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),
quoted_inbox_object_id=None,
)
db_session.add(inbox_object)
@ -1590,20 +1647,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
@ -1639,6 +1704,28 @@ async def _process_note_object(
)
db_session.add(notif)
await db_session.flush()
if ro.quote_url and process_quoted_url:
try:
quoted_raw_object = await ap.fetch(ro.quote_url)
quoted_object_actor = await fetch_actor(
db_session, ap.get_actor_id(quoted_raw_object)
)
quoted_ro = RemoteObject(quoted_raw_object, quoted_object_actor)
quoted_inbox_object = await _process_note_object(
db_session,
inbox_object,
from_actor=quoted_object_actor,
ro=quoted_ro,
process_quoted_url=False,
)
inbox_object.quoted_inbox_object_id = quoted_inbox_object.id
except Exception:
logger.exception("Failed to process quoted object")
return inbox_object
async def _handle_vote_answer(
db_session: AsyncSession,
@ -1783,6 +1870,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)
)
@ -1844,6 +1937,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,6 +1948,28 @@ 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:
@ -1867,7 +1983,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
@ -2068,6 +2184,9 @@ 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)
else:
logger.warning(f"Received an unknown {inbox_object.ap_type} object")

View File

@ -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}
@ -195,10 +200,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

View File

@ -115,11 +115,8 @@ async def _get_public_key(db_session: AsyncSession, key_id: str) -> Key:
# 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]:
actor = await ap.fetch(key_id, disable_httpsig=False)
else:
raise
except ap.ObjectUnavailableError:
actor = await ap.fetch(key_id, disable_httpsig=False)
if actor["type"] == "Key":
# The Key is not embedded in the Person

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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,15 +427,27 @@ async def followers(
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
) -> ActivityPubResponse | templates.TemplateResponse:
if is_activitypub_requested(request):
return ActivityPubResponse(
await _build_followx_collection(
db_session=db_session,
model_cls=models.Follower,
path="/followers",
page=page,
next_cursor=next_cursor,
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,
model_cls=models.Follower,
path="/followers",
page=page,
next_cursor=next_cursor,
)
)
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(
@ -460,15 +486,27 @@ async def following(
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
) -> ActivityPubResponse | templates.TemplateResponse:
if is_activitypub_requested(request):
return ActivityPubResponse(
await _build_followx_collection(
db_session=db_session,
model_cls=models.Following,
path="/following",
page=page,
next_cursor=next_cursor,
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,
model_cls=models.Following,
path="/following",
page=page,
next_cursor=next_cursor,
)
)
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 = (

View File

@ -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)
@ -113,6 +113,18 @@ class InboxObject(Base, BaseObject):
uselist=False,
)
quoted_inbox_object_id = Column(
Integer,
ForeignKey("inbox.id", name="fk_quoted_inbox_object_id"),
nullable=True,
)
quoted_inbox_object: Mapped[Optional["InboxObject"]] = relationship(
"InboxObject",
foreign_keys=quoted_inbox_object_id,
remote_side=id,
uselist=False,
)
undone_by_inbox_object_id = Column(Integer, ForeignKey("inbox.id"), nullable=True)
# Link the oubox AP ID to allow undo without any extra query
@ -147,6 +159,12 @@ class InboxObject(Base, BaseObject):
def is_from_inbox(self) -> bool:
return True
@property
def quoted_object(self) -> Optional["InboxObject"]:
if self.quoted_inbox_object_id:
return self.quoted_inbox_object
return None
class OutboxObject(Base, BaseObject):
__tablename__ = "outbox"
@ -160,7 +178,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)
@ -281,6 +299,10 @@ class OutboxObject(Base, BaseObject):
def is_from_outbox(self) -> bool:
return True
@property
def quoted_object(self) -> Optional["InboxObject"]:
return None
class Follower(Base):
__tablename__ = "follower"

View File

@ -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 {
@ -509,3 +519,9 @@ nav.flexbox {
}
}
}
.error-title {
a {
text-decoration: underline;
}
}

View File

@ -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

View File

@ -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>

View File

@ -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 %}

View File

@ -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>

View File

@ -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) %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -295,7 +295,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>
@ -395,6 +395,13 @@
{{ object.content | clean_html(object) | safe }}
</div>
{% if object.quoted_object %}
<div class="ap-object-expanded ap-quoted-object">
{{ display_object(object.quoted_object) }}
</div>
{% endif %}
{% if object.ap_type == "Question" %}
{% set can_vote = is_admin and object.is_from_inbox and not object.is_poll_ended and not object.voted_for_answers %}
{% if can_vote %}
@ -442,10 +449,11 @@
</form>
{% endif %}
{{ display_og_meta(object) }}
{% endif %}
{{ display_og_meta(object) }}
</div>
{% if object.summary %}
</div>

View File

@ -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)

View File

@ -66,7 +66,8 @@ async def external_urls(
tags_hrefs = set()
for tag in ro.tags:
if tag_href := tag.get("href"):
tags_hrefs.add(tag_href)
if tag_href and tag_href not in filter(None, [ro.quote_url]):
tags_hrefs.add(tag_href)
if tag.get("type") == "Mention":
if tag["href"] != LOCAL_ACTOR.ap_id:
mentioned_actor = await fetch_actor(db_session, tag["href"])
@ -81,6 +82,9 @@ async def external_urls(
soup = BeautifulSoup(ro.content, "html5lib")
for link in soup.find_all("a"):
h = link.get("href")
if not h:
continue
ph = urlparse(h)
mimetype, _ = mimetypes.guess_type(h)
if (

View File

@ -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]

View File

@ -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

View File

@ -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

284
poetry.lock generated
View File

@ -197,7 +197,7 @@ python-versions = "~=3.7"
[[package]]
name = "certifi"
version = "2022.6.15"
version = "2022.9.14"
description = "Python package for providing Mozilla's CA Bundle."
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
@ -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
@ -942,7 +942,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 +991,7 @@ python-versions = ">=3.6"
[[package]]
name = "sqlalchemy"
version = "1.4.40"
version = "1.4.41"
description = "Database Abstraction Library"
category = "main"
optional = false
@ -1110,7 +1110,7 @@ python-versions = "*"
[[package]]
name = "types-markdown"
version = "3.4.1"
version = "3.4.2"
description = "Typing stubs for Markdown"
category = "dev"
optional = false
@ -1134,7 +1134,7 @@ python-versions = "*"
[[package]]
name = "types-requests"
version = "2.28.9"
version = "2.28.10"
description = "Typing stubs for requests"
category = "dev"
optional = false
@ -1153,7 +1153,7 @@ python-versions = "*"
[[package]]
name = "types-urllib3"
version = "1.26.23"
version = "1.26.24"
description = "Typing stubs for urllib3"
category = "dev"
optional = false
@ -1204,16 +1204,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 +1228,7 @@ watchmedo = ["PyYAML (>=3.10)"]
[[package]]
name = "watchfiles"
version = "0.16.1"
version = "0.17.0"
description = "Simple, modern and high performance file watching and code reload in python."
category = "main"
optional = false
@ -1275,7 +1275,7 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "be26936c88285524ab781d4130c8a97fc940d70dd0a88bf37c61d7db5bdbd232"
content-hash = "84b3a6dcfc055fb0712c6abbf1bf94d9526eda940c4ddb0bd275664e68a4c3e3"
[metadata.files]
aiosqlite = [
@ -1429,8 +1429,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.14-py3-none-any.whl", hash = "sha256:e232343de1ab72c2aa521b625c80f699e356830fd0e2c620b465b304b17b0516"},
{file = "certifi-2022.9.14.tar.gz", hash = "sha256:36973885b9542e6bd01dea287b2b4b3b21236307c56324fcc3f1160f2d655ed5"},
]
cffi = [
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
@ -1612,40 +1612,47 @@ 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"},
@ -1657,8 +1664,8 @@ humanize = [
]
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"},
@ -2095,8 +2102,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,42 +2122,47 @@ 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.41-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:13e397a9371ecd25573a7b90bd037db604331cf403f5318038c46ee44908c44d"},
{file = "SQLAlchemy-1.4.41-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2d6495f84c4fd11584f34e62f9feec81bf373787b3942270487074e35cbe5330"},
{file = "SQLAlchemy-1.4.41-cp27-cp27m-win32.whl", hash = "sha256:e570cfc40a29d6ad46c9aeaddbdcee687880940a3a327f2c668dd0e4ef0a441d"},
{file = "SQLAlchemy-1.4.41-cp27-cp27m-win_amd64.whl", hash = "sha256:5facb7fd6fa8a7353bbe88b95695e555338fb038ad19ceb29c82d94f62775a05"},
{file = "SQLAlchemy-1.4.41-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f37fa70d95658763254941ddd30ecb23fc4ec0c5a788a7c21034fc2305dab7cc"},
{file = "SQLAlchemy-1.4.41-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:361f6b5e3f659e3c56ea3518cf85fbdae1b9e788ade0219a67eeaaea8a4e4d2a"},
{file = "SQLAlchemy-1.4.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0990932f7cca97fece8017414f57fdd80db506a045869d7ddf2dda1d7cf69ecc"},
{file = "SQLAlchemy-1.4.41-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cd767cf5d7252b1c88fcfb58426a32d7bd14a7e4942497e15b68ff5d822b41ad"},
{file = "SQLAlchemy-1.4.41-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5102fb9ee2c258a2218281adcb3e1918b793c51d6c2b4666ce38c35101bb940e"},
{file = "SQLAlchemy-1.4.41-cp310-cp310-win32.whl", hash = "sha256:2082a2d2fca363a3ce21cfa3d068c5a1ce4bf720cf6497fb3a9fc643a8ee4ddd"},
{file = "SQLAlchemy-1.4.41-cp310-cp310-win_amd64.whl", hash = "sha256:e4b12e3d88a8fffd0b4ca559f6d4957ed91bd4c0613a4e13846ab8729dc5c251"},
{file = "SQLAlchemy-1.4.41-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:90484a2b00baedad361402c257895b13faa3f01780f18f4a104a2f5c413e4536"},
{file = "SQLAlchemy-1.4.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b67fc780cfe2b306180e56daaa411dd3186bf979d50a6a7c2a5b5036575cbdbb"},
{file = "SQLAlchemy-1.4.41-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad2b727fc41c7f8757098903f85fafb4bf587ca6605f82d9bf5604bd9c7cded"},
{file = "SQLAlchemy-1.4.41-cp311-cp311-win32.whl", hash = "sha256:59bdc291165b6119fc6cdbc287c36f7f2859e6051dd923bdf47b4c55fd2f8bd0"},
{file = "SQLAlchemy-1.4.41-cp311-cp311-win_amd64.whl", hash = "sha256:d2e054aed4645f9b755db85bc69fc4ed2c9020c19c8027976f66576b906a74f1"},
{file = "SQLAlchemy-1.4.41-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:4ba7e122510bbc07258dc42be6ed45997efdf38129bde3e3f12649be70683546"},
{file = "SQLAlchemy-1.4.41-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0dcf127bb99458a9d211e6e1f0f3edb96c874dd12f2503d4d8e4f1fd103790b"},
{file = "SQLAlchemy-1.4.41-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e16c2be5cb19e2c08da7bd3a87fed2a0d4e90065ee553a940c4fc1a0fb1ab72b"},
{file = "SQLAlchemy-1.4.41-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5ebeeec5c14533221eb30bad716bc1fd32f509196318fb9caa7002c4a364e4c"},
{file = "SQLAlchemy-1.4.41-cp36-cp36m-win32.whl", hash = "sha256:3e2ef592ac3693c65210f8b53d0edcf9f4405925adcfc031ff495e8d18169682"},
{file = "SQLAlchemy-1.4.41-cp36-cp36m-win_amd64.whl", hash = "sha256:eb30cf008850c0a26b72bd1b9be6730830165ce049d239cfdccd906f2685f892"},
{file = "SQLAlchemy-1.4.41-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:c23d64a0b28fc78c96289ffbd0d9d1abd48d267269b27f2d34e430ea73ce4b26"},
{file = "SQLAlchemy-1.4.41-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8eb8897367a21b578b26f5713833836f886817ee2ffba1177d446fa3f77e67c8"},
{file = "SQLAlchemy-1.4.41-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:14576238a5f89bcf504c5f0a388d0ca78df61fb42cb2af0efe239dc965d4f5c9"},
{file = "SQLAlchemy-1.4.41-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:639e1ae8d48b3c86ffe59c0daa9a02e2bfe17ca3d2b41611b30a0073937d4497"},
{file = "SQLAlchemy-1.4.41-cp37-cp37m-win32.whl", hash = "sha256:0005bd73026cd239fc1e8ccdf54db58b6193be9a02b3f0c5983808f84862c767"},
{file = "SQLAlchemy-1.4.41-cp37-cp37m-win_amd64.whl", hash = "sha256:5323252be2bd261e0aa3f33cb3a64c45d76829989fa3ce90652838397d84197d"},
{file = "SQLAlchemy-1.4.41-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:05f0de3a1dc3810a776275763764bb0015a02ae0f698a794646ebc5fb06fad33"},
{file = "SQLAlchemy-1.4.41-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0002e829142b2af00b4eaa26c51728f3ea68235f232a2e72a9508a3116bd6ed0"},
{file = "SQLAlchemy-1.4.41-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22ff16cedab5b16a0db79f1bc99e46a6ddececb60c396562e50aab58ddb2871c"},
{file = "SQLAlchemy-1.4.41-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccfd238f766a5bb5ee5545a62dd03f316ac67966a6a658efb63eeff8158a4bbf"},
{file = "SQLAlchemy-1.4.41-cp38-cp38-win32.whl", hash = "sha256:58bb65b3274b0c8a02cea9f91d6f44d0da79abc993b33bdedbfec98c8440175a"},
{file = "SQLAlchemy-1.4.41-cp38-cp38-win_amd64.whl", hash = "sha256:ce8feaa52c1640de9541eeaaa8b5fb632d9d66249c947bb0d89dd01f87c7c288"},
{file = "SQLAlchemy-1.4.41-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:199a73c31ac8ea59937cc0bf3dfc04392e81afe2ec8a74f26f489d268867846c"},
{file = "SQLAlchemy-1.4.41-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676d51c9f6f6226ae8f26dc83ec291c088fe7633269757d333978df78d931ab"},
{file = "SQLAlchemy-1.4.41-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:036d8472356e1d5f096c5e0e1a7e0f9182140ada3602f8fff6b7329e9e7cfbcd"},
{file = "SQLAlchemy-1.4.41-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2307495d9e0ea00d0c726be97a5b96615035854972cc538f6e7eaed23a35886c"},
{file = "SQLAlchemy-1.4.41-cp39-cp39-win32.whl", hash = "sha256:9c56e19780cd1344fcd362fd6265a15f48aa8d365996a37fab1495cae8fcd97d"},
{file = "SQLAlchemy-1.4.41-cp39-cp39-win_amd64.whl", hash = "sha256:f5fa526d027d804b1f85cdda1eb091f70bde6fb7d87892f6dd5a48925bc88898"},
{file = "SQLAlchemy-1.4.41.tar.gz", hash = "sha256:0292f70d1797e3c54e862e6f30ae474014648bc9c723e14a2fda730adb0a9791"},
]
sqlalchemy2-stubs = [
{file = "sqlalchemy2-stubs-0.0.2a27.tar.gz", hash = "sha256:f79bce50b7837a2c2374ef4480b41e2b8a8226f313f347dc2a70526a4191db93"},
@ -2177,8 +2189,8 @@ types-cachetools = [
]
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.tar.gz", hash = "sha256:7c5b2346873ae12d548de684a25920009f2446f0ea42ce8c5559eb9a3ac10072"},
{file = "types_Markdown-3.4.2-py3-none-any.whl", hash = "sha256:7dd9fe8bd2645ec984f438b255f42b795507b5cd2a998cc1970ec13f443dc832"},
]
types-pillow = [
{file = "types-Pillow-9.2.1.tar.gz", hash = "sha256:9781104ee2176f680576523fa2a2b83b134957aec6f4d62582cc9e74c93a60b4"},
@ -2186,13 +2198,13 @@ types-pillow = [
]
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.10.tar.gz", hash = "sha256:97d8f40aa1ffe1e58c3726c77d63c182daea9a72d9f1fa2cafdea756b2a19f2c"},
{file = "types_requests-2.28.10-py3-none-any.whl", hash = "sha256:45b485725ed58752f2b23461252f1c1ad9205b884a1e35f786bb295525a3e16a"},
]
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.24.tar.gz", hash = "sha256:a1b3aaea7dda3eb1b51699ee723aadd235488e4dc4648e030f09bc429ecff42f"},
{file = "types_urllib3-1.26.24-py3-none-any.whl", hash = "sha256:cf7918503d02d3576e503bbfb419b0e047c4617653bba09624756ab7175e15c9"},
]
typing-extensions = [
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
@ -2207,22 +2219,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 +2278,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.17.0-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:c7e1ffbd03cbcb46d1b7833e10e7d6b678ab083b4e4b80db06cfff5baca3c93f"},
{file = "watchfiles-0.17.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:539bcdb55a487126776c9d8c011094214d1df3f9a2321a6c0b1583197309405a"},
{file = "watchfiles-0.17.0-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:00e5f307a58752ec1478eeb738863544bde21cc7a2728bd1c216060406bde9c1"},
{file = "watchfiles-0.17.0-cp37-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:92675f379a9d5adbc6a52179f3e39aa56944c6eecb80384608fff2ed2619103a"},
{file = "watchfiles-0.17.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1dd1e3181ad5d83ca35e9147c72e24f39437fcdf570c9cdc532016399fb62957"},
{file = "watchfiles-0.17.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:204950f1d6083539af5c8b7d4f5f8039c3ce36fa692da12d9743448f3199cb15"},
{file = "watchfiles-0.17.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:4056398d8f6d4972fe0918707b59d4cb84470c91d3c37f0e11e5a66c2a598760"},
{file = "watchfiles-0.17.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ffff3418dc753a2aed2d00200a4daeaac295c40458f8012836a65555f288be8b"},
{file = "watchfiles-0.17.0-cp37-abi3-win32.whl", hash = "sha256:b5c334cd3bc88aa4a8a1e08ec9f702b63c947211275defdc2dd79dc037fcb500"},
{file = "watchfiles-0.17.0-cp37-abi3-win_amd64.whl", hash = "sha256:53a2faeb121bc51bb6b960984f46901227e2e2475acc5a8d4c905a600436752d"},
{file = "watchfiles-0.17.0-cp37-abi3-win_arm64.whl", hash = "sha256:58dc3140dcf02a8aa76464a77a093016f10e89306fec21a4814922a64f3e8b9f"},
{file = "watchfiles-0.17.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:adcf15ecc2182ea9d2358c1a8c2b53203c3909484918776929b7bbe205522c0e"},
{file = "watchfiles-0.17.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:afd35a1bd3b9e68efe384ae7538481ae725597feb66f56f4bd23ecdbda726da0"},
{file = "watchfiles-0.17.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad2bdcae4c0f07ca6c090f5a2c30188cc6edba011b45e7c96eb1896648092367"},
{file = "watchfiles-0.17.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:a53cb6c06e5c1f216c792fbb432ce315239d432cb8b68d508547100939ec0399"},
{file = "watchfiles-0.17.0-pp39-pypy39_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6a3d6c699f3ce238dfa90bcef501f331a69b0d9b076f14459ed8eab26ba2f4cf"},
{file = "watchfiles-0.17.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7f4271af86569bdbf131dd5c7c121c45d0ed194f3c88b88326e48a3b6a2db12"},
{file = "watchfiles-0.17.0.tar.gz", hash = "sha256:ae7c57ef920589a40270d5ef3216d693f4e6f8864d8fc8b6cb7885ca98ad2a61"},
]
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},

View File

@ -44,6 +44,7 @@ invoke = "^1.7.1"
boussole = "^2.0.0"
uvicorn = {extras = ["standard"], version = "^0.18.3"}
Brotli = "^1.0.9"
greenlet = "^1.1.3"
[tool.poetry.dev-dependencies]
black = "^22.3.0"

View File

@ -264,7 +264,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 +312,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")

View File

@ -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

View File

@ -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")