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,127 @@
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 — Configured HTTP client with retry and timeouts
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Standard HTTP client module for service-to-service calls with
20
+ # configurable timeouts, retry policy with exponential backoff + jitter,
21
+ # and automatic header propagation (X-Request-Id, X-App-Id, API key).
22
+ # Related requirements: FR9.1, FR9.2
23
+ # Related architecture: CC1.15
24
+
25
+ """Configured HTTP client for service-to-service calls."""
26
+
27
+ from __future__ import annotations
28
+
29
+ from dataclasses import dataclass
30
+
31
+ import httpx
32
+
33
+ from cloud_dog_api_kit.correlation.context import get_request_id, get_app_id
34
+ from cloud_dog_api_kit.clients.retry import RetryPolicy, RetryTransport
35
+
36
+
37
+ @dataclass
38
+ class ClientTimeout:
39
+ """HTTP client timeout configuration.
40
+
41
+ Attributes:
42
+ connect: Connection timeout in seconds.
43
+ read: Read timeout in seconds.
44
+ total: Total request timeout in seconds.
45
+
46
+ Related tests: UT1.23_HTTPClient
47
+ """
48
+
49
+ connect: float = 5.0
50
+ read: float = 30.0
51
+ total: float = 60.0
52
+
53
+
54
+ def create_http_client(
55
+ base_url: str | None = None,
56
+ timeout: ClientTimeout | None = None,
57
+ retry_policy: RetryPolicy | None = None,
58
+ app_id: str | None = None,
59
+ api_key: str | None = None,
60
+ ) -> httpx.AsyncClient:
61
+ """Create a configured async HTTP client for service-to-service calls.
62
+
63
+ The client automatically propagates X-Request-Id and X-App-Id headers
64
+ and attaches an API key if provided.
65
+
66
+ Args:
67
+ base_url: Base URL for all requests.
68
+ timeout: Timeout configuration. Uses defaults if None.
69
+ retry_policy: Retry policy. Uses defaults if None.
70
+ app_id: Calling service application ID.
71
+ api_key: API key for authentication.
72
+
73
+ Returns:
74
+ A configured httpx.AsyncClient.
75
+
76
+ Related tests: UT1.23_HTTPClient, ST1.11_HTTPClientEndToEnd
77
+ """
78
+ ct = timeout or ClientTimeout()
79
+ policy = retry_policy or RetryPolicy()
80
+
81
+ httpx_timeout = httpx.Timeout(
82
+ timeout=ct.total,
83
+ connect=ct.connect,
84
+ read=ct.read,
85
+ pool=ct.total,
86
+ )
87
+
88
+ headers: dict[str, str] = {}
89
+ if app_id:
90
+ headers["X-App-Id"] = app_id
91
+ if api_key:
92
+ headers["X-API-Key"] = api_key
93
+
94
+ return httpx.AsyncClient(
95
+ base_url=base_url or "",
96
+ timeout=httpx_timeout,
97
+ headers=headers,
98
+ transport=RetryTransport(policy=policy, transport=httpx.AsyncHTTPTransport()),
99
+ event_hooks={
100
+ "request": [_inject_correlation_headers],
101
+ },
102
+ )
103
+
104
+
105
+ async def _inject_correlation_headers(request: httpx.Request) -> None:
106
+ """Event hook to inject correlation headers into outgoing requests.
107
+
108
+ Args:
109
+ request: The outgoing HTTP request.
110
+ """
111
+ try:
112
+ request_id = get_request_id()
113
+ request.headers["X-Request-Id"] = request_id
114
+ except Exception:
115
+ pass
116
+
117
+ try:
118
+ app_id = get_app_id()
119
+ if app_id:
120
+ request.headers["X-App-Id"] = app_id
121
+ except Exception:
122
+ pass
123
+
124
+
125
+ def create_retry_transport(policy: RetryPolicy, transport: httpx.AsyncBaseTransport) -> RetryTransport:
126
+ """Create a retry transport wrapper for httpx."""
127
+ return RetryTransport(policy=policy, transport=transport)
@@ -0,0 +1,83 @@
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 — Retry policy
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Retry policy with exponential backoff + jitter for HTTP clients.
20
+ # Related requirements: FR9.2
21
+ # Related architecture: SA1
22
+
23
+ """Retry policy utilities for cloud_dog_api_kit."""
24
+
25
+ from __future__ import annotations
26
+
27
+ import asyncio
28
+ import random
29
+ from dataclasses import dataclass
30
+
31
+ import httpx
32
+
33
+
34
+ @dataclass
35
+ class RetryPolicy:
36
+ """Retry policy for idempotent operations.
37
+
38
+ Related tests: UT1.24_RetryPolicy
39
+ """
40
+
41
+ max_retries: int = 3
42
+ backoff_base: float = 0.5
43
+ backoff_max: float = 30.0
44
+ jitter: bool = True
45
+ retry_status_codes: tuple[int, ...] = (502, 503, 504)
46
+
47
+ def get_delay(self, attempt: int) -> float:
48
+ """Return delay."""
49
+ delay = min(self.backoff_base * (2**attempt), self.backoff_max)
50
+ if self.jitter:
51
+ delay = delay * (0.5 + random.random() * 0.5)
52
+ return delay
53
+
54
+
55
+ class RetryTransport(httpx.AsyncBaseTransport):
56
+ """HTTP transport wrapper with retry logic."""
57
+
58
+ def __init__(self, policy: RetryPolicy, transport: httpx.AsyncBaseTransport) -> None:
59
+ self._policy = policy
60
+ self._transport = transport
61
+
62
+ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
63
+ """Handle handle async request."""
64
+ last_response: httpx.Response | None = None
65
+ last_exc: Exception | None = None
66
+
67
+ for attempt in range(self._policy.max_retries + 1):
68
+ try:
69
+ response = await self._transport.handle_async_request(request)
70
+ if response.status_code not in self._policy.retry_status_codes:
71
+ return response
72
+ last_response = response
73
+ except (httpx.ConnectError, httpx.ReadTimeout) as exc:
74
+ last_exc = exc
75
+
76
+ if attempt < self._policy.max_retries:
77
+ await asyncio.sleep(self._policy.get_delay(attempt))
78
+
79
+ if last_response is not None:
80
+ return last_response
81
+ if last_exc is not None:
82
+ raise last_exc
83
+ raise httpx.ConnectError("All retry attempts exhausted")
@@ -0,0 +1,37 @@
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 — Compatibility and migration exports
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Migration compatibility middleware exports.
20
+ # Related requirements: FR18.3, FR18.4, FR18.6
21
+ # Related architecture: SA1
22
+
23
+ """Compatibility middleware exports."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from cloud_dog_api_kit.compat.envelope import LegacyEnvelopeMiddleware, legacy_envelope_route
28
+ from cloud_dog_api_kit.compat.profile import ProfileContextMiddleware
29
+ from cloud_dog_api_kit.compat.routes import LegacyRouteAdapter, LegacyRouteAdapterMiddleware
30
+
31
+ __all__ = [
32
+ "LegacyEnvelopeMiddleware",
33
+ "LegacyRouteAdapter",
34
+ "LegacyRouteAdapterMiddleware",
35
+ "ProfileContextMiddleware",
36
+ "legacy_envelope_route",
37
+ ]
@@ -0,0 +1,120 @@
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 — Legacy envelope compatibility middleware
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Middleware to wrap legacy non-enveloped responses into standard
20
+ # success/error envelopes during phased migrations.
21
+ # Related requirements: FR18.3
22
+ # Related architecture: SA1
23
+
24
+ """Legacy response envelope compatibility middleware."""
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ from typing import Any, Callable
30
+
31
+ from starlette.middleware.base import BaseHTTPMiddleware
32
+ from starlette.requests import Request
33
+ from starlette.responses import JSONResponse, Response
34
+
35
+ from cloud_dog_api_kit.envelopes import error_envelope, success_envelope
36
+
37
+
38
+ def legacy_envelope_route(func: Callable) -> Callable:
39
+ """Mark a route endpoint as legacy-envelope compatible."""
40
+ setattr(func, "__legacy_envelope__", True)
41
+ return func
42
+
43
+
44
+ class LegacyEnvelopeMiddleware(BaseHTTPMiddleware):
45
+ """Wrap legacy responses in standard envelopes for opt-in routes.
46
+
47
+ Args:
48
+ app: ASGI app.
49
+ opt_in_paths: Exact route paths that require envelope wrapping.
50
+ opt_in_header: Optional request header to force legacy envelope mode.
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ app: Any,
56
+ *,
57
+ opt_in_paths: set[str] | None = None,
58
+ opt_in_header: str = "X-Legacy-Envelope",
59
+ ) -> None:
60
+ super().__init__(app)
61
+ self._opt_in_paths = set(opt_in_paths or set())
62
+ self._opt_in_header = opt_in_header.lower()
63
+
64
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
65
+ """Apply compatibility envelopes on opt-in routes."""
66
+ response = await call_next(request)
67
+ if not self._should_wrap(request):
68
+ return response
69
+ if response.status_code == 204:
70
+ return response
71
+
72
+ body_bytes = b""
73
+ async for chunk in response.body_iterator:
74
+ body_bytes += chunk
75
+
76
+ payload: Any
77
+ if body_bytes:
78
+ try:
79
+ payload = json.loads(body_bytes)
80
+ except json.JSONDecodeError:
81
+ payload = {"message": body_bytes.decode("utf-8", errors="replace")}
82
+ else:
83
+ payload = {}
84
+
85
+ request_id = getattr(request.state, "request_id", "")
86
+ correlation_id = getattr(request.state, "correlation_id", None)
87
+
88
+ if isinstance(payload, dict) and "ok" in payload and ("data" in payload or "error" in payload):
89
+ envelope = payload
90
+ elif response.status_code >= 400:
91
+ message = "Request failed"
92
+ details = None
93
+ if isinstance(payload, dict):
94
+ message = str(payload.get("message", payload.get("error", message)))
95
+ details = payload.get("details")
96
+ envelope = error_envelope(
97
+ code="INVALID_REQUEST" if response.status_code < 500 else "INTERNAL_ERROR",
98
+ message=message,
99
+ details=details,
100
+ request_id=request_id,
101
+ correlation_id=correlation_id,
102
+ )
103
+ else:
104
+ envelope = success_envelope(
105
+ data=payload,
106
+ request_id=request_id,
107
+ correlation_id=correlation_id,
108
+ )
109
+
110
+ headers = {k: v for k, v in response.headers.items() if k.lower() != "content-length"}
111
+ return JSONResponse(status_code=response.status_code, content=envelope, headers=headers)
112
+
113
+ def _should_wrap(self, request: Request) -> bool:
114
+ """Determine whether current request should be wrapped."""
115
+ endpoint = request.scope.get("endpoint")
116
+ if endpoint is not None and bool(getattr(endpoint, "__legacy_envelope__", False)):
117
+ return True
118
+ if request.url.path in self._opt_in_paths:
119
+ return True
120
+ return request.headers.get(self._opt_in_header, "").strip().lower() in {"1", "true", "yes"}
@@ -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 — Profile context middleware
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Middleware that resolves request profile context from headers,
20
+ # path patterns, or query parameters and stores it in request.state.
21
+ # Related requirements: FR18.6
22
+ # Related architecture: SA1
23
+
24
+ """Profile context resolution middleware."""
25
+
26
+ from __future__ import annotations
27
+
28
+ import re
29
+ from typing import Any, Callable, Pattern
30
+
31
+ from starlette.middleware.base import BaseHTTPMiddleware
32
+ from starlette.requests import Request
33
+ from starlette.responses import JSONResponse, Response
34
+
35
+ from cloud_dog_api_kit.envelopes.error import error_envelope
36
+
37
+ DEFAULT_PATH_PATTERNS = (
38
+ re.compile(r"/profiles/(?P<profile>[A-Za-z0-9._-]+)"),
39
+ re.compile(r"/profile/(?P<profile>[A-Za-z0-9._-]+)"),
40
+ )
41
+
42
+
43
+ class ProfileContextMiddleware(BaseHTTPMiddleware):
44
+ """Resolve per-request profile context.
45
+
46
+ Resolution order:
47
+ 1. Header (default: `X-Profile`)
48
+ 2. Path pattern match
49
+ 3. Query parameter (default: `profile`)
50
+ 4. Default profile
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ app: Any,
56
+ *,
57
+ header_name: str = "X-Profile",
58
+ query_param: str = "profile",
59
+ default_profile: str | None = None,
60
+ allowed_profiles: set[str] | None = None,
61
+ path_patterns: tuple[Pattern[str], ...] | None = None,
62
+ ) -> None:
63
+ super().__init__(app)
64
+ self._header_name = header_name
65
+ self._query_param = query_param
66
+ self._default_profile = default_profile
67
+ self._allowed_profiles = allowed_profiles
68
+ self._path_patterns = path_patterns or DEFAULT_PATH_PATTERNS
69
+
70
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
71
+ """Resolve profile context and validate allowed profile list."""
72
+ profile = (
73
+ request.headers.get(self._header_name)
74
+ or self._profile_from_path(request.url.path)
75
+ or request.query_params.get(self._query_param)
76
+ or self._default_profile
77
+ )
78
+
79
+ if self._allowed_profiles is not None and profile and profile not in self._allowed_profiles:
80
+ request_id = getattr(request.state, "request_id", "")
81
+ correlation_id = getattr(request.state, "correlation_id", None)
82
+ return JSONResponse(
83
+ status_code=400,
84
+ content=error_envelope(
85
+ code="INVALID_REQUEST",
86
+ message="Invalid profile",
87
+ details={"profile": profile},
88
+ request_id=request_id,
89
+ correlation_id=correlation_id,
90
+ ),
91
+ )
92
+
93
+ request.state.profile = profile
94
+ return await call_next(request)
95
+
96
+ def _profile_from_path(self, path: str) -> str | None:
97
+ """Extract profile identifier from known path patterns."""
98
+ for pattern in self._path_patterns:
99
+ match = pattern.search(path)
100
+ if match:
101
+ return match.group("profile")
102
+ return None
@@ -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 — Legacy route migration adapter
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Route migration adapter that maps legacy non-versioned paths to
20
+ # versioned API paths and adds deprecation headers.
21
+ # Related requirements: FR18.4
22
+ # Related architecture: SA1
23
+
24
+ """Legacy route migration helpers."""
25
+
26
+ from __future__ import annotations
27
+
28
+ from dataclasses import dataclass, field
29
+ from typing import Any, Callable
30
+
31
+ from fastapi import FastAPI
32
+ from starlette.middleware.base import BaseHTTPMiddleware
33
+ from starlette.requests import Request
34
+ from starlette.responses import RedirectResponse, Response
35
+
36
+
37
+ @dataclass(slots=True)
38
+ class LegacyRouteAdapter:
39
+ """Adapter mapping legacy routes to versioned paths."""
40
+
41
+ route_map: dict[str, str]
42
+ sunset: str | None = None
43
+ link: str | None = None
44
+ redirect: bool = False
45
+ deprecation: str = "true"
46
+ methods: set[str] = field(default_factory=lambda: {"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"})
47
+
48
+ def resolve(self, path: str) -> str | None:
49
+ """Resolve a legacy path to a versioned path."""
50
+ return self.route_map.get(path)
51
+
52
+ def deprecation_headers(self) -> dict[str, str]:
53
+ """Build response headers for legacy route responses."""
54
+ headers = {"Deprecation": self.deprecation}
55
+ if self.sunset:
56
+ headers["Sunset"] = self.sunset
57
+ if self.link:
58
+ headers["Link"] = self.link
59
+ return headers
60
+
61
+ def register(self, app: FastAPI) -> None:
62
+ """Register legacy route adapter middleware on an app."""
63
+ app.add_middleware(LegacyRouteAdapterMiddleware, adapter=self)
64
+
65
+
66
+ class LegacyRouteAdapterMiddleware(BaseHTTPMiddleware):
67
+ """Middleware implementing legacy route mapping and deprecation headers."""
68
+
69
+ def __init__(self, app: Any, adapter: LegacyRouteAdapter) -> None:
70
+ super().__init__(app)
71
+ self._adapter = adapter
72
+
73
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
74
+ """Map legacy route to versioned path and add deprecation headers."""
75
+ target_path = self._adapter.resolve(request.url.path)
76
+ if target_path is None or request.method.upper() not in self._adapter.methods:
77
+ return await call_next(request)
78
+
79
+ headers = self._adapter.deprecation_headers()
80
+ if self._adapter.redirect:
81
+ query = request.url.query
82
+ location = target_path if not query else f"{target_path}?{query}"
83
+ return RedirectResponse(url=location, status_code=307, headers=headers)
84
+
85
+ request.scope["path"] = target_path
86
+ request.scope["raw_path"] = target_path.encode("utf-8")
87
+ response = await call_next(request)
88
+ for name, value in headers.items():
89
+ response.headers[name] = value
90
+ return response
@@ -0,0 +1,54 @@
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 — Configuration model
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Settings model for wiring cloud_dog_api_kit in a service. This
20
+ # module intentionally keeps configuration minimal and provider-agnostic.
21
+ # Related requirements: FR10.3, FR11.1, FR13.1
22
+ # Related architecture: SA1
23
+
24
+ """Configuration model for cloud_dog_api_kit."""
25
+
26
+ from __future__ import annotations
27
+
28
+ from pydantic import BaseModel, Field
29
+
30
+
31
+ class APIKitSettings(BaseModel):
32
+ """Settings for cloud_dog_api_kit.
33
+
34
+ These settings can be sourced from `cloud_dog_config` when available, but
35
+ are also usable standalone (e.g. local tests).
36
+ """
37
+
38
+ api_prefix: str = Field(default="/api/v1", description="API prefix for versioned routes")
39
+ api_version: str = Field(default="v1", description="API version string for headers and metadata")
40
+ enable_docs: bool = Field(default=True, description="Whether to enable /docs and /redoc")
41
+ enable_request_logging: bool = Field(default=True, description="Whether to enable request logging middleware")
42
+ enable_cors: bool = Field(default=True, description="Whether to enable CORS middleware")
43
+ cors_origins: list[str] = Field(default_factory=list, description="Allowed CORS origins")
44
+ timeout_seconds: float = Field(default=30.0, ge=0.1, description="Request timeout in seconds")
45
+ max_request_body_bytes: int | None = Field(
46
+ default=None,
47
+ ge=1,
48
+ description="Maximum request body size in bytes; None disables size checks",
49
+ )
50
+ shutdown_drain_timeout_seconds: float = Field(
51
+ default=5.0,
52
+ ge=0.0,
53
+ description="Graceful shutdown request-drain timeout in seconds",
54
+ )
@@ -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 — Correlation ID context and middleware
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Context-local correlation ID, app ID, and host ID management
20
+ # with ASGI middleware for automatic extraction/propagation.
21
+ # Related requirements: FR5.1, FR3.2
22
+ # Related architecture: CC1.8
23
+
24
+ """Correlation ID context and middleware for cloud_dog_api_kit."""
25
+
26
+ from cloud_dog_api_kit.correlation.context import (
27
+ get_app_id,
28
+ get_correlation_id,
29
+ get_host_id,
30
+ get_request_id,
31
+ set_app_id,
32
+ set_correlation_id,
33
+ set_host_id,
34
+ set_request_id,
35
+ clear_context,
36
+ )
37
+ from cloud_dog_api_kit.correlation.middleware import CorrelationMiddleware
38
+
39
+ __all__ = [
40
+ "get_app_id",
41
+ "get_correlation_id",
42
+ "get_host_id",
43
+ "get_request_id",
44
+ "set_app_id",
45
+ "set_correlation_id",
46
+ "set_host_id",
47
+ "set_request_id",
48
+ "clear_context",
49
+ "CorrelationMiddleware",
50
+ ]