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,105 @@
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 — A2A gateway helpers
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Helpers for mapping REST endpoints to A2A handlers.
20
+ # A2A endpoints MUST be thin wrappers over REST calls.
21
+ # Related requirements: FR15.1
22
+ # Related architecture: CC1.18
23
+
24
+ """A2A gateway helpers for cloud_dog_api_kit."""
25
+
26
+ from __future__ import annotations
27
+
28
+ from dataclasses import dataclass, field
29
+ from typing import Any
30
+
31
+
32
+ @dataclass
33
+ class A2AHandler:
34
+ """A2A handler definition mapped from a REST endpoint.
35
+
36
+ Attributes:
37
+ name: The handler name.
38
+ description: Human-readable description.
39
+ endpoint_path: The REST endpoint path this handler wraps.
40
+ method: HTTP method. Defaults to ``POST``.
41
+ input_schema: JSON Schema for the handler's input.
42
+ output_schema: JSON Schema for the handler's output.
43
+
44
+ Related tests: UT1.33_A2AGateway
45
+ """
46
+
47
+ name: str
48
+ description: str
49
+ endpoint_path: str
50
+ method: str = "POST"
51
+ input_schema: dict[str, Any] = field(default_factory=dict)
52
+ output_schema: dict[str, Any] = field(default_factory=dict)
53
+
54
+ def to_dict(self) -> dict[str, Any]:
55
+ """Convert to a dictionary suitable for A2A registration.
56
+
57
+ Returns:
58
+ A dictionary with the handler definition.
59
+ """
60
+ return {
61
+ "name": self.name,
62
+ "description": self.description,
63
+ "endpoint_path": self.endpoint_path,
64
+ "method": self.method,
65
+ "inputSchema": self.input_schema,
66
+ "outputSchema": self.output_schema,
67
+ }
68
+
69
+
70
+ def create_a2a_handler_from_endpoint(
71
+ endpoint_path: str,
72
+ method: str = "POST",
73
+ description: str = "",
74
+ name: str | None = None,
75
+ input_schema: dict[str, Any] | None = None,
76
+ output_schema: dict[str, Any] | None = None,
77
+ ) -> A2AHandler:
78
+ """Create an A2A handler from a REST endpoint.
79
+
80
+ Args:
81
+ endpoint_path: The REST endpoint path.
82
+ method: HTTP method. Defaults to ``POST``.
83
+ description: Handler description.
84
+ name: Override handler name. Derived from path if None.
85
+ input_schema: JSON Schema for inputs.
86
+ output_schema: JSON Schema for outputs.
87
+
88
+ Returns:
89
+ An A2AHandler instance.
90
+
91
+ Related tests: UT1.33_A2AGateway
92
+ """
93
+ if name is None:
94
+ parts = endpoint_path.strip("/").split("/")
95
+ filtered = [p for p in parts if not p.startswith("{") and p not in ("api", "v1", "v2")]
96
+ name = "_".join(filtered).replace(":", "_")
97
+
98
+ return A2AHandler(
99
+ name=name,
100
+ description=description,
101
+ endpoint_path=endpoint_path,
102
+ method=method,
103
+ input_schema=input_schema or {},
104
+ output_schema=output_schema or {},
105
+ )
@@ -0,0 +1,107 @@
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
+ """A2A skill audit middleware for PS-50 compliance.
16
+
17
+ License: Apache 2.0
18
+ Ownership: Cloud-Dog, Viewdeck Engineering Limited
19
+ Description: Wraps A2A skill handlers with structured audit logging.
20
+ Requirements: PS-50.AUD2
21
+ Tasks: W28A-737
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ import time
28
+ import uuid
29
+ from datetime import datetime, timezone
30
+ from typing import Any, Callable, Optional
31
+
32
+
33
+ def a2a_skill_audit_middleware(
34
+ skill_name: str,
35
+ handler: Callable,
36
+ *,
37
+ service: str,
38
+ logger: Optional[logging.Logger] = None,
39
+ ) -> Callable:
40
+ """Wrap an A2A skill handler to emit audit log entries for every invocation.
41
+
42
+ Args:
43
+ skill_name: The name of the A2A skill being wrapped.
44
+ handler: The original skill handler callable.
45
+ service: The emitting service name (e.g. ``"file-mcp-server"``).
46
+ logger: Optional logger instance. Falls back to stdlib logging.
47
+
48
+ Returns:
49
+ A wrapped handler that logs audit entries for each skill invocation.
50
+
51
+ Audit record fields:
52
+ - correlation_id
53
+ - service
54
+ - skill_name (which skill was invoked)
55
+ - actor (requesting agent or user)
56
+ - task_id (A2A task ID)
57
+ - outcome ("success" | "error")
58
+ - duration_ms
59
+ - timestamp
60
+ - error_detail (if outcome is "error")
61
+ """
62
+ log = logger or logging.getLogger(f"cloud_dog_api_kit.a2a.audit.{service}")
63
+
64
+ def _wrapped(text: str, *, task_id: str = "", actor: str = "a2a-caller", **kwargs: Any) -> Any:
65
+ correlation_id = task_id or str(uuid.uuid4().hex[:16])
66
+ ts = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
67
+ t0 = time.monotonic()
68
+ try:
69
+ result = handler(text, **kwargs)
70
+ duration_ms = round((time.monotonic() - t0) * 1000, 2)
71
+ log.info(
72
+ "a2a_skill_invocation",
73
+ extra={
74
+ "event_type": "a2a_skill_invocation",
75
+ "correlation_id": correlation_id,
76
+ "service": service,
77
+ "skill_name": skill_name,
78
+ "actor": actor,
79
+ "task_id": task_id,
80
+ "outcome": "success",
81
+ "duration_ms": duration_ms,
82
+ "timestamp": ts,
83
+ },
84
+ )
85
+ return result
86
+ except Exception as exc:
87
+ duration_ms = round((time.monotonic() - t0) * 1000, 2)
88
+ log.warning(
89
+ "a2a_skill_invocation",
90
+ extra={
91
+ "event_type": "a2a_skill_invocation",
92
+ "correlation_id": correlation_id,
93
+ "service": service,
94
+ "skill_name": skill_name,
95
+ "actor": actor,
96
+ "task_id": task_id,
97
+ "outcome": "error",
98
+ "duration_ms": duration_ms,
99
+ "timestamp": ts,
100
+ "error_detail": str(exc),
101
+ },
102
+ )
103
+ raise
104
+
105
+ _wrapped.__name__ = handler.__name__ if hasattr(handler, "__name__") else skill_name
106
+ _wrapped.__doc__ = handler.__doc__
107
+ return _wrapped
@@ -0,0 +1,35 @@
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 — Authentication and authorisation
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Auth dependency, RBAC helpers, and tenant isolation.
20
+ # Related requirements: FR3.1, FR3.2, FR3.3, FR3.4, FR3.5
21
+ # Related architecture: CC1.5, CC1.6
22
+
23
+ """Authentication and authorisation for cloud_dog_api_kit."""
24
+
25
+ from cloud_dog_api_kit.auth.dependency import create_auth_dependency
26
+ from cloud_dog_api_kit.auth.rbac import require_admin, require_permission, require_tenant
27
+ from cloud_dog_api_kit.auth.service_auth import create_service_auth_dependency
28
+
29
+ __all__ = [
30
+ "create_auth_dependency",
31
+ "create_service_auth_dependency",
32
+ "require_admin",
33
+ "require_permission",
34
+ "require_tenant",
35
+ ]
@@ -0,0 +1,121 @@
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 — Authentication dependency for FastAPI
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: FastAPI dependency factory for API key / Bearer authentication.
20
+ # Supports both API key and Bearer token with configurable verify functions.
21
+ # Related requirements: FR3.4, FR3.5
22
+ # Related architecture: CC1.5
23
+
24
+ """Authentication dependency factory for FastAPI."""
25
+
26
+ from __future__ import annotations
27
+
28
+ from typing import Any, Callable
29
+
30
+ from fastapi import Request
31
+
32
+ from cloud_dog_api_kit.errors.exceptions import UnauthenticatedError
33
+
34
+
35
+ def create_auth_dependency(
36
+ api_key_header: str = "X-API-Key",
37
+ bearer_verify_fn: Callable | None = None,
38
+ api_key_verify_fn: Callable | None = None,
39
+ config_api_key: str | None = None,
40
+ ) -> Callable:
41
+ """Create a FastAPI dependency for API key / Bearer authentication.
42
+
43
+ The dependency tries Bearer token first, then falls back to API key.
44
+ On success, sets ``request.state.user``, ``request.state.api_key``,
45
+ and ``request.state.tenant_id``.
46
+
47
+ Args:
48
+ api_key_header: Header name for API key. Defaults to ``X-API-Key``.
49
+ bearer_verify_fn: Async/sync callable to verify Bearer tokens.
50
+ Should return a dict with ``user_id``, ``roles``, ``tenant_id``.
51
+ api_key_verify_fn: Async/sync callable to verify API keys.
52
+ Should return a dict with ``user_id``, ``roles``, ``tenant_id``.
53
+ config_api_key: Static API key from config for simple verification.
54
+
55
+ Returns:
56
+ A FastAPI dependency callable.
57
+
58
+ Related tests: UT1.6_AuthDependency, UT1.7_BearerAuth, UT1.10_ServiceAuth
59
+ """
60
+
61
+ async def _auth_dependency(request: Request) -> dict[str, Any]:
62
+ # Try Bearer token first
63
+ auth_header = request.headers.get("authorization", "")
64
+ if auth_header.startswith("Bearer "):
65
+ token = auth_header[7:].strip()
66
+ if not token:
67
+ raise UnauthenticatedError(message="Empty Bearer token")
68
+ if bearer_verify_fn is not None:
69
+ import asyncio
70
+
71
+ if asyncio.iscoroutinefunction(bearer_verify_fn):
72
+ result = await bearer_verify_fn(token)
73
+ else:
74
+ result = bearer_verify_fn(token)
75
+ if result is None:
76
+ raise UnauthenticatedError(message="Invalid Bearer token")
77
+ _set_request_state(request, result)
78
+ return result
79
+ raise UnauthenticatedError(message="Bearer token verification not configured")
80
+
81
+ # Try API key
82
+ api_key = request.headers.get(api_key_header.lower(), "") or request.headers.get(api_key_header, "")
83
+ if not api_key:
84
+ raise UnauthenticatedError(message="Missing credentials")
85
+
86
+ if api_key_verify_fn is not None:
87
+ import asyncio
88
+
89
+ if asyncio.iscoroutinefunction(api_key_verify_fn):
90
+ result = await api_key_verify_fn(api_key)
91
+ else:
92
+ result = api_key_verify_fn(api_key)
93
+ if result is None:
94
+ raise UnauthenticatedError(message="Invalid API key")
95
+ _set_request_state(request, result)
96
+ return result
97
+
98
+ # Fall back to config-based verification
99
+ if config_api_key is not None:
100
+ if api_key == config_api_key:
101
+ result = {"user_id": "api_key_user", "roles": [], "tenant_id": None}
102
+ _set_request_state(request, result)
103
+ return result
104
+ raise UnauthenticatedError(message="Invalid API key")
105
+
106
+ raise UnauthenticatedError(message="No authentication method configured")
107
+
108
+ return _auth_dependency
109
+
110
+
111
+ def _set_request_state(request: Request, auth_result: dict[str, Any]) -> None:
112
+ """Set authentication result on request state.
113
+
114
+ Args:
115
+ request: The FastAPI request.
116
+ auth_result: Dict with user_id, roles, tenant_id.
117
+ """
118
+ request.state.user = auth_result.get("user_id")
119
+ request.state.api_key = True
120
+ request.state.tenant_id = auth_result.get("tenant_id")
121
+ request.state.roles = auth_result.get("roles", [])
@@ -0,0 +1,107 @@
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 — RBAC helpers
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Role-based access control helpers for endpoint-level authorisation.
20
+ # Default-deny: all endpoints require auth unless explicitly marked public.
21
+ # Related requirements: FR3.3
22
+ # Related architecture: CC1.6
23
+
24
+ """RBAC helpers for cloud_dog_api_kit."""
25
+
26
+ from __future__ import annotations
27
+
28
+ from typing import Callable
29
+
30
+ from fastapi import Request
31
+
32
+ from cloud_dog_api_kit.errors.exceptions import UnauthorisedError
33
+
34
+
35
+ def require_permission(permission: str) -> Callable:
36
+ """Create a FastAPI dependency that checks for a specific permission.
37
+
38
+ Args:
39
+ permission: The required permission string.
40
+
41
+ Returns:
42
+ A FastAPI dependency callable that raises UnauthorisedError if
43
+ the user does not have the required permission.
44
+
45
+ Related tests: UT1.8_RBACHelpers, SEC1.2_RBACEnforcement
46
+ """
47
+
48
+ async def _check(request: Request) -> None:
49
+ roles = getattr(request.state, "roles", []) or []
50
+ permissions = getattr(request.state, "permissions", []) or []
51
+ all_perms = set(roles) | set(permissions)
52
+ if permission not in all_perms:
53
+ raise UnauthorisedError(message=f"Missing required permission: {permission}")
54
+
55
+ return _check
56
+
57
+
58
+ def require_admin() -> Callable:
59
+ """Create a FastAPI dependency that checks for admin role.
60
+
61
+ Returns:
62
+ A FastAPI dependency callable that raises UnauthorisedError if
63
+ the user does not have the admin role.
64
+
65
+ Related tests: UT1.8_RBACHelpers, SEC1.2_RBACEnforcement
66
+ """
67
+
68
+ async def _check(request: Request) -> None:
69
+ roles = getattr(request.state, "roles", []) or []
70
+ if "admin" not in roles:
71
+ raise UnauthorisedError(message="Admin access required")
72
+
73
+ return _check
74
+
75
+
76
+ def require_tenant(tenant_id_param: str = "tenant_id") -> Callable:
77
+ """Create a FastAPI dependency that enforces tenant isolation.
78
+
79
+ Checks that the authenticated user's tenant_id matches the requested
80
+ tenant_id from the path or query parameters.
81
+
82
+ Args:
83
+ tenant_id_param: The name of the path/query parameter containing
84
+ the tenant ID. Defaults to ``tenant_id``.
85
+
86
+ Returns:
87
+ A FastAPI dependency callable that raises UnauthorisedError on
88
+ tenant mismatch.
89
+
90
+ Related tests: UT1.9_TenantIsolation, SEC1.3_TenantIsolation
91
+ """
92
+
93
+ async def _check(request: Request) -> None:
94
+ user_tenant = getattr(request.state, "tenant_id", None)
95
+ if user_tenant is None:
96
+ return # No tenant context — skip check
97
+
98
+ # Check path params
99
+ requested_tenant = request.path_params.get(tenant_id_param)
100
+ if requested_tenant is None:
101
+ # Check query params
102
+ requested_tenant = request.query_params.get(tenant_id_param)
103
+
104
+ if requested_tenant is not None and requested_tenant != user_tenant:
105
+ raise UnauthorisedError(message="Access denied: tenant mismatch")
106
+
107
+ return _check
@@ -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 — Service-to-service authentication
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Service-to-service auth using X-App-Id + service key.
20
+ # Related requirements: FR3.5
21
+ # Related architecture: SA1
22
+
23
+ """Service-to-service authentication helpers."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Any, Callable
28
+
29
+ from fastapi import Header
30
+
31
+ from cloud_dog_api_kit.errors.exceptions import UnauthenticatedError
32
+
33
+
34
+ def create_service_auth_dependency(
35
+ service_key_verify_fn: Callable[[str, str], dict[str, Any] | None],
36
+ app_id_header: str = "X-App-Id",
37
+ key_header: str = "X-Service-Key",
38
+ ) -> Callable:
39
+ """Create a dependency enforcing service-to-service authentication."""
40
+
41
+ async def _dep(
42
+ app_id: str | None = Header(default=None, alias=app_id_header),
43
+ service_key: str | None = Header(default=None, alias=key_header),
44
+ ) -> dict[str, Any]:
45
+ if not app_id or not service_key:
46
+ raise UnauthenticatedError(message="Missing service credentials")
47
+
48
+ result = service_key_verify_fn(app_id, service_key)
49
+ if result is None:
50
+ raise UnauthenticatedError(message="Invalid service credentials")
51
+
52
+ return result
53
+
54
+ return _dep
@@ -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 — HTTP client helpers
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Service-to-service HTTP client creation and retry utilities.
20
+ # Related requirements: FR9.1, FR9.2
21
+ # Related architecture: SA1
22
+
23
+ """HTTP client helpers for cloud_dog_api_kit."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from cloud_dog_api_kit.clients.http_client import ClientTimeout, RetryPolicy, create_http_client
28
+
29
+ __all__ = ["ClientTimeout", "RetryPolicy", "create_http_client"]
@@ -0,0 +1,39 @@
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 — Circuit breaker hook (optional)
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Optional circuit breaker hooks for HTTP client integrations.
20
+ # Related requirements: FR9.1
21
+ # Related architecture: SA1
22
+
23
+ """Circuit breaker hooks for cloud_dog_api_kit."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Protocol
28
+
29
+
30
+ class CircuitBreaker(Protocol):
31
+ """Optional circuit breaker protocol."""
32
+
33
+ def on_success(self) -> None:
34
+ """Record a successful protected call."""
35
+ ...
36
+
37
+ def on_failure(self) -> None:
38
+ """Record a failed protected call."""
39
+ ...