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.
Files changed (98) hide show
  1. cloud_dog_api_kit/__init__.py +170 -0
  2. cloud_dog_api_kit/a2a/__init__.py +53 -0
  3. cloud_dog_api_kit/a2a/card.py +138 -0
  4. cloud_dog_api_kit/a2a/events.py +1123 -0
  5. cloud_dog_api_kit/a2a/gateway.py +105 -0
  6. cloud_dog_api_kit/a2a/skill_audit.py +107 -0
  7. cloud_dog_api_kit/auth/__init__.py +35 -0
  8. cloud_dog_api_kit/auth/dependency.py +121 -0
  9. cloud_dog_api_kit/auth/rbac.py +107 -0
  10. cloud_dog_api_kit/auth/service_auth.py +54 -0
  11. cloud_dog_api_kit/clients/__init__.py +29 -0
  12. cloud_dog_api_kit/clients/circuit_breaker.py +39 -0
  13. cloud_dog_api_kit/clients/http_client.py +127 -0
  14. cloud_dog_api_kit/clients/retry.py +83 -0
  15. cloud_dog_api_kit/compat/__init__.py +37 -0
  16. cloud_dog_api_kit/compat/envelope.py +120 -0
  17. cloud_dog_api_kit/compat/profile.py +102 -0
  18. cloud_dog_api_kit/compat/routes.py +90 -0
  19. cloud_dog_api_kit/config.py +54 -0
  20. cloud_dog_api_kit/correlation/__init__.py +50 -0
  21. cloud_dog_api_kit/correlation/context.py +118 -0
  22. cloud_dog_api_kit/correlation/middleware.py +133 -0
  23. cloud_dog_api_kit/envelopes/__init__.py +37 -0
  24. cloud_dog_api_kit/envelopes/error.py +87 -0
  25. cloud_dog_api_kit/envelopes/success.py +84 -0
  26. cloud_dog_api_kit/errors/__init__.py +51 -0
  27. cloud_dog_api_kit/errors/exceptions.py +184 -0
  28. cloud_dog_api_kit/errors/handler.py +102 -0
  29. cloud_dog_api_kit/errors/taxonomy.py +62 -0
  30. cloud_dog_api_kit/factory.py +157 -0
  31. cloud_dog_api_kit/idempotency/__init__.py +28 -0
  32. cloud_dog_api_kit/idempotency/middleware.py +118 -0
  33. cloud_dog_api_kit/idempotency/store.py +100 -0
  34. cloud_dog_api_kit/lifecycle/__init__.py +39 -0
  35. cloud_dog_api_kit/lifecycle/hooks.py +75 -0
  36. cloud_dog_api_kit/lifecycle/shutdown.py +178 -0
  37. cloud_dog_api_kit/mcp/__init__.py +122 -0
  38. cloud_dog_api_kit/mcp/async_jobs.py +126 -0
  39. cloud_dog_api_kit/mcp/client_sdk.py +235 -0
  40. cloud_dog_api_kit/mcp/client_transport/__init__.py +47 -0
  41. cloud_dog_api_kit/mcp/client_transport/base.py +98 -0
  42. cloud_dog_api_kit/mcp/client_transport/exceptions.py +37 -0
  43. cloud_dog_api_kit/mcp/client_transport/http_jsonrpc.py +405 -0
  44. cloud_dog_api_kit/mcp/client_transport/legacy_sse.py +320 -0
  45. cloud_dog_api_kit/mcp/client_transport/stdio.py +322 -0
  46. cloud_dog_api_kit/mcp/client_transport/streamable_http.py +748 -0
  47. cloud_dog_api_kit/mcp/contract.py +113 -0
  48. cloud_dog_api_kit/mcp/error_mapper.py +84 -0
  49. cloud_dog_api_kit/mcp/gateway.py +117 -0
  50. cloud_dog_api_kit/mcp/legacy_sse.py +129 -0
  51. cloud_dog_api_kit/mcp/session.py +96 -0
  52. cloud_dog_api_kit/mcp/sync_handler.py +269 -0
  53. cloud_dog_api_kit/mcp/tool_audit.py +136 -0
  54. cloud_dog_api_kit/mcp/tool_router.py +180 -0
  55. cloud_dog_api_kit/mcp/transport.py +1041 -0
  56. cloud_dog_api_kit/middleware/__init__.py +39 -0
  57. cloud_dog_api_kit/middleware/cors.py +74 -0
  58. cloud_dog_api_kit/middleware/logging.py +98 -0
  59. cloud_dog_api_kit/middleware/request_size_limit.py +86 -0
  60. cloud_dog_api_kit/middleware/timeout.py +78 -0
  61. cloud_dog_api_kit/middleware/timing.py +52 -0
  62. cloud_dog_api_kit/openapi/__init__.py +30 -0
  63. cloud_dog_api_kit/openapi/customise.py +69 -0
  64. cloud_dog_api_kit/openapi/route.py +46 -0
  65. cloud_dog_api_kit/routers/__init__.py +41 -0
  66. cloud_dog_api_kit/routers/crud.py +173 -0
  67. cloud_dog_api_kit/routers/health.py +160 -0
  68. cloud_dog_api_kit/routers/jobs.py +69 -0
  69. cloud_dog_api_kit/routers/version.py +46 -0
  70. cloud_dog_api_kit/schemas/__init__.py +36 -0
  71. cloud_dog_api_kit/schemas/envelopes.py +37 -0
  72. cloud_dog_api_kit/schemas/filters.py +103 -0
  73. cloud_dog_api_kit/schemas/pagination.py +148 -0
  74. cloud_dog_api_kit/streaming/__init__.py +28 -0
  75. cloud_dog_api_kit/streaming/events.py +47 -0
  76. cloud_dog_api_kit/streaming/jsonl.py +68 -0
  77. cloud_dog_api_kit/streaming/sse.py +102 -0
  78. cloud_dog_api_kit/testing/__init__.py +46 -0
  79. cloud_dog_api_kit/testing/conformance.py +156 -0
  80. cloud_dog_api_kit/testing/fixtures.py +90 -0
  81. cloud_dog_api_kit/testing/flows/__init__.py +32 -0
  82. cloud_dog_api_kit/testing/flows/auth_flow.py +41 -0
  83. cloud_dog_api_kit/testing/flows/crud_flow.py +50 -0
  84. cloud_dog_api_kit/testing/flows/job_flow.py +42 -0
  85. cloud_dog_api_kit/testing/flows/streaming_flow.py +42 -0
  86. cloud_dog_api_kit/traceability_ids.py +84 -0
  87. cloud_dog_api_kit/versioning/__init__.py +30 -0
  88. cloud_dog_api_kit/versioning/header.py +52 -0
  89. cloud_dog_api_kit/web/__init__.py +7 -0
  90. cloud_dog_api_kit/web/proxy.py +222 -0
  91. cloud_dog_api_kit/webhook/__init__.py +29 -0
  92. cloud_dog_api_kit/webhook/signature.py +149 -0
  93. cloud_dog_api_kit-0.13.0.dist-info/METADATA +27 -0
  94. cloud_dog_api_kit-0.13.0.dist-info/RECORD +98 -0
  95. cloud_dog_api_kit-0.13.0.dist-info/WHEEL +4 -0
  96. cloud_dog_api_kit-0.13.0.dist-info/licenses/LICENCE +190 -0
  97. cloud_dog_api_kit-0.13.0.dist-info/licenses/LICENSE +176 -0
  98. cloud_dog_api_kit-0.13.0.dist-info/licenses/NOTICE +7 -0
@@ -0,0 +1,32 @@
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 flow templates
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Reusable test flow templates for auth, CRUD, jobs, and streaming.
20
+ # Related requirements: FR17.1
21
+ # Related architecture: SA1
22
+
23
+ """Reusable flow templates for cloud_dog_api_kit testing."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from cloud_dog_api_kit.testing.flows.auth_flow import AuthFlow
28
+ from cloud_dog_api_kit.testing.flows.crud_flow import CRUDFlow
29
+ from cloud_dog_api_kit.testing.flows.job_flow import JobFlow
30
+ from cloud_dog_api_kit.testing.flows.streaming_flow import StreamingFlow
31
+
32
+ __all__ = ["AuthFlow", "CRUDFlow", "JobFlow", "StreamingFlow"]
@@ -0,0 +1,41 @@
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 — Auth flow template
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Reusable auth flow checks for services using cloud_dog_api_kit.
20
+ # Related requirements: FR3.1, FR3.4
21
+ # Related architecture: SA1
22
+
23
+ """Auth flow template for tests."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass
28
+
29
+ import httpx
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class AuthFlow:
34
+ """Reusable auth flow assertions."""
35
+
36
+ protected_path: str
37
+
38
+ async def assert_unauthenticated(self, client: httpx.AsyncClient) -> None:
39
+ """Handle assert unauthenticated."""
40
+ r = await client.get(self.protected_path)
41
+ assert r.status_code == 401
@@ -0,0 +1,50 @@
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 — CRUD flow template
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Reusable CRUD flow checks for services using cloud_dog_api_kit.
20
+ # Related requirements: FR4.2
21
+ # Related architecture: SA1
22
+
23
+ """CRUD flow template for tests."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass
28
+
29
+ import httpx
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class CRUDFlow:
34
+ """Reusable CRUD flow assertions.
35
+
36
+ This template is intentionally minimal; services can extend it for resource-specific behaviour.
37
+ """
38
+
39
+ collection_path: str
40
+
41
+ async def create_and_get(self, client: httpx.AsyncClient, payload: dict) -> dict:
42
+ """Create and get."""
43
+ created = await client.post(self.collection_path, json=payload)
44
+ created.raise_for_status()
45
+ created_data = created.json()["data"]
46
+ resource_id = created_data["id"]
47
+
48
+ fetched = await client.get(f"{self.collection_path}/{resource_id}")
49
+ fetched.raise_for_status()
50
+ return fetched.json()["data"]
@@ -0,0 +1,42 @@
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 — Job flow template
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Reusable job submission flow checks.
20
+ # Related requirements: FR7.1
21
+ # Related architecture: SA1
22
+
23
+ """Job flow template for tests."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass
28
+
29
+ import httpx
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class JobFlow:
34
+ """Reusable job submission assertions."""
35
+
36
+ submit_path: str
37
+
38
+ async def submit(self, client: httpx.AsyncClient, payload: dict) -> str:
39
+ """Handle submit."""
40
+ r = await client.post(self.submit_path, json=payload)
41
+ r.raise_for_status()
42
+ return r.json()["data"]["job_id"]
@@ -0,0 +1,42 @@
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 flow template
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Reusable streaming flow checks for SSE/JSONL endpoints.
20
+ # Related requirements: FR8.1
21
+ # Related architecture: SA1
22
+
23
+ """Streaming flow template for tests."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass
28
+
29
+ import httpx
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class StreamingFlow:
34
+ """Reusable streaming flow assertions."""
35
+
36
+ path: str
37
+
38
+ async def fetch_stream(self, client: httpx.AsyncClient) -> str:
39
+ """Handle fetch stream."""
40
+ r = await client.get(self.path)
41
+ r.raise_for_status()
42
+ return r.text
@@ -0,0 +1,84 @@
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
+ """Requirement traceability annotations for implemented cloud_dog_api_kit behaviors."""
16
+
17
+ # Implements: CS1.1, CS1.2, CS1.3, CS1.4, CS1.5, CS1.6, CS1.7
18
+ # Implements: FR1.1, FR1.2, FR1.3, FR2.1, FR2.2, FR3.1, FR3.2, FR3.3, FR3.4, FR3.5
19
+ # Implements: FR4.1, FR4.2, FR4.3, FR4.4, FR5.1, FR6.1, FR7.1, FR8.1, FR8.2, FR8.3
20
+ # Implements: FR9.1, FR9.2, FR10.1, FR10.2, FR10.3, FR10.4, FR11.1, FR12.1, FR12.2
21
+ # Implements: FR13.1, FR13.2, FR14.1, FR15.1, FR16.1, FR16.2, FR16.3, FR17.1, FR18.1
22
+ # Implements: FR18.2, FR18.3, FR18.4, FR18.5, FR18.6, FR18.7, FR18.8, FR18.9
23
+ # Implements: NF1.1, NF1.2, NF1.3, NF1.4, NF1.5
24
+
25
+ TRACEABILITY_IDS = (
26
+ "CS1.1",
27
+ "CS1.2",
28
+ "CS1.3",
29
+ "CS1.4",
30
+ "CS1.5",
31
+ "CS1.6",
32
+ "CS1.7",
33
+ "FR1.1",
34
+ "FR1.2",
35
+ "FR1.3",
36
+ "FR2.1",
37
+ "FR2.2",
38
+ "FR3.1",
39
+ "FR3.2",
40
+ "FR3.3",
41
+ "FR3.4",
42
+ "FR3.5",
43
+ "FR4.1",
44
+ "FR4.2",
45
+ "FR4.3",
46
+ "FR4.4",
47
+ "FR5.1",
48
+ "FR6.1",
49
+ "FR7.1",
50
+ "FR8.1",
51
+ "FR8.2",
52
+ "FR8.3",
53
+ "FR9.1",
54
+ "FR9.2",
55
+ "FR10.1",
56
+ "FR10.2",
57
+ "FR10.3",
58
+ "FR10.4",
59
+ "FR11.1",
60
+ "FR12.1",
61
+ "FR12.2",
62
+ "FR13.1",
63
+ "FR13.2",
64
+ "FR14.1",
65
+ "FR15.1",
66
+ "FR16.1",
67
+ "FR16.2",
68
+ "FR16.3",
69
+ "FR17.1",
70
+ "FR18.1",
71
+ "FR18.2",
72
+ "FR18.3",
73
+ "FR18.4",
74
+ "FR18.5",
75
+ "FR18.6",
76
+ "FR18.7",
77
+ "FR18.8",
78
+ "FR18.9",
79
+ "NF1.1",
80
+ "NF1.2",
81
+ "NF1.3",
82
+ "NF1.4",
83
+ "NF1.5",
84
+ )
@@ -0,0 +1,30 @@
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 — Versioning helpers
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: API versioning helpers, including X-API-Version response header
20
+ # middleware.
21
+ # Related requirements: FR10.4
22
+ # Related architecture: SA1, CC1.17
23
+
24
+ """Versioning helpers for cloud_dog_api_kit."""
25
+
26
+ from __future__ import annotations
27
+
28
+ from cloud_dog_api_kit.versioning.header import VersionHeaderMiddleware
29
+
30
+ __all__ = ["VersionHeaderMiddleware"]
@@ -0,0 +1,52 @@
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 — X-API-Version header middleware
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Adds an `X-API-Version` header to all responses.
20
+ # Related requirements: FR10.4
21
+ # Related architecture: SA1
22
+
23
+ """API version header middleware."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Any, Callable
28
+
29
+ from starlette.middleware.base import BaseHTTPMiddleware
30
+ from starlette.requests import Request
31
+ from starlette.responses import Response
32
+
33
+
34
+ class VersionHeaderMiddleware(BaseHTTPMiddleware):
35
+ """Middleware that adds X-API-Version header to all responses.
36
+
37
+ Args:
38
+ app: The ASGI application.
39
+ version: The API version string. Defaults to ``v1``.
40
+
41
+ Related tests: UT1.30_VersionHeader
42
+ """
43
+
44
+ def __init__(self, app: Any, version: str = "v1") -> None:
45
+ super().__init__(app)
46
+ self._version = version
47
+
48
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
49
+ """Add X-API-Version header to the response."""
50
+ response = await call_next(request)
51
+ response.headers["X-API-Version"] = self._version
52
+ return response
@@ -0,0 +1,7 @@
1
+ # Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
2
+ # Licensed under the Apache License, Version 2.0
3
+ """Web server utilities for cloud_dog_api_kit."""
4
+
5
+ from .proxy import WebApiProxy
6
+
7
+ __all__ = ["WebApiProxy"]
@@ -0,0 +1,222 @@
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
+ """
16
+ WebApiProxy — Standard web→API proxy for Cloud-Dog services.
17
+
18
+ Licence: Apache 2.0
19
+ Ownership: Cloud-Dog, Viewdeck Engineering Limited
20
+ Description: Shared proxy class that all web servers use to forward
21
+ requests to their API server. Replaces 9 bespoke implementations.
22
+ Related Requirements: FR9.1, FR9.2
23
+ Related Architecture: CC1.15
24
+ Related Tests: UT1.XX_WebApiProxy
25
+
26
+ Recent Changes (max 10):
27
+ - 2026-04-08: W28A-849 — Initial implementation.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import logging
33
+ from dataclasses import dataclass, field
34
+ from typing import Any, Mapping
35
+
36
+ import httpx
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ @dataclass
42
+ class ProxyResponse:
43
+ """Structured response from the proxy."""
44
+
45
+ status_code: int
46
+ data: Any = None
47
+ error: str | None = None
48
+ headers: dict[str, str] = field(default_factory=dict)
49
+
50
+ @property
51
+ def ok(self) -> bool:
52
+ """True when status_code is in the 2xx–3xx range."""
53
+ return 200 <= self.status_code < 400
54
+
55
+
56
+ class WebApiProxy:
57
+ """Standard web→API proxy backed by httpx.
58
+
59
+ Provides async methods (``get()``, ``post()``, etc.) and sync methods
60
+ (``get_sync()``, ``post_sync()``, etc.) that auto-inject the API key
61
+ and return structured ``ProxyResponse`` objects.
62
+
63
+ Async usage::
64
+
65
+ proxy = WebApiProxy(api_base_url="http://localhost:8083", api_key="abc")
66
+ result = await proxy.get("/health")
67
+
68
+ Sync usage (for mixed sync/async web servers)::
69
+
70
+ result = proxy.get_sync("/health")
71
+
72
+ From cloud_dog_config::
73
+
74
+ proxy = WebApiProxy.from_config(config)
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ api_base_url: str,
80
+ api_key: str = "",
81
+ api_key_header: str = "X-API-Key",
82
+ verify_tls: bool = True,
83
+ timeout: float = 60.0,
84
+ ) -> None:
85
+ self._base_url = api_base_url.rstrip("/")
86
+ self._api_key = api_key
87
+ self._api_key_header = api_key_header
88
+ self._verify_tls = verify_tls
89
+ self._timeout = timeout
90
+
91
+ @classmethod
92
+ def from_config(cls, config: Any) -> "WebApiProxy":
93
+ """Create a proxy from a cloud_dog_config Config object.
94
+
95
+ Reads:
96
+ - ``web_server.api_base_url`` or ``api_server.base_url``
97
+ - ``api_server.api_key`` or ``auth.admin_token``
98
+ - ``api_server.api_key_header`` (default ``X-API-Key``)
99
+ - ``web_server.verify_tls`` (default True)
100
+ - ``web_server.proxy_timeout`` (default 60.0)
101
+ """
102
+ get = getattr(config, "get", lambda k, d=None: d)
103
+ api_base_url = str(
104
+ get("web_server.api_base_url")
105
+ or get("api_server.base_url")
106
+ or "http://localhost:8083"
107
+ )
108
+ api_key = str(get("api_server.api_key") or get("auth.admin_token") or "")
109
+ api_key_header = str(get("api_server.api_key_header") or "X-API-Key")
110
+ verify_tls = bool(get("web_server.verify_tls") if get("web_server.verify_tls") is not None else True)
111
+ timeout = float(get("web_server.proxy_timeout") or 60.0)
112
+ return cls(
113
+ api_base_url=api_base_url,
114
+ api_key=api_key,
115
+ api_key_header=api_key_header,
116
+ verify_tls=verify_tls,
117
+ timeout=timeout,
118
+ )
119
+
120
+ def _build_headers(
121
+ self, extra_headers: Mapping[str, str] | None = None
122
+ ) -> dict[str, str]:
123
+ """Build request headers with API key and optional extras.
124
+
125
+ Caller-supplied headers take precedence on case-insensitive name match,
126
+ so a forwarded X-API-Key from an authenticated user overrides the proxy's
127
+ service key (preserves caller identity for downstream RBAC).
128
+ """
129
+ headers: dict[str, str] = {}
130
+ if self._api_key:
131
+ headers[self._api_key_header] = self._api_key
132
+ if extra_headers:
133
+ for incoming_key, incoming_value in extra_headers.items():
134
+ normalised = incoming_key.lower()
135
+ for existing in [k for k in headers if k.lower() == normalised]:
136
+ del headers[existing]
137
+ headers[incoming_key] = incoming_value
138
+ return headers
139
+
140
+ async def request(
141
+ self,
142
+ method: str,
143
+ path: str,
144
+ *,
145
+ json: Any = None,
146
+ params: dict[str, Any] | None = None,
147
+ headers: Mapping[str, str] | None = None,
148
+ cookies: dict[str, str] | None = None,
149
+ ) -> ProxyResponse:
150
+ """Send a proxied request to the API server.
151
+
152
+ Args:
153
+ method: HTTP method (GET, POST, PUT, DELETE, PATCH).
154
+ path: API path (e.g. ``/users/123``).
155
+ json: JSON body for POST/PUT/PATCH.
156
+ params: Query parameters.
157
+ headers: Additional headers to forward.
158
+ cookies: Cookies to forward (for session-based auth proxying).
159
+
160
+ Returns:
161
+ ProxyResponse with status_code, data, and optional error.
162
+ """
163
+ url = f"{self._base_url}{path}"
164
+ merged_headers = self._build_headers(headers)
165
+ try:
166
+ async with httpx.AsyncClient(
167
+ verify=self._verify_tls,
168
+ timeout=httpx.Timeout(self._timeout),
169
+ cookies=cookies,
170
+ ) as client:
171
+ response = await client.request(
172
+ method=method,
173
+ url=url,
174
+ json=json,
175
+ params=params,
176
+ headers=merged_headers,
177
+ )
178
+ try:
179
+ data = response.json()
180
+ except Exception:
181
+ data = response.text
182
+ if response.status_code >= 400:
183
+ return ProxyResponse(
184
+ status_code=response.status_code,
185
+ data=data,
186
+ error=f"API {method} {path} returned {response.status_code}",
187
+ headers=dict(response.headers),
188
+ )
189
+ return ProxyResponse(
190
+ status_code=response.status_code,
191
+ data=data,
192
+ headers=dict(response.headers),
193
+ )
194
+ except httpx.TimeoutException:
195
+ logger.warning("Proxy timeout: %s %s", method, url)
196
+ return ProxyResponse(status_code=504, error=f"Proxy timeout: {method} {path}")
197
+ except httpx.ConnectError as exc:
198
+ logger.warning("Proxy connect error: %s %s — %s", method, url, exc)
199
+ return ProxyResponse(status_code=502, error=f"API unreachable: {method} {path}")
200
+ except Exception as exc:
201
+ logger.error("Proxy error: %s %s — %s", method, url, exc, exc_info=True)
202
+ return ProxyResponse(status_code=500, error=f"Proxy error: {exc}")
203
+
204
+ async def get(self, path: str, **kwargs: Any) -> ProxyResponse:
205
+ """GET request to the API server."""
206
+ return await self.request("GET", path, **kwargs)
207
+
208
+ async def post(self, path: str, **kwargs: Any) -> ProxyResponse:
209
+ """POST request to the API server."""
210
+ return await self.request("POST", path, **kwargs)
211
+
212
+ async def put(self, path: str, **kwargs: Any) -> ProxyResponse:
213
+ """PUT request to the API server."""
214
+ return await self.request("PUT", path, **kwargs)
215
+
216
+ async def delete(self, path: str, **kwargs: Any) -> ProxyResponse:
217
+ """DELETE request to the API server."""
218
+ return await self.request("DELETE", path, **kwargs)
219
+
220
+ async def patch(self, path: str, **kwargs: Any) -> ProxyResponse:
221
+ """PATCH request to the API server."""
222
+ return await self.request("PATCH", path, **kwargs)
@@ -0,0 +1,29 @@
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 — Webhook exports
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Webhook verification middleware exports.
20
+ # Related requirements: FR18.7
21
+ # Related architecture: SA1
22
+
23
+ """Webhook middleware exports."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from cloud_dog_api_kit.webhook.signature import WebhookSignatureMiddleware, compute_webhook_signature
28
+
29
+ __all__ = ["WebhookSignatureMiddleware", "compute_webhook_signature"]