cloud-dog-api-kit 0.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cloud_dog_api_kit/__init__.py +170 -0
- cloud_dog_api_kit/a2a/__init__.py +53 -0
- cloud_dog_api_kit/a2a/card.py +138 -0
- cloud_dog_api_kit/a2a/events.py +1123 -0
- cloud_dog_api_kit/a2a/gateway.py +105 -0
- cloud_dog_api_kit/a2a/skill_audit.py +107 -0
- cloud_dog_api_kit/auth/__init__.py +35 -0
- cloud_dog_api_kit/auth/dependency.py +121 -0
- cloud_dog_api_kit/auth/rbac.py +107 -0
- cloud_dog_api_kit/auth/service_auth.py +54 -0
- cloud_dog_api_kit/clients/__init__.py +29 -0
- cloud_dog_api_kit/clients/circuit_breaker.py +39 -0
- cloud_dog_api_kit/clients/http_client.py +127 -0
- cloud_dog_api_kit/clients/retry.py +83 -0
- cloud_dog_api_kit/compat/__init__.py +37 -0
- cloud_dog_api_kit/compat/envelope.py +120 -0
- cloud_dog_api_kit/compat/profile.py +102 -0
- cloud_dog_api_kit/compat/routes.py +90 -0
- cloud_dog_api_kit/config.py +54 -0
- cloud_dog_api_kit/correlation/__init__.py +50 -0
- cloud_dog_api_kit/correlation/context.py +118 -0
- cloud_dog_api_kit/correlation/middleware.py +133 -0
- cloud_dog_api_kit/envelopes/__init__.py +37 -0
- cloud_dog_api_kit/envelopes/error.py +87 -0
- cloud_dog_api_kit/envelopes/success.py +84 -0
- cloud_dog_api_kit/errors/__init__.py +51 -0
- cloud_dog_api_kit/errors/exceptions.py +184 -0
- cloud_dog_api_kit/errors/handler.py +102 -0
- cloud_dog_api_kit/errors/taxonomy.py +62 -0
- cloud_dog_api_kit/factory.py +157 -0
- cloud_dog_api_kit/idempotency/__init__.py +28 -0
- cloud_dog_api_kit/idempotency/middleware.py +118 -0
- cloud_dog_api_kit/idempotency/store.py +100 -0
- cloud_dog_api_kit/lifecycle/__init__.py +39 -0
- cloud_dog_api_kit/lifecycle/hooks.py +75 -0
- cloud_dog_api_kit/lifecycle/shutdown.py +178 -0
- cloud_dog_api_kit/mcp/__init__.py +122 -0
- cloud_dog_api_kit/mcp/async_jobs.py +126 -0
- cloud_dog_api_kit/mcp/client_sdk.py +235 -0
- cloud_dog_api_kit/mcp/client_transport/__init__.py +47 -0
- cloud_dog_api_kit/mcp/client_transport/base.py +98 -0
- cloud_dog_api_kit/mcp/client_transport/exceptions.py +37 -0
- cloud_dog_api_kit/mcp/client_transport/http_jsonrpc.py +405 -0
- cloud_dog_api_kit/mcp/client_transport/legacy_sse.py +320 -0
- cloud_dog_api_kit/mcp/client_transport/stdio.py +322 -0
- cloud_dog_api_kit/mcp/client_transport/streamable_http.py +748 -0
- cloud_dog_api_kit/mcp/contract.py +113 -0
- cloud_dog_api_kit/mcp/error_mapper.py +84 -0
- cloud_dog_api_kit/mcp/gateway.py +117 -0
- cloud_dog_api_kit/mcp/legacy_sse.py +129 -0
- cloud_dog_api_kit/mcp/session.py +96 -0
- cloud_dog_api_kit/mcp/sync_handler.py +269 -0
- cloud_dog_api_kit/mcp/tool_audit.py +136 -0
- cloud_dog_api_kit/mcp/tool_router.py +180 -0
- cloud_dog_api_kit/mcp/transport.py +1041 -0
- cloud_dog_api_kit/middleware/__init__.py +39 -0
- cloud_dog_api_kit/middleware/cors.py +74 -0
- cloud_dog_api_kit/middleware/logging.py +98 -0
- cloud_dog_api_kit/middleware/request_size_limit.py +86 -0
- cloud_dog_api_kit/middleware/timeout.py +78 -0
- cloud_dog_api_kit/middleware/timing.py +52 -0
- cloud_dog_api_kit/openapi/__init__.py +30 -0
- cloud_dog_api_kit/openapi/customise.py +69 -0
- cloud_dog_api_kit/openapi/route.py +46 -0
- cloud_dog_api_kit/routers/__init__.py +41 -0
- cloud_dog_api_kit/routers/crud.py +173 -0
- cloud_dog_api_kit/routers/health.py +160 -0
- cloud_dog_api_kit/routers/jobs.py +69 -0
- cloud_dog_api_kit/routers/version.py +46 -0
- cloud_dog_api_kit/schemas/__init__.py +36 -0
- cloud_dog_api_kit/schemas/envelopes.py +37 -0
- cloud_dog_api_kit/schemas/filters.py +103 -0
- cloud_dog_api_kit/schemas/pagination.py +148 -0
- cloud_dog_api_kit/streaming/__init__.py +28 -0
- cloud_dog_api_kit/streaming/events.py +47 -0
- cloud_dog_api_kit/streaming/jsonl.py +68 -0
- cloud_dog_api_kit/streaming/sse.py +102 -0
- cloud_dog_api_kit/testing/__init__.py +46 -0
- cloud_dog_api_kit/testing/conformance.py +156 -0
- cloud_dog_api_kit/testing/fixtures.py +90 -0
- cloud_dog_api_kit/testing/flows/__init__.py +32 -0
- cloud_dog_api_kit/testing/flows/auth_flow.py +41 -0
- cloud_dog_api_kit/testing/flows/crud_flow.py +50 -0
- cloud_dog_api_kit/testing/flows/job_flow.py +42 -0
- cloud_dog_api_kit/testing/flows/streaming_flow.py +42 -0
- cloud_dog_api_kit/traceability_ids.py +84 -0
- cloud_dog_api_kit/versioning/__init__.py +30 -0
- cloud_dog_api_kit/versioning/header.py +52 -0
- cloud_dog_api_kit/web/__init__.py +7 -0
- cloud_dog_api_kit/web/proxy.py +222 -0
- cloud_dog_api_kit/webhook/__init__.py +29 -0
- cloud_dog_api_kit/webhook/signature.py +149 -0
- cloud_dog_api_kit-0.13.0.dist-info/METADATA +27 -0
- cloud_dog_api_kit-0.13.0.dist-info/RECORD +98 -0
- cloud_dog_api_kit-0.13.0.dist-info/WHEEL +4 -0
- cloud_dog_api_kit-0.13.0.dist-info/licenses/LICENCE +190 -0
- cloud_dog_api_kit-0.13.0.dist-info/licenses/LICENSE +176 -0
- cloud_dog_api_kit-0.13.0.dist-info/licenses/NOTICE +7 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# cloud_dog_api_kit — Pagination models and dependency
|
|
16
|
+
#
|
|
17
|
+
# Licence: Proprietary — Cloud-Dog AI Platform
|
|
18
|
+
# Owner: Cloud-Dog AI
|
|
19
|
+
# Description: PaginationParams dataclass and FastAPI dependency for extracting
|
|
20
|
+
# pagination, sort, and filter parameters from list requests.
|
|
21
|
+
# Related requirements: FR4.3
|
|
22
|
+
# Related architecture: CC1.10
|
|
23
|
+
|
|
24
|
+
"""Pagination models and FastAPI dependency."""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import Any, Generic, TypeVar
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
|
|
31
|
+
from fastapi import Query
|
|
32
|
+
from pydantic import BaseModel
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class PaginationParams:
|
|
37
|
+
"""Pagination, sorting, and filtering parameters.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
offset: The starting offset. Defaults to 0.
|
|
41
|
+
limit: The page size. Defaults to 50.
|
|
42
|
+
sort: The field to sort by, or None for default ordering.
|
|
43
|
+
sort_dir: Sort direction — ``asc`` or ``desc``. Defaults to ``asc``.
|
|
44
|
+
|
|
45
|
+
Related tests: UT1.13_PaginationModels, UT1.14_PaginationDependency
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
offset: int = 0
|
|
49
|
+
limit: int = 50
|
|
50
|
+
sort: str | None = None
|
|
51
|
+
sort_dir: str = "asc"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_pagination(
|
|
55
|
+
offset: int = Query(default=0, ge=0, description="Starting offset"),
|
|
56
|
+
limit: int = Query(default=50, ge=1, le=1000, description="Page size"),
|
|
57
|
+
sort: str | None = Query(default=None, description="Sort field (e.g., created_at:desc)"),
|
|
58
|
+
) -> PaginationParams:
|
|
59
|
+
"""FastAPI dependency for extracting pagination + sort parameters.
|
|
60
|
+
|
|
61
|
+
Parses the ``sort`` parameter if it contains a colon-separated direction
|
|
62
|
+
(e.g., ``created_at:desc``).
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
offset: Starting offset (query param).
|
|
66
|
+
limit: Page size (query param).
|
|
67
|
+
sort: Sort specification (query param).
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A populated PaginationParams instance.
|
|
71
|
+
|
|
72
|
+
Related tests: UT1.14_PaginationDependency
|
|
73
|
+
"""
|
|
74
|
+
sort_field = sort
|
|
75
|
+
sort_dir = "asc"
|
|
76
|
+
if sort and ":" in sort:
|
|
77
|
+
parts = sort.rsplit(":", 1)
|
|
78
|
+
sort_field = parts[0]
|
|
79
|
+
if parts[1].lower() in ("asc", "desc"):
|
|
80
|
+
sort_dir = parts[1].lower()
|
|
81
|
+
|
|
82
|
+
return PaginationParams(
|
|
83
|
+
offset=offset,
|
|
84
|
+
limit=limit,
|
|
85
|
+
sort=sort_field,
|
|
86
|
+
sort_dir=sort_dir,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class PageInfo(BaseModel):
|
|
91
|
+
"""Pagination metadata for list responses.
|
|
92
|
+
|
|
93
|
+
Related tests: UT1.13_PaginationModels
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
limit: int
|
|
97
|
+
offset: int
|
|
98
|
+
total: int | None = None
|
|
99
|
+
has_more: bool
|
|
100
|
+
cursor: str | None = None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
T = TypeVar("T")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class PaginatedData(BaseModel, Generic[T]):
|
|
107
|
+
"""Paginated list data within the success envelope.
|
|
108
|
+
|
|
109
|
+
Related tests: UT1.13_PaginationModels
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
items: list[T]
|
|
113
|
+
page: PageInfo
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def paginated_envelope(
|
|
117
|
+
items: list[Any],
|
|
118
|
+
limit: int,
|
|
119
|
+
offset: int,
|
|
120
|
+
total: int | None = None,
|
|
121
|
+
has_more: bool = False,
|
|
122
|
+
cursor: str | None = None,
|
|
123
|
+
request_id: str = "",
|
|
124
|
+
correlation_id: str | None = None,
|
|
125
|
+
version: str = "v1",
|
|
126
|
+
) -> dict[str, Any]:
|
|
127
|
+
"""Build a paginated success response envelope dictionary.
|
|
128
|
+
|
|
129
|
+
Related tests: UT1.13_PaginationModels
|
|
130
|
+
"""
|
|
131
|
+
return {
|
|
132
|
+
"ok": True,
|
|
133
|
+
"data": {
|
|
134
|
+
"items": items,
|
|
135
|
+
"page": {
|
|
136
|
+
"limit": limit,
|
|
137
|
+
"offset": offset,
|
|
138
|
+
"total": total,
|
|
139
|
+
"has_more": has_more,
|
|
140
|
+
"cursor": cursor,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
"meta": {
|
|
144
|
+
"request_id": request_id,
|
|
145
|
+
"correlation_id": correlation_id,
|
|
146
|
+
"version": version,
|
|
147
|
+
},
|
|
148
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# cloud_dog_api_kit — Streaming helpers (SSE, JSONL)
|
|
16
|
+
#
|
|
17
|
+
# Licence: Proprietary — Cloud-Dog AI Platform
|
|
18
|
+
# Owner: Cloud-Dog AI
|
|
19
|
+
# Description: Helpers for creating SSE and JSONL streaming endpoints.
|
|
20
|
+
# Related requirements: FR8.1, FR8.2, FR8.3
|
|
21
|
+
# Related architecture: CC1.14
|
|
22
|
+
|
|
23
|
+
"""Streaming helpers for cloud_dog_api_kit."""
|
|
24
|
+
|
|
25
|
+
from cloud_dog_api_kit.streaming.sse import create_sse_endpoint, SSEEvent
|
|
26
|
+
from cloud_dog_api_kit.streaming.jsonl import create_jsonl_endpoint
|
|
27
|
+
|
|
28
|
+
__all__ = ["create_sse_endpoint", "create_jsonl_endpoint", "SSEEvent"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# cloud_dog_api_kit — Streaming event models
|
|
16
|
+
#
|
|
17
|
+
# Licence: Proprietary — Cloud-Dog AI Platform
|
|
18
|
+
# Owner: Cloud-Dog AI
|
|
19
|
+
# Description: Standard SSE event model and serialisation.
|
|
20
|
+
# Related requirements: FR8.2
|
|
21
|
+
# Related architecture: SA1
|
|
22
|
+
|
|
23
|
+
"""Streaming event models for cloud_dog_api_kit."""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class SSEEvent:
|
|
34
|
+
"""Standard server-sent event payload.
|
|
35
|
+
|
|
36
|
+
Related tests: UT1.22_SSEEventModel
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
type: str
|
|
40
|
+
data: Any = None
|
|
41
|
+
request_id: str = ""
|
|
42
|
+
job_id: str | None = None
|
|
43
|
+
|
|
44
|
+
def to_sse(self) -> str:
|
|
45
|
+
"""Serialise to SSE wire format."""
|
|
46
|
+
payload = {"type": self.type, "data": self.data, "request_id": self.request_id, "job_id": self.job_id}
|
|
47
|
+
return f"event: {self.type}\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# cloud_dog_api_kit — JSONL streaming helpers
|
|
16
|
+
#
|
|
17
|
+
# Licence: Proprietary — Cloud-Dog AI Platform
|
|
18
|
+
# Owner: Cloud-Dog AI
|
|
19
|
+
# Description: Line-delimited JSON streaming endpoint helpers.
|
|
20
|
+
# Related requirements: FR8.1, FR8.3
|
|
21
|
+
# Related architecture: CC1.14
|
|
22
|
+
|
|
23
|
+
"""JSONL streaming helpers for cloud_dog_api_kit."""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
from typing import AsyncGenerator
|
|
29
|
+
|
|
30
|
+
from starlette.responses import StreamingResponse
|
|
31
|
+
|
|
32
|
+
from cloud_dog_api_kit.correlation.context import get_request_id
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def _jsonl_generator(data_generator: AsyncGenerator) -> AsyncGenerator[str, None]:
|
|
36
|
+
"""Wrap an async generator to produce JSONL output.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
data_generator: Async generator yielding dicts or serialisable objects.
|
|
40
|
+
|
|
41
|
+
Yields:
|
|
42
|
+
JSON Lines strings (one JSON object per line).
|
|
43
|
+
"""
|
|
44
|
+
request_id = get_request_id()
|
|
45
|
+
async for item in data_generator:
|
|
46
|
+
if isinstance(item, dict):
|
|
47
|
+
item["request_id"] = request_id
|
|
48
|
+
yield json.dumps(item, default=str) + "\n"
|
|
49
|
+
else:
|
|
50
|
+
yield json.dumps({"data": item, "request_id": request_id}, default=str) + "\n"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def create_jsonl_endpoint(data_generator: AsyncGenerator) -> StreamingResponse:
|
|
54
|
+
"""Create a StreamingResponse for JSONL output.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
data_generator: Async generator yielding data items.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
A StreamingResponse with ``application/x-ndjson`` content type.
|
|
61
|
+
|
|
62
|
+
Related tests: UT1.21_JSONLStreaming
|
|
63
|
+
"""
|
|
64
|
+
return StreamingResponse(
|
|
65
|
+
content=_jsonl_generator(data_generator),
|
|
66
|
+
media_type="application/x-ndjson",
|
|
67
|
+
headers={"Cache-Control": "no-cache"},
|
|
68
|
+
)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# cloud_dog_api_kit — SSE streaming helpers
|
|
16
|
+
#
|
|
17
|
+
# Licence: Proprietary — Cloud-Dog AI Platform
|
|
18
|
+
# Owner: Cloud-Dog AI
|
|
19
|
+
# Description: Server-Sent Events (SSE) streaming endpoint helpers with
|
|
20
|
+
# standard event types per PS-20.
|
|
21
|
+
# Related requirements: FR8.1, FR8.2, FR8.3
|
|
22
|
+
# Related architecture: CC1.14
|
|
23
|
+
|
|
24
|
+
"""SSE streaming helpers for cloud_dog_api_kit."""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
from typing import AsyncGenerator
|
|
30
|
+
|
|
31
|
+
from starlette.responses import StreamingResponse
|
|
32
|
+
|
|
33
|
+
from cloud_dog_api_kit.correlation.context import get_request_id
|
|
34
|
+
from cloud_dog_api_kit.streaming.events import SSEEvent
|
|
35
|
+
|
|
36
|
+
STANDARD_EVENT_TYPES = frozenset({"started", "delta", "progress", "tool_call", "completed", "error"})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def _sse_generator(
|
|
40
|
+
event_generator: AsyncGenerator,
|
|
41
|
+
event_type_field: str = "type",
|
|
42
|
+
) -> AsyncGenerator[str, None]:
|
|
43
|
+
"""Wrap an async generator to produce SSE-formatted output.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
event_generator: Async generator yielding event dicts or SSEEvent objects.
|
|
47
|
+
event_type_field: The key in event dicts that specifies the event type.
|
|
48
|
+
|
|
49
|
+
Yields:
|
|
50
|
+
SSE-formatted strings.
|
|
51
|
+
"""
|
|
52
|
+
request_id = get_request_id()
|
|
53
|
+
try:
|
|
54
|
+
async for event in event_generator:
|
|
55
|
+
if isinstance(event, SSEEvent):
|
|
56
|
+
if not event.request_id:
|
|
57
|
+
event.request_id = request_id
|
|
58
|
+
yield event.to_sse()
|
|
59
|
+
elif isinstance(event, dict):
|
|
60
|
+
sse_event = SSEEvent(
|
|
61
|
+
type=event.get(event_type_field, "delta"),
|
|
62
|
+
data=event.get("data", event),
|
|
63
|
+
request_id=request_id,
|
|
64
|
+
job_id=event.get("job_id"),
|
|
65
|
+
)
|
|
66
|
+
yield sse_event.to_sse()
|
|
67
|
+
else:
|
|
68
|
+
yield f"event: delta\ndata: {json.dumps({'data': str(event), 'request_id': request_id})}\n\n"
|
|
69
|
+
|
|
70
|
+
# Send completion event
|
|
71
|
+
completion = SSEEvent(type="completed", request_id=request_id)
|
|
72
|
+
yield completion.to_sse()
|
|
73
|
+
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
error_event = SSEEvent(
|
|
76
|
+
type="error",
|
|
77
|
+
data={"message": str(exc)},
|
|
78
|
+
request_id=request_id,
|
|
79
|
+
)
|
|
80
|
+
yield error_event.to_sse()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def create_sse_endpoint(
|
|
84
|
+
event_generator: AsyncGenerator,
|
|
85
|
+
event_type_field: str = "type",
|
|
86
|
+
) -> StreamingResponse:
|
|
87
|
+
"""Create a StreamingResponse for SSE output.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
event_generator: Async generator yielding events.
|
|
91
|
+
event_type_field: The key in event dicts for event type.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
A StreamingResponse with ``text/event-stream`` content type.
|
|
95
|
+
|
|
96
|
+
Related tests: UT1.20_SSEStreaming, ST1.9_StreamingEndToEnd
|
|
97
|
+
"""
|
|
98
|
+
return StreamingResponse(
|
|
99
|
+
content=_sse_generator(event_generator, event_type_field),
|
|
100
|
+
media_type="text/event-stream",
|
|
101
|
+
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
|
102
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# cloud_dog_api_kit — Test scaffolding (fixtures, conformance, flow templates)
|
|
16
|
+
#
|
|
17
|
+
# Licence: Proprietary — Cloud-Dog AI Platform
|
|
18
|
+
# Owner: Cloud-Dog AI
|
|
19
|
+
# Description: Reusable test fixtures, conformance validators, and baseline
|
|
20
|
+
# flow templates for verifying API compliance.
|
|
21
|
+
# Related requirements: FR16.1, FR16.2, FR16.3
|
|
22
|
+
# Related architecture: CC1.19
|
|
23
|
+
|
|
24
|
+
"""Test scaffolding for cloud_dog_api_kit."""
|
|
25
|
+
|
|
26
|
+
from cloud_dog_api_kit.testing.fixtures import create_test_client, create_auth_headers
|
|
27
|
+
from cloud_dog_api_kit.testing.conformance import (
|
|
28
|
+
validate_error_envelope,
|
|
29
|
+
validate_success_envelope,
|
|
30
|
+
validate_pagination_response,
|
|
31
|
+
validate_correlation_id,
|
|
32
|
+
)
|
|
33
|
+
from cloud_dog_api_kit.testing.flows import AuthFlow, CRUDFlow, JobFlow, StreamingFlow
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"create_test_client",
|
|
37
|
+
"create_auth_headers",
|
|
38
|
+
"validate_error_envelope",
|
|
39
|
+
"validate_success_envelope",
|
|
40
|
+
"validate_pagination_response",
|
|
41
|
+
"validate_correlation_id",
|
|
42
|
+
"AuthFlow",
|
|
43
|
+
"CRUDFlow",
|
|
44
|
+
"JobFlow",
|
|
45
|
+
"StreamingFlow",
|
|
46
|
+
]
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# cloud_dog_api_kit — Conformance test helpers
|
|
16
|
+
#
|
|
17
|
+
# Licence: Proprietary — Cloud-Dog AI Platform
|
|
18
|
+
# Owner: Cloud-Dog AI
|
|
19
|
+
# Description: Conformance validators for verifying API responses match
|
|
20
|
+
# the standard envelope schemas, pagination format, and correlation ID rules.
|
|
21
|
+
# Related requirements: FR16.2
|
|
22
|
+
# Related architecture: CC1.19
|
|
23
|
+
|
|
24
|
+
"""Conformance test helpers for cloud_dog_api_kit."""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_success_envelope(response_json: dict[str, Any]) -> list[str]:
|
|
32
|
+
"""Validate a response against the success envelope schema.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
response_json: The parsed JSON response body.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
A list of validation error messages. Empty if valid.
|
|
39
|
+
|
|
40
|
+
Related tests: UT1.35_ConformanceValidators
|
|
41
|
+
"""
|
|
42
|
+
errors: list[str] = []
|
|
43
|
+
if not isinstance(response_json, dict):
|
|
44
|
+
return ["Response is not a dict"]
|
|
45
|
+
|
|
46
|
+
if response_json.get("ok") is not True:
|
|
47
|
+
errors.append("Missing or false 'ok' field")
|
|
48
|
+
|
|
49
|
+
if "data" not in response_json:
|
|
50
|
+
errors.append("Missing 'data' field")
|
|
51
|
+
|
|
52
|
+
if "meta" not in response_json:
|
|
53
|
+
errors.append("Missing 'meta' field")
|
|
54
|
+
else:
|
|
55
|
+
meta = response_json["meta"]
|
|
56
|
+
if not isinstance(meta, dict):
|
|
57
|
+
errors.append("'meta' is not a dict")
|
|
58
|
+
else:
|
|
59
|
+
if "request_id" not in meta:
|
|
60
|
+
errors.append("Missing 'meta.request_id'")
|
|
61
|
+
|
|
62
|
+
return errors
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def validate_error_envelope(response_json: dict[str, Any]) -> list[str]:
|
|
66
|
+
"""Validate a response against the error envelope schema.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
response_json: The parsed JSON response body.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
A list of validation error messages. Empty if valid.
|
|
73
|
+
|
|
74
|
+
Related tests: UT1.35_ConformanceValidators
|
|
75
|
+
"""
|
|
76
|
+
errors: list[str] = []
|
|
77
|
+
if not isinstance(response_json, dict):
|
|
78
|
+
return ["Response is not a dict"]
|
|
79
|
+
|
|
80
|
+
if response_json.get("ok") is not False:
|
|
81
|
+
errors.append("'ok' field should be false")
|
|
82
|
+
|
|
83
|
+
if "error" not in response_json:
|
|
84
|
+
errors.append("Missing 'error' field")
|
|
85
|
+
else:
|
|
86
|
+
error = response_json["error"]
|
|
87
|
+
if not isinstance(error, dict):
|
|
88
|
+
errors.append("'error' is not a dict")
|
|
89
|
+
else:
|
|
90
|
+
if "code" not in error:
|
|
91
|
+
errors.append("Missing 'error.code'")
|
|
92
|
+
if "message" not in error:
|
|
93
|
+
errors.append("Missing 'error.message'")
|
|
94
|
+
if "retryable" not in error:
|
|
95
|
+
errors.append("Missing 'error.retryable'")
|
|
96
|
+
|
|
97
|
+
if "meta" not in response_json:
|
|
98
|
+
errors.append("Missing 'meta' field")
|
|
99
|
+
|
|
100
|
+
return errors
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def validate_pagination_response(response_json: dict[str, Any]) -> list[str]:
|
|
104
|
+
"""Validate a paginated list response.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
response_json: The parsed JSON response body.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
A list of validation error messages. Empty if valid.
|
|
111
|
+
|
|
112
|
+
Related tests: UT1.35_ConformanceValidators
|
|
113
|
+
"""
|
|
114
|
+
errors = validate_success_envelope(response_json)
|
|
115
|
+
if errors:
|
|
116
|
+
return errors
|
|
117
|
+
|
|
118
|
+
data = response_json.get("data", {})
|
|
119
|
+
if "items" not in data:
|
|
120
|
+
errors.append("Missing 'data.items'")
|
|
121
|
+
elif not isinstance(data["items"], list):
|
|
122
|
+
errors.append("'data.items' is not a list")
|
|
123
|
+
|
|
124
|
+
if "page" not in data:
|
|
125
|
+
errors.append("Missing 'data.page'")
|
|
126
|
+
else:
|
|
127
|
+
page = data["page"]
|
|
128
|
+
if not isinstance(page, dict):
|
|
129
|
+
errors.append("'data.page' is not a dict")
|
|
130
|
+
else:
|
|
131
|
+
for field in ("limit", "offset", "has_more"):
|
|
132
|
+
if field not in page:
|
|
133
|
+
errors.append(f"Missing 'data.page.{field}'")
|
|
134
|
+
|
|
135
|
+
return errors
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def validate_correlation_id(response_headers: dict[str, str]) -> list[str]:
|
|
139
|
+
"""Validate that correlation ID is present in response headers.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
response_headers: The HTTP response headers.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
A list of validation error messages. Empty if valid.
|
|
146
|
+
|
|
147
|
+
Related tests: UT1.35_ConformanceValidators
|
|
148
|
+
"""
|
|
149
|
+
errors: list[str] = []
|
|
150
|
+
# Check for X-Request-Id (case-insensitive)
|
|
151
|
+
header_keys_lower = {k.lower(): v for k, v in response_headers.items()}
|
|
152
|
+
if "x-request-id" not in header_keys_lower:
|
|
153
|
+
errors.append("Missing X-Request-Id response header")
|
|
154
|
+
elif not header_keys_lower["x-request-id"]:
|
|
155
|
+
errors.append("Empty X-Request-Id response header")
|
|
156
|
+
return errors
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# cloud_dog_api_kit — Reusable test fixtures
|
|
16
|
+
#
|
|
17
|
+
# Licence: Proprietary — Cloud-Dog AI Platform
|
|
18
|
+
# Owner: Cloud-Dog AI
|
|
19
|
+
# Description: Reusable test fixtures for API testing — configured TestClient
|
|
20
|
+
# with auth headers and database session protocol.
|
|
21
|
+
# Related requirements: FR16.1
|
|
22
|
+
# Related architecture: CC1.19
|
|
23
|
+
|
|
24
|
+
"""Reusable test fixtures for cloud_dog_api_kit."""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
from fastapi import FastAPI
|
|
30
|
+
from httpx import ASGITransport, AsyncClient
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_test_client(
|
|
34
|
+
app: FastAPI,
|
|
35
|
+
base_url: str = "http://test",
|
|
36
|
+
api_key: str | None = None,
|
|
37
|
+
bearer_token: str | None = None,
|
|
38
|
+
) -> AsyncClient:
|
|
39
|
+
"""Create a configured async test client for a FastAPI application.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
app: The FastAPI application.
|
|
43
|
+
base_url: Base URL for the test client.
|
|
44
|
+
api_key: Optional API key to include in all requests.
|
|
45
|
+
bearer_token: Optional Bearer token to include in all requests.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
A configured httpx.AsyncClient.
|
|
49
|
+
|
|
50
|
+
Related tests: UT1.34_TestFixtures
|
|
51
|
+
"""
|
|
52
|
+
headers: dict[str, str] = {}
|
|
53
|
+
if api_key:
|
|
54
|
+
headers["X-API-Key"] = api_key
|
|
55
|
+
if bearer_token:
|
|
56
|
+
headers["Authorization"] = f"Bearer {bearer_token}"
|
|
57
|
+
|
|
58
|
+
transport = ASGITransport(app=app)
|
|
59
|
+
return AsyncClient(
|
|
60
|
+
transport=transport,
|
|
61
|
+
base_url=base_url,
|
|
62
|
+
headers=headers,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def create_auth_headers(
|
|
67
|
+
api_key: str | None = None,
|
|
68
|
+
bearer_token: str | None = None,
|
|
69
|
+
app_id: str | None = None,
|
|
70
|
+
) -> dict[str, str]:
|
|
71
|
+
"""Create standard authentication headers.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
api_key: API key value.
|
|
75
|
+
bearer_token: Bearer token value.
|
|
76
|
+
app_id: Application ID for service-to-service calls.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
A dictionary of HTTP headers.
|
|
80
|
+
|
|
81
|
+
Related tests: UT1.34_TestFixtures
|
|
82
|
+
"""
|
|
83
|
+
headers: dict[str, str] = {}
|
|
84
|
+
if api_key:
|
|
85
|
+
headers["X-API-Key"] = api_key
|
|
86
|
+
if bearer_token:
|
|
87
|
+
headers["Authorization"] = f"Bearer {bearer_token}"
|
|
88
|
+
if app_id:
|
|
89
|
+
headers["X-App-Id"] = app_id
|
|
90
|
+
return headers
|