mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-06-24 22:12:00 +00:00
perf: some swagger api (#15203)
* perf: some swagger api * perf: update deps * perf: Update Dockerfile with new base image tag --------- Co-authored-by: ibuler <ibuler@qq.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
8b9fe3c72b
commit
5390fbacec
@ -1,4 +1,4 @@
|
|||||||
FROM jumpserver/core-base:20250224_065619 AS stage-build
|
FROM jumpserver/core-base:20250415_032719 AS stage-build
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ class VirtualAccountViewSet(OrgBulkModelViewSet):
|
|||||||
serializer_class = VirtualAccountSerializer
|
serializer_class = VirtualAccountSerializer
|
||||||
search_fields = ('alias',)
|
search_fields = ('alias',)
|
||||||
filterset_fields = ('alias',)
|
filterset_fields = ('alias',)
|
||||||
|
http_method_names = ['get']
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return VirtualAccount.get_or_init_queryset()
|
return VirtualAccount.get_or_init_queryset()
|
||||||
|
@ -39,9 +39,10 @@ class AutomationAssetsListApi(generics.ListAPIView):
|
|||||||
return assets
|
return assets
|
||||||
|
|
||||||
|
|
||||||
class AutomationRemoveAssetApi(generics.RetrieveUpdateAPIView):
|
class AutomationRemoveAssetApi(generics.UpdateAPIView):
|
||||||
model = BaseAutomation
|
model = BaseAutomation
|
||||||
serializer_class = serializers.UpdateAssetSerializer
|
serializer_class = serializers.UpdateAssetSerializer
|
||||||
|
http_method_names = ['put']
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
@ -56,9 +57,10 @@ class AutomationRemoveAssetApi(generics.RetrieveUpdateAPIView):
|
|||||||
return Response({'msg': 'ok'})
|
return Response({'msg': 'ok'})
|
||||||
|
|
||||||
|
|
||||||
class AutomationAddAssetApi(generics.RetrieveUpdateAPIView):
|
class AutomationAddAssetApi(generics.UpdateAPIView):
|
||||||
model = BaseAutomation
|
model = BaseAutomation
|
||||||
serializer_class = serializers.UpdateAssetSerializer
|
serializer_class = serializers.UpdateAssetSerializer
|
||||||
|
http_method_names = ['put']
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
@ -72,9 +74,10 @@ class AutomationAddAssetApi(generics.RetrieveUpdateAPIView):
|
|||||||
return Response({"error": serializer.errors})
|
return Response({"error": serializer.errors})
|
||||||
|
|
||||||
|
|
||||||
class AutomationNodeAddRemoveApi(generics.RetrieveUpdateAPIView):
|
class AutomationNodeAddRemoveApi(generics.UpdateAPIView):
|
||||||
model = BaseAutomation
|
model = BaseAutomation
|
||||||
serializer_class = serializers.UpdateNodeSerializer
|
serializer_class = serializers.UpdateNodeSerializer
|
||||||
|
http_method_names = ['put']
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
action_params = ['add', 'remove']
|
action_params = ['add', 'remove']
|
||||||
|
@ -147,6 +147,7 @@ class CheckAccountEngineViewSet(JMSModelViewSet):
|
|||||||
serializer_class = serializers.CheckAccountEngineSerializer
|
serializer_class = serializers.CheckAccountEngineSerializer
|
||||||
permission_classes = [RBACPermission, IsValidLicense]
|
permission_classes = [RBACPermission, IsValidLicense]
|
||||||
perm_model = CheckAccountEngine
|
perm_model = CheckAccountEngine
|
||||||
|
http_method_names = ['get']
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return CheckAccountEngine.get_default_engines()
|
return CheckAccountEngine.get_default_engines()
|
||||||
|
@ -97,7 +97,7 @@ class AssetFilterSet(BaseFilterSet):
|
|||||||
return queryset.filter(protocols__name__in=value).distinct()
|
return queryset.filter(protocols__name__in=value).distinct()
|
||||||
|
|
||||||
|
|
||||||
class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
class BaseAssetViewSet(OrgBulkModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint that allows Asset to be viewed or edited.
|
API endpoint that allows Asset to be viewed or edited.
|
||||||
"""
|
"""
|
||||||
@ -143,6 +143,19 @@ class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
|||||||
return retrieve_cls
|
return retrieve_cls
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
if request.path.find('/api/v1/assets/assets/') > -1:
|
||||||
|
error = _('Cannot create asset directly, you should create a host or other')
|
||||||
|
return Response({'error': error}, status=400)
|
||||||
|
|
||||||
|
if not settings.XPACK_LICENSE_IS_VALID and self.model.objects.order_by().count() >= 5000:
|
||||||
|
error = _('The number of assets exceeds the limit of 5000')
|
||||||
|
return Response({'error': error}, status=400)
|
||||||
|
|
||||||
|
return super().create(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class AssetViewSet(SuggestionMixin, BaseAssetViewSet):
|
||||||
@action(methods=["GET"], detail=True, url_path="platform")
|
@action(methods=["GET"], detail=True, url_path="platform")
|
||||||
def platform(self, *args, **kwargs):
|
def platform(self, *args, **kwargs):
|
||||||
asset = super().get_object()
|
asset = super().get_object()
|
||||||
@ -197,17 +210,6 @@ class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
|||||||
Protocol.objects.bulk_create(objs)
|
Protocol.objects.bulk_create(objs)
|
||||||
return Response(status=status.HTTP_200_OK)
|
return Response(status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
|
||||||
if request.path.find('/api/v1/assets/assets/') > -1:
|
|
||||||
error = _('Cannot create asset directly, you should create a host or other')
|
|
||||||
return Response({'error': error}, status=400)
|
|
||||||
|
|
||||||
if not settings.XPACK_LICENSE_IS_VALID and self.model.objects.order_by().count() >= 5000:
|
|
||||||
error = _('The number of assets exceeds the limit of 5000')
|
|
||||||
return Response({'error': error}, status=400)
|
|
||||||
|
|
||||||
return super().create(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def filter_bulk_update_data(self):
|
def filter_bulk_update_data(self):
|
||||||
bulk_data = []
|
bulk_data = []
|
||||||
skip_assets = []
|
skip_assets = []
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
from assets.models import Cloud, Asset
|
from assets.models import Cloud, Asset
|
||||||
from assets.serializers import CloudSerializer
|
from assets.serializers import CloudSerializer
|
||||||
|
|
||||||
from .asset import AssetViewSet
|
from .asset import BaseAssetViewSet
|
||||||
|
|
||||||
__all__ = ['CloudViewSet']
|
__all__ = ['CloudViewSet']
|
||||||
|
|
||||||
|
|
||||||
class CloudViewSet(AssetViewSet):
|
class CloudViewSet(BaseAssetViewSet):
|
||||||
model = Cloud
|
model = Cloud
|
||||||
perm_model = Asset
|
perm_model = Asset
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
from assets.models import Custom, Asset
|
from assets.models import Custom, Asset
|
||||||
from assets.serializers import CustomSerializer
|
from assets.serializers import CustomSerializer
|
||||||
|
|
||||||
from .asset import AssetViewSet
|
from .asset import BaseAssetViewSet
|
||||||
|
|
||||||
__all__ = ['CustomViewSet']
|
__all__ = ['CustomViewSet']
|
||||||
|
|
||||||
|
|
||||||
class CustomViewSet(AssetViewSet):
|
class CustomViewSet(BaseAssetViewSet):
|
||||||
model = Custom
|
model = Custom
|
||||||
perm_model = Asset
|
perm_model = Asset
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
from assets.models import Database, Asset
|
from assets.models import Database, Asset
|
||||||
from assets.serializers import DatabaseSerializer
|
from assets.serializers import DatabaseSerializer
|
||||||
|
|
||||||
from .asset import AssetViewSet
|
from .asset import BaseAssetViewSet
|
||||||
|
|
||||||
__all__ = ['DatabaseViewSet']
|
__all__ = ['DatabaseViewSet']
|
||||||
|
|
||||||
|
|
||||||
class DatabaseViewSet(AssetViewSet):
|
class DatabaseViewSet(BaseAssetViewSet):
|
||||||
model = Database
|
model = Database
|
||||||
perm_model = Asset
|
perm_model = Asset
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
from assets.serializers import DeviceSerializer
|
|
||||||
from assets.models import Device, Asset
|
from assets.models import Device, Asset
|
||||||
from .asset import AssetViewSet
|
from assets.serializers import DeviceSerializer
|
||||||
|
from .asset import BaseAssetViewSet
|
||||||
|
|
||||||
__all__ = ['DeviceViewSet']
|
__all__ = ['DeviceViewSet']
|
||||||
|
|
||||||
|
|
||||||
class DeviceViewSet(AssetViewSet):
|
class DeviceViewSet(BaseAssetViewSet):
|
||||||
model = Device
|
model = Device
|
||||||
perm_model = Asset
|
perm_model = Asset
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
from assets.models import DirectoryService, Asset
|
from assets.models import DirectoryService, Asset
|
||||||
from assets.serializers import DSSerializer
|
from assets.serializers import DSSerializer
|
||||||
|
|
||||||
from .asset import AssetViewSet
|
from .asset import BaseAssetViewSet
|
||||||
|
|
||||||
__all__ = ['DSViewSet']
|
__all__ = ['DSViewSet']
|
||||||
|
|
||||||
|
|
||||||
class DSViewSet(AssetViewSet):
|
class DSViewSet(BaseAssetViewSet):
|
||||||
model = DirectoryService
|
model = DirectoryService
|
||||||
perm_model = Asset
|
perm_model = Asset
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
from assets.models import GPT, Asset
|
from assets.models import GPT, Asset
|
||||||
from assets.serializers import GPTSerializer
|
from assets.serializers import GPTSerializer
|
||||||
|
|
||||||
from .asset import AssetViewSet
|
from .asset import BaseAssetViewSet
|
||||||
|
|
||||||
__all__ = ['GPTViewSet']
|
__all__ = ['GPTViewSet']
|
||||||
|
|
||||||
|
|
||||||
class GPTViewSet(AssetViewSet):
|
class GPTViewSet(BaseAssetViewSet):
|
||||||
model = GPT
|
model = GPT
|
||||||
perm_model = Asset
|
perm_model = Asset
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
from assets.models import Host, Asset
|
from assets.models import Host, Asset
|
||||||
from assets.serializers import HostSerializer
|
from assets.serializers import HostSerializer
|
||||||
from .asset import AssetViewSet
|
from .asset import BaseAssetViewSet
|
||||||
|
|
||||||
__all__ = ['HostViewSet']
|
__all__ = ['HostViewSet']
|
||||||
|
|
||||||
|
|
||||||
class HostViewSet(AssetViewSet):
|
class HostViewSet(BaseAssetViewSet):
|
||||||
model = Host
|
model = Host
|
||||||
perm_model = Asset
|
perm_model = Asset
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
from assets.models import Web, Asset
|
from assets.models import Web, Asset
|
||||||
from assets.serializers import WebSerializer
|
from assets.serializers import WebSerializer
|
||||||
|
|
||||||
from .asset import AssetViewSet
|
from .asset import BaseAssetViewSet
|
||||||
|
|
||||||
__all__ = ['WebViewSet']
|
__all__ = ['WebViewSet']
|
||||||
|
|
||||||
|
|
||||||
class WebViewSet(AssetViewSet):
|
class WebViewSet(BaseAssetViewSet):
|
||||||
model = Web
|
model = Web
|
||||||
perm_model = Asset
|
perm_model = Asset
|
||||||
|
|
||||||
|
@ -188,6 +188,9 @@ class ActivityUnionLogSerializer(serializers.Serializer):
|
|||||||
class FileSerializer(serializers.Serializer):
|
class FileSerializer(serializers.Serializer):
|
||||||
file = serializers.FileField(allow_empty_file=True)
|
file = serializers.FileField(allow_empty_file=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ref_name = 'AuditFileSerializer'
|
||||||
|
|
||||||
|
|
||||||
class UserSessionSerializer(serializers.ModelSerializer):
|
class UserSessionSerializer(serializers.ModelSerializer):
|
||||||
type = LabeledChoiceField(choices=LoginTypeChoices.choices, label=_("Type"))
|
type = LabeledChoiceField(choices=LoginTypeChoices.choices, label=_("Type"))
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import uuid
|
||||||
|
|
||||||
import private_storage.urls
|
import private_storage.urls
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -74,12 +75,19 @@ urlpatterns += [
|
|||||||
path('core/jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
|
path('core/jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
DOC_TTL = 60 * 60
|
||||||
|
DOC_VERSION = uuid.uuid4().hex
|
||||||
|
cache_kwargs = {
|
||||||
|
'cache_timeout': DOC_TTL,
|
||||||
|
'cache_kwargs': {
|
||||||
|
'key_prefix': 'swagger-cache-' + DOC_VERSION,
|
||||||
|
},
|
||||||
|
}
|
||||||
# docs 路由
|
# docs 路由
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
re_path('^api/swagger(?P<format>\.json|\.yaml)$',
|
path('api/swagger.<format>', views.get_swagger_view(False).without_ui(**cache_kwargs), name='schema-json'),
|
||||||
views.get_swagger_view().without_ui(cache_timeout=1), name='schema-json'),
|
re_path('api/docs/?', views.get_swagger_view().with_ui('swagger', **cache_kwargs), name="docs"),
|
||||||
re_path('api/docs/?', views.get_swagger_view().with_ui('swagger', cache_timeout=1), name="docs"),
|
re_path('api/redoc/?', views.get_swagger_view().with_ui('redoc', **cache_kwargs), name='redoc'),
|
||||||
re_path('api/redoc/?', views.get_swagger_view().with_ui('redoc', cache_timeout=1), name='redoc'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if os.environ.get('DEBUG_TOOLBAR', False):
|
if os.environ.get('DEBUG_TOOLBAR', False):
|
||||||
|
@ -1,8 +1,54 @@
|
|||||||
from drf_yasg.inspectors import SwaggerAutoSchema
|
|
||||||
|
|
||||||
from rest_framework import permissions
|
|
||||||
from drf_yasg.views import get_schema_view
|
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
|
from drf_yasg.generators import OpenAPISchemaGenerator
|
||||||
|
from drf_yasg.inspectors import SwaggerAutoSchema
|
||||||
|
from drf_yasg.views import get_schema_view
|
||||||
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
|
||||||
|
class CustomSchemaGenerator(OpenAPISchemaGenerator):
|
||||||
|
from_mcp = False
|
||||||
|
|
||||||
|
def get_schema(self, request=None, public=False):
|
||||||
|
self.from_mcp = request.query_params.get('mcp') or request.path.endswith('swagger.json')
|
||||||
|
return super().get_schema(request, public)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def exclude_some_paths(path):
|
||||||
|
# 这里可以对 paths 进行处理
|
||||||
|
excludes = ['/report/', '/render-to-json/', '/suggestions/', 'executions', 'automations']
|
||||||
|
for p in excludes:
|
||||||
|
if path.find(p) >= 0:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def exclude_some_app(self, path):
|
||||||
|
parts = path.split('/')
|
||||||
|
if len(parts) < 4:
|
||||||
|
return False
|
||||||
|
|
||||||
|
apps = []
|
||||||
|
if self.from_mcp:
|
||||||
|
apps = [
|
||||||
|
'ops', 'tickets', 'common', 'authentication',
|
||||||
|
'settings', 'xpack', 'terminal', 'rbac'
|
||||||
|
]
|
||||||
|
|
||||||
|
app_name = parts[3]
|
||||||
|
if app_name in apps:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_operation(self, view, path, prefix, method, components, request):
|
||||||
|
# 这里可以对 path 进行处理
|
||||||
|
if self.exclude_some_paths(path):
|
||||||
|
return None
|
||||||
|
if self.exclude_some_app(path):
|
||||||
|
return None
|
||||||
|
operation = super().get_operation(view, path, prefix, method, components, request)
|
||||||
|
operation_id = operation.get('operationId')
|
||||||
|
if 'bulk' in operation_id:
|
||||||
|
return None
|
||||||
|
return operation
|
||||||
|
|
||||||
|
|
||||||
class CustomSwaggerAutoSchema(SwaggerAutoSchema):
|
class CustomSwaggerAutoSchema(SwaggerAutoSchema):
|
||||||
@ -59,17 +105,25 @@ api_info = openapi.Info(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_swagger_view(version='v1'):
|
def get_swagger_view(with_auth=True):
|
||||||
from ..urls import api_v1
|
from ..urls import api_v1
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
api_v1_patterns = [
|
patterns = [
|
||||||
path('api/v1/', include(api_v1))
|
path('api/v1/', include(api_v1))
|
||||||
]
|
]
|
||||||
patterns = api_v1_patterns
|
|
||||||
|
if with_auth:
|
||||||
|
permission_classes = (permissions.IsAuthenticated,)
|
||||||
|
public = False
|
||||||
|
else:
|
||||||
|
permission_classes = []
|
||||||
|
public = True
|
||||||
|
|
||||||
schema_view = get_schema_view(
|
schema_view = get_schema_view(
|
||||||
api_info,
|
api_info,
|
||||||
|
public=public,
|
||||||
patterns=patterns,
|
patterns=patterns,
|
||||||
permission_classes=(permissions.IsAuthenticated,),
|
generator_class=CustomSchemaGenerator,
|
||||||
|
permission_classes=permission_classes
|
||||||
)
|
)
|
||||||
return schema_view
|
return schema_view
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_filters import rest_framework as filters
|
from django_filters import rest_framework as filters
|
||||||
@ -10,7 +9,7 @@ from rest_framework import generics
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.views import APIView, Response
|
from rest_framework.views import APIView, Response
|
||||||
|
|
||||||
from common.api import JMSBulkModelViewSet
|
from common.api import JMSModelViewSet
|
||||||
from common.drf.filters import BaseFilterSet
|
from common.drf.filters import BaseFilterSet
|
||||||
from common.exceptions import JMSException
|
from common.exceptions import JMSException
|
||||||
from common.permissions import WithBootstrapToken, IsServiceAccount
|
from common.permissions import WithBootstrapToken, IsServiceAccount
|
||||||
@ -43,7 +42,7 @@ class TerminalFilterSet(BaseFilterSet):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class TerminalViewSet(JMSBulkModelViewSet):
|
class TerminalViewSet(JMSModelViewSet):
|
||||||
queryset = Terminal.objects.filter(is_deleted=False)
|
queryset = Terminal.objects.filter(is_deleted=False)
|
||||||
serializer_class = serializers.TerminalSerializer
|
serializer_class = serializers.TerminalSerializer
|
||||||
filterset_class = TerminalFilterSet
|
filterset_class = TerminalFilterSet
|
||||||
|
@ -12,7 +12,6 @@ app_name = 'terminal'
|
|||||||
router = BulkRouter()
|
router = BulkRouter()
|
||||||
router.register(r'sessions', api.SessionViewSet, 'session')
|
router.register(r'sessions', api.SessionViewSet, 'session')
|
||||||
router.register(r'terminals/((?P<terminal>[^/.]{36})/)?status', api.StatusViewSet, 'terminal-status')
|
router.register(r'terminals/((?P<terminal>[^/.]{36})/)?status', api.StatusViewSet, 'terminal-status')
|
||||||
router.register(r'terminals/((?P<terminal>[^/.]{36})/)?sessions', api.SessionViewSet, 'terminal-sessions')
|
|
||||||
router.register(r'terminals', api.TerminalViewSet, 'terminal')
|
router.register(r'terminals', api.TerminalViewSet, 'terminal')
|
||||||
router.register(r'tasks', api.TaskViewSet, 'tasks')
|
router.register(r'tasks', api.TaskViewSet, 'tasks')
|
||||||
router.register(r'commands', api.CommandViewSet, 'command')
|
router.register(r'commands', api.CommandViewSet, 'command')
|
||||||
|
2265
poetry.lock
generated
2265
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -99,7 +99,7 @@ django-private-storage = "3.1"
|
|||||||
drf-nested-routers = "0.93.4"
|
drf-nested-routers = "0.93.4"
|
||||||
drf-writable-nested = "0.7.0"
|
drf-writable-nested = "0.7.0"
|
||||||
rest-condition = "1.0.3"
|
rest-condition = "1.0.3"
|
||||||
drf-yasg = "1.21.7"
|
drf-yasg = "1.21.10"
|
||||||
coreapi = "2.3.3"
|
coreapi = "2.3.3"
|
||||||
coreschema = "0.0.4"
|
coreschema = "0.0.4"
|
||||||
openapi-codec = "1.3.2"
|
openapi-codec = "1.3.2"
|
||||||
@ -148,7 +148,7 @@ mistune = "2.0.3"
|
|||||||
openai = "^1.29.0"
|
openai = "^1.29.0"
|
||||||
xlsxwriter = "^3.1.9"
|
xlsxwriter = "^3.1.9"
|
||||||
exchangelib = "^5.1.0"
|
exchangelib = "^5.1.0"
|
||||||
xmlsec = "^1.3.13"
|
xmlsec = "1.3.14"
|
||||||
lxml = "5.2.1"
|
lxml = "5.2.1"
|
||||||
pydantic = "^2.7.4"
|
pydantic = "^2.7.4"
|
||||||
annotated-types = "^0.6.0"
|
annotated-types = "^0.6.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user