ccp-sdk 0.1.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.
ccp_sdk/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Reference Python SDK for Cortex Capsule Protocol (CCP)."""
2
+
3
+ from .client import CCPClient
4
+ from .errors import CCPError, CCPHTTPError, CCPSchemaError
5
+
6
+ __all__ = [
7
+ "CCPClient",
8
+ "CCPError",
9
+ "CCPHTTPError",
10
+ "CCPSchemaError",
11
+ ]
ccp_sdk/client.py ADDED
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ import httpx
6
+
7
+ from .errors import CCPHTTPError
8
+ from .messages import hello as hello_msg
9
+ from .messages import session_end as session_end_msg
10
+ from .messages import session_start as session_start_msg
11
+ from .messages import turn_audio_ref as turn_audio_ref_msg
12
+ from .messages import turn_text as turn_text_msg
13
+ from .validator import validate_message
14
+
15
+
16
+ class CCPClient:
17
+ def __init__(
18
+ self,
19
+ *,
20
+ base_url: str,
21
+ device_token: Optional[str] = None,
22
+ timeout_s: float = 10.0,
23
+ validate: bool = True,
24
+ ):
25
+ self._base_url = base_url.rstrip("/")
26
+ self._device_token = device_token
27
+ self._timeout_s = timeout_s
28
+ self._validate = validate
29
+ self._client = httpx.Client(timeout=timeout_s)
30
+
31
+ def close(self) -> None:
32
+ self._client.close()
33
+
34
+ def __enter__(self) -> "CCPClient":
35
+ return self
36
+
37
+ def __exit__(self, exc_type, exc, tb) -> None:
38
+ self.close()
39
+
40
+ def _headers(self) -> Dict[str, str]:
41
+ headers = {"Content-Type": "application/json"}
42
+ if self._device_token:
43
+ headers["Authorization"] = f"Device {self._device_token}"
44
+ return headers
45
+
46
+ def _post_ccp(self, path: str, message: Dict[str, Any]) -> Dict[str, Any]:
47
+ if self._validate:
48
+ validate_message(message)
49
+
50
+ url = f"{self._base_url}{path}"
51
+ resp = self._client.post(url, headers=self._headers(), json=message)
52
+ if resp.status_code < 200 or resp.status_code >= 300:
53
+ body: Any
54
+ try:
55
+ body = resp.json()
56
+ except Exception:
57
+ body = resp.text
58
+ raise CCPHTTPError(resp.status_code, body)
59
+
60
+ data = resp.json()
61
+ if self._validate and isinstance(data, dict):
62
+ validate_message(data)
63
+ return data
64
+
65
+ def hello(
66
+ self,
67
+ *,
68
+ device_id: str,
69
+ device_token: str,
70
+ firmware: str,
71
+ capabilities: Dict[str, Any],
72
+ locale: Optional[str] = None,
73
+ ) -> Dict[str, Any]:
74
+ msg = hello_msg(
75
+ device_id=device_id,
76
+ device_token=device_token,
77
+ firmware=firmware,
78
+ capabilities=capabilities,
79
+ locale=locale,
80
+ )
81
+ return self._post_ccp("/ccp/hello", msg)
82
+
83
+ def session_start(self, *, device_id: str) -> Dict[str, Any]:
84
+ return self._post_ccp("/ccp/session/start", session_start_msg(device_id=device_id))
85
+
86
+ def turn_text(self, *, session_id: str, text: str) -> Dict[str, Any]:
87
+ return self._post_ccp("/ccp/turn", turn_text_msg(session_id=session_id, text=text))
88
+
89
+ def turn_audio_ref(self, *, session_id: str, audio_ref: str) -> Dict[str, Any]:
90
+ return self._post_ccp(
91
+ "/ccp/turn", turn_audio_ref_msg(session_id=session_id, audio_ref=audio_ref)
92
+ )
93
+
94
+ def session_end(self, *, session_id: str) -> Dict[str, Any]:
95
+ return self._post_ccp("/ccp/session/end", session_end_msg(session_id=session_id))
96
+
97
+ def upload_audio(self, *, audio_bytes: bytes, content_type: str = "audio/wav") -> str:
98
+ url = f"{self._base_url}/v1/media/audio"
99
+ headers = {k: v for k, v in self._headers().items() if k.lower() != "content-type"}
100
+ headers["Content-Type"] = content_type
101
+ resp = self._client.post(
102
+ url,
103
+ headers=headers,
104
+ content=audio_bytes,
105
+ )
106
+ if resp.status_code < 200 or resp.status_code >= 300:
107
+ body: Any
108
+ try:
109
+ body = resp.json()
110
+ except Exception:
111
+ body = resp.text
112
+ raise CCPHTTPError(resp.status_code, body)
113
+
114
+ data = resp.json()
115
+ audio_ref = data.get("audio_ref") if isinstance(data, dict) else None
116
+ if not isinstance(audio_ref, str) or not audio_ref:
117
+ raise CCPHTTPError(resp.status_code, data)
118
+ return audio_ref
119
+
ccp_sdk/errors.py ADDED
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class CCPError(Exception):
5
+ """Base error for the CCP SDK."""
6
+
7
+
8
+ class CCPSchemaError(CCPError):
9
+ """Raised when a message fails JSON Schema validation."""
10
+
11
+
12
+ class CCPHTTPError(CCPError):
13
+ """Raised when an HTTP call fails (non-2xx or invalid payload)."""
14
+
15
+ def __init__(self, status_code: int, body: object | None = None):
16
+ super().__init__(f"CCP HTTP error: {status_code}")
17
+ self.status_code = status_code
18
+ self.body = body
ccp_sdk/messages.py ADDED
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Any, Dict, Optional
5
+ from uuid import uuid4
6
+
7
+
8
+ def now_ts() -> str:
9
+ return datetime.now(timezone.utc).isoformat()
10
+
11
+
12
+ def envelope(
13
+ *,
14
+ type: str,
15
+ payload: Dict[str, Any],
16
+ ccp_version: str = "0.1",
17
+ id: Optional[str] = None,
18
+ ts: Optional[str] = None,
19
+ ) -> Dict[str, Any]:
20
+ return {
21
+ "ccp_version": ccp_version,
22
+ "type": type,
23
+ "id": id or str(uuid4()),
24
+ "ts": ts or now_ts(),
25
+ "payload": payload,
26
+ }
27
+
28
+
29
+ def hello(
30
+ *,
31
+ device_id: str,
32
+ device_token: str,
33
+ firmware: str,
34
+ capabilities: Dict[str, Any],
35
+ locale: Optional[str] = None,
36
+ ) -> Dict[str, Any]:
37
+ payload: Dict[str, Any] = {
38
+ "device_id": device_id,
39
+ "device_token": device_token,
40
+ "firmware": firmware,
41
+ "capabilities": capabilities,
42
+ }
43
+ if locale is not None:
44
+ payload["locale"] = locale
45
+ return envelope(type="hello", payload=payload)
46
+
47
+
48
+ def session_start(*, device_id: str) -> Dict[str, Any]:
49
+ return envelope(type="session_start", payload={"device_id": device_id})
50
+
51
+
52
+ def turn_text(*, session_id: str, text: str) -> Dict[str, Any]:
53
+ return envelope(
54
+ type="turn",
55
+ payload={"session_id": session_id, "input": {"kind": "text", "text": text}},
56
+ )
57
+
58
+
59
+ def turn_audio_ref(*, session_id: str, audio_ref: str) -> Dict[str, Any]:
60
+ return envelope(
61
+ type="turn",
62
+ payload={
63
+ "session_id": session_id,
64
+ "input": {"kind": "audio_ref", "audio_ref": audio_ref},
65
+ },
66
+ )
67
+
68
+
69
+ def session_end(*, session_id: str) -> Dict[str, Any]:
70
+ return envelope(type="session_end", payload={"session_id": session_id})
ccp_sdk/py.typed ADDED
File without changes
File without changes
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ccp.dev/schemas/ccp-message.json",
4
+ "title": "CCP Message Envelope",
5
+ "type": "object",
6
+ "required": ["ccp_version", "type", "id", "payload"],
7
+ "properties": {
8
+ "ccp_version": { "type": "string", "enum": ["0.1"] },
9
+ "type": { "type": "string" },
10
+ "id": { "type": "string", "minLength": 1 },
11
+ "ts": { "type": "string", "format": "date-time" },
12
+ "payload": { "type": "object" }
13
+ },
14
+ "additionalProperties": false
15
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ccp.dev/schemas/error.json",
4
+ "title": "CCP error",
5
+ "allOf": [
6
+ { "$ref": "ccp-message.json" },
7
+ {
8
+ "properties": {
9
+ "type": { "const": "error" },
10
+ "payload": {
11
+ "type": "object",
12
+ "required": ["code", "message"],
13
+ "properties": {
14
+ "code": {
15
+ "type": "string",
16
+ "enum": [
17
+ "INVALID_REQUEST",
18
+ "UNAUTHORIZED",
19
+ "FORBIDDEN",
20
+ "NOT_FOUND",
21
+ "CONFLICT",
22
+ "RATE_LIMIT",
23
+ "SERVER_BUSY",
24
+ "MEDIA_TOO_LARGE",
25
+ "UNSUPPORTED_CAPABILITY"
26
+ ]
27
+ },
28
+ "message": { "type": "string" },
29
+ "retry_after_ms": { "type": "integer", "minimum": 0 },
30
+ "request_id": { "type": "string" }
31
+ },
32
+ "additionalProperties": true
33
+ }
34
+ }
35
+ }
36
+ ]
37
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ccp.dev/schemas/hello-ack.json",
4
+ "title": "CCP hello_ack",
5
+ "allOf": [
6
+ { "$ref": "ccp-message.json" },
7
+ {
8
+ "properties": {
9
+ "type": { "const": "hello_ack" },
10
+ "payload": {
11
+ "type": "object",
12
+ "required": ["accepted", "device_id"],
13
+ "properties": {
14
+ "accepted": { "type": "boolean" },
15
+ "device_id": { "type": "string" },
16
+ "session_supported": { "type": "boolean" },
17
+ "negotiated": {
18
+ "type": "object",
19
+ "properties": {
20
+ "audio_codec": { "type": "string" },
21
+ "sample_rate": { "type": "integer" },
22
+ "max_audio_seconds": { "type": "number" },
23
+ "preferred_transport": { "type": "string" }
24
+ },
25
+ "additionalProperties": true
26
+ },
27
+ "limits": {
28
+ "type": "object",
29
+ "properties": {
30
+ "turns_per_min": { "type": "integer" },
31
+ "tts_seconds_per_min": { "type": "integer" }
32
+ },
33
+ "additionalProperties": true
34
+ }
35
+ },
36
+ "additionalProperties": true
37
+ }
38
+ }
39
+ }
40
+ ]
41
+ }
@@ -0,0 +1,72 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ccp.dev/schemas/hello.json",
4
+ "title": "CCP hello",
5
+ "allOf": [
6
+ { "$ref": "ccp-message.json" },
7
+ {
8
+ "properties": {
9
+ "type": { "const": "hello" },
10
+ "payload": {
11
+ "type": "object",
12
+ "required": ["device_id", "device_token", "firmware", "capabilities"],
13
+ "properties": {
14
+ "device_id": { "type": "string" },
15
+ "device_token": { "type": "string" },
16
+ "firmware": { "type": "string" },
17
+ "capabilities": {
18
+ "type": "object",
19
+ "properties": {
20
+ "audio_in": {
21
+ "type": "object",
22
+ "properties": {
23
+ "codecs": { "type": "array", "items": { "type": "string" } },
24
+ "sample_rates": { "type": "array", "items": { "type": "integer" } },
25
+ "channels": { "type": "integer", "minimum": 1 }
26
+ },
27
+ "additionalProperties": true
28
+ },
29
+ "audio_out": {
30
+ "type": "object",
31
+ "properties": {
32
+ "codecs": { "type": "array", "items": { "type": "string" } },
33
+ "sample_rates": { "type": "array", "items": { "type": "integer" } },
34
+ "channels": { "type": "integer", "minimum": 1 }
35
+ },
36
+ "additionalProperties": true
37
+ },
38
+ "io": {
39
+ "type": "object",
40
+ "properties": {
41
+ "button": { "type": "boolean" },
42
+ "led": { "type": "boolean" },
43
+ "screen": { "type": "boolean" }
44
+ },
45
+ "additionalProperties": true
46
+ },
47
+ "actions": { "type": "array", "items": { "type": "string" } },
48
+ "streaming": {
49
+ "type": "object",
50
+ "properties": { "supported": { "type": "boolean" } },
51
+ "additionalProperties": true
52
+ },
53
+ "network": {
54
+ "type": "object",
55
+ "properties": {
56
+ "type": { "type": "string" },
57
+ "metered": { "type": "boolean" }
58
+ },
59
+ "additionalProperties": true
60
+ },
61
+ "extensions": { "type": "array", "items": { "type": "string" } }
62
+ },
63
+ "additionalProperties": true
64
+ },
65
+ "locale": { "type": "string" }
66
+ },
67
+ "additionalProperties": true
68
+ }
69
+ }
70
+ }
71
+ ]
72
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ccp.dev/schemas/session-end.json",
4
+ "title": "CCP session_end",
5
+ "allOf": [
6
+ { "$ref": "ccp-message.json" },
7
+ {
8
+ "properties": {
9
+ "type": { "const": "session_end" },
10
+ "payload": {
11
+ "type": "object",
12
+ "required": ["session_id"],
13
+ "properties": {
14
+ "session_id": { "type": "string" }
15
+ },
16
+ "additionalProperties": true
17
+ }
18
+ }
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ccp.dev/schemas/session-ended.json",
4
+ "title": "CCP session_ended",
5
+ "allOf": [
6
+ { "$ref": "ccp-message.json" },
7
+ {
8
+ "properties": {
9
+ "type": { "const": "session_ended" },
10
+ "payload": {
11
+ "type": "object",
12
+ "required": ["session_id"],
13
+ "properties": {
14
+ "session_id": { "type": "string" }
15
+ },
16
+ "additionalProperties": true
17
+ }
18
+ }
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ccp.dev/schemas/session-start.json",
4
+ "title": "CCP session_start",
5
+ "allOf": [
6
+ { "$ref": "ccp-message.json" },
7
+ {
8
+ "properties": {
9
+ "type": { "const": "session_start" },
10
+ "payload": {
11
+ "type": "object",
12
+ "required": ["device_id"],
13
+ "properties": {
14
+ "device_id": { "type": "string" }
15
+ },
16
+ "additionalProperties": true
17
+ }
18
+ }
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ccp.dev/schemas/session-started.json",
4
+ "title": "CCP session_started",
5
+ "allOf": [
6
+ { "$ref": "ccp-message.json" },
7
+ {
8
+ "properties": {
9
+ "type": { "const": "session_started" },
10
+ "payload": {
11
+ "type": "object",
12
+ "required": ["session_id"],
13
+ "properties": {
14
+ "session_id": { "type": "string" },
15
+ "active_capsule_id": { "type": "string" },
16
+ "kid_safe_applied": { "type": "boolean" }
17
+ },
18
+ "additionalProperties": true
19
+ }
20
+ }
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ccp.dev/schemas/turn-result.json",
4
+ "title": "CCP turn_result",
5
+ "allOf": [
6
+ { "$ref": "ccp-message.json" },
7
+ {
8
+ "properties": {
9
+ "type": { "const": "turn_result" },
10
+ "payload": {
11
+ "type": "object",
12
+ "required": ["session_id"],
13
+ "properties": {
14
+ "session_id": { "type": "string" },
15
+ "reply": {
16
+ "type": "object",
17
+ "properties": {
18
+ "text": { "type": "string" },
19
+ "audio_ref": { "type": "string" }
20
+ },
21
+ "additionalProperties": true
22
+ },
23
+ "state": { "type": "object", "additionalProperties": true },
24
+ "safety": {
25
+ "type": "object",
26
+ "properties": {
27
+ "blocked": { "type": "boolean" },
28
+ "kid_safe_applied": { "type": "boolean" },
29
+ "reason": { "type": ["string", "null"] }
30
+ },
31
+ "additionalProperties": true
32
+ },
33
+ "actions": { "type": "array", "items": { "type": "object" } },
34
+ "meta": { "type": "object", "additionalProperties": true }
35
+ },
36
+ "additionalProperties": true
37
+ }
38
+ }
39
+ }
40
+ ]
41
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ccp.dev/schemas/turn.json",
4
+ "title": "CCP turn",
5
+ "allOf": [
6
+ { "$ref": "ccp-message.json" },
7
+ {
8
+ "properties": {
9
+ "type": { "const": "turn" },
10
+ "payload": {
11
+ "type": "object",
12
+ "required": ["session_id", "input"],
13
+ "properties": {
14
+ "session_id": { "type": "string" },
15
+ "input": {
16
+ "type": "object",
17
+ "required": ["kind"],
18
+ "properties": {
19
+ "kind": { "type": "string", "enum": ["text", "audio_ref"] },
20
+ "text": { "type": "string" },
21
+ "audio_ref": { "type": "string" }
22
+ },
23
+ "additionalProperties": true
24
+ }
25
+ },
26
+ "additionalProperties": true
27
+ }
28
+ }
29
+ }
30
+ ]
31
+ }
ccp_sdk/schemas.py ADDED
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from importlib import resources
5
+ from typing import Any, Dict
6
+
7
+ from jsonschema import Draft202012Validator
8
+ from jsonschema.validators import RefResolver
9
+
10
+ from .errors import CCPSchemaError
11
+
12
+
13
+ _MESSAGE_TYPE_TO_SCHEMA = {
14
+ "hello": "hello.json",
15
+ "hello_ack": "hello-ack.json",
16
+ "session_start": "session-start.json",
17
+ "session_started": "session-started.json",
18
+ "turn": "turn.json",
19
+ "turn_result": "turn-result.json",
20
+ "session_end": "session-end.json",
21
+ "session_ended": "session-ended.json",
22
+ "error": "error.json",
23
+ }
24
+
25
+
26
+ def _load_all_schemas() -> Dict[str, Dict[str, Any]]:
27
+ schema_pkg = resources.files("ccp_sdk.schemas")
28
+ schemas: Dict[str, Dict[str, Any]] = {}
29
+ for entry in schema_pkg.iterdir():
30
+ if entry.name.endswith(".json"):
31
+ schemas[entry.name] = json.loads(entry.read_text(encoding="utf-8"))
32
+ return schemas
33
+
34
+
35
+ _ALL_SCHEMAS = _load_all_schemas()
36
+
37
+
38
+ def _make_resolver(schema: Dict[str, Any]) -> RefResolver:
39
+ store: Dict[str, Any] = {}
40
+ for filename, schema_obj in _ALL_SCHEMAS.items():
41
+ store[filename] = schema_obj
42
+ schema_id = schema_obj.get("$id")
43
+ if isinstance(schema_id, str):
44
+ store[schema_id] = schema_obj
45
+
46
+ return RefResolver.from_schema(schema, store=store)
47
+
48
+
49
+ def validate_message(message: Dict[str, Any]) -> None:
50
+ msg_type = message.get("type")
51
+ if not isinstance(msg_type, str) or not msg_type:
52
+ raise CCPSchemaError("Missing or invalid message.type")
53
+
54
+ schema_filename = _MESSAGE_TYPE_TO_SCHEMA.get(msg_type)
55
+ if schema_filename is None:
56
+ raise CCPSchemaError(f"Unknown message type: {msg_type}")
57
+
58
+ schema = _ALL_SCHEMAS.get(schema_filename)
59
+ if schema is None:
60
+ raise CCPSchemaError(f"Schema not bundled: {schema_filename}")
61
+
62
+ validator = Draft202012Validator(schema, resolver=_make_resolver(schema))
63
+ errors = sorted(validator.iter_errors(message), key=lambda e: e.path)
64
+ if errors:
65
+ first = errors[0]
66
+ loc = "/".join(str(p) for p in first.path) or "<root>"
67
+ raise CCPSchemaError(f"Invalid CCP message at {loc}: {first.message}")
ccp_sdk/validator.py ADDED
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from importlib import resources
5
+ from typing import Any, Dict
6
+
7
+ from jsonschema import Draft202012Validator
8
+ from jsonschema.validators import RefResolver
9
+
10
+ from .errors import CCPSchemaError
11
+
12
+
13
+ _MESSAGE_TYPE_TO_SCHEMA = {
14
+ "hello": "hello.json",
15
+ "hello_ack": "hello-ack.json",
16
+ "session_start": "session-start.json",
17
+ "session_started": "session-started.json",
18
+ "turn": "turn.json",
19
+ "turn_result": "turn-result.json",
20
+ "session_end": "session-end.json",
21
+ "session_ended": "session-ended.json",
22
+ "error": "error.json",
23
+ }
24
+
25
+
26
+ def _load_all_schemas() -> Dict[str, Dict[str, Any]]:
27
+ schema_pkg = resources.files("ccp_sdk.schemas")
28
+ schemas: Dict[str, Dict[str, Any]] = {}
29
+ for entry in schema_pkg.iterdir():
30
+ if entry.name.endswith(".json"):
31
+ schemas[entry.name] = json.loads(entry.read_text(encoding="utf-8"))
32
+ return schemas
33
+
34
+
35
+ _ALL_SCHEMAS = _load_all_schemas()
36
+
37
+
38
+ def _make_resolver(schema: Dict[str, Any]) -> RefResolver:
39
+ store: Dict[str, Any] = {}
40
+ for filename, schema_obj in _ALL_SCHEMAS.items():
41
+ store[filename] = schema_obj
42
+ schema_id = schema_obj.get("$id")
43
+ if isinstance(schema_id, str):
44
+ store[schema_id] = schema_obj
45
+
46
+ return RefResolver.from_schema(schema, store=store)
47
+
48
+
49
+ def validate_message(message: Dict[str, Any]) -> None:
50
+ msg_type = message.get("type")
51
+ if not isinstance(msg_type, str) or not msg_type:
52
+ raise CCPSchemaError("Missing or invalid message.type")
53
+
54
+ schema_filename = _MESSAGE_TYPE_TO_SCHEMA.get(msg_type)
55
+ if schema_filename is None:
56
+ raise CCPSchemaError(f"Unknown message type: {msg_type}")
57
+
58
+ schema = _ALL_SCHEMAS.get(schema_filename)
59
+ if schema is None:
60
+ raise CCPSchemaError(f"Schema not bundled: {schema_filename}")
61
+
62
+ validator = Draft202012Validator(schema, resolver=_make_resolver(schema))
63
+ errors = sorted(validator.iter_errors(message), key=lambda e: e.path)
64
+ if errors:
65
+ first = errors[0]
66
+ loc = "/".join(str(p) for p in first.path) or "<root>"
67
+ raise CCPSchemaError(f"Invalid CCP message at {loc}: {first.message}")
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: ccp-sdk
3
+ Version: 0.1.0
4
+ Summary: Reference Python SDK for Cortex Capsule Protocol (CCP)
5
+ Author: CCP contributors
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Jonathan Hidalgo
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/<your-org>/<your-repo>
29
+ Project-URL: Repository, https://github.com/<your-org>/<your-repo>
30
+ Keywords: ccp,protocol,iot,robotics,gateway
31
+ Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3 :: Only
36
+ Classifier: Programming Language :: Python :: 3.9
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Topic :: Software Development :: Libraries
41
+ Requires-Python: >=3.9
42
+ Description-Content-Type: text/markdown
43
+ License-File: LICENSE
44
+ Requires-Dist: httpx>=0.27
45
+ Requires-Dist: jsonschema>=4.21
46
+ Dynamic: license-file
47
+
48
+ # CCP
49
+ Cortex Capsule Protocol (CCP) — v0.1 Draft
50
+
51
+ CCP standardizes communication between devices (robots, microcontrollers, gateways) and Cortex Capsule servers (local or cloud brain runtime).
52
+
53
+ ## Contents
54
+ - Core specification
55
+ - HTTP mapping
56
+ - JSON schemas
57
+ - Message examples
58
+
59
+ ## Structure
60
+ - [docs/](docs/README.md)
61
+ - [schemas/](schemas)
62
+ - [examples/](examples)
63
+
64
+ ## Get started
65
+ 1. Read the core spec: [docs/spec/ccp-core.md](docs/spec/ccp-core.md)
66
+ 2. Check HTTP mapping: [docs/spec/http-binding.md](docs/spec/http-binding.md)
67
+ 3. See schemas: [docs/spec/message-schemas.md](docs/spec/message-schemas.md)
68
+ 4. See examples: [examples/](examples)
69
+
70
+ ## How to use CCP
71
+ - This repo is the **specification**, not a dependency to install.
72
+ - Projects should depend on a **CCP SDK** (language/runtime specific) that implements the protocol.
73
+ - If you need JSON schemas in production, the SDK should bundle them or reference a published schema package.
74
+
75
+ ## Python SDK (reference)
76
+
77
+ This repository now includes a minimal reference Python SDK that you can install with pip.
78
+
79
+ ### Install (local dev)
80
+ From the repo root:
81
+
82
+ ```bash
83
+ python -m pip install -e .
84
+ ```
85
+
86
+ ### Install (from PyPI)
87
+ After you publish it:
88
+
89
+ ```bash
90
+ python -m pip install ccp-sdk
91
+ ```
92
+
93
+ ### Usage
94
+ See a runnable example in [examples/python_client.py](examples/python_client.py).
95
+
96
+ If your ESP32 sends data to a Python gateway API, see: [examples/esp32_gateway_api/](examples/esp32_gateway_api/).
97
+
98
+ ## ESP32 note
99
+ ESP32 (Arduino/ESP-IDF) does not run `pip` packages directly.
100
+
101
+ Typical architecture:
102
+ - ESP32 handles hardware (mic/speaker/LED/display) and sends events/audio to a gateway over Wi-Fi/serial.
103
+ - The gateway (Raspberry Pi / PC) runs the Python CCP SDK and talks to the CCP server over HTTP.
104
+
105
+ Practical mapping:
106
+ - Mic (I2S) -> capture audio -> upload to `/v1/media/audio` -> send `turn` with `audio_ref`
107
+ - Speaker (I2S) <- receive `turn_result.payload.reply.audio_ref` -> download audio -> play
108
+ - LED strip / display <- drive UI based on `turn_result.payload.state`, `safety`, or `actions`
109
+
110
+ ## SDKs (planned)
111
+ - `ccp-python` — reference SDK for capsules/gateways
112
+ - `ccp-esp32` — thin device SDK for microcontrollers
113
+
114
+ ## Version
115
+ CCP v0.1 (Draft)
116
+
117
+ ## Roadmap
118
+ See [docs/roadmap.md](docs/roadmap.md).
119
+
120
+ ## Community
121
+ Discord: invite link TBD.
122
+
123
+ ## Contributing
124
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for how to propose changes and submit PRs.
125
+
126
+ ## Code of Conduct
127
+ See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
128
+
129
+ ## Security
130
+ See [SECURITY.md](SECURITY.md).
@@ -0,0 +1,23 @@
1
+ ccp_sdk/__init__.py,sha256=7K9gvo4O0YQF4z9uHCZSdsw8agu7nBBV3lcMe7h0UWU,242
2
+ ccp_sdk/client.py,sha256=OOIignJwnTDAkUrf9Xlo7j2Ah2YwmCQDNDZoYScig2M,3973
3
+ ccp_sdk/errors.py,sha256=1Ks10R5fSurMWIMOAfqPQLQJP__rSkd0uF0K0J4Gwx4,495
4
+ ccp_sdk/messages.py,sha256=tzaxk6ScYdFrYH_XzDprP-xE3XzA9Ea18cWLMC5jmKQ,1743
5
+ ccp_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ ccp_sdk/schemas.py,sha256=y2Ae7HMkqwt24dt5JrEsroipJEcFbTDSRHnXD2wdmFc,2171
7
+ ccp_sdk/validator.py,sha256=y2Ae7HMkqwt24dt5JrEsroipJEcFbTDSRHnXD2wdmFc,2171
8
+ ccp_sdk/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ ccp_sdk/schemas/ccp-message.json,sha256=S9rWujNB-gSu4dYaxaOq9Qro_ThUmq-0m4GiOl7QOsg,515
10
+ ccp_sdk/schemas/error.json,sha256=0BXliMCW9QadQProosKq06IqLeIvfnLFaiidNVWvdQU,1007
11
+ ccp_sdk/schemas/hello-ack.json,sha256=VPHmNqhrqDZRcpZsj28r3LiK14Q_ZUFfLQVtxSoCSjI,1285
12
+ ccp_sdk/schemas/hello.json,sha256=vKz3zdjeGxedcMpEcYSIizKIblucN5C1U8hhXK1_ZUQ,2683
13
+ ccp_sdk/schemas/session-end.json,sha256=8bzRRB64Vtkq22I3u7Jm_PitHJCq_fi-CLMsiF2d6oE,509
14
+ ccp_sdk/schemas/session-ended.json,sha256=wKYQHGKgfrYf1EwGeVAfNYQ6TZWqgdTP82s66QYAc_o,515
15
+ ccp_sdk/schemas/session-start.json,sha256=vyiOKjnM2fr_rS9cAkkd96h405lyqWc5nyS7iNB4lhE,513
16
+ ccp_sdk/schemas/session-started.json,sha256=QkEKL2cw8KAoQK4YJtePerC_CFiKPZ03shFWb0V1aR8,631
17
+ ccp_sdk/schemas/turn-result.json,sha256=rH9DEQVRg6UMejzzYtAgdT6_Ol7g00y2zIbMHjegNws,1312
18
+ ccp_sdk/schemas/turn.json,sha256=dcFhHUJlcMYeu3NYhkAPUn03PPZCwj6jrFV2dGZj8Es,866
19
+ ccp_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=WxHNtMYSoeOtJM2eaCk5NB6SVpt-4yLjkA5-A7cwCkI,1073
20
+ ccp_sdk-0.1.0.dist-info/METADATA,sha256=Cz-GX8rULDxdtccmqTYaLx4Z_sZZtJMd7aRkvSh_pzg,4739
21
+ ccp_sdk-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
22
+ ccp_sdk-0.1.0.dist-info/top_level.txt,sha256=p9dlm1N-qdBkLNm0BQGeLQ1OFqgcTdDiu5EhOnVhQH4,8
23
+ ccp_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonathan Hidalgo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ ccp_sdk