start oauth
This commit is contained in:
parent
aeb044d1a0
commit
e4cd62d741
14 changed files with 283 additions and 46 deletions
18
.gitignore
vendored
18
.gitignore
vendored
|
@ -1,9 +1,9 @@
|
|||
__pycache__/
|
||||
.pytest_cache/
|
||||
.env
|
||||
node_modules
|
||||
build
|
||||
.svelte-kit
|
||||
package
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
.env
|
||||
node_modules
|
||||
build
|
||||
.svelte-kit
|
||||
package
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
from functools import wraps
|
||||
|
||||
from quart import g
|
||||
|
||||
from foxnouns.exceptions import ErrorCode, ForbiddenError
|
||||
|
||||
|
||||
def require_auth(*, scope: str | None = None):
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
if "user" not in g or "token" not in g:
|
||||
raise ForbiddenError("Not authenticated", type=ErrorCode.Forbidden)
|
||||
|
||||
if scope and not g.token.has_scope(scope):
|
||||
raise ForbiddenError(
|
||||
f"Missing scope '{scope}'", type=ErrorCode.MissingScope
|
||||
)
|
||||
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
|
@ -1,6 +1,13 @@
|
|||
from .v2.auth import bp as auth_blueprint
|
||||
from .v2.auth.discord import bp as discord_auth_blueprint
|
||||
from .v2.members import bp as members_blueprint
|
||||
from .v2.meta import bp as meta_blueprint
|
||||
from .v2.users import bp as users_blueprint
|
||||
from .v2.auth.discord import bp as discord_auth_blueprint
|
||||
|
||||
__all__ = [users_blueprint, members_blueprint, meta_blueprint, discord_auth_blueprint]
|
||||
__all__ = [
|
||||
users_blueprint,
|
||||
members_blueprint,
|
||||
meta_blueprint,
|
||||
discord_auth_blueprint,
|
||||
auth_blueprint,
|
||||
]
|
||||
|
|
|
@ -1,27 +1,50 @@
|
|||
from datetime import datetime
|
||||
|
||||
from authlib.integrations.httpx_client import AsyncOAuth2Client
|
||||
from pydantic import BaseModel, Field
|
||||
from quart import Blueprint
|
||||
from quart_schema import validate_response
|
||||
|
||||
from foxnouns.settings import BASE_DOMAIN
|
||||
from foxnouns import settings
|
||||
from foxnouns.db import redis
|
||||
from foxnouns.db.redis import discord_state_key
|
||||
from foxnouns.models.user import SelfUserModel
|
||||
|
||||
|
||||
bp = Blueprint("auth_v2", __name__)
|
||||
|
||||
|
||||
def discord_oauth_client(*, state: str | None = None):
|
||||
"""Returns a new Discord OAuth client with the given state.
|
||||
It is the caller's responsibility to check whether the client ID and client secret are set."""
|
||||
|
||||
return AsyncOAuth2Client(
|
||||
settings.DISCORD_CLIENT_ID,
|
||||
settings.DISCORD_CLIENT_SECRET,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
class URLsResponse(BaseModel):
|
||||
discord: str | None = Field(default=None)
|
||||
google: str | None = Field(default=None)
|
||||
tumblr: str | None = Field(default=None)
|
||||
|
||||
|
||||
@bp.post("/api/v2/auth/urls", host=BASE_DOMAIN)
|
||||
@bp.post("/api/v2/auth/urls", host=settings.BASE_DOMAIN)
|
||||
@validate_response(URLsResponse, 200)
|
||||
async def urls():
|
||||
# TODO: build authorization URLs + callback URLs, store state in Redis
|
||||
raise NotImplementedError()
|
||||
if settings.DISCORD_CLIENT_ID and settings.DISCORD_CLIENT_SECRET:
|
||||
client = discord_oauth_client()
|
||||
discord_url, state = client.create_authorization_url(
|
||||
"https://discord.com/api/oauth2/authorize"
|
||||
)
|
||||
|
||||
await redis.set(
|
||||
discord_state_key(state), state, ex=1800
|
||||
) # Expire after 30 minutes
|
||||
|
||||
# TODO: return the other OAuth URLs
|
||||
return URLsResponse(discord=discord_url)
|
||||
|
||||
|
||||
class OAuthCallbackRequest(BaseModel):
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
from quart import Blueprint
|
||||
from quart_schema import validate_request, validate_response
|
||||
|
||||
from foxnouns.settings import BASE_DOMAIN
|
||||
from foxnouns import settings
|
||||
from foxnouns.decorators import require_config_key
|
||||
|
||||
from . import BaseCallbackResponse, OAuthCallbackRequest
|
||||
|
||||
bp = Blueprint("discord_v2", __name__)
|
||||
|
||||
|
||||
@bp.post("/api/v2/auth/discord/callback", host=BASE_DOMAIN)
|
||||
@bp.post("/api/v2/auth/discord/callback", host=settings.BASE_DOMAIN)
|
||||
@require_config_key(keys=[settings.DISCORD_CLIENT_ID, settings.DISCORD_CLIENT_SECRET])
|
||||
@validate_request(OAuthCallbackRequest)
|
||||
@validate_response(BaseCallbackResponse)
|
||||
async def discord_callback(data: OAuthCallbackRequest):
|
||||
|
|
|
@ -3,10 +3,10 @@ from quart import Blueprint, g
|
|||
from quart_schema import validate_request, validate_response
|
||||
|
||||
from foxnouns import tasks
|
||||
from foxnouns.auth import require_auth
|
||||
from foxnouns.db import Member
|
||||
from foxnouns.db.aio import async_session
|
||||
from foxnouns.db.util import user_from_ref
|
||||
from foxnouns.decorators import require_auth
|
||||
from foxnouns.exceptions import ErrorCode, NotFoundError
|
||||
from foxnouns.models.member import FullMemberModel, MemberPatchModel
|
||||
from foxnouns.settings import BASE_DOMAIN
|
||||
|
|
|
@ -4,11 +4,11 @@ from quart_schema import validate_request, validate_response
|
|||
from sqlalchemy import select
|
||||
|
||||
from foxnouns import tasks
|
||||
from foxnouns.auth import require_auth
|
||||
from foxnouns.db import User
|
||||
from foxnouns.db.aio import async_session
|
||||
from foxnouns.db.snowflake import Snowflake
|
||||
from foxnouns.db.util import create_token, generate_token, is_self, user_from_ref
|
||||
from foxnouns.decorators import require_auth
|
||||
from foxnouns.exceptions import ErrorCode, NotFoundError
|
||||
from foxnouns.models import BasePatchModel
|
||||
from foxnouns.models.user import SelfUserModel, UserModel, check_username
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from .base import Base
|
||||
from .member import Member
|
||||
from .redis import redis
|
||||
from .user import AuthMethod, FediverseApp, Token, User
|
||||
|
||||
__all__ = [Base, User, Token, AuthMethod, FediverseApp, Member]
|
||||
__all__ = [Base, User, Token, AuthMethod, FediverseApp, Member, redis]
|
||||
|
|
|
@ -3,3 +3,7 @@ from redis import asyncio as aioredis
|
|||
from foxnouns.settings import REDIS_URL
|
||||
|
||||
redis = aioredis.from_url(REDIS_URL)
|
||||
|
||||
|
||||
def discord_state_key(state: str) -> str:
|
||||
return f"discord-state:{state}"
|
||||
|
|
45
foxnouns/decorators.py
Normal file
45
foxnouns/decorators.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
from functools import wraps
|
||||
from typing import Any
|
||||
|
||||
from quart import g
|
||||
|
||||
from foxnouns.exceptions import ErrorCode, ForbiddenError, UnsupportedEndpointError
|
||||
|
||||
|
||||
def require_auth(*, scope: str | None = None):
|
||||
"""Decorator that requires a token with the given scopes.
|
||||
If no token is given or the required scopes aren't set on it, execution is aborted."""
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
if "user" not in g or "token" not in g:
|
||||
raise ForbiddenError("Not authenticated", type=ErrorCode.Forbidden)
|
||||
|
||||
if scope and not g.token.has_scope(scope):
|
||||
raise ForbiddenError(
|
||||
f"Missing scope '{scope}'", type=ErrorCode.MissingScope
|
||||
)
|
||||
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def require_config_key(*, keys: list[Any]):
|
||||
"""Decorator that requires one or more config keys to be set.
|
||||
If any of them are None, execution is aborted."""
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
for key in keys:
|
||||
if not key:
|
||||
raise UnsupportedEndpointError()
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
|
@ -80,3 +80,14 @@ class ForbiddenError(ExpectedError):
|
|||
def __init__(self, msg: str, type=ErrorCode.Forbidden):
|
||||
self.type = type
|
||||
super().__init__(msg, type)
|
||||
|
||||
|
||||
class UnsupportedEndpointError(ExpectedError):
|
||||
status_code = 404
|
||||
|
||||
def __init__(self):
|
||||
self.type = ErrorCode.NotFound
|
||||
super().__init__(
|
||||
"Endpoint is not supported on this instance",
|
||||
type=ErrorCode.NotFound,
|
||||
)
|
||||
|
|
|
@ -27,6 +27,10 @@ with env.prefixed("MINIO_"):
|
|||
"REGION": env("REGION", "auto"),
|
||||
}
|
||||
|
||||
# Discord OAuth credentials. If these are not set the Discord OAuth endpoints will not work.
|
||||
DISCORD_CLIENT_ID = env("DISCORD_CLIENT_ID", None)
|
||||
DISCORD_CLIENT_SECRET = env("DISCORD_CLIENT_SECRET", None)
|
||||
|
||||
# The base domain the API is served on. This must be set.
|
||||
BASE_DOMAIN = env("BASE_DOMAIN")
|
||||
# The base domain for short URLs.
|
||||
|
|
164
poetry.lock
generated
164
poetry.lock
generated
|
@ -59,6 +59,27 @@ files = [
|
|||
{file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.3.0"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"},
|
||||
{file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
idna = ">=2.8"
|
||||
sniffio = ">=1.1"
|
||||
|
||||
[package.extras]
|
||||
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
||||
trio = ["trio (>=0.23)"]
|
||||
|
||||
[[package]]
|
||||
name = "argon2-cffi"
|
||||
version = "23.1.0"
|
||||
|
@ -188,6 +209,21 @@ async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""}
|
|||
docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
|
||||
test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "authlib"
|
||||
version = "1.3.0"
|
||||
description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "Authlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:9637e4de1fb498310a56900b3e2043a206b03cb11c05422014b0302cbc814be3"},
|
||||
{file = "Authlib-1.3.0.tar.gz", hash = "sha256:959ea62a5b7b5123c5059758296122b57cd2585ae2ed1c0622c21b371ffdae06"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = "*"
|
||||
|
||||
[[package]]
|
||||
name = "billiard"
|
||||
version = "4.2.0"
|
||||
|
@ -425,6 +461,61 @@ files = [
|
|||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "42.0.5"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-win32.whl", hash = "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-win32.whl", hash = "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-win_amd64.whl", hash = "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400"},
|
||||
{file = "cryptography-42.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8"},
|
||||
{file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2"},
|
||||
{file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c"},
|
||||
{file = "cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576"},
|
||||
{file = "cryptography-42.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6"},
|
||||
{file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e"},
|
||||
{file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac"},
|
||||
{file = "cryptography-42.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd"},
|
||||
{file = "cryptography-42.0.5.tar.gz", hash = "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
|
||||
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
|
||||
nox = ["nox"]
|
||||
pep8test = ["check-sdist", "click", "mypy", "ruff"]
|
||||
sdist = ["build"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
||||
test-randomorder = ["pytest-randomly"]
|
||||
|
||||
[[package]]
|
||||
name = "environs"
|
||||
version = "11.0.0"
|
||||
|
@ -581,6 +672,53 @@ files = [
|
|||
{file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.5"
|
||||
description = "A minimal low-level HTTP client."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
|
||||
{file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
h11 = ">=0.13,<0.15"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (>=1.0.0,<2.0.0)"]
|
||||
trio = ["trio (>=0.22.0,<0.26.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.27.0"
|
||||
description = "The next generation HTTP client."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
|
||||
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = "*"
|
||||
certifi = "*"
|
||||
httpcore = ">=1.0.0,<2.0.0"
|
||||
idna = "*"
|
||||
sniffio = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (>=1.0.0,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "hypercorn"
|
||||
version = "0.16.0"
|
||||
|
@ -617,6 +755,18 @@ files = [
|
|||
{file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
|
||||
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
|
@ -1304,6 +1454,18 @@ files = [
|
|||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
description = "Sniff out which async library your code is running under"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.28"
|
||||
|
@ -1513,4 +1675,4 @@ h11 = ">=0.9.0,<1"
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "3013b1d9175f34895133e708e35dea7d3401a07b5869c857e30d0aac41ce7add"
|
||||
content-hash = "b3f536cfeeaa31d6df0ebf4debcffaf80492aca3d4f97a44ef1aa4a62e42b3eb"
|
||||
|
|
|
@ -26,6 +26,8 @@ quart-cors = "^0.7.0"
|
|||
minio = "^7.2.5"
|
||||
pyvips = "^2.2.2"
|
||||
redis = "^5.0.3"
|
||||
authlib = "^1.3.0"
|
||||
httpx = "^0.27.0"
|
||||
|
||||
[tool.poetry.group.dev]
|
||||
optional = true
|
||||
|
|
Loading…
Reference in a new issue