mirror of
https://github.com/FunnyWolf/agentic-soc-platform.git
synced 2026-08-22 13:12:56 +02:00
Implement ASP CLI agent API foundation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+2
-1
@@ -34,4 +34,5 @@ node_modules/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
.claude/*
|
||||
.claude/*
|
||||
.asp/
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AgentApiConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.agent_api"
|
||||
@@ -0,0 +1,24 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
def request_id(request):
|
||||
return request.headers.get("X-Request-ID") or f"req_{uuid4().hex}"
|
||||
|
||||
|
||||
def agent_response(request, *, operation, data, status=200, pagination=None):
|
||||
meta = {
|
||||
"operation": operation,
|
||||
"request_id": request_id(request),
|
||||
}
|
||||
if pagination is not None:
|
||||
meta["pagination"] = pagination
|
||||
return Response({"data": data, "meta": meta}, status=status)
|
||||
|
||||
|
||||
def pagination_meta(page):
|
||||
return {
|
||||
"next_cursor": page.next_cursor,
|
||||
"has_more": page.has_more,
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import mimetypes
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
def dt(value):
|
||||
return value.isoformat() if value else None
|
||||
|
||||
|
||||
def serialize_case(case, *, include_related=False):
|
||||
data = {
|
||||
"case_id": case.case_id,
|
||||
"title": case.title,
|
||||
"severity": case.severity,
|
||||
"confidence": case.confidence,
|
||||
"impact": case.impact,
|
||||
"priority": case.priority,
|
||||
"status": case.status,
|
||||
"verdict": case.verdict,
|
||||
"severity_ai": case.severity_ai,
|
||||
"confidence_ai": case.confidence_ai,
|
||||
"impact_ai": case.impact_ai,
|
||||
"priority_ai": case.priority_ai,
|
||||
"verdict_ai": case.verdict_ai,
|
||||
"summary": case.summary,
|
||||
"correlation_uid": case.correlation_uid,
|
||||
"tags": case.tags,
|
||||
"created_at": dt(case.created_at),
|
||||
"updated_at": dt(case.updated_at),
|
||||
}
|
||||
if include_related:
|
||||
data["alerts"] = [serialize_alert(alert, include_related=False) for alert in case.alerts.all()[:50]]
|
||||
return data
|
||||
|
||||
|
||||
def serialize_alert(alert, *, include_related=False):
|
||||
data = {
|
||||
"alert_id": alert.alert_id,
|
||||
"case_id": alert.case.case_id if alert.case_id else "",
|
||||
"title": alert.title,
|
||||
"severity": alert.severity,
|
||||
"confidence": alert.confidence,
|
||||
"impact": alert.impact,
|
||||
"status": alert.status,
|
||||
"correlation_uid": alert.correlation_uid,
|
||||
"source_uid": alert.source_uid,
|
||||
"rule_id": alert.rule_id,
|
||||
"rule_name": alert.rule_name,
|
||||
"created_at": dt(alert.created_at),
|
||||
"updated_at": dt(alert.updated_at),
|
||||
}
|
||||
if include_related:
|
||||
data["artifacts"] = [serialize_artifact(artifact, include_related=False) for artifact in alert.artifacts.all()[:50]]
|
||||
return data
|
||||
|
||||
|
||||
def serialize_artifact(artifact, *, include_related=False):
|
||||
data = {
|
||||
"artifact_id": artifact.artifact_id,
|
||||
"name": artifact.name,
|
||||
"type": artifact.type,
|
||||
"role": artifact.role,
|
||||
"value": artifact.value,
|
||||
"created_at": dt(artifact.created_at),
|
||||
"updated_at": dt(artifact.updated_at),
|
||||
}
|
||||
if include_related:
|
||||
data["alerts"] = [
|
||||
{
|
||||
"alert_id": alert.alert_id,
|
||||
"case_id": alert.case.case_id if alert.case_id else "",
|
||||
"title": alert.title,
|
||||
"severity": alert.severity,
|
||||
"status": alert.status,
|
||||
}
|
||||
for alert in artifact.alerts.select_related("case").all()[:50]
|
||||
]
|
||||
return data
|
||||
|
||||
|
||||
def serialize_knowledge(knowledge):
|
||||
return {
|
||||
"knowledge_id": knowledge.knowledge_id,
|
||||
"title": knowledge.title,
|
||||
"body": knowledge.body,
|
||||
"expires_at": dt(knowledge.expires_at),
|
||||
"source": knowledge.source,
|
||||
"tags": knowledge.tags,
|
||||
"case_id": knowledge.case.case_id if knowledge.case_id else "",
|
||||
"created_at": dt(knowledge.created_at),
|
||||
"updated_at": dt(knowledge.updated_at),
|
||||
}
|
||||
|
||||
|
||||
def serialize_attachment(attachment, *, request=None):
|
||||
path = reverse("attachment-download", kwargs={"access_key": attachment.access_key})
|
||||
url = request.build_absolute_uri(path) if request is not None else path
|
||||
return {
|
||||
"file_key": str(attachment.access_key),
|
||||
"filename": attachment.filename,
|
||||
"size": attachment.size,
|
||||
"content_type": mimetypes.guess_type(attachment.filename)[0] or "application/octet-stream",
|
||||
"download_url": url,
|
||||
"uploaded_at": dt(attachment.uploaded_at),
|
||||
}
|
||||
|
||||
|
||||
def serialize_comment(comment, *, request=None):
|
||||
return {
|
||||
"id": comment.id,
|
||||
"target": {
|
||||
"content_type": comment.content_type.model,
|
||||
"object_id": comment.object_id,
|
||||
},
|
||||
"body": comment.body,
|
||||
"author": comment.author.username if comment.author else "",
|
||||
"parent_id": comment.parent_id,
|
||||
"mentions": [user.username for user in comment.mentions.all()],
|
||||
"attachments": [serialize_attachment(item, request=request) for item in comment.attachments.all()],
|
||||
"created_at": dt(comment.created_at),
|
||||
"updated_at": dt(comment.updated_at),
|
||||
}
|
||||
|
||||
|
||||
def serialize_enrichment(enrichment):
|
||||
target = ""
|
||||
if enrichment.case_id:
|
||||
target = enrichment.case.case_id
|
||||
elif enrichment.alert_id:
|
||||
target = enrichment.alert.alert_id
|
||||
elif enrichment.artifact_id:
|
||||
target = enrichment.artifact.artifact_id
|
||||
return {
|
||||
"enrichment_id": enrichment.enrichment_id,
|
||||
"target_id": target,
|
||||
"name": enrichment.name,
|
||||
"type": enrichment.type,
|
||||
"provider": enrichment.provider,
|
||||
"uid": enrichment.uid,
|
||||
"value": enrichment.value,
|
||||
"desc": enrichment.desc,
|
||||
"data": enrichment.data,
|
||||
"created_at": dt(enrichment.created_at),
|
||||
"updated_at": dt(enrichment.updated_at),
|
||||
}
|
||||
|
||||
|
||||
def serialize_playbook(playbook, *, include_related=False):
|
||||
data = {
|
||||
"playbook_id": playbook.playbook_id,
|
||||
"case_id": playbook.case.case_id if playbook.case_id else "",
|
||||
"name": playbook.name,
|
||||
"user_input": playbook.user_input,
|
||||
"job_status": playbook.job_status,
|
||||
"job_id": playbook.job_id,
|
||||
"remark": playbook.remark,
|
||||
"created_at": dt(playbook.created_at),
|
||||
"updated_at": dt(playbook.updated_at),
|
||||
}
|
||||
if include_related and playbook.case_id:
|
||||
data["case"] = serialize_case(playbook.case, include_related=False)
|
||||
return data
|
||||
@@ -0,0 +1,65 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import (
|
||||
AgentVersionView,
|
||||
AlertDetailView,
|
||||
AlertListView,
|
||||
ArtifactDetailView,
|
||||
ArtifactListView,
|
||||
CaseAIAnalysisView,
|
||||
CaseDetailView,
|
||||
CaseListView,
|
||||
CommentListCreateView,
|
||||
EnrichmentCreateView,
|
||||
FileDetailView,
|
||||
FileReadTextView,
|
||||
FileUploadView,
|
||||
KnowledgeDetailView,
|
||||
KnowledgeListView,
|
||||
CMDBLookupView,
|
||||
DevStreamHeadView,
|
||||
DevStreamReadView,
|
||||
PlaybookDetailView,
|
||||
PlaybookListView,
|
||||
PlaybookRunView,
|
||||
PlaybookTemplateListView,
|
||||
SIEMAdaptiveQueryView,
|
||||
SIEMDiscoverFieldsView,
|
||||
SIEMESQLQueryView,
|
||||
SIEMKeywordSearchView,
|
||||
SIEMSchemaView,
|
||||
SIEMSPLQueryView,
|
||||
ThreatIntelQueryView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("version/", AgentVersionView.as_view(), name="agent-api-version"),
|
||||
path("cases/", CaseListView.as_view(), name="agent-api-case-list"),
|
||||
path("cases/<str:case_id>/", CaseDetailView.as_view(), name="agent-api-case-detail"),
|
||||
path("cases/<str:case_id>/ai-analysis/", CaseAIAnalysisView.as_view(), name="agent-api-case-ai-analysis"),
|
||||
path("alerts/", AlertListView.as_view(), name="agent-api-alert-list"),
|
||||
path("alerts/<str:alert_id>/", AlertDetailView.as_view(), name="agent-api-alert-detail"),
|
||||
path("artifacts/", ArtifactListView.as_view(), name="agent-api-artifact-list"),
|
||||
path("artifacts/<str:artifact_id>/", ArtifactDetailView.as_view(), name="agent-api-artifact-detail"),
|
||||
path("knowledge/", KnowledgeListView.as_view(), name="agent-api-knowledge-list"),
|
||||
path("knowledge/<str:knowledge_id>/", KnowledgeDetailView.as_view(), name="agent-api-knowledge-detail"),
|
||||
path("comments/", CommentListCreateView.as_view(), name="agent-api-comment-list-create"),
|
||||
path("files/", FileUploadView.as_view(), name="agent-api-file-upload"),
|
||||
path("files/<uuid:file_key>/", FileDetailView.as_view(), name="agent-api-file-detail"),
|
||||
path("files/<uuid:file_key>/read-text/", FileReadTextView.as_view(), name="agent-api-file-read-text"),
|
||||
path("enrichments/", EnrichmentCreateView.as_view(), name="agent-api-enrichment-create"),
|
||||
path("playbooks/templates/", PlaybookTemplateListView.as_view(), name="agent-api-playbook-template-list"),
|
||||
path("playbooks/", PlaybookListView.as_view(), name="agent-api-playbook-list"),
|
||||
path("playbooks/run/", PlaybookRunView.as_view(), name="agent-api-playbook-run"),
|
||||
path("playbooks/<str:playbook_id>/", PlaybookDetailView.as_view(), name="agent-api-playbook-detail"),
|
||||
path("siem/schema/", SIEMSchemaView.as_view(), name="agent-api-siem-schema"),
|
||||
path("siem/search/keyword/", SIEMKeywordSearchView.as_view(), name="agent-api-siem-keyword-search"),
|
||||
path("siem/query/adaptive/", SIEMAdaptiveQueryView.as_view(), name="agent-api-siem-adaptive-query"),
|
||||
path("siem/query/spl/", SIEMSPLQueryView.as_view(), name="agent-api-siem-spl-query"),
|
||||
path("siem/query/esql/", SIEMESQLQueryView.as_view(), name="agent-api-siem-esql-query"),
|
||||
path("siem/fields/discover/", SIEMDiscoverFieldsView.as_view(), name="agent-api-siem-discover-fields"),
|
||||
path("threat-intel/query/", ThreatIntelQueryView.as_view(), name="agent-api-threat-intel-query"),
|
||||
path("cmdb/lookup/", CMDBLookupView.as_view(), name="agent-api-cmdb-lookup"),
|
||||
path("dev/streams/head/", DevStreamHeadView.as_view(), name="agent-api-dev-stream-head"),
|
||||
path("dev/streams/message/", DevStreamReadView.as_view(), name="agent-api-dev-stream-read"),
|
||||
]
|
||||
@@ -0,0 +1,42 @@
|
||||
from datetime import timezone as datetime_timezone
|
||||
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
|
||||
def bool_param(value, *, default=False):
|
||||
if value is None:
|
||||
return default
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
normalized = str(value).strip().lower()
|
||||
if not normalized:
|
||||
return default
|
||||
return normalized not in {"0", "false", "no", "off"}
|
||||
|
||||
|
||||
def list_param(query_params, name):
|
||||
values = []
|
||||
for raw_value in query_params.getlist(name):
|
||||
if raw_value is None:
|
||||
continue
|
||||
for item in str(raw_value).split(","):
|
||||
item = item.strip()
|
||||
if item:
|
||||
values.append(item)
|
||||
return values
|
||||
|
||||
|
||||
def parse_tags(query_params):
|
||||
return list_param(query_params, "tag") + list_param(query_params, "tags")
|
||||
|
||||
|
||||
def parse_timezone_aware_datetime(value, field_name):
|
||||
if value in (None, ""):
|
||||
return None
|
||||
parsed = parse_datetime(str(value).strip())
|
||||
if parsed is None:
|
||||
raise ValidationError({field_name: "Must be ISO 8601 datetime with timezone."})
|
||||
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
||||
raise ValidationError({field_name: "Must include timezone, e.g. 2026-06-23T12:00:00Z."})
|
||||
return parsed.astimezone(datetime_timezone.utc)
|
||||
@@ -0,0 +1,750 @@
|
||||
import json
|
||||
import mimetypes
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from rest_framework import parsers, permissions, status
|
||||
from rest_framework.exceptions import NotFound, ValidationError
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.accounts.permissions import IsBusinessWriterOrReadOnly
|
||||
from apps.accounts.models import UserApiKey
|
||||
from apps.alerts.models import Alert
|
||||
from apps.artifacts.models import Artifact
|
||||
from apps.attachments.models import Attachment
|
||||
from apps.audit.context import audit_actor
|
||||
from apps.cases.models import Case
|
||||
from apps.comments.models import Comment
|
||||
from apps.comments.services import create_record_comment
|
||||
from apps.common.cursor_pagination import paginate_created_at_cursor
|
||||
from apps.common.redis_stream import RedisStreamClient
|
||||
from apps.enrichments.models import Enrichment, EnrichmentProvider
|
||||
from apps.knowledge.models import Knowledge
|
||||
from apps.agentic.services.playbooks import create_pending_playbook_run, list_playbook_definitions
|
||||
from apps.playbooks.models import Playbook
|
||||
from integrations.cmdb.service import lookup_artifact_context
|
||||
from integrations.siem import service as siem_service
|
||||
from integrations.siem.models import (
|
||||
AdaptiveQueryInput,
|
||||
DiscoverIndexFieldsInput,
|
||||
ESQLQueryInput,
|
||||
KeywordSearchInput,
|
||||
SPLQueryInput,
|
||||
SchemaExplorerInput,
|
||||
)
|
||||
from integrations.threat_intel.service import query_indicator
|
||||
|
||||
from .responses import agent_response, pagination_meta
|
||||
from .serializers import (
|
||||
serialize_alert,
|
||||
serialize_artifact,
|
||||
serialize_attachment,
|
||||
serialize_case,
|
||||
serialize_comment,
|
||||
serialize_enrichment,
|
||||
serialize_knowledge,
|
||||
serialize_playbook,
|
||||
)
|
||||
from .utils import bool_param, list_param, parse_tags, parse_timezone_aware_datetime
|
||||
|
||||
|
||||
API_VERSION = "v1"
|
||||
MIN_CLI_VERSION = "0.1.0"
|
||||
SERVER_VERSION = "0.1.0"
|
||||
FOUNDATION_CAPABILITIES = [
|
||||
"agent.version",
|
||||
"case.list",
|
||||
"case.show",
|
||||
"case.update_ai",
|
||||
"alert.list",
|
||||
"alert.show",
|
||||
"artifact.list",
|
||||
"artifact.show",
|
||||
"knowledge.search",
|
||||
"knowledge.show",
|
||||
"knowledge.update",
|
||||
"comment.list",
|
||||
"comment.add",
|
||||
"file.upload",
|
||||
"file.info",
|
||||
"file.read_text",
|
||||
"enrichment.create",
|
||||
"playbook.template.list",
|
||||
"playbook.list",
|
||||
"playbook.show",
|
||||
"playbook.run",
|
||||
"siem.schema",
|
||||
"siem.search.keyword",
|
||||
"siem.query.adaptive",
|
||||
"siem.fields.discover",
|
||||
"siem.query.spl",
|
||||
"siem.query.esql",
|
||||
"ti.query",
|
||||
"cmdb.lookup",
|
||||
"dev.stream.head",
|
||||
"dev.stream.read",
|
||||
]
|
||||
|
||||
|
||||
class AgentVersionView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
api_key = request.auth if isinstance(request.auth, UserApiKey) else None
|
||||
data = {
|
||||
"api_version": API_VERSION,
|
||||
"server_version": getattr(settings, "ASP_VERSION", SERVER_VERSION),
|
||||
"min_cli_version": MIN_CLI_VERSION,
|
||||
"capabilities": FOUNDATION_CAPABILITIES,
|
||||
"user": {
|
||||
"username": request.user.username,
|
||||
"email": request.user.email,
|
||||
"role": request.user.role,
|
||||
"is_superuser": request.user.is_superuser,
|
||||
},
|
||||
"api_key": _api_key_payload(api_key),
|
||||
}
|
||||
return agent_response(request, operation="agent.version", data=data)
|
||||
|
||||
|
||||
def _api_key_payload(api_key):
|
||||
if api_key is None:
|
||||
return None
|
||||
return {
|
||||
"name": api_key.name,
|
||||
"expires_at": api_key.expires_at.isoformat() if api_key.expires_at else None,
|
||||
"last_used_at": api_key.last_used_at.isoformat() if api_key.last_used_at else None,
|
||||
}
|
||||
|
||||
|
||||
class CaseListView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
queryset = Case.objects.all()
|
||||
if statuses := list_param(request.query_params, "status"):
|
||||
queryset = queryset.filter(status__in=statuses)
|
||||
if severities := list_param(request.query_params, "severity"):
|
||||
queryset = queryset.filter(severity__in=severities)
|
||||
if confidences := list_param(request.query_params, "confidence"):
|
||||
queryset = queryset.filter(confidence__in=confidences)
|
||||
if verdicts := list_param(request.query_params, "verdict"):
|
||||
queryset = queryset.filter(verdict__in=verdicts)
|
||||
if correlation_uid := request.query_params.get("correlation_uid"):
|
||||
queryset = queryset.filter(correlation_uid=correlation_uid)
|
||||
if title := request.query_params.get("title"):
|
||||
queryset = queryset.filter(title__icontains=title)
|
||||
for tag in parse_tags(request.query_params):
|
||||
queryset = queryset.filter(tags__contains=[tag])
|
||||
|
||||
include_related = bool_param(request.query_params.get("include_related"), default=False)
|
||||
if include_related:
|
||||
queryset = queryset.prefetch_related("alerts")
|
||||
page = paginate_created_at_cursor(queryset, request)
|
||||
data = [serialize_case(case, include_related=include_related) for case in page.results]
|
||||
return agent_response(request, operation="case.list", data=data, pagination=pagination_meta(page))
|
||||
|
||||
|
||||
class CaseDetailView(APIView):
|
||||
permission_classes = [IsBusinessWriterOrReadOnly]
|
||||
|
||||
def get(self, request, case_id):
|
||||
case = _find_case(case_id)
|
||||
include_related = bool_param(request.query_params.get("include_related"), default=True)
|
||||
if include_related:
|
||||
case = Case.objects.prefetch_related("alerts").get(pk=case.pk)
|
||||
return agent_response(request, operation="case.show", data=serialize_case(case, include_related=include_related))
|
||||
|
||||
|
||||
class CaseAIAnalysisView(APIView):
|
||||
permission_classes = [IsBusinessWriterOrReadOnly]
|
||||
|
||||
def patch(self, request, case_id):
|
||||
case = _find_case(case_id)
|
||||
allowed_fields = {"severity_ai", "confidence_ai", "impact_ai", "priority_ai", "verdict_ai", "summary"}
|
||||
updates = {field: request.data[field] for field in allowed_fields if field in request.data}
|
||||
if not updates:
|
||||
raise ValidationError({"detail": "At least one AI analysis field is required."})
|
||||
for field, value in updates.items():
|
||||
setattr(case, field, value)
|
||||
with audit_actor(request.user):
|
||||
case.full_clean()
|
||||
case.save(update_fields=[*updates.keys(), "updated_at"])
|
||||
return agent_response(request, operation="case.update_ai", data=serialize_case(case, include_related=True), status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class AlertListView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
queryset = Alert.objects.select_related("case")
|
||||
if statuses := list_param(request.query_params, "status"):
|
||||
queryset = queryset.filter(status__in=statuses)
|
||||
if severities := list_param(request.query_params, "severity"):
|
||||
queryset = queryset.filter(severity__in=severities)
|
||||
if confidences := list_param(request.query_params, "confidence"):
|
||||
queryset = queryset.filter(confidence__in=confidences)
|
||||
if correlation_uid := request.query_params.get("correlation_uid"):
|
||||
queryset = queryset.filter(correlation_uid=correlation_uid)
|
||||
if case_id := request.query_params.get("case_id"):
|
||||
queryset = queryset.filter(case__case_id=_record_id(case_id))
|
||||
include_related = bool_param(request.query_params.get("include_related"), default=False)
|
||||
if include_related:
|
||||
queryset = queryset.prefetch_related("artifacts")
|
||||
page = paginate_created_at_cursor(queryset, request)
|
||||
data = [serialize_alert(alert, include_related=include_related) for alert in page.results]
|
||||
return agent_response(request, operation="alert.list", data=data, pagination=pagination_meta(page))
|
||||
|
||||
|
||||
class AlertDetailView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request, alert_id):
|
||||
alert = _find_alert(alert_id)
|
||||
include_related = bool_param(request.query_params.get("include_related"), default=True)
|
||||
if include_related:
|
||||
alert = Alert.objects.select_related("case").prefetch_related("artifacts").get(pk=alert.pk)
|
||||
return agent_response(request, operation="alert.show", data=serialize_alert(alert, include_related=include_related))
|
||||
|
||||
|
||||
class ArtifactListView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
queryset = Artifact.objects.all()
|
||||
if types := list_param(request.query_params, "type"):
|
||||
queryset = queryset.filter(type__in=types)
|
||||
if roles := list_param(request.query_params, "role"):
|
||||
queryset = queryset.filter(role__in=roles)
|
||||
if value := request.query_params.get("value"):
|
||||
queryset = queryset.filter(value=value)
|
||||
include_related = bool_param(request.query_params.get("include_related"), default=False)
|
||||
if include_related:
|
||||
queryset = queryset.prefetch_related("alerts", "alerts__case")
|
||||
page = paginate_created_at_cursor(queryset, request)
|
||||
data = [serialize_artifact(artifact, include_related=include_related) for artifact in page.results]
|
||||
return agent_response(request, operation="artifact.list", data=data, pagination=pagination_meta(page))
|
||||
|
||||
|
||||
class ArtifactDetailView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request, artifact_id):
|
||||
artifact = _find_artifact(artifact_id)
|
||||
include_related = bool_param(request.query_params.get("include_related"), default=True)
|
||||
if include_related:
|
||||
artifact = Artifact.objects.prefetch_related("alerts", "alerts__case").get(pk=artifact.pk)
|
||||
return agent_response(request, operation="artifact.show", data=serialize_artifact(artifact, include_related=include_related))
|
||||
|
||||
|
||||
class KnowledgeListView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
queryset = Knowledge.objects.select_related("case")
|
||||
if keyword := request.query_params.get("keyword"):
|
||||
terms = [item.strip() for item in keyword.split(",") if item.strip()]
|
||||
query = Q()
|
||||
for term in terms:
|
||||
query |= Q(title__icontains=term) | Q(body__icontains=term) | Q(tags__contains=[term])
|
||||
queryset = queryset.filter(query)
|
||||
if source := request.query_params.get("source"):
|
||||
queryset = queryset.filter(source=source)
|
||||
if case_id := request.query_params.get("case_id"):
|
||||
queryset = queryset.filter(case__case_id=_record_id(case_id))
|
||||
for tag in parse_tags(request.query_params):
|
||||
queryset = queryset.filter(tags__contains=[tag])
|
||||
page = paginate_created_at_cursor(queryset, request)
|
||||
data = [serialize_knowledge(knowledge) for knowledge in page.results]
|
||||
return agent_response(request, operation="knowledge.search", data=data, pagination=pagination_meta(page))
|
||||
|
||||
|
||||
class KnowledgeDetailView(APIView):
|
||||
permission_classes = [IsBusinessWriterOrReadOnly]
|
||||
|
||||
def get(self, request, knowledge_id):
|
||||
return agent_response(request, operation="knowledge.show", data=serialize_knowledge(_find_knowledge(knowledge_id)))
|
||||
|
||||
def patch(self, request, knowledge_id):
|
||||
knowledge = _find_knowledge(knowledge_id)
|
||||
allowed_fields = {"title", "body", "expires_at", "tags"}
|
||||
updates = {field: request.data[field] for field in allowed_fields if field in request.data}
|
||||
if not updates:
|
||||
raise ValidationError({"detail": "At least one knowledge field is required."})
|
||||
if "expires_at" in updates:
|
||||
updates["expires_at"] = parse_timezone_aware_datetime(updates["expires_at"], "expires_at")
|
||||
for field, value in updates.items():
|
||||
setattr(knowledge, field, value)
|
||||
with audit_actor(request.user):
|
||||
knowledge.full_clean()
|
||||
knowledge.save(update_fields=[*updates.keys(), "updated_at"])
|
||||
return agent_response(request, operation="knowledge.update", data=serialize_knowledge(knowledge))
|
||||
|
||||
|
||||
class CommentListCreateView(APIView):
|
||||
permission_classes = [IsBusinessWriterOrReadOnly]
|
||||
|
||||
def get(self, request):
|
||||
target_id = request.query_params.get("target_id")
|
||||
if not target_id:
|
||||
raise ValidationError({"target_id": "This query parameter is required."})
|
||||
target = _find_comment_target(target_id)
|
||||
content_type = _content_type_for_record(target)
|
||||
queryset = Comment.objects.filter(
|
||||
content_type=content_type,
|
||||
object_id=str(target.pk),
|
||||
).select_related("author", "content_type", "parent").prefetch_related("mentions", "attachments").order_by("-created_at", "-id")
|
||||
page = paginate_created_at_cursor(queryset, request)
|
||||
data = [serialize_comment(comment, request=request) for comment in reversed(page.results)]
|
||||
return agent_response(request, operation="comment.list", data=data, pagination=pagination_meta(page))
|
||||
|
||||
def post(self, request):
|
||||
target_id = request.data.get("target_id")
|
||||
if not target_id:
|
||||
raise ValidationError({"target_id": "This field is required."})
|
||||
target = _find_comment_target(target_id)
|
||||
attachments = _attachments_from_file_keys(request.data.get("file_keys") or request.data.get("file_key"))
|
||||
body = str(request.data.get("body") or "")
|
||||
if not body.strip() and not attachments:
|
||||
raise ValidationError({"detail": "body or file_keys are required."})
|
||||
comment = create_record_comment(
|
||||
author=request.user,
|
||||
content_object=target,
|
||||
body=body,
|
||||
parent=_parent_comment_for_target(target, request.data.get("parent_id")),
|
||||
mentions=_mention_users(request.data.get("mentions")),
|
||||
attachments=attachments,
|
||||
)
|
||||
return agent_response(request, operation="comment.add", data=serialize_comment(comment, request=request), status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class FileUploadView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
parser_classes = [parsers.MultiPartParser, parsers.FormParser]
|
||||
|
||||
def post(self, request):
|
||||
if "file" not in request.FILES:
|
||||
raise ValidationError({"file": "This field is required."})
|
||||
uploaded = request.FILES["file"]
|
||||
attachment = Attachment.objects.create(
|
||||
uploaded_by=request.user,
|
||||
file=uploaded,
|
||||
filename=uploaded.name,
|
||||
size=uploaded.size,
|
||||
)
|
||||
return agent_response(request, operation="file.upload", data=serialize_attachment(attachment, request=request), status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class FileDetailView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request, file_key):
|
||||
return agent_response(request, operation="file.info", data=serialize_attachment(_find_attachment(file_key), request=request))
|
||||
|
||||
|
||||
class FileReadTextView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request, file_key):
|
||||
attachment = _find_attachment(file_key)
|
||||
max_bytes = _max_text_bytes(request.query_params.get("max_bytes"))
|
||||
content_type = mimetypes.guess_type(attachment.filename)[0] or "application/octet-stream"
|
||||
if not _is_text_content_type(content_type):
|
||||
raise ValidationError({"file_key": f"File is not a supported text type: {content_type}"})
|
||||
with attachment.file.open("rb") as handle:
|
||||
raw = handle.read(max_bytes + 1)
|
||||
truncated = len(raw) > max_bytes
|
||||
if truncated:
|
||||
raw = raw[:max_bytes]
|
||||
data = {
|
||||
**serialize_attachment(attachment, request=request),
|
||||
"text": raw.decode("utf-8", errors="replace"),
|
||||
"truncated": truncated,
|
||||
"max_bytes": max_bytes,
|
||||
}
|
||||
return agent_response(request, operation="file.read_text", data=data)
|
||||
|
||||
|
||||
class EnrichmentCreateView(APIView):
|
||||
permission_classes = [IsBusinessWriterOrReadOnly]
|
||||
|
||||
def post(self, request):
|
||||
target = _find_enrichment_target(request.data.get("target_id"))
|
||||
enrichment = Enrichment(
|
||||
name=request.data.get("name", ""),
|
||||
type=request.data.get("type", "Other"),
|
||||
provider=EnrichmentProvider.ASP,
|
||||
uid=request.data.get("uid", ""),
|
||||
value=request.data.get("value", ""),
|
||||
desc=request.data.get("desc", ""),
|
||||
data=_json_object(request.data.get("data", {}), "data"),
|
||||
)
|
||||
if isinstance(target, Case):
|
||||
enrichment.case = target
|
||||
elif isinstance(target, Alert):
|
||||
enrichment.alert = target
|
||||
elif isinstance(target, Artifact):
|
||||
enrichment.artifact = target
|
||||
with audit_actor(request.user):
|
||||
enrichment.full_clean()
|
||||
enrichment.save()
|
||||
return agent_response(request, operation="enrichment.create", data=serialize_enrichment(enrichment), status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class PlaybookTemplateListView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
return agent_response(request, operation="playbook.template.list", data=list_playbook_definitions(include_path=False))
|
||||
|
||||
|
||||
class PlaybookListView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
queryset = Playbook.objects.select_related("case").all()
|
||||
if playbook_id := request.query_params.get("playbook_id"):
|
||||
queryset = queryset.filter(playbook_id=_record_id(playbook_id))
|
||||
if case_id := request.query_params.get("case_id"):
|
||||
queryset = queryset.filter(case__case_id=_record_id(case_id))
|
||||
if statuses := list_param(request.query_params, "job_status"):
|
||||
queryset = queryset.filter(job_status__in=statuses)
|
||||
include_related = bool_param(request.query_params.get("include_related"), default=False)
|
||||
page = paginate_created_at_cursor(queryset, request)
|
||||
data = [serialize_playbook(playbook, include_related=include_related) for playbook in page.results]
|
||||
return agent_response(request, operation="playbook.list", data=data, pagination=pagination_meta(page))
|
||||
|
||||
|
||||
class PlaybookDetailView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request, playbook_id):
|
||||
playbook = _find_playbook(playbook_id)
|
||||
include_related = bool_param(request.query_params.get("include_related"), default=True)
|
||||
if include_related:
|
||||
playbook = Playbook.objects.select_related("case").get(pk=playbook.pk)
|
||||
return agent_response(request, operation="playbook.show", data=serialize_playbook(playbook, include_related=include_related))
|
||||
|
||||
|
||||
class PlaybookRunView(APIView):
|
||||
permission_classes = [IsBusinessWriterOrReadOnly]
|
||||
|
||||
def post(self, request):
|
||||
name = request.data.get("name")
|
||||
case_id = request.data.get("case_id")
|
||||
if not name:
|
||||
raise ValidationError({"name": "This field is required."})
|
||||
if not case_id:
|
||||
raise ValidationError({"case_id": "This field is required."})
|
||||
try:
|
||||
with audit_actor(request.user):
|
||||
playbook = create_pending_playbook_run(
|
||||
name=name,
|
||||
case=_find_case(case_id),
|
||||
user=request.user,
|
||||
user_input=request.data.get("user_input", ""),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise ValidationError({"detail": str(exc)}) from exc
|
||||
return agent_response(request, operation="playbook.run", data=serialize_playbook(playbook), status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class SIEMSchemaView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
result = siem_service.explore_schema(SchemaExplorerInput(target_index=request.query_params.get("target_index")))
|
||||
return agent_response(request, operation="siem.schema", data=_dump(result))
|
||||
|
||||
|
||||
class SIEMKeywordSearchView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
result = siem_service.keyword_search(KeywordSearchInput(**request.data))
|
||||
return agent_response(request, operation="siem.search.keyword", data=_dump(result))
|
||||
|
||||
|
||||
class SIEMAdaptiveQueryView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
result = siem_service.execute_adaptive_query(AdaptiveQueryInput(**request.data))
|
||||
return agent_response(request, operation="siem.query.adaptive", data=_dump(result))
|
||||
|
||||
|
||||
class SIEMDiscoverFieldsView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
result = siem_service.discover_index_fields(DiscoverIndexFieldsInput(**request.data))
|
||||
return agent_response(request, operation="siem.fields.discover", data=_dump(result))
|
||||
|
||||
|
||||
class SIEMSPLQueryView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
result = siem_service.execute_spl(SPLQueryInput(**request.data))
|
||||
return agent_response(request, operation="siem.query.spl", data=_dump(result))
|
||||
|
||||
|
||||
class SIEMESQLQueryView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
result = siem_service.execute_esql(ESQLQueryInput(**request.data))
|
||||
return agent_response(request, operation="siem.query.esql", data=_dump(result))
|
||||
|
||||
|
||||
class ThreatIntelQueryView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
result = query_indicator(
|
||||
request.data.get("indicator"),
|
||||
artifact_type=request.data.get("artifact_type", "Unknown"),
|
||||
provider=request.data.get("provider"),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise ValidationError({"detail": str(exc)}) from exc
|
||||
return agent_response(request, operation="ti.query", data=_dump(result))
|
||||
|
||||
|
||||
class CMDBLookupView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
result = lookup_artifact_context(
|
||||
request.data.get("artifact_type"),
|
||||
request.data.get("artifact_value"),
|
||||
provider=request.data.get("provider"),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise ValidationError({"detail": str(exc)}) from exc
|
||||
return agent_response(request, operation="cmdb.lookup", data=_dump(result))
|
||||
|
||||
|
||||
class DevStreamHeadView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
stream_name = request.query_params.get("stream_name")
|
||||
if not stream_name:
|
||||
raise ValidationError({"stream_name": "This query parameter is required."})
|
||||
n = _bounded_int(request.query_params.get("n"), default=3, maximum=100)
|
||||
data = RedisStreamClient().read_stream_head(stream_name, n)
|
||||
return agent_response(request, operation="dev.stream.head", data=data)
|
||||
|
||||
|
||||
class DevStreamReadView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
stream_name = request.query_params.get("stream_name")
|
||||
message_id = request.query_params.get("message_id")
|
||||
if not stream_name:
|
||||
raise ValidationError({"stream_name": "This query parameter is required."})
|
||||
if not message_id:
|
||||
raise ValidationError({"message_id": "This query parameter is required."})
|
||||
data = RedisStreamClient().read_stream_message_by_id(stream_name, message_id)
|
||||
return agent_response(request, operation="dev.stream.read", data=data)
|
||||
|
||||
|
||||
def _record_id(value):
|
||||
return str(value or "").strip().lower()
|
||||
|
||||
|
||||
def _find_case(case_id):
|
||||
try:
|
||||
return Case.objects.get(case_id=_record_id(case_id))
|
||||
except Case.DoesNotExist as exc:
|
||||
raise NotFound(f"Case not found: {case_id}") from exc
|
||||
|
||||
|
||||
def _find_alert(alert_id):
|
||||
try:
|
||||
return Alert.objects.select_related("case").get(alert_id=_record_id(alert_id))
|
||||
except Alert.DoesNotExist as exc:
|
||||
raise NotFound(f"Alert not found: {alert_id}") from exc
|
||||
|
||||
|
||||
def _find_artifact(artifact_id):
|
||||
try:
|
||||
return Artifact.objects.get(artifact_id=_record_id(artifact_id))
|
||||
except Artifact.DoesNotExist as exc:
|
||||
raise NotFound(f"Artifact not found: {artifact_id}") from exc
|
||||
|
||||
|
||||
def _find_knowledge(knowledge_id):
|
||||
try:
|
||||
return Knowledge.objects.select_related("case").get(knowledge_id=_record_id(knowledge_id))
|
||||
except Knowledge.DoesNotExist as exc:
|
||||
raise NotFound(f"Knowledge not found: {knowledge_id}") from exc
|
||||
|
||||
|
||||
def _find_playbook(playbook_id):
|
||||
try:
|
||||
return Playbook.objects.select_related("case").get(playbook_id=_record_id(playbook_id))
|
||||
except Playbook.DoesNotExist as exc:
|
||||
raise NotFound(f"Playbook not found: {playbook_id}") from exc
|
||||
|
||||
|
||||
def _find_attachment(file_key):
|
||||
try:
|
||||
return Attachment.objects.get(access_key=file_key)
|
||||
except (Attachment.DoesNotExist, ValueError) as exc:
|
||||
raise NotFound(f"File not found: {file_key}") from exc
|
||||
|
||||
|
||||
def _find_comment_target(target_id):
|
||||
target_id = _record_id(target_id)
|
||||
if target_id.startswith("case_"):
|
||||
return _find_case(target_id)
|
||||
if target_id.startswith("alert_"):
|
||||
return _find_alert(target_id)
|
||||
if target_id.startswith("artifact_"):
|
||||
return _find_artifact(target_id)
|
||||
if target_id.startswith("enrichment_"):
|
||||
return _find_enrichment(target_id)
|
||||
if target_id.startswith("knowledge_"):
|
||||
return _find_knowledge(target_id)
|
||||
if target_id.startswith("playbook_"):
|
||||
return _find_playbook(target_id)
|
||||
raise ValidationError({"target_id": "Must start with case_, alert_, artifact_, enrichment_, knowledge_, or playbook_."})
|
||||
|
||||
|
||||
def _find_enrichment_target(target_id):
|
||||
target_id = _record_id(target_id)
|
||||
if target_id.startswith("case_"):
|
||||
return _find_case(target_id)
|
||||
if target_id.startswith("alert_"):
|
||||
return _find_alert(target_id)
|
||||
if target_id.startswith("artifact_"):
|
||||
return _find_artifact(target_id)
|
||||
raise ValidationError({"target_id": "Must start with case_, alert_, or artifact_."})
|
||||
|
||||
|
||||
def _find_enrichment(enrichment_id):
|
||||
try:
|
||||
return Enrichment.objects.select_related("case", "alert", "artifact").get(enrichment_id=_record_id(enrichment_id))
|
||||
except Enrichment.DoesNotExist as exc:
|
||||
raise NotFound(f"Enrichment not found: {enrichment_id}") from exc
|
||||
|
||||
|
||||
def _content_type_for_record(record):
|
||||
return ContentType.objects.get_for_model(record, for_concrete_model=False)
|
||||
|
||||
|
||||
def _parent_comment_for_target(content_object, parent_id):
|
||||
if parent_id in (None, ""):
|
||||
return None
|
||||
try:
|
||||
parent = Comment.objects.get(pk=int(parent_id))
|
||||
except (TypeError, ValueError, Comment.DoesNotExist) as exc:
|
||||
raise NotFound(f"Parent comment not found: {parent_id}") from exc
|
||||
content_type = _content_type_for_record(content_object)
|
||||
if parent.content_type_id != content_type.id or parent.object_id != str(content_object.pk):
|
||||
raise ValidationError({"parent_id": "Must belong to the same target."})
|
||||
return parent
|
||||
|
||||
|
||||
def _mention_users(mentions):
|
||||
users = []
|
||||
seen_ids = set()
|
||||
user_model = get_user_model()
|
||||
for item in _coerce_list(mentions):
|
||||
text = str(item or "").strip()
|
||||
if not text:
|
||||
continue
|
||||
user = user_model.objects.filter(username=text).first()
|
||||
if user is None and text.isdigit():
|
||||
user = user_model.objects.filter(pk=int(text)).first()
|
||||
if user is None:
|
||||
raise ValidationError({"mentions": f"User not found: {text}"})
|
||||
if user.id not in seen_ids:
|
||||
users.append(user)
|
||||
seen_ids.add(user.id)
|
||||
return users
|
||||
|
||||
|
||||
def _attachments_from_file_keys(file_keys):
|
||||
keys = _coerce_list(file_keys)
|
||||
attachments = []
|
||||
for key in keys:
|
||||
attachments.append(_find_attachment(key))
|
||||
return attachments
|
||||
|
||||
|
||||
def _coerce_list(value):
|
||||
if value in (None, ""):
|
||||
return []
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
if isinstance(value, tuple | set):
|
||||
return list(value)
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
if not stripped:
|
||||
return []
|
||||
if stripped.startswith("[") and stripped.endswith("]"):
|
||||
try:
|
||||
decoded = json.loads(stripped)
|
||||
except json.JSONDecodeError:
|
||||
decoded = None
|
||||
if isinstance(decoded, list):
|
||||
return decoded
|
||||
return [item.strip() for item in stripped.split(",") if item.strip()]
|
||||
return [value]
|
||||
|
||||
|
||||
def _json_object(value, field_name):
|
||||
if value in (None, ""):
|
||||
return {}
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
payload = json.loads(value)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValidationError({field_name: "Must be a valid JSON object."}) from exc
|
||||
if isinstance(payload, dict):
|
||||
return payload
|
||||
raise ValidationError({field_name: "Must be a valid JSON object."})
|
||||
|
||||
|
||||
def _max_text_bytes(value):
|
||||
try:
|
||||
parsed = int(value or 65536)
|
||||
except (TypeError, ValueError):
|
||||
parsed = 65536
|
||||
return max(1, min(parsed, 262144))
|
||||
|
||||
|
||||
def _is_text_content_type(content_type):
|
||||
return (
|
||||
content_type.startswith("text/")
|
||||
or content_type in {"application/json", "application/xml", "application/yaml", "application/x-yaml"}
|
||||
)
|
||||
|
||||
|
||||
def _bounded_int(value, *, default, maximum):
|
||||
try:
|
||||
parsed = int(value or default)
|
||||
except (TypeError, ValueError):
|
||||
parsed = default
|
||||
return max(1, min(parsed, maximum))
|
||||
|
||||
|
||||
def _dump(value):
|
||||
if isinstance(value, list):
|
||||
return [_dump(item) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {key: _dump(item) for key, item in value.items()}
|
||||
if hasattr(value, "model_dump"):
|
||||
return value.model_dump()
|
||||
return value
|
||||
@@ -51,6 +51,7 @@ INSTALLED_APPS = [
|
||||
"apps.webhook",
|
||||
"apps.mcp",
|
||||
"apps.agentic",
|
||||
"apps.agent_api",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@@ -17,4 +17,5 @@ urlpatterns = [
|
||||
path("api/", include("apps.inbox.urls")),
|
||||
path("api/", include("apps.preferences.urls")),
|
||||
path("api/", include("apps.webhook.urls")),
|
||||
path("api/agent/v1/", include("apps.agent_api.urls")),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[project]
|
||||
name = "asp-cli"
|
||||
version = "0.1.0"
|
||||
description = "Command line client for Agentic SOC Platform"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"httpx>=0.28.1",
|
||||
"jmespath>=1.0.1",
|
||||
"pydantic>=2.13.4",
|
||||
"rich>=14.2.0",
|
||||
"typer>=0.20.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
asp = "asp_cli.main:run"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling>=1.28"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/asp_cli"]
|
||||
|
||||
[tool.hatch.build.targets.wheel.force-include]
|
||||
"src/asp_cli/spec/operations.json" = "asp_cli/spec/operations.json"
|
||||
@@ -0,0 +1 @@
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from rich.console import Console
|
||||
|
||||
from . import __version__
|
||||
from .config import redact_secret
|
||||
from .errors import (
|
||||
CliError,
|
||||
EXIT_AUTH,
|
||||
EXIT_NETWORK,
|
||||
EXIT_NOT_FOUND,
|
||||
EXIT_PERMISSION,
|
||||
EXIT_SERVER,
|
||||
EXIT_USAGE,
|
||||
)
|
||||
|
||||
|
||||
class AspClient:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_url: str,
|
||||
api_key: str | None = None,
|
||||
verbose: bool = False,
|
||||
console: Console | None = None,
|
||||
timeout: float = 20.0,
|
||||
) -> None:
|
||||
self.base_url = _normalize_base_url(api_url)
|
||||
self.api_key = api_key
|
||||
self.verbose = verbose
|
||||
self.console = console or Console(stderr=True)
|
||||
self.timeout = timeout
|
||||
|
||||
def health(self) -> dict[str, Any]:
|
||||
return self.request("GET", "/api/health/", authenticated=False)
|
||||
|
||||
def version(self) -> dict[str, Any]:
|
||||
return self.request("GET", "/api/agent/v1/version/")
|
||||
|
||||
def request(self, method: str, path: str, *, authenticated: bool = True, json: Any = None, files: Any = None) -> dict[str, Any]:
|
||||
if authenticated and not self.api_key:
|
||||
raise CliError("missing_api_key", "API key is required", {}, EXIT_AUTH)
|
||||
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"User-Agent": f"asp-cli/{__version__}",
|
||||
}
|
||||
if authenticated and self.api_key:
|
||||
headers["Authorization"] = f"Api-Key {self.api_key}"
|
||||
|
||||
url = f"{self.base_url}{path}"
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
response = httpx.request(method, url, headers=headers, json=json, files=files, timeout=self.timeout)
|
||||
except httpx.HTTPError as exc:
|
||||
raise CliError("network_error", f"Unable to reach ASP server: {exc}", {"url": _redact_url(url)}, EXIT_NETWORK) from exc
|
||||
|
||||
elapsed_ms = int((time.perf_counter() - started) * 1000)
|
||||
if self.verbose:
|
||||
self.console.print(f"{method} {path} -> {response.status_code} ({elapsed_ms}ms)", style="dim")
|
||||
|
||||
if response.status_code >= 400:
|
||||
self._raise_http_error(response, path)
|
||||
|
||||
if not response.content:
|
||||
return {}
|
||||
try:
|
||||
payload = response.json()
|
||||
except ValueError as exc:
|
||||
raise CliError("invalid_response", "Server returned non-JSON response", {"status_code": response.status_code}, EXIT_SERVER) from exc
|
||||
if not isinstance(payload, dict):
|
||||
raise CliError("invalid_response", "Server response must be a JSON object", {"status_code": response.status_code}, EXIT_SERVER)
|
||||
return payload
|
||||
|
||||
def _raise_http_error(self, response: httpx.Response, path: str) -> None:
|
||||
message = _response_message(response)
|
||||
details = {"status_code": response.status_code, "path": path}
|
||||
if response.status_code == 400:
|
||||
raise CliError("bad_request", message, details, EXIT_USAGE)
|
||||
if response.status_code == 401:
|
||||
raise CliError("authentication_failed", message, details, EXIT_AUTH)
|
||||
if response.status_code == 403:
|
||||
raise CliError("permission_denied", message, details, EXIT_PERMISSION)
|
||||
if response.status_code == 404:
|
||||
raise CliError("not_found", message, details, EXIT_NOT_FOUND)
|
||||
raise CliError("server_error", message, details, EXIT_SERVER)
|
||||
|
||||
|
||||
def _normalize_base_url(api_url: str) -> str:
|
||||
base = api_url.strip().rstrip("/")
|
||||
if base.endswith("/api"):
|
||||
base = base[:-4]
|
||||
if not base:
|
||||
raise CliError("missing_api_url", "ASP API URL is required", {}, EXIT_USAGE)
|
||||
return base
|
||||
|
||||
|
||||
def _response_message(response: httpx.Response) -> str:
|
||||
try:
|
||||
payload = response.json()
|
||||
except ValueError:
|
||||
return response.text.strip() or f"HTTP {response.status_code}"
|
||||
if isinstance(payload, dict):
|
||||
detail = payload.get("detail")
|
||||
if isinstance(detail, str):
|
||||
return detail
|
||||
error = payload.get("error")
|
||||
if isinstance(error, dict) and isinstance(error.get("message"), str):
|
||||
return error["message"]
|
||||
return f"HTTP {response.status_code}"
|
||||
|
||||
|
||||
def _redact_url(url: str) -> str:
|
||||
return url.replace(redact_secret(url), "****") if "asp_" in url else url
|
||||
@@ -0,0 +1,176 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .errors import CliError, EXIT_CONFIG
|
||||
|
||||
GLOBAL_SETTINGS_PATH = Path.home() / ".asp" / "settings.json"
|
||||
LOCAL_SETTINGS_DIR = ".asp"
|
||||
SETTINGS_FILENAME = "settings.json"
|
||||
SUPPORTED_KEYS = {"api_url", "api_key"}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResolvedConfig:
|
||||
api_url: str | None
|
||||
api_key: str | None
|
||||
sources: dict[str, str]
|
||||
global_path: Path
|
||||
local_path: Path | None
|
||||
|
||||
@property
|
||||
def has_auth(self) -> bool:
|
||||
return bool(self.api_url and self.api_key)
|
||||
|
||||
|
||||
def resolve_config(*, cwd: Path | None = None, api_url: str | None = None, api_key: str | None = None) -> ResolvedConfig:
|
||||
cwd = (cwd or Path.cwd()).resolve()
|
||||
global_settings = read_settings(GLOBAL_SETTINGS_PATH)
|
||||
local_path = find_local_settings(cwd)
|
||||
local_settings = read_settings(local_path) if local_path else {}
|
||||
values: dict[str, Any] = {}
|
||||
sources: dict[str, str] = {}
|
||||
|
||||
_merge(values, sources, global_settings, "global")
|
||||
if local_path:
|
||||
_merge(values, sources, local_settings, "local")
|
||||
_merge(
|
||||
values,
|
||||
sources,
|
||||
{
|
||||
"api_url": os.environ.get("ASP_API_URL"),
|
||||
"api_key": os.environ.get("ASP_API_KEY"),
|
||||
},
|
||||
"env",
|
||||
)
|
||||
_merge(values, sources, {"api_url": api_url, "api_key": api_key}, "flags")
|
||||
|
||||
return ResolvedConfig(
|
||||
api_url=_clean(values.get("api_url")),
|
||||
api_key=_clean(values.get("api_key")),
|
||||
sources=sources,
|
||||
global_path=GLOBAL_SETTINGS_PATH,
|
||||
local_path=local_path,
|
||||
)
|
||||
|
||||
|
||||
def auth_settings_path(*, local: bool, cwd: Path | None = None) -> Path:
|
||||
if local:
|
||||
return (cwd or Path.cwd()).resolve() / LOCAL_SETTINGS_DIR / SETTINGS_FILENAME
|
||||
return GLOBAL_SETTINGS_PATH
|
||||
|
||||
|
||||
def save_auth(*, api_url: str, api_key: str, local: bool = False, cwd: Path | None = None) -> Path:
|
||||
path = auth_settings_path(local=local, cwd=cwd)
|
||||
settings = read_settings(path)
|
||||
settings["api_url"] = api_url.rstrip("/")
|
||||
settings["api_key"] = api_key
|
||||
write_settings(path, settings)
|
||||
return path
|
||||
|
||||
|
||||
def clear_auth(*, local: bool = False, cwd: Path | None = None) -> Path:
|
||||
path = auth_settings_path(local=local, cwd=cwd)
|
||||
settings = read_settings(path)
|
||||
settings.pop("api_url", None)
|
||||
settings.pop("api_key", None)
|
||||
write_settings(path, settings)
|
||||
return path
|
||||
|
||||
|
||||
def set_config_value(key: str, value: str, *, local: bool = False, cwd: Path | None = None) -> Path:
|
||||
if key not in SUPPORTED_KEYS:
|
||||
raise CliError("invalid_config_key", f"Unsupported config key: {key}", {"supported": sorted(SUPPORTED_KEYS)}, EXIT_CONFIG)
|
||||
path = auth_settings_path(local=local, cwd=cwd)
|
||||
settings = read_settings(path)
|
||||
settings[key] = value.rstrip("/") if key == "api_url" else value
|
||||
write_settings(path, settings)
|
||||
return path
|
||||
|
||||
|
||||
def get_config_value(key: str, *, cwd: Path | None = None, api_url: str | None = None, api_key: str | None = None) -> tuple[str | None, str | None]:
|
||||
if key not in SUPPORTED_KEYS:
|
||||
raise CliError("invalid_config_key", f"Unsupported config key: {key}", {"supported": sorted(SUPPORTED_KEYS)}, EXIT_CONFIG)
|
||||
config = resolve_config(cwd=cwd, api_url=api_url, api_key=api_key)
|
||||
return getattr(config, key), config.sources.get(key)
|
||||
|
||||
|
||||
def read_settings(path: Path | None) -> dict[str, Any]:
|
||||
if path is None or not path.exists():
|
||||
return {}
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise CliError("invalid_config", f"Invalid JSON in settings file: {path}", {"path": str(path)}, EXIT_CONFIG) from exc
|
||||
if not isinstance(payload, dict):
|
||||
raise CliError("invalid_config", f"Settings file must contain a JSON object: {path}", {"path": str(path)}, EXIT_CONFIG)
|
||||
return payload
|
||||
|
||||
|
||||
def write_settings(path: Path, settings: dict[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8") as handle:
|
||||
json.dump(settings, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
_restrict_permissions(path)
|
||||
|
||||
|
||||
def find_local_settings(cwd: Path) -> Path | None:
|
||||
git_root = find_git_root(cwd)
|
||||
if git_root is None:
|
||||
candidate = cwd / LOCAL_SETTINGS_DIR / SETTINGS_FILENAME
|
||||
return candidate if candidate.exists() else None
|
||||
|
||||
current = cwd
|
||||
while True:
|
||||
candidate = current / LOCAL_SETTINGS_DIR / SETTINGS_FILENAME
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
if current == git_root:
|
||||
return None
|
||||
current = current.parent
|
||||
|
||||
|
||||
def find_git_root(cwd: Path) -> Path | None:
|
||||
current = cwd
|
||||
while True:
|
||||
if (current / ".git").exists():
|
||||
return current
|
||||
if current == current.parent:
|
||||
return None
|
||||
current = current.parent
|
||||
|
||||
|
||||
def redact_secret(value: str | None) -> str:
|
||||
if not value:
|
||||
return ""
|
||||
if len(value) <= 8:
|
||||
return "****"
|
||||
return f"{value[:4]}...{value[-4:]}"
|
||||
|
||||
|
||||
def _merge(values: dict[str, Any], sources: dict[str, str], incoming: dict[str, Any], source: str) -> None:
|
||||
for key in SUPPORTED_KEYS:
|
||||
value = _clean(incoming.get(key))
|
||||
if value:
|
||||
values[key] = value
|
||||
sources[key] = source
|
||||
|
||||
|
||||
def _clean(value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
text = str(value).strip()
|
||||
return text or None
|
||||
|
||||
|
||||
def _restrict_permissions(path: Path) -> None:
|
||||
try:
|
||||
os.chmod(path, 0o600)
|
||||
except OSError:
|
||||
return
|
||||
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
EXIT_USAGE = 2
|
||||
EXIT_CONFIG = 3
|
||||
EXIT_AUTH = 4
|
||||
EXIT_PERMISSION = 5
|
||||
EXIT_NOT_FOUND = 6
|
||||
EXIT_CONFLICT = 7
|
||||
EXIT_VERSION = 8
|
||||
EXIT_NETWORK = 70
|
||||
EXIT_SERVER = 75
|
||||
|
||||
|
||||
@dataclass
|
||||
class CliError(Exception):
|
||||
code: str
|
||||
message: str
|
||||
details: dict[str, Any] = field(default_factory=dict)
|
||||
exit_code: int = EXIT_USAGE
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from .errors import CliError
|
||||
|
||||
|
||||
class OutputFormat(str, Enum):
|
||||
human = "human"
|
||||
json = "json"
|
||||
|
||||
|
||||
def emit_success(
|
||||
console: Console,
|
||||
*,
|
||||
output: OutputFormat,
|
||||
operation: str,
|
||||
data: Any,
|
||||
meta: dict[str, Any] | None = None,
|
||||
human: str | None = None,
|
||||
) -> None:
|
||||
if output == OutputFormat.json:
|
||||
payload = {
|
||||
"data": data,
|
||||
"meta": {
|
||||
"operation": operation,
|
||||
**(meta or {}),
|
||||
},
|
||||
}
|
||||
console.print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
return
|
||||
if human is not None:
|
||||
console.print(human)
|
||||
return
|
||||
console.print(data)
|
||||
|
||||
|
||||
def emit_error(console: Console, *, output: OutputFormat, error: CliError, operation: str | None = None) -> None:
|
||||
if output == OutputFormat.json:
|
||||
payload = {
|
||||
"error": {
|
||||
"code": error.code,
|
||||
"message": error.message,
|
||||
"details": error.details,
|
||||
},
|
||||
"meta": {
|
||||
"operation": operation,
|
||||
},
|
||||
}
|
||||
console.print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
return
|
||||
console.print(f"Error: {error.message}", style="bold red")
|
||||
|
||||
|
||||
def key_value_table(title: str, rows: list[tuple[str, Any]]) -> Table:
|
||||
table = Table(title=title, show_header=False)
|
||||
table.add_column("Key", style="cyan", no_wrap=True)
|
||||
table.add_column("Value")
|
||||
for key, value in rows:
|
||||
table.add_row(key, "" if value is None else str(value))
|
||||
return table
|
||||
@@ -0,0 +1,404 @@
|
||||
{
|
||||
"schema_version": "0.1.0",
|
||||
"api_version": "v1",
|
||||
"min_cli_version": "0.1.0",
|
||||
"operations": [
|
||||
{
|
||||
"id": "agent.version",
|
||||
"cli_path": "doctor",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/version/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["agent.version"],
|
||||
"aliases": [],
|
||||
"examples": [
|
||||
"asp doctor",
|
||||
"asp doctor --output json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "auth.login",
|
||||
"cli_path": "auth login",
|
||||
"method": "local",
|
||||
"endpoint": null,
|
||||
"permission": "local",
|
||||
"capabilities": [],
|
||||
"aliases": [],
|
||||
"examples": [
|
||||
"asp auth login --api-url https://asp.example.com --api-key asp_xxx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "auth.status",
|
||||
"cli_path": "auth status",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/version/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["agent.version"],
|
||||
"aliases": [],
|
||||
"examples": [
|
||||
"asp auth status",
|
||||
"asp auth status --output json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "auth.logout",
|
||||
"cli_path": "auth logout",
|
||||
"method": "local",
|
||||
"endpoint": null,
|
||||
"permission": "local",
|
||||
"capabilities": [],
|
||||
"aliases": [],
|
||||
"examples": [
|
||||
"asp auth logout"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "config.list",
|
||||
"cli_path": "config list",
|
||||
"method": "local",
|
||||
"endpoint": null,
|
||||
"permission": "local",
|
||||
"capabilities": [],
|
||||
"aliases": [],
|
||||
"examples": [
|
||||
"asp config list",
|
||||
"asp --output json config list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "config.get",
|
||||
"cli_path": "config get",
|
||||
"method": "local",
|
||||
"endpoint": null,
|
||||
"permission": "local",
|
||||
"capabilities": [],
|
||||
"aliases": [],
|
||||
"examples": [
|
||||
"asp config get api_url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "config.set",
|
||||
"cli_path": "config set",
|
||||
"method": "local",
|
||||
"endpoint": null,
|
||||
"permission": "local",
|
||||
"capabilities": [],
|
||||
"aliases": [],
|
||||
"examples": [
|
||||
"asp config set api_url https://asp.example.com"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "case.list",
|
||||
"cli_path": "case list",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/cases/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["case.list"],
|
||||
"aliases": ["list_cases"],
|
||||
"examples": ["asp case list --status New --output json"]
|
||||
},
|
||||
{
|
||||
"id": "case.show",
|
||||
"cli_path": "case show",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/cases/{case_id}/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["case.show"],
|
||||
"aliases": ["list_cases(case_id=...)"],
|
||||
"examples": ["asp case show case_000001 --output json"]
|
||||
},
|
||||
{
|
||||
"id": "case.update_ai",
|
||||
"cli_path": "case update-ai",
|
||||
"method": "PATCH",
|
||||
"endpoint": "/api/agent/v1/cases/{case_id}/ai-analysis/",
|
||||
"permission": "business_writer",
|
||||
"capabilities": ["case.update_ai"],
|
||||
"aliases": ["update_case"],
|
||||
"examples": ["asp case update-ai case_000001 --summary-file summary.md --output json"]
|
||||
},
|
||||
{
|
||||
"id": "alert.list",
|
||||
"cli_path": "alert list",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/alerts/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["alert.list"],
|
||||
"aliases": ["list_alerts"],
|
||||
"examples": ["asp alert list --case-id case_000001 --output json"]
|
||||
},
|
||||
{
|
||||
"id": "alert.show",
|
||||
"cli_path": "alert show",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/alerts/{alert_id}/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["alert.show"],
|
||||
"aliases": ["list_alerts(alert_id=...)"],
|
||||
"examples": ["asp alert show alert_000001 --output json"]
|
||||
},
|
||||
{
|
||||
"id": "artifact.list",
|
||||
"cli_path": "artifact list",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/artifacts/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["artifact.list"],
|
||||
"aliases": ["list_artifacts"],
|
||||
"examples": ["asp artifact list --type \"IP Address\" --output json"]
|
||||
},
|
||||
{
|
||||
"id": "artifact.show",
|
||||
"cli_path": "artifact show",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/artifacts/{artifact_id}/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["artifact.show"],
|
||||
"aliases": ["list_artifacts(artifact_id=...)"],
|
||||
"examples": ["asp artifact show artifact_000001 --output json"]
|
||||
},
|
||||
{
|
||||
"id": "knowledge.search",
|
||||
"cli_path": "knowledge search",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/knowledge/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["knowledge.search"],
|
||||
"aliases": ["search_knowledge"],
|
||||
"examples": ["asp knowledge search phishing --output json"]
|
||||
},
|
||||
{
|
||||
"id": "knowledge.show",
|
||||
"cli_path": "knowledge show",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/knowledge/{knowledge_id}/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["knowledge.show"],
|
||||
"aliases": [],
|
||||
"examples": ["asp knowledge show knowledge_000001 --output json"]
|
||||
},
|
||||
{
|
||||
"id": "knowledge.update",
|
||||
"cli_path": "knowledge update",
|
||||
"method": "PATCH",
|
||||
"endpoint": "/api/agent/v1/knowledge/{knowledge_id}/",
|
||||
"permission": "business_writer",
|
||||
"capabilities": ["knowledge.update"],
|
||||
"aliases": ["update_knowledge"],
|
||||
"examples": ["asp knowledge update knowledge_000001 --body-file note.md --output json"]
|
||||
},
|
||||
{
|
||||
"id": "comment.list",
|
||||
"cli_path": "comment list",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/comments/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["comment.list"],
|
||||
"aliases": [],
|
||||
"examples": ["asp comment list case_000001 --output json"]
|
||||
},
|
||||
{
|
||||
"id": "comment.add",
|
||||
"cli_path": "comment add",
|
||||
"method": "POST",
|
||||
"endpoint": "/api/agent/v1/comments/",
|
||||
"permission": "business_writer",
|
||||
"capabilities": ["comment.add"],
|
||||
"aliases": ["add_comment"],
|
||||
"examples": ["asp comment add case_000001 --body-file note.md --output json"]
|
||||
},
|
||||
{
|
||||
"id": "file.upload",
|
||||
"cli_path": "file upload",
|
||||
"method": "POST",
|
||||
"endpoint": "/api/agent/v1/files/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["file.upload"],
|
||||
"aliases": [],
|
||||
"examples": ["asp file upload evidence.txt --output json"]
|
||||
},
|
||||
{
|
||||
"id": "file.info",
|
||||
"cli_path": "file info",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/files/{file_key}/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["file.info"],
|
||||
"aliases": ["get_file"],
|
||||
"examples": ["asp file info 6f2c5d7e-31c6-4f48-9e3c-6d9b5f92c457 --output json"]
|
||||
},
|
||||
{
|
||||
"id": "file.download",
|
||||
"cli_path": "file download",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/files/{file_key}/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["file.info"],
|
||||
"aliases": [],
|
||||
"examples": ["asp file download 6f2c5d7e-31c6-4f48-9e3c-6d9b5f92c457 --output-path evidence.txt"]
|
||||
},
|
||||
{
|
||||
"id": "file.read_text",
|
||||
"cli_path": "file read-text",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/files/{file_key}/read-text/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["file.read_text"],
|
||||
"aliases": [],
|
||||
"examples": ["asp file read-text 6f2c5d7e-31c6-4f48-9e3c-6d9b5f92c457 --max-bytes 4096 --output json"]
|
||||
},
|
||||
{
|
||||
"id": "enrichment.create",
|
||||
"cli_path": "enrichment create",
|
||||
"method": "POST",
|
||||
"endpoint": "/api/agent/v1/enrichments/",
|
||||
"permission": "business_writer",
|
||||
"capabilities": ["enrichment.create"],
|
||||
"aliases": ["create_enrichment"],
|
||||
"examples": ["asp enrichment create case_000001 --name ti --data-file enrichment.json --output json"]
|
||||
},
|
||||
{
|
||||
"id": "playbook.template.list",
|
||||
"cli_path": "playbook template list",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/playbooks/templates/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["playbook.template.list"],
|
||||
"aliases": ["list_playbook_templates"],
|
||||
"examples": ["asp playbook template list --output json"]
|
||||
},
|
||||
{
|
||||
"id": "playbook.list",
|
||||
"cli_path": "playbook list",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/playbooks/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["playbook.list"],
|
||||
"aliases": ["list_playbooks"],
|
||||
"examples": ["asp playbook list --case-id case_000001 --output json"]
|
||||
},
|
||||
{
|
||||
"id": "playbook.show",
|
||||
"cli_path": "playbook show",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/playbooks/{playbook_id}/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["playbook.show"],
|
||||
"aliases": ["list_playbooks(playbook_id=...)"],
|
||||
"examples": ["asp playbook show playbook_000001 --output json"]
|
||||
},
|
||||
{
|
||||
"id": "playbook.run",
|
||||
"cli_path": "playbook run",
|
||||
"method": "POST",
|
||||
"endpoint": "/api/agent/v1/playbooks/run/",
|
||||
"permission": "business_writer",
|
||||
"capabilities": ["playbook.run"],
|
||||
"aliases": ["execute_playbook"],
|
||||
"examples": ["asp playbook run collect_case_context case_000001 --user-input-file prompt.md --output json"]
|
||||
},
|
||||
{
|
||||
"id": "siem.schema",
|
||||
"cli_path": "siem schema list|show",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/siem/schema/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["siem.schema"],
|
||||
"aliases": ["siem_explore_schema"],
|
||||
"examples": ["asp siem schema list --output json", "asp siem schema show logs-security --output json"]
|
||||
},
|
||||
{
|
||||
"id": "siem.search.keyword",
|
||||
"cli_path": "siem search keyword",
|
||||
"method": "POST",
|
||||
"endpoint": "/api/agent/v1/siem/search/keyword/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["siem.search.keyword"],
|
||||
"aliases": ["siem_keyword_search"],
|
||||
"examples": ["asp siem search keyword 1.2.3.4 --from 2026-07-02T00:00:00Z --to 2026-07-02T01:00:00Z --output json"]
|
||||
},
|
||||
{
|
||||
"id": "siem.query.adaptive",
|
||||
"cli_path": "siem query adaptive",
|
||||
"method": "POST",
|
||||
"endpoint": "/api/agent/v1/siem/query/adaptive/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["siem.query.adaptive"],
|
||||
"aliases": ["siem_adaptive_query"],
|
||||
"examples": ["asp siem query adaptive logs-security --from 2026-07-02T00:00:00Z --to 2026-07-02T01:00:00Z --filters-file filters.json --output json"]
|
||||
},
|
||||
{
|
||||
"id": "siem.fields.discover",
|
||||
"cli_path": "siem fields discover",
|
||||
"method": "POST",
|
||||
"endpoint": "/api/agent/v1/siem/fields/discover/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["siem.fields.discover"],
|
||||
"aliases": ["siem_discover_index_fields"],
|
||||
"examples": ["asp siem fields discover logs-security ELK --from 2026-07-02T00:00:00Z --to 2026-07-02T01:00:00Z --output json"]
|
||||
},
|
||||
{
|
||||
"id": "siem.query.spl",
|
||||
"cli_path": "siem query spl",
|
||||
"method": "POST",
|
||||
"endpoint": "/api/agent/v1/siem/query/spl/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["siem.query.spl"],
|
||||
"aliases": ["siem_execute_spl"],
|
||||
"examples": ["asp siem query spl \"index=main error\" --from 2026-07-02T00:00:00Z --to 2026-07-02T01:00:00Z --output json"]
|
||||
},
|
||||
{
|
||||
"id": "siem.query.esql",
|
||||
"cli_path": "siem query esql",
|
||||
"method": "POST",
|
||||
"endpoint": "/api/agent/v1/siem/query/esql/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["siem.query.esql"],
|
||||
"aliases": ["siem_execute_esql"],
|
||||
"examples": ["asp siem query esql \"FROM logs-* | LIMIT 10\" --from 2026-07-02T00:00:00Z --to 2026-07-02T01:00:00Z --output json"]
|
||||
},
|
||||
{
|
||||
"id": "ti.query",
|
||||
"cli_path": "ti query",
|
||||
"method": "POST",
|
||||
"endpoint": "/api/agent/v1/threat-intel/query/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["ti.query"],
|
||||
"aliases": ["ti_query", "threat-intel query"],
|
||||
"examples": ["asp ti query 1.2.3.4 --artifact-type \"IP Address\" --output json"]
|
||||
},
|
||||
{
|
||||
"id": "cmdb.lookup",
|
||||
"cli_path": "cmdb lookup",
|
||||
"method": "POST",
|
||||
"endpoint": "/api/agent/v1/cmdb/lookup/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["cmdb.lookup"],
|
||||
"aliases": ["cmdb_lookup"],
|
||||
"examples": ["asp cmdb lookup \"IP Address\" 1.2.3.4 --output json"]
|
||||
},
|
||||
{
|
||||
"id": "dev.stream.head",
|
||||
"cli_path": "dev stream head",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/dev/streams/head/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["dev.stream.head"],
|
||||
"aliases": ["read_stream_head"],
|
||||
"examples": ["asp dev stream head custom-module-events -n 3 --output json"]
|
||||
},
|
||||
{
|
||||
"id": "dev.stream.read",
|
||||
"cli_path": "dev stream read",
|
||||
"method": "GET",
|
||||
"endpoint": "/api/agent/v1/dev/streams/message/",
|
||||
"permission": "authenticated",
|
||||
"capabilities": ["dev.stream.read"],
|
||||
"aliases": ["read_stream_message_by_id"],
|
||||
"examples": ["asp dev stream read custom-module-events 0-1 --output json"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
+333
@@ -0,0 +1,333 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.4"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.14.1"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asp-cli"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "jmespath" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "jmespath", specifier = ">=1.0.1" },
|
||||
{ name = "pydantic", specifier = ">=2.13.4" },
|
||||
{ name = "rich", specifier = ">=14.2.0" },
|
||||
{ name = "typer", specifier = ">=0.20.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.6.17"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.18"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jmespath"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.2.0"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.13.4"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.46.4"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "15.0.0"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.26.8"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7" },
|
||||
]
|
||||
Reference in New Issue
Block a user