3 Commits
17 changed files with 142 additions and 61 deletions
+2 -2
View File
@@ -11,7 +11,7 @@ from repositories.proxy import (
from repositories.proxy import ( from repositories.proxy import (
list_proxies as list_proxy_documents, list_proxies as list_proxy_documents,
) )
from services.proxy.validator import ProxyCodeValidationError, validate_proxy_code from services.mitmproxy.validator import ProxyCodeValidationError, validate_proxy_code
router = APIRouter( router = APIRouter(
prefix="/proxy", prefix="/proxy",
@@ -60,7 +60,7 @@ async def create_proxy_handler(payload: ProxyCreateInput):
return await create_proxy( return await create_proxy(
proxy_port=payload.proxy_port, proxy_port=payload.proxy_port,
name=payload.name, name=payload.name,
service={"address": payload.service.address, "port": payload.service.port}, service={"port": payload.service.port},
code=payload.code, code=payload.code,
) )
+8 -3
View File
@@ -7,13 +7,14 @@ from authlib.jose import jwt # type: ignore
from authlib.jose.errors import JoseError # type: ignore from authlib.jose.errors import JoseError # type: ignore
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from passlib.context import CryptContext from pwdlib import PasswordHash
from pwdlib.hashers.argon2 import Argon2Hasher
from core.config import settings from core.config import settings
from models.auth import AccessTokenPayload, AuthUserDocument from models.auth import AccessTokenPayload, AuthUserDocument
from repositories.auth import get_user_by_username, is_token_revoked from repositories.auth import get_user_by_username, is_token_revoked
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = PasswordHash((Argon2Hasher(),))
bearer_scheme = HTTPBearer(auto_error=False) bearer_scheme = HTTPBearer(auto_error=False)
@@ -41,7 +42,7 @@ def hash_password(password: str) -> str:
def verify_password(plain_password: str, password_hash: str) -> bool: def verify_password(plain_password: str, password_hash: str) -> bool:
return pwd_context.verify(plain_password, password_hash) return pwd_context.check(plain_password, password_hash)
def create_access_token(*, subject: str) -> str: def create_access_token(*, subject: str) -> str:
@@ -53,14 +54,18 @@ def create_access_token(*, subject: str) -> str:
"alg": settings.JWT_ALGORITHM, "alg": settings.JWT_ALGORITHM,
"typ": "JWT", "typ": "JWT",
} }
payload = { payload = {
"sub": subject, "sub": subject,
"jti": str(uuid4()), "jti": str(uuid4()),
"exp": expires_at, "exp": expires_at,
} }
token = jwt.encode(header, payload, settings.JWT_SECRET_KEY) token = jwt.encode(header, payload, settings.JWT_SECRET_KEY)
if isinstance(token, bytes): if isinstance(token, bytes):
return token.decode("utf-8") return token.decode("utf-8")
return token return token
+2 -4
View File
@@ -2,15 +2,13 @@ from integrations.docker.compose import (
find_docker_compose, find_docker_compose,
get_all_services, get_all_services,
get_services, get_services,
get_visible_folders,
) )
from integrations.docker.editor import change_port from integrations.docker.editor import change_port
from integrations.docker.models import DockerService from integrations.docker.models import DockerContainer
__all__ = [ __all__ = [
"DockerService", "DockerContainer",
"find_docker_compose", "find_docker_compose",
"get_visible_folders",
"get_services", "get_services",
"get_all_services", "get_all_services",
"change_port", "change_port",
+11 -11
View File
@@ -2,7 +2,7 @@ from pathlib import Path
import yaml import yaml
from integrations.docker.models import DockerService from integrations.docker.models import DockerContainer
from integrations.docker.ports import _get_service_port from integrations.docker.ports import _get_service_port
COMPOSE_NAMES = ( COMPOSE_NAMES = (
@@ -13,7 +13,7 @@ COMPOSE_NAMES = (
) )
def get_visible_folders(directory: Path) -> list[Path]: def _get_visible_folders(directory: Path) -> list[Path]:
return [ return [
p for p in Path(directory).iterdir() if p.is_dir() and not p.name.startswith(".") p for p in Path(directory).iterdir() if p.is_dir() and not p.name.startswith(".")
] ]
@@ -32,8 +32,8 @@ def find_docker_compose(path: Path) -> tuple[dict, str] | None:
return None return None
def get_services(folder: Path) -> list[DockerService] | None: def get_services(folder: Path) -> list[DockerContainer] | None:
services: list[DockerService] = [] services: list[DockerContainer] = []
compose_data = find_docker_compose(folder) compose_data = find_docker_compose(folder)
if compose_data is None: if compose_data is None:
return None return None
@@ -56,20 +56,20 @@ def get_services(folder: Path) -> list[DockerService] | None:
if not ports: if not ports:
continue continue
services.append({"name": service_name, "ports": ports}) services.append({"name": service_name, "ports": ports, "folder": folder})
return services or None return services
def get_all_services(directory: str | Path) -> list[DockerService] | None: def get_all_services(directory: str | Path) -> list[DockerContainer]:
path = Path(directory) path = Path(directory)
if not path.is_dir(): if not path.is_dir():
return None return []
services: list[DockerService] = [] services: list[DockerContainer] = []
for sub_folder in get_visible_folders(path): for sub_folder in _get_visible_folders(path):
sub_services = get_services(sub_folder) sub_services = get_services(sub_folder)
if sub_services: if sub_services:
services.extend(sub_services) services.extend(sub_services)
return services or None return services
+3 -1
View File
@@ -1,6 +1,8 @@
from pathlib import Path
from typing import TypedDict from typing import TypedDict
class DockerService(TypedDict): class DockerContainer(TypedDict):
name: str name: str
ports: list[int] ports: list[int]
folder: Path
+15
View File
@@ -1,4 +1,6 @@
import sys
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI from fastapi import FastAPI
@@ -8,6 +10,7 @@ from core.config import settings
from core.mongodb import close_mongodb, init_mongodb from core.mongodb import close_mongodb, init_mongodb
from core.redis import close_redis, init_redis from core.redis import close_redis, init_redis
from core.security import hash_password from core.security import hash_password
from integrations.docker import get_all_services
from repositories.auth import ensure_auth_indexes, ensure_single_account from repositories.auth import ensure_auth_indexes, ensure_single_account
from repositories.proxy import ensure_proxy_indexes from repositories.proxy import ensure_proxy_indexes
@@ -22,6 +25,18 @@ async def lifespan(_: FastAPI):
password_hash=hash_password(settings.AUTH_PASSWORD), password_hash=hash_password(settings.AUTH_PASSWORD),
) )
await ensure_proxy_indexes() await ensure_proxy_indexes()
home_dir = str(Path.home())
app.state.services = get_all_services(home_dir) or []
if not app.state.services:
warning = (
"\n" + "!" * 60 + "\n"
" WARNING: No services found in home directory!\n"
f" Scanned: {home_dir}\n"
+ "!" * 60 + "\n"
)
print(warning, file=sys.stderr)
try: try:
yield yield
finally: finally:
-1
View File
@@ -4,5 +4,4 @@ from pydantic import BaseModel, ConfigDict, Field
class ServiceEndpointDocument(BaseModel): class ServiceEndpointDocument(BaseModel):
model_config = ConfigDict(extra="forbid") model_config = ConfigDict(extra="forbid")
address: str = Field(..., description="Upstream service address")
port: int = Field(..., description="Upstream service port") port: int = Field(..., description="Upstream service port")
+1 -2
View File
@@ -4,11 +4,11 @@ from typing import Any
from motor.motor_asyncio import AsyncIOMotorCollection from motor.motor_asyncio import AsyncIOMotorCollection
from pymongo import ReturnDocument from pymongo import ReturnDocument
from services.proxy.models import ServiceConfig
from core.mongodb import get_database from core.mongodb import get_database
from models.proxy import ProxyDocument, ProxyUpdateInput from models.proxy import ProxyDocument, ProxyUpdateInput
from models.service import ServiceEndpointDocument from models.service import ServiceEndpointDocument
from services.mitmproxy.models import ServiceConfig
COLLECTION_NAME = "proxies" COLLECTION_NAME = "proxies"
@@ -47,7 +47,6 @@ async def create_proxy(
name=name, name=name,
code=code, code=code,
service=ServiceEndpointDocument( service=ServiceEndpointDocument(
address=service["address"],
port=service["port"], port=service["port"],
), ),
created_at=now, created_at=now,
+27
View File
@@ -0,0 +1,27 @@
import uuid
from pathlib import Path
from integrations.docker import DockerContainer
from services.mitmproxy.models import ServiceConfig
async def create_proxy(
port: int,
name: str,
service: ServiceConfig,
code: str,
services: list[DockerContainer]
) -> int:
"""
Utility function that will:
- run ``integrations/docker`` utilities to change the old port of original service to a random one
- write ``code`` into ``/tmp/theplug/{random}.py``
- start a subprocess via asyncio
- return the proxy infos (pid, )
"""
path = Path("/tmp/theplug/{random}.py".format(random=str(uuid.uuid4())))
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(code, encoding="utf-8")
return 0
@@ -2,5 +2,4 @@ from typing import TypedDict
class ServiceConfig(TypedDict): class ServiceConfig(TypedDict):
name: str
port: int port: int
-10
View File
@@ -1,10 +0,0 @@
from services.proxy.models import ServiceConfig
async def create_proxy(
port: int,
name: str,
service: ServiceConfig,
code: str
):
return
+1 -1
View File
@@ -29,7 +29,7 @@ services:
start_period: 10s start_period: 10s
mongodb: mongodb:
image: mongo:8 image: mongo:8.2
container_name: theplug-mongodb container_name: theplug-mongodb
restart: unless-stopped restart: unless-stopped
environment: environment:
+2 -1
View File
@@ -7,7 +7,8 @@ dependencies = [
"authlib>=1.6.0", "authlib>=1.6.0",
"fastapi[standard]>=0.135.3", "fastapi[standard]>=0.135.3",
"motor>=3.7.1", "motor>=3.7.1",
"passlib[bcrypt]>=1.7.4", "pwdlib[argon2,bcrypt]>=0.3.0",
"pydantic>=2.12.5",
"pydantic-settings>=2.13.1", "pydantic-settings>=2.13.1",
"pyyaml>=6.0.3", "pyyaml>=6.0.3",
"redis>=7.4.0", "redis>=7.4.0",
+6 -8
View File
@@ -3,7 +3,7 @@ import importlib
from core.security import get_auth_context, require_authenticated_user from core.security import get_auth_context, require_authenticated_user
from models.proxy import ProxyDocument from models.proxy import ProxyDocument
from models.service import ServiceEndpointDocument from models.service import ServiceEndpointDocument
from services.proxy.validator import ProxyCodeValidationError from services.mitmproxy.validator import ProxyCodeValidationError
proxy_router_module = importlib.import_module("api.proxy.router") proxy_router_module = importlib.import_module("api.proxy.router")
@@ -13,14 +13,13 @@ def make_proxy_document(
proxy_port: int, proxy_port: int,
name: str = "svc", name: str = "svc",
code: str = "proxy-code", code: str = "proxy-code",
address: str = "127.0.0.1",
port: int = 9000, port: int = 9000,
) -> ProxyDocument: ) -> ProxyDocument:
return ProxyDocument( return ProxyDocument(
proxy_port=proxy_port, proxy_port=proxy_port,
name=name, name=name,
code=code, code=code,
service=ServiceEndpointDocument(address=address, port=port), service=ServiceEndpointDocument(port=port),
) )
@@ -87,7 +86,7 @@ def test_create_proxy_returns_409_on_port_conflict(client, monkeypatch: object):
"proxy_port": 3671, "proxy_port": 3671,
"name": "new", "name": "new",
"code": "abc", "code": "abc",
"service": {"address": "localhost", "port": 8080}, "service": {"port": 8080},
}, },
) )
@@ -117,7 +116,6 @@ def test_create_proxy_returns_created_document(client, monkeypatch: object):
proxy_port=proxy_port, proxy_port=proxy_port,
name=name, name=name,
code=code, code=code,
address=str(service["address"]),
port=int(service["port"]), port=int(service["port"]),
) )
@@ -142,7 +140,7 @@ def test_create_proxy_returns_created_document(client, monkeypatch: object):
"proxy_port": 4040, "proxy_port": 4040,
"name": "edge", "name": "edge",
"code": "def proxy_handler():\n return 'ok'\n", "code": "def proxy_handler():\n return 'ok'\n",
"service": {"address": "service.local", "port": 8181}, "service": {"port": 8181},
}, },
) )
@@ -151,7 +149,7 @@ def test_create_proxy_returns_created_document(client, monkeypatch: object):
assert captured == { assert captured == {
"proxy_port": 4040, "proxy_port": 4040,
"name": "edge", "name": "edge",
"service": {"address": "service.local", "port": 8181}, "service": {"port": 8181},
"code": "def proxy_handler():\n return 'ok'\n", "code": "def proxy_handler():\n return 'ok'\n",
} }
@@ -208,7 +206,7 @@ def test_create_proxy_returns_422_when_code_validation_fails(
"proxy_port": 4000, "proxy_port": 4000,
"name": "broken", "name": "broken",
"code": "invalid-code", "code": "invalid-code",
"service": {"address": "service.local", "port": 9000}, "service": {"port": 9000},
}, },
) )
Generated
+64 -16
View File
@@ -32,6 +32,49 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
] ]
[[package]]
name = "argon2-cffi"
version = "25.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "argon2-cffi-bindings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" },
]
[[package]]
name = "argon2-cffi-bindings"
version = "25.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" },
{ url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" },
{ url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" },
{ url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" },
{ url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" },
{ url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" },
{ url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" },
{ url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" },
{ url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" },
{ url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" },
{ url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" },
{ url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" },
{ url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" },
{ url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" },
{ url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" },
{ url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" },
{ url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" },
{ url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" },
{ url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" },
{ url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" },
]
[[package]] [[package]]
name = "authlib" name = "authlib"
version = "1.6.9" version = "1.6.9"
@@ -494,20 +537,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
] ]
[[package]]
name = "passlib"
version = "1.7.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" },
]
[package.optional-dependencies]
bcrypt = [
{ name = "bcrypt" },
]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.6.0" version = "1.6.0"
@@ -517,6 +546,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
] ]
[[package]]
name = "pwdlib"
version = "0.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/41/a7c0d8a003c36ce3828ae3ed0391fe6a15aad65f082dbd6bec817ea95c0b/pwdlib-0.3.0.tar.gz", hash = "sha256:6ca30f9642a1467d4f5d0a4d18619de1c77f17dfccb42dd200b144127d3c83fc", size = 215810, upload-time = "2025-10-25T12:44:24.395Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/0c/9086a357d02a050fbb3270bf5043ac284dbfb845670e16c9389a41defc9e/pwdlib-0.3.0-py3-none-any.whl", hash = "sha256:f86c15c138858c09f3bba0a10984d4f9178158c55deaa72eac0210849b1a140d", size = 8633, upload-time = "2025-10-25T12:44:23.406Z" },
]
[package.optional-dependencies]
argon2 = [
{ name = "argon2-cffi" },
]
bcrypt = [
{ name = "bcrypt" },
]
[[package]] [[package]]
name = "pycparser" name = "pycparser"
version = "3.0" version = "3.0"
@@ -853,7 +899,8 @@ dependencies = [
{ name = "authlib" }, { name = "authlib" },
{ name = "fastapi", extra = ["standard"] }, { name = "fastapi", extra = ["standard"] },
{ name = "motor" }, { name = "motor" },
{ name = "passlib", extra = ["bcrypt"] }, { name = "pwdlib", extra = ["argon2", "bcrypt"] },
{ name = "pydantic" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "redis" }, { name = "redis" },
@@ -870,7 +917,8 @@ requires-dist = [
{ name = "authlib", specifier = ">=1.6.0" }, { name = "authlib", specifier = ">=1.6.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.135.3" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.135.3" },
{ name = "motor", specifier = ">=3.7.1" }, { name = "motor", specifier = ">=3.7.1" },
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" }, { name = "pwdlib", extras = ["argon2", "bcrypt"], specifier = ">=0.3.0" },
{ name = "pydantic", specifier = ">=2.12.5" },
{ name = "pydantic-settings", specifier = ">=2.13.1" }, { name = "pydantic-settings", specifier = ">=2.13.1" },
{ name = "pyyaml", specifier = ">=6.0.3" }, { name = "pyyaml", specifier = ">=6.0.3" },
{ name = "redis", specifier = ">=7.4.0" }, { name = "redis", specifier = ">=7.4.0" },