mirror of
https://github.com/suitenumerique/docs.git
synced 2026-04-26 01:25:05 +02:00
Compare commits
11 Commits
config/inc
...
hack2025/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cb7aeb7ec | ||
|
|
23860065e1 | ||
|
|
f459c56121 | ||
|
|
4a81e1526e | ||
|
|
abcd61cf2f | ||
|
|
c1a591fb4f | ||
|
|
83d8478b5d | ||
|
|
6bd136c76e | ||
|
|
e929fcc682 | ||
|
|
fa819bc1ff | ||
|
|
43e529da2a |
41
.github/workflows/docker-hub.yml
vendored
41
.github/workflows/docker-hub.yml
vendored
@@ -5,13 +5,7 @@ on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'ci/trivy-fails'
|
||||
- 'do-not-merge/hackathon-2025'
|
||||
|
||||
env:
|
||||
DOCKER_USER: 1001:127
|
||||
@@ -31,7 +25,6 @@ jobs:
|
||||
images: lasuite/impress-backend
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
|
||||
-
|
||||
name: Run trivy scan
|
||||
@@ -43,10 +36,10 @@ jobs:
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
target: backend-production
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -64,7 +57,6 @@ jobs:
|
||||
images: lasuite/impress-frontend
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
|
||||
-
|
||||
name: Run trivy scan
|
||||
@@ -76,13 +68,13 @@ jobs:
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
file: ./src/frontend/Dockerfile
|
||||
target: frontend-production
|
||||
build-args: |
|
||||
DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
PUBLISH_AS_MIT=false
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -100,7 +92,6 @@ jobs:
|
||||
images: lasuite/impress-y-provider
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
|
||||
-
|
||||
name: Run trivy scan
|
||||
@@ -112,11 +103,34 @@ jobs:
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
file: ./src/frontend/servers/y-provider/Dockerfile
|
||||
target: y-provider
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
build-and-push-mcp-server:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: lasuite/impress-mcp-server
|
||||
- name: Login to DockerHub
|
||||
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
context: ./src/mcp_server
|
||||
file: ./src/mcp_server/Dockerfile
|
||||
build-args: |
|
||||
DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -125,7 +139,6 @@ jobs:
|
||||
- build-and-push-frontend
|
||||
- build-and-push-backend
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- uses: numerique-gouv/action-argocd-webhook-notification@main
|
||||
id: notify
|
||||
|
||||
11
bin/Tiltfile
11
bin/Tiltfile
@@ -39,10 +39,19 @@ docker_build(
|
||||
]
|
||||
)
|
||||
|
||||
docker_build(
|
||||
'localhost:5001/impress-mcp-server:latest',
|
||||
context='../src/mcp_server',
|
||||
dockerfile='../src/mcp_server/Dockerfile',
|
||||
)
|
||||
|
||||
k8s_resource('impress-docs-backend-migrate', resource_deps=['postgres-postgresql'])
|
||||
k8s_resource('impress-docs-backend-createsuperuser', resource_deps=['impress-docs-backend-migrate'])
|
||||
k8s_resource('impress-docs-backend', resource_deps=['impress-docs-backend-migrate'])
|
||||
k8s_yaml(local('cd ../src/helm && helmfile -n impress -e dev template .'))
|
||||
|
||||
# helmfile in docker mount the current working directory and the helmfile.yaml
|
||||
# requires the keycloak config in another directory
|
||||
k8s_yaml(local('cd .. && helmfile -n impress -e ${DEV_ENV:-dev} template --file ./src/helm/helmfile.yaml'))
|
||||
|
||||
migration = '''
|
||||
set -eu
|
||||
|
||||
@@ -25,13 +25,15 @@ from django.utils.translation import gettext_lazy as _
|
||||
import requests
|
||||
import rest_framework as drf
|
||||
from botocore.exceptions import ClientError
|
||||
from knox.auth import TokenAuthentication
|
||||
from lasuite.malware_detection import malware_detection
|
||||
from lasuite.oidc_resource_server.authentication import ResourceServerAuthentication
|
||||
from rest_framework import filters, status, viewsets
|
||||
from rest_framework import response as drf_response
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.throttling import UserRateThrottle
|
||||
|
||||
from core import authentication, enums, models
|
||||
from core import authentication, enums, models, utils as core_utils
|
||||
from core.services.ai_services import AIService
|
||||
from core.services.collaboration_services import CollaborationService
|
||||
from core.utils import extract_attachments, filter_descendants
|
||||
@@ -430,9 +432,7 @@ class DocumentViewSet(
|
||||
ordering = ["-updated_at"]
|
||||
ordering_fields = ["created_at", "updated_at", "title"]
|
||||
pagination_class = Pagination
|
||||
permission_classes = [
|
||||
permissions.DocumentAccessPermission,
|
||||
]
|
||||
permission_classes = [permissions.DocumentAccessPermission]
|
||||
queryset = models.Document.objects.all()
|
||||
serializer_class = serializers.DocumentSerializer
|
||||
ai_translate_serializer_class = serializers.AITranslateSerializer
|
||||
@@ -669,10 +669,14 @@ class DocumentViewSet(
|
||||
return self.get_response_for_queryset(queryset)
|
||||
|
||||
@drf.decorators.action(
|
||||
authentication_classes=[authentication.ServerToServerAuthentication],
|
||||
authentication_classes=[
|
||||
authentication.ServerToServerAuthentication,
|
||||
ResourceServerAuthentication,
|
||||
TokenAuthentication,
|
||||
],
|
||||
detail=False,
|
||||
methods=["post"],
|
||||
permission_classes=[],
|
||||
permission_classes=[permissions.IsAuthenticated],
|
||||
url_path="create-for-owner",
|
||||
)
|
||||
@transaction.atomic
|
||||
@@ -1349,6 +1353,25 @@ class DocumentViewSet(
|
||||
}
|
||||
|
||||
return drf.response.Response(body, status=drf.status.HTTP_200_OK)
|
||||
|
||||
@drf.decorators.action(detail=True, methods=["get"], url_path="content")
|
||||
def content(self, request, *args, **kwargs):
|
||||
"""
|
||||
Get the content of a document
|
||||
"""
|
||||
|
||||
document = self.get_object()
|
||||
|
||||
# content_type = response.headers.get("Content-Type", "")
|
||||
|
||||
base64_yjs_content = document.content
|
||||
content = core_utils.base64_yjs_to_markdown(base64_yjs_content)
|
||||
|
||||
body = {
|
||||
"content": content,
|
||||
}
|
||||
|
||||
return drf.response.Response(body, status=drf.status.HTTP_200_OK)
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=True,
|
||||
|
||||
@@ -6,6 +6,15 @@ from rest_framework.authentication import BaseAuthentication
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
|
||||
|
||||
class AuthenticatedServer:
|
||||
"""
|
||||
Simple class to represent an authenticated server to be used along the
|
||||
IsAuthenticated permission.
|
||||
"""
|
||||
|
||||
is_authenticated = True
|
||||
|
||||
|
||||
class ServerToServerAuthentication(BaseAuthentication):
|
||||
"""
|
||||
Custom authentication class for server-to-server requests.
|
||||
@@ -39,13 +48,16 @@ class ServerToServerAuthentication(BaseAuthentication):
|
||||
# Validate token format and existence
|
||||
auth_parts = auth_header.split(" ")
|
||||
if len(auth_parts) != 2 or auth_parts[0] != self.TOKEN_TYPE:
|
||||
raise AuthenticationFailed("Invalid authorization header.")
|
||||
# Do not raise here to leave the door open for other authentication methods
|
||||
return None
|
||||
|
||||
token = auth_parts[1]
|
||||
if token not in settings.SERVER_TO_SERVER_API_TOKENS:
|
||||
raise AuthenticationFailed("Invalid server-to-server token.")
|
||||
# Do not raise here to leave the door open for other authentication methods
|
||||
return None
|
||||
|
||||
# Authentication is successful, but no user is authenticated
|
||||
# Authentication is successful
|
||||
return AuthenticatedServer(), token
|
||||
|
||||
def authenticate_header(self, request):
|
||||
"""Return the WWW-Authenticate header value."""
|
||||
|
||||
@@ -839,6 +839,7 @@ class Document(MP_Node, BaseModel):
|
||||
"children_list": can_get,
|
||||
"children_create": can_update and user.is_authenticated,
|
||||
"collaboration_auth": can_get,
|
||||
"content": can_get,
|
||||
"cors_proxy": can_get,
|
||||
"descendants": can_get,
|
||||
"destroy": is_owner,
|
||||
|
||||
131
src/backend/core/tests/test_user_token_api.py
Normal file
131
src/backend/core/tests/test_user_token_api.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
Test user_token API endpoints in the impress core app.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from knox.models import get_token_model
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
AuthToken = get_token_model()
|
||||
|
||||
def test_api_user_token_list_anonymous(client):
|
||||
"""Anonymous users should not be allowed to list user tokens."""
|
||||
response = client.get("/api/v1.0/user-tokens/")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
|
||||
def test_api_user_token_list_authenticated(client):
|
||||
"""
|
||||
Authenticated users should be able to list their own tokens.
|
||||
Tokens are identified by digest, and include created/expiry.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
# Knox creates a token instance and a character string token key.
|
||||
# The create method returns a tuple: (instance, token_key_string)
|
||||
token_instance_1, _ = AuthToken.objects.create(user=user)
|
||||
AuthToken.objects.create(user=user) # Another token for the same user
|
||||
AuthToken.objects.create(user=factories.UserFactory()) # Token for a different user
|
||||
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get("/api/v1.0/user-tokens/")
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content) == 2
|
||||
|
||||
# Check that the response contains the digests of the tokens created for the user
|
||||
response_token_digests = {item["digest"] for item in content}
|
||||
assert token_instance_1.digest in response_token_digests
|
||||
|
||||
# Ensure the token_key is not listed
|
||||
for item in content:
|
||||
assert "token_key" not in item
|
||||
assert "digest" in item
|
||||
assert "created" in item
|
||||
assert "expiry" in item
|
||||
|
||||
|
||||
def test_api_user_token_create_anonymous(client):
|
||||
"""Anonymous users should not be allowed to create user tokens."""
|
||||
# The create endpoint does not take any parameters as per TokenCreateSerializer
|
||||
# (user is implicit, other fields are read_only)
|
||||
response = client.post("/api/v1.0/user-tokens/", data={})
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
|
||||
def test_api_user_token_create_authenticated(client):
|
||||
"""
|
||||
Authenticated users should be able to create a new token.
|
||||
The token key should be returned in the response upon creation.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client.force_login(user)
|
||||
|
||||
# The create endpoint does not take any parameters as per TokenCreateSerializer
|
||||
response = client.post("/api/v1.0/user-tokens/", data={})
|
||||
assert response.status_code == 201
|
||||
content = response.json()
|
||||
|
||||
# Based on TokenCreateSerializer, these fields should be in the response
|
||||
assert "token_key" in content
|
||||
assert "digest" in content
|
||||
assert "created" in content
|
||||
assert "expiry" in content
|
||||
assert len(content["token_key"]) > 0 # Knox token key should be non-empty
|
||||
|
||||
# Verify the token was actually created in the database for the user
|
||||
assert AuthToken.objects.filter(user=user, digest=content["digest"]).exists()
|
||||
|
||||
def test_api_user_token_destroy_anonymous(client):
|
||||
"""Anonymous users should not be allowed to delete user tokens."""
|
||||
user = factories.UserFactory()
|
||||
token_instance, _ = AuthToken.objects.create(user=user)
|
||||
response = client.delete(f"/api/v1.0/user-tokens/{token_instance.digest}/")
|
||||
assert response.status_code == 403
|
||||
assert AuthToken.objects.filter(digest=token_instance.digest).exists()
|
||||
|
||||
|
||||
def test_api_user_token_destroy_authenticated_own_token(client):
|
||||
"""Authenticated users should be able to delete their own tokens."""
|
||||
user = factories.UserFactory()
|
||||
token_instance, _ = AuthToken.objects.create(user=user)
|
||||
|
||||
client.force_login(user)
|
||||
|
||||
response = client.delete(f"/api/v1.0/user-tokens/{token_instance.digest}/")
|
||||
assert response.status_code == 204
|
||||
assert not AuthToken.objects.filter(digest=token_instance.digest).exists()
|
||||
|
||||
|
||||
def test_api_user_token_destroy_authenticated_other_user_token(client):
|
||||
"""Authenticated users should not be able to delete other users' tokens."""
|
||||
user = factories.UserFactory()
|
||||
other_user = factories.UserFactory()
|
||||
other_user_token_instance, _ = AuthToken.objects.create(user=other_user)
|
||||
|
||||
client.force_login(user) # Log in as 'user'
|
||||
|
||||
response = client.delete(f"/api/v1.0/user-tokens/{other_user_token_instance.digest}/")
|
||||
# The default behavior for a non-found or non-permissioned item in DestroyModelMixin
|
||||
# when the queryset is filtered (as in get_queryset) is often a 404.
|
||||
assert response.status_code == 404
|
||||
assert AuthToken.objects.filter(digest=other_user_token_instance.digest).exists()
|
||||
|
||||
|
||||
def test_api_user_token_destroy_non_existent_token(client):
|
||||
"""Attempting to delete a non-existent token should result in a 404."""
|
||||
user = factories.UserFactory()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.delete("/api/v1.0/user-tokens/nonexistentdigest/")
|
||||
assert response.status_code == 404
|
||||
@@ -4,15 +4,22 @@ from django.conf import settings
|
||||
from django.urls import include, path, re_path
|
||||
|
||||
from lasuite.oidc_login.urls import urlpatterns as oidc_urls
|
||||
from lasuite.oidc_resource_server.urls import urlpatterns as resource_server_urls
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from core.api import viewsets
|
||||
from core.user_token import viewsets as user_token_viewsets
|
||||
|
||||
# - Main endpoints
|
||||
router = DefaultRouter()
|
||||
router.register("templates", viewsets.TemplateViewSet, basename="templates")
|
||||
router.register("documents", viewsets.DocumentViewSet, basename="documents")
|
||||
router.register("users", viewsets.UserViewSet, basename="users")
|
||||
router.register(
|
||||
"user-tokens",
|
||||
user_token_viewsets.UserTokenViewset,
|
||||
basename="user_tokens",
|
||||
)
|
||||
|
||||
# - Routes nested under a document
|
||||
document_related_router = DefaultRouter()
|
||||
@@ -44,6 +51,7 @@ urlpatterns = [
|
||||
[
|
||||
*router.urls,
|
||||
*oidc_urls,
|
||||
*resource_server_urls,
|
||||
re_path(
|
||||
r"^documents/(?P<resource_id>[0-9a-z-]*)/",
|
||||
include(document_related_router.urls),
|
||||
|
||||
0
src/backend/core/user_token/__init__.py
Normal file
0
src/backend/core/user_token/__init__.py
Normal file
27
src/backend/core/user_token/serializers.py
Normal file
27
src/backend/core/user_token/serializers.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from knox.models import get_token_model
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class TokenReadSerializer(serializers.ModelSerializer):
|
||||
"""Serialize token for list purpose."""
|
||||
|
||||
class Meta:
|
||||
model = get_token_model()
|
||||
fields = ["digest", "created", "expiry"]
|
||||
read_only_fields = ["digest", "created", "expiry"]
|
||||
|
||||
|
||||
class TokenCreateSerializer(serializers.ModelSerializer):
|
||||
"""Serialize token for creation purpose."""
|
||||
|
||||
class Meta:
|
||||
model = get_token_model()
|
||||
fields = ["user", "digest", "token_key", "created", "expiry"]
|
||||
read_only_fields = ["digest", "token_key", "created", "expiry"]
|
||||
extra_kwargs = {"user": {"write_only": True}}
|
||||
|
||||
def create(self, validated_data):
|
||||
"""The default knox token create manager returns a tuple."""
|
||||
instance, token = super().create(validated_data)
|
||||
instance.token_key = token # warning do not save this
|
||||
return instance
|
||||
50
src/backend/core/user_token/viewsets.py
Normal file
50
src/backend/core/user_token/viewsets.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""API endpoints for user token management"""
|
||||
|
||||
from knox.models import get_token_model
|
||||
from rest_framework import permissions, viewsets, mixins
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
|
||||
from . import serializers
|
||||
|
||||
|
||||
class UserTokenViewset(
|
||||
mixins.CreateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
"""API ViewSet for user invitations to document.
|
||||
|
||||
This view access is restricted to the session ie from frontend.
|
||||
|
||||
GET /api/v1.0/user-token/
|
||||
Return list of existing tokens.
|
||||
|
||||
POST /api/v1.0/user-token/
|
||||
Return newly created token.
|
||||
|
||||
DELETE /api/v1.0/user-token/<token_id>/
|
||||
Delete targeted token.
|
||||
"""
|
||||
|
||||
authentication_classes = [SessionAuthentication]
|
||||
pagination_class = None
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
queryset = get_token_model().objects.all()
|
||||
serializer_class = serializers.TokenReadSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the queryset restricted to the logged-in user."""
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(user_id=self.request.user.pk)
|
||||
return queryset
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
return serializers.TokenCreateSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Enforce request data to use current user."""
|
||||
request.data["user"] = self.request.user.pk
|
||||
return super().create(request, *args, **kwargs)
|
||||
@@ -66,6 +66,116 @@ def base64_yjs_to_text(base64_string):
|
||||
soup = BeautifulSoup(blocknote_structure, "lxml-xml")
|
||||
return soup.get_text(separator=" ", strip=True)
|
||||
|
||||
def base64_yjs_to_markdown(base64_string: str) -> str:
|
||||
xml_content = base64_yjs_to_xml(base64_string)
|
||||
soup = BeautifulSoup(xml_content, "lxml-xml")
|
||||
|
||||
md_lines: list[str] = []
|
||||
|
||||
def walk(node) -> None:
|
||||
if not getattr(node, "name", None):
|
||||
return
|
||||
|
||||
# Treat the synthetic “[document]” tag exactly like a wrapper
|
||||
if node.name in {"[document]", "blockGroup", "blockContainer"}:
|
||||
for child in node.find_all(recursive=False):
|
||||
walk(child)
|
||||
if node.name == "blockContainer":
|
||||
md_lines.append("") # paragraph break
|
||||
return
|
||||
|
||||
# ----------- content nodes -------------
|
||||
if node.name == "heading":
|
||||
level = int(node.get("level", 1))
|
||||
md_lines.extend([("#" * level) + " " + process_inline_formatting(node), ""])
|
||||
|
||||
elif node.name == "paragraph":
|
||||
md_lines.extend([process_inline_formatting(node), ""])
|
||||
|
||||
elif node.name == "bulletListItem":
|
||||
md_lines.append("- " + process_inline_formatting(node))
|
||||
|
||||
elif node.name == "numberedListItem":
|
||||
idx = node.get("index", "1")
|
||||
md_lines.append(f"{idx}. " + process_inline_formatting(node))
|
||||
|
||||
elif node.name == "checkListItem":
|
||||
checked = "x" if node.get("checked") == "true" else " "
|
||||
md_lines.append(f"- [{checked}] " + process_inline_formatting(node))
|
||||
|
||||
elif node.name == "codeBlock":
|
||||
lang = node.get("language", "")
|
||||
code = node.get_text("", strip=False)
|
||||
md_lines.extend([f"```{lang}", code, "```", ""])
|
||||
|
||||
elif node.name in {"quote", "blockquote"}:
|
||||
quote = process_inline_formatting(node)
|
||||
for line in quote.splitlines() or [""]:
|
||||
md_lines.append("> " + line)
|
||||
md_lines.append("")
|
||||
|
||||
elif node.name == "divider":
|
||||
md_lines.extend(["---", ""])
|
||||
|
||||
elif node.name == "callout":
|
||||
emoji = node.get("emoji", "💡")
|
||||
md_lines.extend([f"> {emoji} {process_inline_formatting(node)}", ""])
|
||||
|
||||
elif node.name == "img":
|
||||
src = node.get("src", "")
|
||||
alt = node.get("alt", "")
|
||||
md_lines.extend([f"", ""])
|
||||
|
||||
# unknown tags are ignored
|
||||
|
||||
# kick-off: start at the synthetic root
|
||||
walk(soup)
|
||||
|
||||
# collapse accidental multiple blank lines
|
||||
cleaned: list[str] = []
|
||||
for line in md_lines:
|
||||
if line == "" and (not cleaned or cleaned[-1] == ""):
|
||||
continue
|
||||
cleaned.append(line)
|
||||
|
||||
return "\n".join(cleaned).rstrip() + "\n"
|
||||
|
||||
def process_inline_formatting(element):
|
||||
"""
|
||||
Process inline formatting elements like bold, italic, underline, etc.
|
||||
and convert them to markdown syntax.
|
||||
"""
|
||||
result = ""
|
||||
|
||||
# If it's just a text node, return the text
|
||||
if isinstance(element, str):
|
||||
return element
|
||||
|
||||
# Process children elements
|
||||
for child in element.contents:
|
||||
if isinstance(child, str):
|
||||
result += child
|
||||
elif hasattr(child, 'name'):
|
||||
if child.name == "bold":
|
||||
result += "**" + process_inline_formatting(child) + "**"
|
||||
elif child.name == "italic":
|
||||
result += "*" + process_inline_formatting(child) + "*"
|
||||
elif child.name == "underline":
|
||||
result += "__" + process_inline_formatting(child) + "__"
|
||||
elif child.name == "strike":
|
||||
result += "~~" + process_inline_formatting(child) + "~~"
|
||||
elif child.name == "code":
|
||||
result += "`" + process_inline_formatting(child) + "`"
|
||||
elif child.name == "link":
|
||||
href = child.get("href", "")
|
||||
text = process_inline_formatting(child)
|
||||
result += f"[{text}]({href})"
|
||||
else:
|
||||
# For other elements, just process their contents
|
||||
result += process_inline_formatting(child)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def extract_attachments(content):
|
||||
"""Helper method to extract media paths from a document's content."""
|
||||
|
||||
@@ -10,6 +10,9 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import tomllib
|
||||
from socket import gethostbyname, gethostname
|
||||
@@ -303,6 +306,7 @@ class Base(Configuration):
|
||||
"django_filters",
|
||||
"dockerflow.django",
|
||||
"rest_framework",
|
||||
"knox",
|
||||
"parler",
|
||||
"treebeard",
|
||||
"easy_thumbnails",
|
||||
@@ -327,8 +331,9 @@ class Base(Configuration):
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
"mozilla_django_oidc.contrib.drf.OIDCAuthentication",
|
||||
"rest_framework.authentication.SessionAuthentication",
|
||||
"knox.auth.TokenAuthentication",
|
||||
"lasuite.oidc_resource_server.authentication.ResourceServerAuthentication",
|
||||
),
|
||||
"DEFAULT_PARSER_CLASSES": [
|
||||
"rest_framework.parsers.JSONParser",
|
||||
@@ -593,6 +598,72 @@ class Base(Configuration):
|
||||
default=True, environ_name="ALLOW_LOGOUT_GET_METHOD", environ_prefix=None
|
||||
)
|
||||
|
||||
# OIDC - Docs as a resource server
|
||||
OIDC_OP_URL = values.Value(
|
||||
default=None, environ_name="OIDC_OP_URL", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_INTROSPECTION_ENDPOINT = values.Value(
|
||||
environ_name="OIDC_OP_INTROSPECTION_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_VERIFY_SSL = values.BooleanValue(
|
||||
default=True, environ_name="OIDC_VERIFY_SSL", environ_prefix=None
|
||||
)
|
||||
OIDC_TIMEOUT = values.IntegerValue(
|
||||
default=3, environ_name="OIDC_TIMEOUT", environ_prefix=None
|
||||
)
|
||||
OIDC_PROXY = values.Value(None, environ_name="OIDC_PROXY", environ_prefix=None)
|
||||
|
||||
OIDC_RS_BACKEND_CLASS = "lasuite.oidc_resource_server.backend.ResourceServerBackend"
|
||||
OIDC_RS_AUDIENCE_CLAIM = values.Value( # The claim used to identify the audience
|
||||
default="client_id", environ_name="OIDC_RS_AUDIENCE_CLAIM", environ_prefix=None
|
||||
)
|
||||
OIDC_RS_PRIVATE_KEY_STR = values.Value(
|
||||
default=None,
|
||||
environ_name="OIDC_RS_PRIVATE_KEY_STR",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OIDC_RS_ENCRYPTION_KEY_TYPE = values.Value(
|
||||
default="RSA",
|
||||
environ_name="OIDC_RS_ENCRYPTION_KEY_TYPE",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OIDC_RS_ENCRYPTION_ALGO = values.Value(
|
||||
default="RSA-OAEP",
|
||||
environ_name="OIDC_RS_ENCRYPTION_ALGO",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OIDC_RS_ENCRYPTION_ENCODING = values.Value(
|
||||
default="A256GCM",
|
||||
environ_name="OIDC_RS_ENCRYPTION_ENCODING",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OIDC_RS_CLIENT_ID = values.Value(
|
||||
None, environ_name="OIDC_RS_CLIENT_ID", environ_prefix=None
|
||||
)
|
||||
OIDC_RS_CLIENT_SECRET = values.Value(
|
||||
None,
|
||||
environ_name="OIDC_RS_CLIENT_SECRET",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OIDC_RS_SIGNING_ALGO = values.Value(
|
||||
default="ES256", environ_name="OIDC_RS_SIGNING_ALGO", environ_prefix=None
|
||||
)
|
||||
OIDC_RS_SCOPES = values.ListValue(
|
||||
[], environ_name="OIDC_RS_SCOPES", environ_prefix=None
|
||||
)
|
||||
|
||||
# User token (knox)
|
||||
REST_KNOX = {
|
||||
"SECURE_HASH_ALGORITHM": "hashlib.sha512",
|
||||
"AUTH_TOKEN_CHARACTER_LENGTH": 64,
|
||||
"TOKEN_TTL": datetime.timedelta(hours=24 * 7),
|
||||
"TOKEN_LIMIT_PER_USER": None,
|
||||
"AUTO_REFRESH": False,
|
||||
"AUTO_REFRESH_MAX_TTL": None,
|
||||
"MIN_REFRESH_INTERVAL": 60,
|
||||
"AUTH_HEADER_PREFIX": "Token",
|
||||
}
|
||||
|
||||
# AI service
|
||||
AI_FEATURE_ENABLED = values.BooleanValue(
|
||||
default=False, environ_name="AI_FEATURE_ENABLED", environ_prefix=None
|
||||
|
||||
@@ -36,6 +36,7 @@ dependencies = [
|
||||
"django-lasuite[all]==0.0.9",
|
||||
"django-parler==2.3",
|
||||
"django-redis==5.4.0",
|
||||
"django-rest-knox==5.0.2",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django==5.1.9",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
@@ -16,6 +18,7 @@ import { Title } from './Title';
|
||||
|
||||
export const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
|
||||
@@ -63,6 +66,13 @@ export const Header = () => {
|
||||
) : (
|
||||
<Box $align="center" $gap={spacingsTokens['sm']} $direction="row">
|
||||
<ButtonLogin />
|
||||
<Button
|
||||
onClick={() => router.push(`/user-tokens`)}
|
||||
aria-label={t('API Tokens', 'API Tokens')}
|
||||
color="primary-text"
|
||||
>
|
||||
{t('API Tokens', 'API Tokens')}
|
||||
</Button>
|
||||
<LanguagePicker />
|
||||
<LaGaufre />
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './useListUserTokens';
|
||||
export * from './useCreateUserToken';
|
||||
export * from './useDeleteUserToken';
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
import { NewUserToken } from '../types';
|
||||
|
||||
export const createUserToken = async (): Promise<NewUserToken> => {
|
||||
const response = await fetchAPI(`user-tokens/`, {
|
||||
method: 'POST',
|
||||
// The backend test indicates no data is sent for creation, so body is an empty object
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
'Failed to create user token',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
|
||||
return response.json() as Promise<NewUserToken>;
|
||||
};
|
||||
|
||||
export function useCreateUserToken() {
|
||||
return useMutation<NewUserToken, APIError>({
|
||||
mutationFn: createUserToken,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
export const deleteUserToken = async (digest: string): Promise<void> => {
|
||||
const response = await fetchAPI(`user-tokens/${digest}/`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok && response.status !== 204) {
|
||||
// 204 is a valid response for delete
|
||||
throw new APIError(
|
||||
'Failed to delete user token',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
// For 204, there's no content, and for other successful deletions, we don't expect content.
|
||||
// So, we don't try to parse JSON.
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
export type DeleteUserTokenParams = {
|
||||
digest: string;
|
||||
};
|
||||
|
||||
export function useDeleteUserToken() {
|
||||
return useMutation<void, APIError, DeleteUserTokenParams>({
|
||||
mutationFn: ({ digest }) => deleteUserToken(digest),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
import { UserToken } from '../types';
|
||||
|
||||
export const listUserTokens = async (): Promise<UserToken[]> => {
|
||||
const response = await fetchAPI(`user-tokens/`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
'Failed to list user tokens',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
|
||||
return response.json() as Promise<UserToken[]>;
|
||||
};
|
||||
|
||||
export function useListUserTokens() {
|
||||
return useQuery<UserToken[], APIError>({
|
||||
queryKey: ['userTokens'],
|
||||
queryFn: listUserTokens,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import {
|
||||
Button as CunninghamButton,
|
||||
DataGrid,
|
||||
Modal,
|
||||
ModalSize,
|
||||
} from '@openfun/cunningham-react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Box, Card } from '@/components';
|
||||
|
||||
import { createUserToken, deleteUserToken, listUserTokens } from '../api/index';
|
||||
import { NewUserToken, UserToken } from '../types';
|
||||
|
||||
const formatTimeAgo = (dateString: string) => {
|
||||
const now = new Date();
|
||||
const date = new Date(dateString);
|
||||
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
||||
if (seconds < 60) {
|
||||
return `${seconds} seconds ago`;
|
||||
}
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) {
|
||||
return `${minutes} minutes ago`;
|
||||
}
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) {
|
||||
return `${hours} hours ago`;
|
||||
}
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days} days ago`;
|
||||
};
|
||||
|
||||
// Add id to UserToken type for DataGrid compatibility
|
||||
interface UserTokenWithId extends UserToken {
|
||||
id: string;
|
||||
}
|
||||
|
||||
// Define proper type for DataGrid columns
|
||||
interface ColumnDef {
|
||||
field: string;
|
||||
headerName: string;
|
||||
width?: number;
|
||||
renderCell: (params: { row: UserTokenWithId }) => React.ReactNode;
|
||||
}
|
||||
|
||||
export const UserTokenManager: React.FC = () => {
|
||||
const [tokens, setTokens] = useState<UserTokenWithId[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [newToken, setNewToken] = useState<NewUserToken | null>(null);
|
||||
const [modalState, setModalState] = useState<{
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
message: React.ReactNode;
|
||||
onConfirm?: () => void;
|
||||
confirmText?: string;
|
||||
isConfirmation: boolean;
|
||||
type?: 'success' | 'error' | 'warning' | 'info';
|
||||
size: ModalSize;
|
||||
}>({
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
isConfirmation: false,
|
||||
size: ModalSize.MEDIUM, // Default size using ModalSize enum
|
||||
});
|
||||
|
||||
const showNotification = (
|
||||
message: string,
|
||||
type: 'success' | 'error' = 'success',
|
||||
size: ModalSize = ModalSize.SMALL,
|
||||
) => {
|
||||
setModalState({
|
||||
isOpen: true,
|
||||
title: type === 'success' ? 'Success' : 'Error',
|
||||
message,
|
||||
isConfirmation: false,
|
||||
type: type,
|
||||
size,
|
||||
});
|
||||
};
|
||||
|
||||
const showConfirmation = (
|
||||
title: string,
|
||||
message: React.ReactNode,
|
||||
onConfirm: () => void,
|
||||
confirmText: string = 'Confirm',
|
||||
size: ModalSize = ModalSize.MEDIUM,
|
||||
) => {
|
||||
setModalState({
|
||||
isOpen: true,
|
||||
title,
|
||||
message,
|
||||
onConfirm,
|
||||
confirmText,
|
||||
isConfirmation: true,
|
||||
size,
|
||||
});
|
||||
};
|
||||
|
||||
const fetchTokens = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const fetchedTokens = await listUserTokens();
|
||||
// Add id to each token
|
||||
setTokens(fetchedTokens.map((token) => ({ ...token, id: token.digest })));
|
||||
} catch (err) {
|
||||
setError(
|
||||
'Failed to fetch tokens. Please ensure you are logged in and have permissions.',
|
||||
);
|
||||
showNotification('Failed to fetch tokens', 'error');
|
||||
console.error(err);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchTokens();
|
||||
}, [fetchTokens]);
|
||||
|
||||
const handleCreateToken = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
setNewToken(null);
|
||||
try {
|
||||
const generatedToken = await createUserToken();
|
||||
setNewToken(generatedToken);
|
||||
showNotification(
|
||||
'Token created successfully! Store the token key safely, it will not be shown again.',
|
||||
'success',
|
||||
ModalSize.LARGE,
|
||||
);
|
||||
void fetchTokens();
|
||||
} catch (err) {
|
||||
setError('Failed to create token.');
|
||||
showNotification('Failed to create token', 'error');
|
||||
console.error(err);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const handleDeleteToken = (digest: string) => {
|
||||
showConfirmation(
|
||||
'Confirm Deletion',
|
||||
'Are you sure you want to delete this token?',
|
||||
() => {
|
||||
void (async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await deleteUserToken(digest);
|
||||
showNotification('Token deleted successfully!');
|
||||
setNewToken(null);
|
||||
await fetchTokens();
|
||||
} catch (err) {
|
||||
setError('Failed to delete token.');
|
||||
showNotification('Failed to delete token', 'error');
|
||||
console.error(err);
|
||||
}
|
||||
setIsLoading(false);
|
||||
})();
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const columns: ColumnDef[] = [
|
||||
{
|
||||
field: 'digest',
|
||||
headerName: 'Name',
|
||||
renderCell: ({ row }: { row: UserTokenWithId }) => <>{row.digest}</>,
|
||||
},
|
||||
{
|
||||
field: 'created',
|
||||
headerName: 'Updated at',
|
||||
renderCell: ({ row }: { row: UserTokenWithId }) => (
|
||||
<>{formatTimeAgo(row.created)}</>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'expires',
|
||||
headerName: 'Expires at',
|
||||
renderCell: ({ row }: { row: UserTokenWithId }) => <>{row.expiry}</>,
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
headerName: '',
|
||||
width: 50,
|
||||
renderCell: ({ row }: { row: UserTokenWithId }) => (
|
||||
<CunninghamButton
|
||||
onClick={() => {
|
||||
handleDeleteToken(row.digest);
|
||||
}}
|
||||
color="danger"
|
||||
size="small"
|
||||
icon={<span className="material-icons">delete</span>}
|
||||
aria-label="Delete token"
|
||||
>
|
||||
Delete
|
||||
</CunninghamButton>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card
|
||||
$direction="column"
|
||||
$width="100%"
|
||||
$padding={{
|
||||
top: 'base',
|
||||
horizontal: 'md',
|
||||
bottom: 'md',
|
||||
}}
|
||||
>
|
||||
<Box $direction="row" $justify="space-between" $align="center">
|
||||
<h2 style={{ marginBottom: 'var(--c--theme--spacing--medium, 16px)' }}>
|
||||
User token management
|
||||
</h2>
|
||||
<CunninghamButton
|
||||
onClick={() => void handleCreateToken()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Generating...' : 'Generate New Token'}
|
||||
</CunninghamButton>
|
||||
</Box>
|
||||
|
||||
{newToken && (
|
||||
<Box
|
||||
$background="var(--c--theme--colors--success-100)"
|
||||
$padding="md"
|
||||
$radius="10px"
|
||||
$margin={{ bottom: 'var(--c--theme--spacing--medium, 16px)' }}
|
||||
$direction="column"
|
||||
>
|
||||
<span style={{ marginLeft: 16 }}>
|
||||
<strong>New Token:</strong> <code>{newToken.token_key}</code>
|
||||
</span>
|
||||
<span style={{ marginLeft: 16 }}>
|
||||
<strong>Digest:</strong> <code>{newToken.digest}</code>
|
||||
</span>
|
||||
<span style={{ marginLeft: 16 }}>
|
||||
<strong>Expires:</strong>{' '}
|
||||
<code>{new Date(newToken.expiry).toLocaleString()}</code>
|
||||
</span>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isLoading && !tokens.length && (
|
||||
<Box $margin={{ bottom: 'var(--c--theme--spacing--small, 8px)' }}>
|
||||
Loading...
|
||||
</Box>
|
||||
)}
|
||||
{error && (
|
||||
<Box
|
||||
$color="var(--c--theme--colors--danger-500, red)"
|
||||
$margin={{ bottom: 'var(--c--theme--spacing--small, 8px)' }}
|
||||
>
|
||||
{error}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<DataGrid<UserTokenWithId>
|
||||
rows={tokens}
|
||||
columns={columns}
|
||||
isLoading={isLoading}
|
||||
emptyCta={<div>No tokens found.</div>}
|
||||
/>
|
||||
{modalState.isOpen && (
|
||||
<Modal
|
||||
isOpen={modalState.isOpen}
|
||||
onClose={() => setModalState((prev) => ({ ...prev, isOpen: false }))}
|
||||
title={modalState.title}
|
||||
size={modalState.size} // Use ModalSize enum directly
|
||||
actions={
|
||||
modalState.isConfirmation ? (
|
||||
<Box $width="100%" $direction="row" $justify="space-between">
|
||||
<CunninghamButton
|
||||
onClick={() =>
|
||||
setModalState((prev) => ({ ...prev, isOpen: false }))
|
||||
}
|
||||
color="secondary"
|
||||
>
|
||||
Cancel
|
||||
</CunninghamButton>
|
||||
<CunninghamButton
|
||||
onClick={() => {
|
||||
if (modalState.onConfirm) {
|
||||
modalState.onConfirm();
|
||||
}
|
||||
setModalState((prev) => ({ ...prev, isOpen: false }));
|
||||
}}
|
||||
color="danger"
|
||||
>
|
||||
{modalState.confirmText || 'Confirm'}
|
||||
</CunninghamButton>
|
||||
</Box>
|
||||
) : (
|
||||
<CunninghamButton
|
||||
onClick={() =>
|
||||
setModalState((prev) => ({ ...prev, isOpen: false }))
|
||||
}
|
||||
color="primary"
|
||||
>
|
||||
Close
|
||||
</CunninghamButton>
|
||||
)
|
||||
}
|
||||
>
|
||||
{modalState.message}
|
||||
</Modal>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { UserTokenManager } from './components/UserTokenManager';
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface UserToken {
|
||||
digest: string;
|
||||
created: string; // Assuming ISO date string
|
||||
expiry: string; // Assuming ISO date string
|
||||
}
|
||||
|
||||
export interface NewUserToken extends UserToken {
|
||||
token_key: string;
|
||||
}
|
||||
13
src/frontend/apps/impress/src/pages/user-tokens/index.tsx
Normal file
13
src/frontend/apps/impress/src/pages/user-tokens/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { UserTokenManager } from '@/features/user-tokens';
|
||||
import { MainLayout } from '@/layouts';
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
const UserTokensPage: NextPageWithLayout = () => {
|
||||
return <UserTokenManager />;
|
||||
};
|
||||
|
||||
UserTokensPage.getLayout = function getLayout(page: React.ReactElement) {
|
||||
return <MainLayout backgroundColor="grey">{page}</MainLayout>;
|
||||
};
|
||||
|
||||
export default UserTokensPage;
|
||||
@@ -29,19 +29,25 @@ backend:
|
||||
DJANGO_EMAIL_PORT: 1025
|
||||
DJANGO_EMAIL_USE_SSL: False
|
||||
LOGGING_LEVEL_HANDLERS_CONSOLE: ERROR
|
||||
LOGGING_LEVEL_LOGGERS_ROOT: INFO
|
||||
LOGGING_LEVEL_LOGGERS_APP: INFO
|
||||
LOGGING_LEVEL_LOGGERS_ROOT: DEBUG
|
||||
LOGGING_LEVEL_LOGGERS_APP: DEBUG
|
||||
OIDC_USERINFO_SHORTNAME_FIELD: "given_name"
|
||||
OIDC_USERINFO_FULLNAME_FIELDS: "given_name,usual_name"
|
||||
OIDC_OP_URL: https://keycloak.127.0.0.1.nip.io/realms/people
|
||||
OIDC_OP_JWKS_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/certs
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/auth
|
||||
OIDC_OP_TOKEN_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/token
|
||||
OIDC_OP_USER_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/userinfo
|
||||
OIDC_OP_LOGOUT_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/logout
|
||||
OIDC_OP_INTROSPECTION_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/token/introspect
|
||||
OIDC_RP_CLIENT_ID: impress
|
||||
OIDC_RP_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
|
||||
OIDC_RP_SIGN_ALGO: RS256
|
||||
OIDC_RP_SCOPES: "openid email"
|
||||
OIDC_RS_CLIENT_ID: impress
|
||||
OIDC_RS_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
|
||||
OIDC_RS_SIGNING_ALGO: RS256
|
||||
OIDC_RS_SCOPES: "openid,profile,email"
|
||||
LOGIN_REDIRECT_URL: https://impress.127.0.0.1.nip.io
|
||||
LOGIN_REDIRECT_URL_FAILURE: https://impress.127.0.0.1.nip.io
|
||||
LOGOUT_REDIRECT_URL: https://impress.127.0.0.1.nip.io
|
||||
@@ -171,3 +177,20 @@ ingressMedia:
|
||||
serviceMedia:
|
||||
host: minio.impress.svc.cluster.local
|
||||
port: 9000
|
||||
|
||||
|
||||
mcpServer:
|
||||
replicas: 1
|
||||
|
||||
image:
|
||||
repository: localhost:5001/impress-mcp-server
|
||||
pullPolicy: Always
|
||||
tag: "latest"
|
||||
|
||||
envVars:
|
||||
DOCS_API_URL: https://impress.127.0.0.1.nip.io/
|
||||
SERVER_TRANSPORT: STREAMABLE_HTTP
|
||||
|
||||
ingressMcpServer:
|
||||
enabled: true
|
||||
host: impress.127.0.0.1.nip.io
|
||||
|
||||
@@ -178,6 +178,15 @@ Requires top level scope
|
||||
{{ include "impress.fullname" . }}-celery-worker
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Full name for the MCP server
|
||||
|
||||
Requires top level scope
|
||||
*/}}
|
||||
{{- define "impress.mcpServer.fullname" -}}
|
||||
{{ include "impress.fullname" . }}-mcp-server
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Usage : {{ include "impress.secret.dockerconfigjson.name" (dict "fullname" (include "impress.fullname" .) "imageCredentials" .Values.path.to.the.image1) }}
|
||||
*/}}
|
||||
|
||||
89
src/helm/impress/templates/ingress_mcp_server.yaml
Normal file
89
src/helm/impress/templates/ingress_mcp_server.yaml
Normal file
@@ -0,0 +1,89 @@
|
||||
{{- if .Values.ingressMcpServer.enabled -}}
|
||||
{{- $fullName := include "impress.fullname" . -}}
|
||||
{{- if and .Values.ingressMcpServer.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingressMcpServer.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingressMcpServer.annotations "kubernetes.io/ingress.class" .Values.ingressMcpServer.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}-mcp-server
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "impress.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingressMcpServer.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingressMcpServer.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingressMcpServer.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingressMcpServer.tls.enabled }}
|
||||
tls:
|
||||
{{- if .Values.ingressMcpServer.host }}
|
||||
- secretName: {{ .Values.ingressMcpServer.tls.secretName | default (printf "%s-tls" $fullName) | quote }}
|
||||
hosts:
|
||||
- {{ .Values.ingressMcpServer.host | quote }}
|
||||
{{- end }}
|
||||
{{- range .Values.ingressMcpServer.tls.additional }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- if .Values.ingressMcpServer.host }}
|
||||
- host: {{ .Values.ingressMcpServer.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ .Values.ingressMcpServer.path | quote }}
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ include "impress.mcpServer.fullname" . }}
|
||||
port:
|
||||
number: {{ .Values.mcpServer.service.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ include "impress.mcpServer.fullname" . }}
|
||||
servicePort: {{ .Values.mcpServer.service.port }}
|
||||
{{- end }}
|
||||
{{- with .Values.ingressMcpServer.customBackends }}
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range .Values.ingressMcpServer.hosts }}
|
||||
- host: {{ . | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ $.Values.ingressMcpServer.path | quote }}
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ include "impress.mcpServer.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.mcpServer.service.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ include "impress.mcpServer.fullname" $ }}
|
||||
servicePort: {{ $.Values.mcpServer.service.port }}
|
||||
{{- end }}
|
||||
{{- with $.Values.ingressMcpServer.customBackends }}
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
153
src/helm/impress/templates/mcp_server_deployment.yaml
Normal file
153
src/helm/impress/templates/mcp_server_deployment.yaml
Normal file
@@ -0,0 +1,153 @@
|
||||
{{- $envVars := include "impress.common.env" (list . .Values.mcpServer) -}}
|
||||
{{- $fullName := include "impress.mcpServer.fullname" . -}}
|
||||
{{- $component := "mcp-server" -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
annotations:
|
||||
{{- with .Values.mcpServer.dpAnnotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "impress.common.labels" (list . $component) | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.backend.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "impress.common.selectorLabels" (list . $component) | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
{{- with .Values.backend.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "impress.common.selectorLabels" (list . $component) | nindent 8 }}
|
||||
spec:
|
||||
{{- if $.Values.image.credentials }}
|
||||
imagePullSecrets:
|
||||
- name: {{ include "impress.secret.dockerconfigjson.name" (dict "fullname" (include "impress.fullname" .) "imageCredentials" $.Values.image.credentials) }}
|
||||
{{- end}}
|
||||
shareProcessNamespace: {{ .Values.backend.shareProcessNamespace }}
|
||||
containers:
|
||||
{{- with .Values.mcpServer.sidecars }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ (.Values.mcpServer.image | default dict).repository }}:{{ (.Values.mcpServer.image | default dict).tag }}"
|
||||
imagePullPolicy: {{ (.Values.mcpServer.image | default dict).pullPolicy }}
|
||||
{{- with .Values.mcpServer.command }}
|
||||
command:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.mcpServer.args }}
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars}}
|
||||
{{- $envVars | indent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.mcpServer.securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.mcpServer.service.targetPort }}
|
||||
protocol: TCP
|
||||
{{- if .Values.mcpServer.probes.liveness }}
|
||||
livenessProbe:
|
||||
{{- include "impress.probes.abstract" (merge .Values.mcpServer.probes.liveness (dict "targetPort" .Values.mcpServer.service.targetPort )) | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if .Values.mcpServer.probes.readiness }}
|
||||
readinessProbe:
|
||||
{{- include "impress.probes.abstract" (merge .Values.mcpServer.probes.readiness (dict "targetPort" .Values.mcpServer.service.targetPort )) | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if .Values.mcpServer.probes.startup }}
|
||||
startupProbe:
|
||||
{{- include "impress.probes.abstract" (merge .Values.mcpServer.probes.startup (dict "targetPort" .Values.mcpServer.service.targetPort )) | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.mcpServer.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
{{- range $index, $value := .Values.mountFiles }}
|
||||
- name: "files-{{ $index }}"
|
||||
mountPath: {{ $value.path }}
|
||||
subPath: content
|
||||
{{- end }}
|
||||
{{- range $name, $volume := .Values.mcpServer.persistence }}
|
||||
- name: "{{ $name }}"
|
||||
mountPath: "{{ $volume.mountPath }}"
|
||||
{{- end }}
|
||||
{{- range .Values.mcpServer.extraVolumeMounts }}
|
||||
- name: {{ .name }}
|
||||
mountPath: {{ .mountPath }}
|
||||
subPath: {{ .subPath | default "" }}
|
||||
readOnly: {{ .readOnly }}
|
||||
{{- end }}
|
||||
{{- with .Values.mcpServer.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.mcpServer.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.mcpServer.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
{{- range $index, $value := .Values.mountFiles }}
|
||||
- name: "files-{{ $index }}"
|
||||
configMap:
|
||||
name: "{{ include "impress.fullname" $ }}-files-{{ $index }}"
|
||||
{{- end }}
|
||||
{{- range $name, $volume := .Values.mcpServer.persistence }}
|
||||
- name: "{{ $name }}"
|
||||
{{- if eq $volume.type "emptyDir" }}
|
||||
emptyDir: {}
|
||||
{{- else }}
|
||||
persistentVolumeClaim:
|
||||
claimName: "{{ $fullName }}-{{ $name }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range .Values.mcpServer.extraVolumes }}
|
||||
- name: {{ .name }}
|
||||
{{- if .existingClaim }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .existingClaim }}
|
||||
{{- else if .hostPath }}
|
||||
hostPath:
|
||||
{{ toYaml .hostPath | nindent 12 }}
|
||||
{{- else if .csi }}
|
||||
csi:
|
||||
{{- toYaml .csi | nindent 12 }}
|
||||
{{- else if .configMap }}
|
||||
configMap:
|
||||
{{- toYaml .configMap | nindent 12 }}
|
||||
{{- else if .emptyDir }}
|
||||
emptyDir:
|
||||
{{- toYaml .emptyDir | nindent 12 }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
---
|
||||
{{ if .Values.mcpServer.pdb.enabled }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
maxUnavailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "impress.common.selectorLabels" (list . $component) | nindent 6 }}
|
||||
{{ end }}
|
||||
21
src/helm/impress/templates/mcp_server_svc.yaml
Normal file
21
src/helm/impress/templates/mcp_server_svc.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
{{- $envVars := include "impress.common.env" (list . .Values.mcpServer) -}}
|
||||
{{- $fullName := include "impress.mcpServer.fullname" . -}}
|
||||
{{- $component := "mcp-server" -}}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "impress.common.labels" (list . $component) | nindent 4 }}
|
||||
annotations:
|
||||
{{- toYaml $.Values.mcpServer.service.annotations | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.mcpServer.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.mcpServer.service.port }}
|
||||
targetPort: {{ .Values.mcpServer.service.targetPort }}
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "impress.common.selectorLabels" (list . $component) | nindent 4 }}
|
||||
@@ -50,6 +50,31 @@ ingress:
|
||||
## @param ingress.customBackends Add custom backends to ingress
|
||||
customBackends: []
|
||||
|
||||
## @param ingressMcpServer.enabled whether to enable the Ingress or not
|
||||
## @param ingressMcpServer.className IngressClass to use for the Ingress
|
||||
## @param ingressMcpServer.host Host for the Ingress
|
||||
## @param ingressMcpServer.path Path to use for the Ingress
|
||||
ingressMcpServer:
|
||||
enabled: false
|
||||
className: null
|
||||
host: impress.example.com
|
||||
path: /mcp/docs/
|
||||
## @param ingressMcpServer.hosts Additional host to configure for the Ingress
|
||||
hosts: []
|
||||
# - chart-example.local
|
||||
## @param ingressMcpServer.tls.enabled Weather to enable TLS for the Ingress
|
||||
## @param ingressMcpServer.tls.secretName Secret name for TLS config
|
||||
## @skip ingressMcpServer.tls.additional
|
||||
## @extra ingressMcpServer.tls.additional[].secretName Secret name for additional TLS config
|
||||
## @extra ingressMcpServer.tls.additional[].hosts[] Hosts for additional TLS config
|
||||
tls:
|
||||
enabled: true
|
||||
secretName: null
|
||||
additional: []
|
||||
|
||||
## @param ingressMcpServer.customBackends Add custom backends to ingress
|
||||
customBackends: []
|
||||
|
||||
## @param ingressCollaborationWS.enabled whether to enable the Ingress or not
|
||||
## @param ingressCollaborationWS.className IngressClass to use for the Ingress
|
||||
## @param ingressCollaborationWS.host Host for the Ingress
|
||||
@@ -348,6 +373,93 @@ backend:
|
||||
timeoutSeconds: 5
|
||||
|
||||
|
||||
## @section mcpServer
|
||||
|
||||
mcpServer:
|
||||
## @param mcpServer.image.repository Repository to use to pull impress's MCP server container image
|
||||
## @param mcpServer.image.tag impress's MCP server container tag
|
||||
## @param mcpServer.image.pullPolicy MCP server container image pull policy
|
||||
image:
|
||||
repository: lasuite/impress-mcp-server
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
|
||||
## @param mcpServer.command Override the MCP server container command
|
||||
command: []
|
||||
|
||||
## @param mcpServer.args Override the backend container args
|
||||
args: []
|
||||
|
||||
## @param mcpServer.envVars Configure MCP server container environment variables
|
||||
envVars: []
|
||||
|
||||
## @param mcpServer.replicas Amount of backend replicas
|
||||
replicas: 3
|
||||
|
||||
## @param mcpServer.podAnnotations Annotations to add to the MCP server Pod
|
||||
podAnnotations: {}
|
||||
|
||||
## @param mcpServer.dpAnnotations Annotations to add to the MCP server Deployment
|
||||
dpAnnotations: {}
|
||||
|
||||
## @param mcpServer.sidecars Add sidecars containers to MCP server deployment
|
||||
sidecars: []
|
||||
|
||||
## @param mcpServer.securityContext Configure MCP server Pod security context
|
||||
securityContext: null
|
||||
|
||||
## @param mcpServer.service.type frontend Service type
|
||||
## @param mcpServer.service.port frontend Service listening port
|
||||
## @param mcpServer.service.targetPort frontend container listening port
|
||||
## @param mcpServer.service.annotations Annotations to add to the frontend Service
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
targetPort: 4200
|
||||
annotations: {}
|
||||
|
||||
## @param mcpServer.probes Configure probe for frontend
|
||||
## @extra mcpServer.probes.liveness.path Configure path for frontend HTTP liveness probe
|
||||
## @extra mcpServer.probes.liveness.targetPort Configure port for frontend HTTP liveness probe
|
||||
## @extra mcpServer.probes.liveness.initialDelaySeconds Configure initial delay for frontend liveness probe
|
||||
## @extra mcpServer.probes.liveness.initialDelaySeconds Configure timeout for frontend liveness probe
|
||||
## @extra mcpServer.probes.startup.path Configure path for frontend HTTP startup probe
|
||||
## @extra mcpServer.probes.startup.targetPort Configure port for frontend HTTP startup probe
|
||||
## @extra mcpServer.probes.startup.initialDelaySeconds Configure initial delay for frontend startup probe
|
||||
## @extra mcpServer.probes.startup.initialDelaySeconds Configure timeout for frontend startup probe
|
||||
## @extra mcpServer.probes.readiness.path Configure path for frontend HTTP readiness probe
|
||||
## @extra mcpServer.probes.readiness.targetPort Configure port for frontend HTTP readiness probe
|
||||
## @extra mcpServer.probes.readiness.initialDelaySeconds Configure initial delay for frontend readiness probe
|
||||
## @extra mcpServer.probes.readiness.initialDelaySeconds Configure timeout for frontend readiness probe
|
||||
probes: {}
|
||||
|
||||
## @param mcpServer.resources Resource requirements for the frontend container
|
||||
resources: {}
|
||||
|
||||
## @param mcpServer.persistence Additional volumes to create and mount on the backend. Used for debugging purposes
|
||||
## @extra mcpServer.persistence.volume-name.size Size of the additional volume
|
||||
## @extra mcpServer.persistence.volume-name.type Type of the additional volume, persistentVolumeClaim or emptyDir
|
||||
## @extra mcpServer.persistence.volume-name.mountPath Path where the volume should be mounted to
|
||||
persistence: {}
|
||||
|
||||
## @param mcpServer.extraVolumeMounts Additional volumes to mount on the backend.
|
||||
extraVolumeMounts: [ ]
|
||||
|
||||
## @param mcpServer.extraVolumes Additional volumes to mount on the backend.
|
||||
extraVolumes: []
|
||||
|
||||
## @param mcpServer.nodeSelector Node selector for the backend Pod
|
||||
nodeSelector: {}
|
||||
|
||||
## @param mcpServer.affinity Affinity for the backend Pod
|
||||
affinity: {}
|
||||
|
||||
## @param mcpServer.tolerations Tolerations for the backend Pod
|
||||
tolerations: []
|
||||
|
||||
## @param mcpServer.pdb.enabled Enable pdb on backend
|
||||
pdb:
|
||||
enabled: true
|
||||
|
||||
## @section frontend
|
||||
|
||||
|
||||
4
src/mcp_server/.dockerignore
Normal file
4
src/mcp_server/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
.venv
|
||||
.env
|
||||
docker-compose.yaml
|
||||
Dockerfile
|
||||
10
src/mcp_server/.env.example
Normal file
10
src/mcp_server/.env.example
Normal file
@@ -0,0 +1,10 @@
|
||||
SERVER_TRANSPORT=SSE
|
||||
|
||||
# Example Docker: DOCS_API_URL=http://app-dev:8000/ or http://host.docker.internal:8071/
|
||||
# Example localhost: DOCS_API_URL=http://localhost:8071/
|
||||
# Example Hackathon: DOCS_API_URL=https://docs-ia.beta.numerique.gouv.fr/
|
||||
DOCS_API_URL=<API_URL_HERE>
|
||||
|
||||
# Token generated from the frontend application
|
||||
# see http://localhost:3000/user-tokens/
|
||||
DOCS_API_TOKEN=<TOKEN_HERE>
|
||||
1
src/mcp_server/.python-version
Normal file
1
src/mcp_server/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12
|
||||
35
src/mcp_server/Dockerfile
Normal file
35
src/mcp_server/Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
USER root
|
||||
|
||||
# Install uv.
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
|
||||
# Change the working directory to the `app` directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --locked --no-install-project
|
||||
|
||||
# Copy the application into the image
|
||||
COPY docs_mcp_server/ /app/docs_mcp_server/
|
||||
|
||||
# Sync the project
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --locked
|
||||
|
||||
# Attach ports
|
||||
EXPOSE 4200
|
||||
ENV SERVER_HOST=0.0.0.0
|
||||
|
||||
# Un-privileged user running the application
|
||||
ARG DOCKER_USER
|
||||
USER ${DOCKER_USER}
|
||||
|
||||
# Run the MCP server.
|
||||
CMD ["uv", "--no-cache", "run", "python", "-m" ,"docs_mcp_server.mcp_server"]
|
||||
84
src/mcp_server/Makefile
Normal file
84
src/mcp_server/Makefile
Normal file
@@ -0,0 +1,84 @@
|
||||
# /!\ /!\ /!\ /!\ /!\ /!\ /!\ DISCLAIMER /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\
|
||||
#
|
||||
# This Makefile is only meant to be used for DEVELOPMENT purpose as we are
|
||||
# changing the user id that will run in the container.
|
||||
#
|
||||
# PLEASE DO NOT USE IT FOR YOUR CI/PRODUCTION/WHATEVER...
|
||||
#
|
||||
# /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\
|
||||
#
|
||||
# Note to developers:
|
||||
#
|
||||
# While editing this file, please respect the following statements:
|
||||
#
|
||||
# 1. Every variable should be defined in the ad hoc VARIABLES section with a
|
||||
# relevant subsection
|
||||
# 2. Every new rule should be defined in the ad hoc RULES section with a
|
||||
# relevant subsection depending on the targeted service
|
||||
# 3. Rules should be sorted alphabetically within their section
|
||||
# 4. When a rule has multiple dependencies, you should:
|
||||
# - duplicate the rule name to add the help string (if required)
|
||||
# - write one dependency per line to increase readability and diffs
|
||||
# 5. .PHONY rule statement should be written after the corresponding rule
|
||||
# ==============================================================================
|
||||
# VARIABLES
|
||||
|
||||
|
||||
|
||||
BOLD := \033[1m
|
||||
RESET := \033[0m
|
||||
GREEN := \033[1;32m
|
||||
|
||||
# Use uv for package management
|
||||
UV = uv
|
||||
|
||||
# ==============================================================================
|
||||
# RULES
|
||||
|
||||
default: help
|
||||
|
||||
help: ## Display this help message
|
||||
@echo "$(BOLD)Docs MCP server Makefile"
|
||||
@echo "Please use 'make $(BOLD)target$(RESET)' where $(BOLD)target$(RESET) is one of:"
|
||||
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(GREEN)%-30s$(RESET) %s\n", $$1, $$2}'
|
||||
.PHONY: help
|
||||
|
||||
install: ## Install the project
|
||||
@$(UV) sync
|
||||
.PHONY: install
|
||||
|
||||
install-dev: ## Install the project with dev dependencies
|
||||
@$(UV) sync --extra dev
|
||||
.PHONY: install-dev
|
||||
|
||||
clean: ## Clean the project folder
|
||||
@rm -rf build/
|
||||
@rm -rf dist/
|
||||
@rm -rf *.egg-info
|
||||
@find . -type d -name __pycache__ -exec rm -rf {} +
|
||||
@find . -type f -name "*.pyc" -delete
|
||||
.PHONY: clean
|
||||
|
||||
format: ## Run the formatter
|
||||
@$(UV) run ruff format
|
||||
.PHONY: format
|
||||
|
||||
lint: format ## Run the linter
|
||||
@$(UV) run ruff check --fix .
|
||||
.PHONY: lint
|
||||
|
||||
test: ## Run the tests
|
||||
@cd tests && PYTHON_PATH=.:$(PYTHON_PATH) $(UV) run python -m pytest . -vvv
|
||||
.PHONY: test
|
||||
|
||||
runserver: ## Run the project server
|
||||
@$(UV) run python -m docs_mcp_server.mcp_server
|
||||
.PHONY: runserver
|
||||
|
||||
runserver-docker: ## Run the project server in a docker container
|
||||
@touch .env
|
||||
@docker compose up --watch
|
||||
.PHONY: runserver-docker
|
||||
|
||||
run_llm: ## Run the LLM server
|
||||
@mcphost -m ollama:qwen2.5:3b --config "$(CWD)/mcphost.json"
|
||||
154
src/mcp_server/README.md
Normal file
154
src/mcp_server/README.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# mcp_server
|
||||
|
||||
`mcp_server` is a backend server application designed to manage and process MCP requests.
|
||||
|
||||
## Features
|
||||
|
||||
- Create a new Docs with a title and markdown content from a request to an agentic LLM.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration options can be set via environment variables or a configuration file (`.env`).
|
||||
|
||||
Common options include:
|
||||
|
||||
- `SERVER_TRANSPORT`: `STDIO`, `SSE` or `STREAMABLE_HTTP` (default: `STDIO` locally or `STREAMABLE_HTTP` in the docker image)
|
||||
- `SERVER_PATH`: The base path of the server tools and resources (default: `/mcp/docs/`)
|
||||
|
||||
You will need to set the following options to allow the MCP server to query Docs API:
|
||||
|
||||
- `DOCS_API_URL`: The Docs base URL without the "/api/v1.0/" (default: `http://localhost:8071`)
|
||||
- `DOCS_API_TOKEN`: The API user token you generate from the Docs frontend if you want
|
||||
to use token based authentication (default: `None`). If not provided, the server will
|
||||
use the authentication forwarder (pass the incoming authentication header to the Docs API call).
|
||||
|
||||
You may customize the following options for local development, while it's not recommended and may break the Docker image:
|
||||
|
||||
- `SERVER_HOST`: (default: `localhost` locally and `0.0.0.0` in the docker Image)
|
||||
- `SERVER_PORT`: (default: `4200`)
|
||||
|
||||
Example when using the server from a Docker instance
|
||||
|
||||
```dotenv
|
||||
SERVER_TRANSPORT=SSE
|
||||
|
||||
DOCS_API_URL=http://host.docker.internal:8071/
|
||||
DOCS_API_TOKEN=<some_token>
|
||||
```
|
||||
|
||||
## Run the MCP server
|
||||
|
||||
### Local
|
||||
You may work on the MCP server project using local configuration with `uv`:
|
||||
|
||||
```shell
|
||||
cd src/mcp_server
|
||||
|
||||
make install
|
||||
make runserver
|
||||
```
|
||||
|
||||
### Docker
|
||||
If you don't have local installation of Python or `uv` you can work using the Docker image:
|
||||
|
||||
```shell
|
||||
cd src/mcp_server
|
||||
|
||||
make runserver-docker
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. Create a local configuration file `.env`
|
||||
|
||||
```dotenv
|
||||
SERVER_TRANSPORT=SSE
|
||||
|
||||
DOCS_API_URL=http://host.docker.internal:8071/
|
||||
DOCS_API_TOKEN=your-token-here
|
||||
```
|
||||
|
||||
2. Run the server
|
||||
|
||||
```shell
|
||||
make runserver-docker
|
||||
```
|
||||
|
||||
### In Cursor IDE
|
||||
|
||||
In Cursor settings, in the MCP section, you can add a new MCP server with the following configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"docs": {
|
||||
"url": "http://127.0.0.1:4200/mcp/docs/"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### In VSCode IDE
|
||||
|
||||
In VSCode settings, you can add a new MCP server with the following configuration:
|
||||
|
||||
```json
|
||||
// .vscode/settings.json
|
||||
{
|
||||
"chat.mcp.discovery.enabled": true,
|
||||
"chat.mcp.enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// .vscode/mcp.json
|
||||
{
|
||||
"servers": {
|
||||
"docs": {
|
||||
"url": "http://localhost:4200/mcp/docs"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Locally with `mcphost` and `ollama`
|
||||
|
||||
1. Install [mcphost](https://github.com/mark3labs/mcphost)
|
||||
2. Install [ollama](https://ollama.ai)
|
||||
3. Start ollama: `ollama serve`
|
||||
4. Pull an agentic model like Qwen2.5 `ollama pull qwen2.5:3b`
|
||||
5. Create an MCP configuration file (e.g. `mcphost.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"docs": {
|
||||
"url": "http://127.0.0.1:4200/mcp/docs/"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. Start mcphost
|
||||
|
||||
```shell
|
||||
mcphost -m ollama:qwen2.5:3b --config "$PWD/mcphost.json"
|
||||
```
|
||||
|
||||
|
||||
## About the authentication forwarder
|
||||
|
||||
The authentication forwarder is a simple proxy that forwards the authentication header from
|
||||
the incoming request to the Docs API call. This allows to use "resource server" authentication.
|
||||
|
||||
For instance:
|
||||
|
||||
- Docs authentication is based on OIDC with Keycloak.
|
||||
- The AI chat is using the same Keycloak instance for authentication.
|
||||
- You can store the access token in the chat session and use it when calling the MCP server.
|
||||
- The MCP server will forward the access token to the Docs API call
|
||||
(actually, it forwards the whole authentication header).
|
||||
- Docs will introspect the access token and authenticate the user.
|
||||
- Conclusion: the user will be able to create a new Doc with the same access token
|
||||
used in the chat session.
|
||||
28
src/mcp_server/docker-compose.yaml
Normal file
28
src/mcp_server/docker-compose.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
services:
|
||||
docs_mcp-server:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
DOCKER_USER: ${DOCKER_USER:-1000}
|
||||
user: ${DOCKER_USER:-1000}
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "4200:4200"
|
||||
|
||||
develop:
|
||||
# Create a `watch` configuration to update the app
|
||||
watch:
|
||||
# Sync the working directory with the `/app` directory in the container
|
||||
- action: sync+restart
|
||||
path: ./docs_mcp_server
|
||||
target: /app/docs_mcp_server/
|
||||
# Exclude the project virtual environment
|
||||
ignore:
|
||||
- .venv/
|
||||
|
||||
# Rebuild the image on changes to the `pyproject.toml`
|
||||
- action: rebuild
|
||||
path: ./pyproject.toml
|
||||
3
src/mcp_server/docs_mcp_server/__init__.py
Normal file
3
src/mcp_server/docs_mcp_server/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""MCP Server package."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
1
src/mcp_server/docs_mcp_server/auth/__init__.py
Normal file
1
src/mcp_server/docs_mcp_server/auth/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Authentication module for the MCP server."""
|
||||
18
src/mcp_server/docs_mcp_server/auth/forwarder.py
Normal file
18
src/mcp_server/docs_mcp_server/auth/forwarder.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Authentication against Docs API via user token."""
|
||||
|
||||
import httpx
|
||||
from fastmcp.server.dependencies import get_http_request
|
||||
|
||||
|
||||
class HeaderForwarderAuthentication(httpx.Auth):
|
||||
"""Authentication class for request made to Docs, work as boilerplate."""
|
||||
|
||||
def auth_flow(self, request):
|
||||
"""Get Authorization header from request and pass it to the client."""
|
||||
_incoming_request = get_http_request()
|
||||
|
||||
# Get authorization header
|
||||
auth_header = _incoming_request.headers.get("authorization", "")
|
||||
|
||||
request.headers["Authorization"] = auth_header
|
||||
yield request
|
||||
16
src/mcp_server/docs_mcp_server/auth/token.py
Normal file
16
src/mcp_server/docs_mcp_server/auth/token.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Authentication against Docs API via user token."""
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
class UserTokenAuthentication(httpx.Auth):
|
||||
"""Authentication class for request made to Docs, using the user token."""
|
||||
|
||||
def __init__(self, token):
|
||||
"""Initialize the authentication class with the user token."""
|
||||
self.token = token
|
||||
|
||||
def auth_flow(self, request):
|
||||
"""Add the Authorization header to the request with the user token."""
|
||||
request.headers["Authorization"] = f"Token {self.token}"
|
||||
yield request
|
||||
11
src/mcp_server/docs_mcp_server/constants.py
Normal file
11
src/mcp_server/docs_mcp_server/constants.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Project constants."""
|
||||
|
||||
import enum
|
||||
|
||||
|
||||
class TransportLayerEnum(enum.Enum):
|
||||
"""Enum for the MCP server transport layer types."""
|
||||
|
||||
STDIO = "stdio"
|
||||
SSE = "sse"
|
||||
STREAMABLE_HTTP = "streamable-http"
|
||||
109
src/mcp_server/docs_mcp_server/mcp_server.py
Normal file
109
src/mcp_server/docs_mcp_server/mcp_server.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""The core of the MCP server for the Docs API."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import logging.config
|
||||
|
||||
import httpx
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from . import settings, utils
|
||||
from .auth.forwarder import HeaderForwarderAuthentication
|
||||
from .auth.token import UserTokenAuthentication
|
||||
|
||||
logging_config = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"default": {
|
||||
"format": "[%(asctime)s] %(levelname)s - %(name)s - %(message)s",
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"default": {
|
||||
"formatter": "default",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stdout",
|
||||
},
|
||||
},
|
||||
"root": {"handlers": ["default"], "level": "DEBUG"},
|
||||
"loggers": {
|
||||
"uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
||||
"uvicorn.error": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
||||
"uvicorn.access": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
||||
"FastMCP": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
||||
},
|
||||
}
|
||||
logging.config.dictConfig(logging_config)
|
||||
logger = logging.getLogger("docs_mcp_server")
|
||||
|
||||
|
||||
class ToolsProvider:
|
||||
"""Provides tools for the MCP server to interact with the Docs API."""
|
||||
|
||||
def __init__(self, mcp_instance):
|
||||
"""Register all the available tools here."""
|
||||
mcp_instance.add_tool(self.create_document_tool)
|
||||
|
||||
@property
|
||||
def api_client(self):
|
||||
"""Create and return an HTTP client for the Docs API."""
|
||||
if settings.DOCS_API_TOKEN:
|
||||
auth_backend = UserTokenAuthentication(token=settings.DOCS_API_TOKEN)
|
||||
else:
|
||||
auth_backend = HeaderForwarderAuthentication()
|
||||
|
||||
return httpx.AsyncClient(
|
||||
base_url=settings.DOCS_API_URL,
|
||||
auth=auth_backend,
|
||||
)
|
||||
|
||||
async def create_document_tool(self, document_title: str, document_content: str) -> None:
|
||||
"""
|
||||
Create a new document with the provided title and content.
|
||||
|
||||
Args:
|
||||
document_title: The title of the document (required)
|
||||
document_content: The content of the document (required)
|
||||
|
||||
"""
|
||||
_api_client = self.api_client
|
||||
|
||||
# Get current user information
|
||||
user_response = await _api_client.get("/api/v1.0/users/me/")
|
||||
user_response.raise_for_status()
|
||||
user_data = user_response.json()
|
||||
|
||||
# Prepare document data
|
||||
data = {
|
||||
"title": document_title,
|
||||
"content": document_content,
|
||||
"sub": user_data["id"],
|
||||
"email": user_data["email"],
|
||||
}
|
||||
|
||||
# Create the document
|
||||
create_response = await _api_client.post(
|
||||
"/api/v1.0/documents/create-for-owner/",
|
||||
json=data,
|
||||
)
|
||||
create_response.raise_for_status()
|
||||
|
||||
await _api_client.aclose()
|
||||
|
||||
|
||||
# Create a server instance from the OpenAPI spec
|
||||
mcp_server = FastMCP(name="Docs MCP Server")
|
||||
ToolsProvider(mcp_server)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(utils.check_mcp(mcp_server))
|
||||
logger.info("Starting Docs MCP Server...")
|
||||
mcp_server.run(
|
||||
transport=settings.SERVER_TRANSPORT.value,
|
||||
host=settings.SERVER_HOST,
|
||||
port=settings.SERVER_PORT,
|
||||
path=settings.SERVER_PATH,
|
||||
)
|
||||
21
src/mcp_server/docs_mcp_server/settings.py
Normal file
21
src/mcp_server/docs_mcp_server/settings.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Settings for the MCP server."""
|
||||
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from .constants import TransportLayerEnum
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# Server settings
|
||||
SERVER_TRANSPORT = TransportLayerEnum[os.getenv("SERVER_TRANSPORT", "STDIO")]
|
||||
SERVER_HOST = str(os.getenv("SERVER_HOST", "localhost"))
|
||||
SERVER_PORT = int(os.getenv("SERVER_PORT", "4200"))
|
||||
SERVER_PATH = str(os.getenv("SERVER_PATH", "/mcp/docs"))
|
||||
|
||||
|
||||
# Docs related settings
|
||||
DOCS_API_URL = str(os.getenv("DOCS_API_URL", "http://localhost:8071"))
|
||||
DOCS_API_TOKEN = str(os.getenv("DOCS_API_TOKEN", "")) or None
|
||||
18
src/mcp_server/docs_mcp_server/utils.py
Normal file
18
src/mcp_server/docs_mcp_server/utils.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Helpers for the project, not named `tools` to avoid confusion."""
|
||||
|
||||
import logging
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
||||
logger = logging.getLogger("docs_mcp_server")
|
||||
|
||||
|
||||
async def check_mcp(mcp: FastMCP):
|
||||
"""List the MCP server components (tools, resources and templates)."""
|
||||
tools = await mcp.get_tools()
|
||||
resources = await mcp.get_resources()
|
||||
templates = await mcp.get_resource_templates()
|
||||
|
||||
logger.info("%d Tool(s): %s", len(tools), ", ".join([t.name for t in tools.values()]))
|
||||
logger.info("%d Resource(s): %s", len(resources), ", ".join([r.name for r in resources.values()]))
|
||||
logger.info("%d Resource Template(s): %s", len(templates), ", ".join([t.name for t in templates.values()]))
|
||||
98
src/mcp_server/pyproject.toml
Normal file
98
src/mcp_server/pyproject.toml
Normal file
@@ -0,0 +1,98 @@
|
||||
[project]
|
||||
name = "mcp-server"
|
||||
dynamic = ["version"]
|
||||
description = "MCP server implementation for Docs"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
license = {file = "LICENSE"}
|
||||
authors = [
|
||||
{name = "DINUM", email = "dev@mail.numerique.gouv.fr"},
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"dotenv>=0.9.9",
|
||||
"fastmcp>=2.3.4",
|
||||
"httpx>=0.28.1",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"ruff",
|
||||
]
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "docs_mcp_server.__version__"}
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py310"
|
||||
lint.select = [
|
||||
# pycodestyle
|
||||
"E", "W",
|
||||
# Pyflakes
|
||||
"F",
|
||||
# pyupgrade
|
||||
"UP",
|
||||
# flake8-bugbear
|
||||
"B",
|
||||
# flake8-simplify
|
||||
"SIM",
|
||||
# isort
|
||||
"I",
|
||||
# flake8-logging-format
|
||||
"G",
|
||||
# flake8-pie
|
||||
"PIE",
|
||||
# flake8-comprehensions
|
||||
"C4",
|
||||
# flake8-django
|
||||
"DJ",
|
||||
# flake8-bandit
|
||||
"S",
|
||||
# flake8-builtins
|
||||
"A",
|
||||
# flake8-datetimez
|
||||
"DTZ",
|
||||
# flake8-gettext
|
||||
"INT",
|
||||
# Pylint
|
||||
"PL",
|
||||
# flake8-fixme
|
||||
"FIX",
|
||||
# flake8-self
|
||||
"SLF",
|
||||
# flake8-return
|
||||
"RET",
|
||||
# pep8-naming (N)
|
||||
"N",
|
||||
# pydocstyle
|
||||
"D",
|
||||
# flake8-pytest-style (PT)
|
||||
"PT",
|
||||
]
|
||||
lint.ignore = [
|
||||
# incorrect-blank-line-before-class
|
||||
"D203",
|
||||
# missing-blank-line-after-summary
|
||||
"D205",
|
||||
# multi-line-summary-first-line
|
||||
"D212",
|
||||
]
|
||||
lint.per-file-ignores = {"**/tests/*"= [
|
||||
# flake8-bandit
|
||||
"S",
|
||||
# flake8-self
|
||||
"SLF",
|
||||
# magic-value-comparison
|
||||
"PLR2004",
|
||||
]}
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["mcp_server"]
|
||||
488
src/mcp_server/uv.lock
generated
Normal file
488
src/mcp_server/uv.lock
generated
Normal file
@@ -0,0 +1,488 @@
|
||||
version = 1
|
||||
revision = 1
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.4.26"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenv"
|
||||
version = "0.9.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "python-dotenv" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastmcp"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup" },
|
||||
{ name = "httpx" },
|
||||
{ name = "mcp" },
|
||||
{ name = "openapi-pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "rich" },
|
||||
{ name = "typer" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/11/2ccd6219eb65692a298e764fa84a15fd756e03c811c7ea217129d6ca545f/fastmcp-2.4.0.tar.gz", hash = "sha256:a08d812939d16c0d4490bdbdaf17ab136f1bdaa8ddcc14a37e33335727343c05", size = 1020290 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/a4/26396706c1b48ebd051da4f7d321594207364077a5c308827722536b927c/fastmcp-2.4.0-py3-none-any.whl", hash = "sha256:fccd0768028a31eec488707fb6bbe4f8659f84ca0c206c4c32dd33947c0faae9", size = 101108 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx-sse" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette" },
|
||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/8d/0f4468582e9e97b0a24604b585c651dfd2144300ecffd1c06a680f5c8861/mcp-1.9.0.tar.gz", hash = "sha256:905d8d208baf7e3e71d70c82803b89112e321581bcd2530f9de0fe4103d28749", size = 281432 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/d5/22e36c95c83c80eb47c83f231095419cf57cf5cca5416f1c960032074c78/mcp-1.9.0-py3-none-any.whl", hash = "sha256:9dfb89c8c56f742da10a5910a1f64b0d2ac2c3ed2bd572ddb1cfab7f35957178", size = 125082 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp-server"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "dotenv" },
|
||||
{ name = "fastmcp" },
|
||||
{ name = "httpx" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "dotenv", specifier = ">=0.9.9" },
|
||||
{ name = "fastmcp", specifier = ">=2.3.4" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "ruff", marker = "extra == 'dev'" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openapi-pydantic"
|
||||
version = "0.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.33.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.20"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.11.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/4c/4a3c5a97faaae6b428b336dcca81d03ad04779f8072c267ad2bd860126bf/ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6", size = 4165632 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/9f/596c628f8824a2ce4cd12b0f0b4c0629a62dfffc5d0f742c19a1d71be108/ruff-0.11.10-py3-none-linux_armv6l.whl", hash = "sha256:859a7bfa7bc8888abbea31ef8a2b411714e6a80f0d173c2a82f9041ed6b50f58", size = 10316243 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/38/c1e0b77ab58b426f8c332c1d1d3432d9fc9a9ea622806e208220cb133c9e/ruff-0.11.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:968220a57e09ea5e4fd48ed1c646419961a0570727c7e069842edd018ee8afed", size = 11083636 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/41/b75e15961d6047d7fe1b13886e56e8413be8467a4e1be0a07f3b303cd65a/ruff-0.11.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1067245bad978e7aa7b22f67113ecc6eb241dca0d9b696144256c3a879663bca", size = 10441624 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/2c/e396b6703f131406db1811ea3d746f29d91b41bbd43ad572fea30da1435d/ruff-0.11.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4854fd09c7aed5b1590e996a81aeff0c9ff51378b084eb5a0b9cd9518e6cff2", size = 10624358 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/8c/ee6cca8bdaf0f9a3704796022851a33cd37d1340bceaf4f6e991eb164e2e/ruff-0.11.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b4564e9f99168c0f9195a0fd5fa5928004b33b377137f978055e40008a082c5", size = 10176850 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/ce/4e27e131a434321b3b7c66512c3ee7505b446eb1c8a80777c023f7e876e6/ruff-0.11.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b6a9cc5b62c03cc1fea0044ed8576379dbaf751d5503d718c973d5418483641", size = 11759787 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/de/1e2e77fc72adc7cf5b5123fd04a59ed329651d3eab9825674a9e640b100b/ruff-0.11.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:607ecbb6f03e44c9e0a93aedacb17b4eb4f3563d00e8b474298a201622677947", size = 12430479 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/ed/af0f2340f33b70d50121628ef175523cc4c37619e98d98748c85764c8d88/ruff-0.11.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b3a522fa389402cd2137df9ddefe848f727250535c70dafa840badffb56b7a4", size = 11919760 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/09/d7b3d3226d535cb89234390f418d10e00a157b6c4a06dfbe723e9322cb7d/ruff-0.11.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f071b0deed7e9245d5820dac235cbdd4ef99d7b12ff04c330a241ad3534319f", size = 14041747 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/b3/a63b4e91850e3f47f78795e6630ee9266cb6963de8f0191600289c2bb8f4/ruff-0.11.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a60e3a0a617eafba1f2e4186d827759d65348fa53708ca547e384db28406a0b", size = 11550657 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/63/a4f95c241d79402ccdbdb1d823d156c89fbb36ebfc4289dce092e6c0aa8f/ruff-0.11.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:da8ec977eaa4b7bf75470fb575bea2cb41a0e07c7ea9d5a0a97d13dbca697bf2", size = 10489671 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/9b/c2238bfebf1e473495659c523d50b1685258b6345d5ab0b418ca3f010cd7/ruff-0.11.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ddf8967e08227d1bd95cc0851ef80d2ad9c7c0c5aab1eba31db49cf0a7b99523", size = 10160135 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/ef/ba7251dd15206688dbfba7d413c0312e94df3b31b08f5d695580b755a899/ruff-0.11.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5a94acf798a82db188f6f36575d80609072b032105d114b0f98661e1679c9125", size = 11170179 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/9f/5c336717293203ba275dbfa2ea16e49b29a9fd9a0ea8b6febfc17e133577/ruff-0.11.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3afead355f1d16d95630df28d4ba17fb2cb9c8dfac8d21ced14984121f639bad", size = 11626021 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/2b/162fa86d2639076667c9aa59196c020dc6d7023ac8f342416c2f5ec4bda0/ruff-0.11.10-py3-none-win32.whl", hash = "sha256:dc061a98d32a97211af7e7f3fa1d4ca2fcf919fb96c28f39551f35fc55bdbc19", size = 10494958 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/f3/66643d8f32f50a4b0d09a4832b7d919145ee2b944d43e604fbd7c144d175/ruff-0.11.10-py3-none-win_amd64.whl", hash = "sha256:5cc725fbb4d25b0f185cb42df07ab6b76c4489b4bfb740a175f3a59c70e8a224", size = 11650285 },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/3a/2e8704d19f376c799748ff9cb041225c1d59f3e7711bc5596c8cfdc24925/ruff-0.11.10-py3-none-win_arm64.whl", hash = "sha256:ef69637b35fb8b210743926778d0e45e1bffa850a7c61e428c6b971549b5f5d1", size = 10765278 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "2.3.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/5f/28f45b1ff14bee871bacafd0a97213f7ec70e389939a80c60c0fb72a9fc9/sse_starlette-2.3.5.tar.gz", hash = "sha256:228357b6e42dcc73a427990e2b4a03c023e2495ecee82e14f07ba15077e334b2", size = 17511 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/48/3e49cf0f64961656402c0023edbc51844fe17afe53ab50e958a6dbbbd499/sse_starlette-2.3.5-py3-none-any.whl", hash = "sha256:251708539a335570f10eaaa21d1848a10c42ee6dc3a9cf37ef42266cdb1c52a8", size = 10233 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.46.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.15.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/89/c527e6c848739be8ceb5c44eb8208c52ea3515c6cf6406aa61932887bf58/typer-0.15.4.tar.gz", hash = "sha256:89507b104f9b6a0730354f27c39fae5b63ccd0c95b1ce1f1a6ba0cfd329997c3", size = 101559 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/62/d4ba7afe2096d5659ec3db8b15d8665bdcb92a3c6ff0b95e99895b335a9c/typer-0.15.4-py3-none-any.whl", hash = "sha256:eb0651654dcdea706780c466cf06d8f174405a659ffff8f163cfbfee98c0e173", size = 45258 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.13.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.34.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "15.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 },
|
||||
]
|
||||
Reference in New Issue
Block a user