pigeonmq 1.1.0__tar.gz

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.
@@ -0,0 +1,46 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Next.js build output (web/, demo/)
5
+ .next/
6
+ next-env.d.ts
7
+
8
+ # Vercel deployment metadata (demo/)
9
+ .vercel
10
+
11
+ # Logs
12
+ *.log
13
+ npm-debug.log*
14
+ yarn-debug.log*
15
+ yarn-error.log*
16
+
17
+ # Internal scratch / generated notes (not for distribution)
18
+ work/
19
+ outputs/
20
+
21
+ # Agent / tooling scratch
22
+ .agents/
23
+ .codex/
24
+
25
+ # Local Claude Code settings (permissions etc.) - personal, not shared.
26
+ # The shared .claude/commands and .claude/agents ARE tracked.
27
+ .claude/settings.local.json
28
+
29
+ # Personal npm-token setup reminder (see release workflow) - not for distribution.
30
+ NPM_TOKEN_SETUP.md
31
+
32
+ # Environment
33
+ .env
34
+ .env.*
35
+ !.env.example
36
+
37
+ # Verification / screenshot artifacts (the banner ships as .svg)
38
+ *.png
39
+ .playwright-mcp/
40
+
41
+ # OS / editor cruft
42
+ .DS_Store
43
+ Thumbs.db
44
+ .idea/
45
+ .vscode/
46
+ *.swp
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.5
2
+ Name: pigeonmq
3
+ Version: 1.1.0
4
+ Summary: Python client for the Pigeon contract-native message broker
5
+ Project-URL: Homepage, https://pigeonmq.cc
6
+ Project-URL: Documentation, https://pigeonmq.cc
7
+ Project-URL: Repository, https://github.com/vishnu-77/pigeon
8
+ Project-URL: Issues, https://github.com/vishnu-77/pigeon/issues
9
+ Author: vishnu-77
10
+ License: Apache-2.0
11
+ Keywords: broker,communication-contracts,governance,messaging
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Pigeon Python SDK
19
+
20
+ Official Python client for the Pigeon contract-native message broker.
21
+
22
+ > PyPI and TestPyPI Trusted Publishing are wired into `.github/workflows/release.yml`. Until the first registry release is cut, install from the repository subdirectory for development.
23
+
24
+ ```bash
25
+ pip install ./sdk/python
26
+ ```
27
+
28
+ After the first PyPI release:
29
+
30
+ ```bash
31
+ pip install pigeonmq
32
+ ```
33
+
34
+ ```python
35
+ from pigeonmq import PigeonClient, PigeonClientError
36
+
37
+ client = PigeonClient(token="checkout-token")
38
+ client.connect(["payments.authorize"])
39
+
40
+ try:
41
+ result = client.request(
42
+ "payments.authorize",
43
+ {
44
+ "merchantId": "m",
45
+ "orderId": "order_42",
46
+ "amount": 42.0,
47
+ "currency": "GBP",
48
+ "paymentToken": "tok",
49
+ },
50
+ intent="authorize_payment",
51
+ idempotency_key="order_42:authorize",
52
+ classification="pci",
53
+ region="uk",
54
+ )
55
+ print(result["status"])
56
+ except PigeonClientError as error:
57
+ print(error.code, error.status)
58
+ ```
59
+
60
+ The client exposes `connect`, `publish`, `request`, `receive`, `subjects`, `audit`, and `quarantine`. Errors surface as `PigeonClientError` with `code`, `status`, and `details`.
@@ -0,0 +1,43 @@
1
+ # Pigeon Python SDK
2
+
3
+ Official Python client for the Pigeon contract-native message broker.
4
+
5
+ > PyPI and TestPyPI Trusted Publishing are wired into `.github/workflows/release.yml`. Until the first registry release is cut, install from the repository subdirectory for development.
6
+
7
+ ```bash
8
+ pip install ./sdk/python
9
+ ```
10
+
11
+ After the first PyPI release:
12
+
13
+ ```bash
14
+ pip install pigeonmq
15
+ ```
16
+
17
+ ```python
18
+ from pigeonmq import PigeonClient, PigeonClientError
19
+
20
+ client = PigeonClient(token="checkout-token")
21
+ client.connect(["payments.authorize"])
22
+
23
+ try:
24
+ result = client.request(
25
+ "payments.authorize",
26
+ {
27
+ "merchantId": "m",
28
+ "orderId": "order_42",
29
+ "amount": 42.0,
30
+ "currency": "GBP",
31
+ "paymentToken": "tok",
32
+ },
33
+ intent="authorize_payment",
34
+ idempotency_key="order_42:authorize",
35
+ classification="pci",
36
+ region="uk",
37
+ )
38
+ print(result["status"])
39
+ except PigeonClientError as error:
40
+ print(error.code, error.status)
41
+ ```
42
+
43
+ The client exposes `connect`, `publish`, `request`, `receive`, `subjects`, `audit`, and `quarantine`. Errors surface as `PigeonClientError` with `code`, `status`, and `details`.
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pigeonmq"
7
+ version = "1.1.0"
8
+ description = "Python client for the Pigeon contract-native message broker"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "vishnu-77" }]
13
+ keywords = ["messaging", "broker", "communication-contracts", "governance"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: Apache Software License",
17
+ "Operating System :: OS Independent"
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://pigeonmq.cc"
22
+ Documentation = "https://pigeonmq.cc"
23
+ Repository = "https://github.com/vishnu-77/pigeon"
24
+ Issues = "https://github.com/vishnu-77/pigeon/issues"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["src/pigeonmq"]
@@ -0,0 +1,3 @@
1
+ from .client import PigeonClient, PigeonClientError
2
+
3
+ __all__ = ["PigeonClient", "PigeonClientError"]
@@ -0,0 +1,143 @@
1
+ """Small standard-library client for Pigeon Protocol v1."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+ from urllib.error import HTTPError
8
+ from urllib.request import Request, urlopen
9
+
10
+
11
+ class PigeonClientError(RuntimeError):
12
+ def __init__(self, code: str, message: str, status: int, details: dict[str, Any] | None = None) -> None:
13
+ super().__init__(message)
14
+ self.code = code
15
+ self.status = status
16
+ self.details = details or {}
17
+
18
+
19
+ class PigeonClient:
20
+ def __init__(
21
+ self,
22
+ url: str = "http://localhost:8787",
23
+ *,
24
+ token: str | None = None,
25
+ region: str = "uk",
26
+ timeout: float = 10.0,
27
+ ) -> None:
28
+ self.url = url.rstrip("/")
29
+ self.token = token
30
+ self.region = region
31
+ self.timeout = timeout
32
+ self.contract: dict[str, Any] | None = None
33
+ self.contract_id: str | None = None
34
+
35
+ def connect(self, subjects: list[str], *, ttl_ms: int | None = None) -> dict[str, Any]:
36
+ body: dict[str, Any] = {"subjects": subjects}
37
+ if ttl_ms is not None:
38
+ body["ttlMs"] = ttl_ms
39
+ payload = self._post("/v1/contracts", body, with_contract=False)
40
+ self.contract = payload["contract"]
41
+ self.contract_id = self.contract["id"]
42
+ return self.contract
43
+
44
+ def publish(self, message: dict[str, Any]) -> dict[str, Any]:
45
+ self._require_contract()
46
+ return self._post("/v1/messages", message, with_contract=True)
47
+
48
+ def request(
49
+ self,
50
+ subject: str,
51
+ data: dict[str, Any],
52
+ *,
53
+ intent: str | None = None,
54
+ idempotency_key: str | None = None,
55
+ classification: str | None = None,
56
+ region: str | None = None,
57
+ message_type: str | None = None,
58
+ source: str = "sdk-python",
59
+ correlation_id: str | None = None,
60
+ ) -> dict[str, Any]:
61
+ message = {
62
+ "subject": subject,
63
+ "type": message_type or f"{subject}.request",
64
+ "source": source,
65
+ "intent": intent,
66
+ "idempotencyKey": idempotency_key,
67
+ "classification": classification,
68
+ "region": region or self.region,
69
+ "correlationId": correlation_id,
70
+ "data": data,
71
+ }
72
+ return self.publish({key: value for key, value in message.items() if value is not None})
73
+
74
+ def receive(self, subject: str, *, max_messages: int = 1) -> list[dict[str, Any]]:
75
+ self._require_contract()
76
+ payload = self._post(
77
+ f"/v1/subjects/{subject}/receive",
78
+ {"max": max_messages},
79
+ with_contract=True,
80
+ )
81
+ return payload["messages"]
82
+
83
+ def subjects(self) -> list[dict[str, Any]]:
84
+ return self._get("/v1/subjects")["subjects"]
85
+
86
+ def audit(self) -> list[dict[str, Any]]:
87
+ return self._get("/v1/audit")["records"]
88
+
89
+ def quarantine(self) -> list[dict[str, Any]]:
90
+ return self._get("/v1/quarantine")["records"]
91
+
92
+ def _require_contract(self) -> None:
93
+ if not self.contract_id:
94
+ raise PigeonClientError(
95
+ "CONTRACT_REQUIRED",
96
+ "Call connect() to negotiate a contract first.",
97
+ 0,
98
+ )
99
+
100
+ def _headers(self, with_contract: bool) -> dict[str, str]:
101
+ headers = {
102
+ "content-type": "application/json",
103
+ "x-pigeon-region": self.region,
104
+ }
105
+ if self.token:
106
+ headers["authorization"] = f"Bearer {self.token}"
107
+ if with_contract and self.contract_id:
108
+ headers["x-pigeon-contract"] = self.contract_id
109
+ return headers
110
+
111
+ def _post(self, path: str, body: dict[str, Any], *, with_contract: bool) -> dict[str, Any]:
112
+ request = Request(
113
+ f"{self.url}{path}",
114
+ data=json.dumps(body).encode("utf-8"),
115
+ headers=self._headers(with_contract),
116
+ method="POST",
117
+ )
118
+ return self._send(request)
119
+
120
+ def _get(self, path: str) -> dict[str, Any]:
121
+ request = Request(
122
+ f"{self.url}{path}",
123
+ headers=self._headers(False),
124
+ method="GET",
125
+ )
126
+ return self._send(request)
127
+
128
+ def _send(self, request: Request) -> dict[str, Any]:
129
+ try:
130
+ with urlopen(request, timeout=self.timeout) as response:
131
+ return json.loads(response.read().decode("utf-8"))
132
+ except HTTPError as error:
133
+ try:
134
+ payload = json.loads(error.read().decode("utf-8"))
135
+ except (json.JSONDecodeError, UnicodeDecodeError):
136
+ payload = {}
137
+ details = payload.get("error", {})
138
+ raise PigeonClientError(
139
+ details.get("code", "REQUEST_FAILED"),
140
+ details.get("message", f"HTTP {error.code}"),
141
+ error.code,
142
+ details.get("details", {}),
143
+ ) from error
@@ -0,0 +1,62 @@
1
+ import os
2
+ import unittest
3
+
4
+ from pigeonmq import PigeonClient, PigeonClientError
5
+
6
+
7
+ BASE = os.environ.get("PIGEON_URL", "http://localhost:8787")
8
+
9
+
10
+ class PigeonIntegrationTests(unittest.TestCase):
11
+ def test_contract_and_publish(self):
12
+ client = PigeonClient(BASE, token="checkout-token")
13
+ contract = client.connect(["payments.authorize"])
14
+ self.assertTrue(contract["id"])
15
+
16
+ result = client.request(
17
+ "payments.authorize",
18
+ {
19
+ "merchantId": "m",
20
+ "orderId": "python_1",
21
+ "amount": 12.5,
22
+ "currency": "GBP",
23
+ "paymentToken": "tok",
24
+ },
25
+ intent="authorize_payment",
26
+ idempotency_key="python_1:authorize",
27
+ classification="pci",
28
+ region="uk",
29
+ )
30
+ self.assertEqual(result["status"], "accepted")
31
+
32
+ def test_unauthorized_principal_is_denied_at_negotiation(self):
33
+ client = PigeonClient(BASE, token="catalog-token")
34
+ with self.assertRaises(PigeonClientError) as raised:
35
+ client.connect(["payments.authorize"])
36
+ self.assertEqual(raised.exception.code, "NO_PERMITTED_SUBJECTS")
37
+ self.assertEqual(raised.exception.status, 403)
38
+
39
+ def test_sensitive_field_violation_is_denied(self):
40
+ client = PigeonClient(BASE, token="checkout-token")
41
+ client.connect(["payments.authorize"])
42
+ with self.assertRaises(PigeonClientError) as raised:
43
+ client.request(
44
+ "payments.authorize",
45
+ {
46
+ "merchantId": "m",
47
+ "orderId": "python_sensitive",
48
+ "amount": 12.5,
49
+ "currency": "GBP",
50
+ "paymentToken": "tok",
51
+ "card": {"pan": "TEST_ONLY"},
52
+ },
53
+ intent="authorize_payment",
54
+ idempotency_key="python_sensitive:authorize",
55
+ classification="pci",
56
+ region="uk",
57
+ )
58
+ self.assertIn(raised.exception.code, {"SENSITIVE_FIELD_DENIED", "RAW_PAN_DETECTED"})
59
+
60
+
61
+ if __name__ == "__main__":
62
+ unittest.main()