patchr 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.
- apps/__init__.py +2 -0
- apps/api/__init__.py +2 -0
- apps/api/main.py +652 -0
- apps/benchmarks/__init__.py +1 -0
- apps/benchmarks/main.py +20 -0
- apps/sandbox/__init__.py +1 -0
- apps/sandbox/main.py +20 -0
- apps/worker/__init__.py +2 -0
- apps/worker/main.py +15 -0
- apps/worker/verify.py +14 -0
- patchr/__init__.py +12 -0
- patchr/sdk/__init__.py +20 -0
- patchr/sdk/client.py +12 -0
- patchr-0.1.0.dist-info/METADATA +137 -0
- patchr-0.1.0.dist-info/RECORD +116 -0
- patchr-0.1.0.dist-info/WHEEL +5 -0
- patchr-0.1.0.dist-info/entry_points.txt +5 -0
- patchr-0.1.0.dist-info/licenses/LICENSE +17 -0
- patchr-0.1.0.dist-info/top_level.txt +3 -0
- picux/__init__.py +6 -0
- picux/agents/__init__.py +5 -0
- picux/agents/registry.py +204 -0
- picux/api/__init__.py +5 -0
- picux/api/service.py +5075 -0
- picux/audit/__init__.py +31 -0
- picux/audit/activity.py +97 -0
- picux/audit/observability.py +55 -0
- picux/audit/verification/__init__.py +21 -0
- picux/audit/verification/ledger.py +633 -0
- picux/benchmarks/__init__.py +5 -0
- picux/benchmarks/local.py +286 -0
- picux/config.py +140 -0
- picux/contracts/__init__.py +22 -0
- picux/contracts/handshake.py +122 -0
- picux/contracts/integration.py +385 -0
- picux/contracts/openapi.py +187 -0
- picux/contracts/protocol_map.py +152 -0
- picux/contracts/routes.py +980 -0
- picux/contracts/schema_catalog.py +125 -0
- picux/core/__init__.py +17 -0
- picux/core/models.py +148 -0
- picux/core/router.py +131 -0
- picux/core/runtime.py +42 -0
- picux/core/state_machine.py +38 -0
- picux/domains/__init__.py +2 -0
- picux/domains/bridge/HostRun.py +1104 -0
- picux/domains/bridge/__init__.py +6 -0
- picux/domains/bridge/engine.py +345 -0
- picux/domains/hunt/__init__.py +6 -0
- picux/domains/hunt/engine.py +307 -0
- picux/domains/hunt/models.py +88 -0
- picux/domains/pay/__init__.py +16 -0
- picux/domains/pay/adapters.py +607 -0
- picux/domains/pay/engine.py +950 -0
- picux/domains/pay/models.py +95 -0
- picux/domains/proxy/__init__.py +5 -0
- picux/domains/proxy/engine.py +466 -0
- picux/domains/resolve/__init__.py +5 -0
- picux/domains/resolve/engine.py +546 -0
- picux/orchestrator/__init__.py +3 -0
- picux/orchestrator/engine.py +2840 -0
- picux/portals/__init__.py +17 -0
- picux/portals/templates.py +272 -0
- picux/protocols/__init__.py +1 -0
- picux/protocols/a2a/__init__.py +6 -0
- picux/protocols/a2a/client.py +51 -0
- picux/protocols/a2a/envelope.py +132 -0
- picux/protocols/mcp/__init__.py +7 -0
- picux/protocols/mcp/client.py +69 -0
- picux/protocols/mcp/contract.py +67 -0
- picux/protocols/mcp/server.py +76 -0
- picux/sandbox/__init__.py +6 -0
- picux/sandbox/midnight_arbitrage.py +215 -0
- picux/sandbox/models.py +90 -0
- picux/sdk/__init__.py +13 -0
- picux/sdk/client.py +768 -0
- picux/sdk/external.py +245 -0
- picux/security/__init__.py +18 -0
- picux/security/auth.py +86 -0
- picux/security/config_validator.py +58 -0
- picux/security/policy.py +158 -0
- picux/security/secrets.py +144 -0
- picux/signals/__init__.py +1 -0
- picux/signals/community/__init__.py +24 -0
- picux/signals/community/adapters/__init__.py +7 -0
- picux/signals/community/adapters/reddit.py +37 -0
- picux/signals/community/adapters/shopify.py +23 -0
- picux/signals/community/adapters/web.py +23 -0
- picux/signals/community/disambiguation.py +51 -0
- picux/signals/community/intake.py +227 -0
- picux/signals/community/models.py +102 -0
- picux/signals/community/rules.py +91 -0
- picux/signals/community/scoring.py +64 -0
- picux/storage/__init__.py +41 -0
- picux/storage/agents.py +50 -0
- picux/storage/cases.py +440 -0
- picux/storage/channels.py +476 -0
- picux/storage/connectors.py +411 -0
- picux/storage/envelopes.py +137 -0
- picux/storage/escrows.py +168 -0
- picux/storage/events.py +989 -0
- picux/storage/keyspace.py +60 -0
- picux/storage/mandates.py +107 -0
- picux/storage/portals.py +222 -0
- picux/storage/postgres.py +2049 -0
- picux/storage/providers.py +148 -0
- picux/storage/proxy.py +231 -0
- picux/storage/receipts.py +131 -0
- picux/storage/signals.py +147 -0
- picux/storage/tasks.py +179 -0
- picux/tools/__init__.py +11 -0
- picux/tools/shared.py +2048 -0
- picux/verification/__init__.py +5 -0
- picux/verification/rollout.py +183 -0
- picux/workflows/__init__.py +5 -0
- picux/workflows/templates.py +74 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from picux import __version__
|
|
7
|
+
from picux.protocols.mcp.contract import PicuxMCPContract
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PicuxMCPServer:
|
|
11
|
+
"""JSON-RPC MCP endpoint backed by the Picux route registry."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, *, service: Any | None = None) -> None:
|
|
14
|
+
self.contract = PicuxMCPContract(service=service)
|
|
15
|
+
|
|
16
|
+
def handle(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
17
|
+
requestId = request.get("id")
|
|
18
|
+
method = str(request.get("method", "") or "")
|
|
19
|
+
params = request.get("params", {}) if isinstance(request.get("params"), dict) else {}
|
|
20
|
+
if request.get("jsonrpc") != "2.0":
|
|
21
|
+
return self._error(requestId, -32600, "invalidRequest", {"reason": "jsonrpc"})
|
|
22
|
+
try:
|
|
23
|
+
result = self._dispatch(method, params)
|
|
24
|
+
except MCPDispatchError as exc:
|
|
25
|
+
return self._error(requestId, exc.code, exc.message, exc.data)
|
|
26
|
+
return {"jsonrpc": "2.0", "id": requestId, "result": result}
|
|
27
|
+
|
|
28
|
+
def _dispatch(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
29
|
+
if method == "initialize":
|
|
30
|
+
return {
|
|
31
|
+
"protocolVersion": "2024-11-05",
|
|
32
|
+
"capabilities": {"tools": {}},
|
|
33
|
+
"serverInfo": {"name": "picux", "version": __version__},
|
|
34
|
+
}
|
|
35
|
+
if method == "ping":
|
|
36
|
+
return {"ok": True}
|
|
37
|
+
if method == "notifications/initialized":
|
|
38
|
+
return {"ok": True}
|
|
39
|
+
if method == "tools/list":
|
|
40
|
+
return self.contract.listTools()
|
|
41
|
+
if method == "tools/call":
|
|
42
|
+
name = str(params.get("name", params.get("tool", "")) or "")
|
|
43
|
+
args = params.get("arguments", params.get("args", {}))
|
|
44
|
+
if not isinstance(args, dict):
|
|
45
|
+
args = {}
|
|
46
|
+
if not name:
|
|
47
|
+
raise MCPDispatchError(-32602, "invalidParams", {"missing": "name"})
|
|
48
|
+
return self._toolResult(self.contract.callTool(name, args))
|
|
49
|
+
if method.startswith("picux."):
|
|
50
|
+
return self.contract.callTool(method, params)
|
|
51
|
+
raise MCPDispatchError(-32601, "methodNotFound", {"method": method})
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def _toolResult(result: dict[str, Any]) -> dict[str, Any]:
|
|
55
|
+
isError = not bool(result.get("ok"))
|
|
56
|
+
return {
|
|
57
|
+
**result,
|
|
58
|
+
"isError": isError,
|
|
59
|
+
"structuredContent": result.get("payload", result),
|
|
60
|
+
"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=True, sort_keys=True)}],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _error(requestId: Any, code: int, message: str, data: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
65
|
+
error = {"code": code, "message": message}
|
|
66
|
+
if data:
|
|
67
|
+
error["data"] = data
|
|
68
|
+
return {"jsonrpc": "2.0", "id": requestId, "error": error}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class MCPDispatchError(Exception):
|
|
72
|
+
def __init__(self, code: int, message: str, data: dict[str, Any] | None = None) -> None:
|
|
73
|
+
super().__init__(message)
|
|
74
|
+
self.code = code
|
|
75
|
+
self.message = message
|
|
76
|
+
self.data = data or {}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from picux.api import PicuxApiService
|
|
8
|
+
from picux.domains.pay import PayDomain
|
|
9
|
+
from picux.domains.pay.models import Money, ProofOfValue, SettlementRequest
|
|
10
|
+
from picux.protocols.mcp import PicuxMCPContract
|
|
11
|
+
from picux.sandbox.models import SandboxResult, TraceEvent, VendorQuote
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
CHAOS_MODES = {"", "latency", "modelError", "unauthorizedSpend", "freeze"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MidnightArbitrageSandbox:
|
|
18
|
+
"""Deterministic local sandbox for the HUNT -> RESOLVE -> PAY loop."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
clock: Callable[[], datetime] | None = None,
|
|
24
|
+
mcp: PicuxMCPContract | None = None,
|
|
25
|
+
pay: PayDomain | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.clock = clock or (lambda: datetime(2026, 5, 9, 12, 0, tzinfo=timezone.utc))
|
|
28
|
+
self.mcp = mcp or PicuxMCPContract(service=PicuxApiService())
|
|
29
|
+
self.pay = pay or PayDomain(verifier=self._verifyMandate, clock=self.clock)
|
|
30
|
+
|
|
31
|
+
def run(self, *, chaos: str = "") -> dict[str, Any]:
|
|
32
|
+
if chaos not in CHAOS_MODES:
|
|
33
|
+
raise ValueError(f"unknownChaos:{chaos}")
|
|
34
|
+
|
|
35
|
+
trace: list[TraceEvent] = []
|
|
36
|
+
task = self._createTask(trace)
|
|
37
|
+
vendors = self._hunt(trace, chaos)
|
|
38
|
+
decisionTree = self._resolve(trace, vendors, chaos)
|
|
39
|
+
mandate = self.mandate()
|
|
40
|
+
self._event(trace, "pay", "mandateView", "ready", "Intent Mandate loaded", {"mandateId": mandate["mandateId"]})
|
|
41
|
+
|
|
42
|
+
if chaos == "modelError":
|
|
43
|
+
freeze = self.pay.freeze(mandateId=mandate["mandateId"], taskId=task["taskId"], reason="modelErrorRollback")
|
|
44
|
+
self._event(trace, "resolve", "rollback", "failedClosed", "Model error detected; settlement blocked", {"receiptId": freeze["receiptId"]})
|
|
45
|
+
return self._result(False, "failedClosed", chaos, task, trace, vendors, decisionTree, mandate, freezeReceipt=freeze)
|
|
46
|
+
|
|
47
|
+
if chaos == "freeze":
|
|
48
|
+
freeze = self.pay.freeze(mandateId=mandate["mandateId"], taskId=task["taskId"], reason="protocolFreeze")
|
|
49
|
+
self._event(trace, "pay", "killSwitch", "failedClosed", "Protocol freeze stopped settlement", {"receiptId": freeze["receiptId"]})
|
|
50
|
+
return self._result(False, "failedClosed", chaos, task, trace, vendors, decisionTree, mandate, freezeReceipt=freeze)
|
|
51
|
+
|
|
52
|
+
selected = decisionTree.get("selected", "")
|
|
53
|
+
if not selected:
|
|
54
|
+
freeze = self.pay.freeze(mandateId=mandate["mandateId"], taskId=task["taskId"], reason="noSafeVendor")
|
|
55
|
+
self._event(trace, "pay", "freeze", "failedClosed", "No safe vendor remained after pruning", {"receiptId": freeze["receiptId"]})
|
|
56
|
+
return self._result(False, "failedClosed", chaos, task, trace, vendors, decisionTree, mandate, freezeReceipt=freeze)
|
|
57
|
+
|
|
58
|
+
quote = next(vendor for vendor in vendors if vendor.vendorId == selected)
|
|
59
|
+
amount = 120.0 if chaos == "unauthorizedSpend" else quote.netUsd
|
|
60
|
+
request = SettlementRequest(
|
|
61
|
+
taskId=task["taskId"],
|
|
62
|
+
vendorId=quote.vendorId,
|
|
63
|
+
amount=Money(amount, "USD"),
|
|
64
|
+
pov=ProofOfValue(
|
|
65
|
+
povId=f"pov_{quote.vendorId}",
|
|
66
|
+
rules=("inventoryVerified", "priceArbitrage20", "regionMatched", "hiddenFeeClear"),
|
|
67
|
+
meta={"discountPct": quote.discountPct, "netUsd": quote.netUsd},
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
settled = self.pay.settle(mandate, request)
|
|
71
|
+
receipt = settled["receipt"]
|
|
72
|
+
if settled["ok"]:
|
|
73
|
+
self._event(trace, "pay", "microEscrow", "settled", "Proof-of-value verified; escrow settled", {"receiptId": receipt["receiptId"]})
|
|
74
|
+
self._event(trace, "pay", "receiptView", "ready", "Settlement receipt available", {"receipt": receipt})
|
|
75
|
+
return self._result(True, "settled", chaos, task, trace, vendors, decisionTree, mandate, receipt=receipt)
|
|
76
|
+
|
|
77
|
+
freeze = self.pay.freeze(mandateId=mandate["mandateId"], taskId=task["taskId"], reason="unauthorizedSpend")
|
|
78
|
+
self._event(
|
|
79
|
+
trace,
|
|
80
|
+
"pay",
|
|
81
|
+
"unauthorizedSpend",
|
|
82
|
+
"failedClosed",
|
|
83
|
+
"PAY rejected unauthorized spend and froze the rail",
|
|
84
|
+
{"errors": settled["decision"]["errors"], "receiptId": freeze["receiptId"]},
|
|
85
|
+
)
|
|
86
|
+
return self._result(False, "failedClosed", chaos, task, trace, vendors, decisionTree, mandate, receipt=receipt, freezeReceipt=freeze)
|
|
87
|
+
|
|
88
|
+
def mandate(self) -> dict[str, Any]:
|
|
89
|
+
return {
|
|
90
|
+
"mandateId": "mandate_midnight_arbitrage",
|
|
91
|
+
"issuer": {"entityId": "tenant_sandbox", "publicKey": "sandboxPub"},
|
|
92
|
+
"validUntil": "2099-01-01T00:00:00Z",
|
|
93
|
+
"constraints": {
|
|
94
|
+
"maxSpend": {"amount": 100.0, "currency": "USD"},
|
|
95
|
+
"allowedVendors": ["vendorA"],
|
|
96
|
+
"resolveRules": ["inventoryVerified", "priceArbitrage20", "regionMatched", "hiddenFeeClear"],
|
|
97
|
+
},
|
|
98
|
+
"signature": "sandboxSig",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
def _createTask(self, trace: list[TraceEvent]) -> dict[str, Any]:
|
|
102
|
+
tools = self.mcp.listTools()["tools"]
|
|
103
|
+
self._event(trace, "mcp", "toolDiscovery", "ready", "Picux MCP tools discovered", {"toolCount": len(tools)})
|
|
104
|
+
result = self.mcp.callTool(
|
|
105
|
+
"picux.createTask",
|
|
106
|
+
{
|
|
107
|
+
"userId": "sandbox",
|
|
108
|
+
"channel": "sandbox",
|
|
109
|
+
"domain": "hunt",
|
|
110
|
+
"goal": "Midnight Arbitrage: capture Resource-X discount before reset",
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
task = result["payload"]["task"]
|
|
114
|
+
self._event(trace, "hunt", "marketAnomaly", "started", "Market anomaly triggered", {"taskId": task["taskId"]})
|
|
115
|
+
return task
|
|
116
|
+
|
|
117
|
+
def _hunt(self, trace: list[TraceEvent], chaos: str) -> tuple[VendorQuote, ...]:
|
|
118
|
+
vendors = (
|
|
119
|
+
VendorQuote("vendorA", 100.0, 80.0, 0.0, 12, "SE", 42),
|
|
120
|
+
VendorQuote("vendorB", 100.0, 75.0, 30.0, 30, "SE", 51),
|
|
121
|
+
VendorQuote("vendorC", 100.0, 70.0, 0.0, 0, "NO", 38),
|
|
122
|
+
)
|
|
123
|
+
if chaos == "latency":
|
|
124
|
+
vendors = tuple(VendorQuote(v.vendorId, v.listUsd, v.offerUsd, v.hiddenFeeUsd, v.stock, v.region, v.latencyMs + (250 if v.vendorId == "vendorA" else 0)) for v in vendors)
|
|
125
|
+
self._event(trace, "bridge", "latencyInject", "rerouted", "Latency injected; protocol route stayed live", {"vendorId": "vendorA", "addedMs": 250})
|
|
126
|
+
|
|
127
|
+
for vendor in vendors:
|
|
128
|
+
self._event(
|
|
129
|
+
trace,
|
|
130
|
+
"hunt",
|
|
131
|
+
"telemetryTick",
|
|
132
|
+
"observed",
|
|
133
|
+
f"{vendor.vendorId} quote observed",
|
|
134
|
+
{"vendor": vendor.toMap()},
|
|
135
|
+
)
|
|
136
|
+
self._event(trace, "hunt", "dealFound", "ready", "20% Resource-X discount candidate found", {"vendorId": "vendorA"})
|
|
137
|
+
return vendors
|
|
138
|
+
|
|
139
|
+
def _resolve(self, trace: list[TraceEvent], vendors: tuple[VendorQuote, ...], chaos: str) -> dict[str, Any]:
|
|
140
|
+
paths: list[dict[str, Any]] = []
|
|
141
|
+
selected = ""
|
|
142
|
+
for vendor in vendors:
|
|
143
|
+
reasons: list[str] = []
|
|
144
|
+
if vendor.discountPct < 20:
|
|
145
|
+
reasons.append("discountBelowThreshold")
|
|
146
|
+
if vendor.hiddenFeeUsd > 0:
|
|
147
|
+
reasons.append("hiddenFee")
|
|
148
|
+
if vendor.stock <= 0:
|
|
149
|
+
reasons.append("stockLimit")
|
|
150
|
+
if vendor.region != "SE":
|
|
151
|
+
reasons.append("regionMismatch")
|
|
152
|
+
status = "pruned" if reasons else "selected"
|
|
153
|
+
if status == "selected":
|
|
154
|
+
selected = vendor.vendorId
|
|
155
|
+
paths.append({"vendorId": vendor.vendorId, "status": status, "reasons": reasons, "netUsd": vendor.netUsd})
|
|
156
|
+
self._event(
|
|
157
|
+
trace,
|
|
158
|
+
"resolve",
|
|
159
|
+
"pathPrune" if reasons else "pathSelect",
|
|
160
|
+
status,
|
|
161
|
+
f"{vendor.vendorId} {status}",
|
|
162
|
+
{"vendorId": vendor.vendorId, "reasons": reasons},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if chaos == "modelError":
|
|
166
|
+
paths.append({"vendorId": "vendorZ", "status": "pruned", "reasons": ["modelHallucination"], "netUsd": 0.0})
|
|
167
|
+
self._event(trace, "resolve", "modelGuard", "failedClosed", "Hallucinated vendor pruned", {"vendorId": "vendorZ"})
|
|
168
|
+
selected = ""
|
|
169
|
+
|
|
170
|
+
tree = {"root": "marketAnomaly", "selected": selected, "paths": paths}
|
|
171
|
+
self._event(trace, "resolve", "decisionTree", "ready", "Decision tree ready", tree)
|
|
172
|
+
return tree
|
|
173
|
+
|
|
174
|
+
def _result(
|
|
175
|
+
self,
|
|
176
|
+
ok: bool,
|
|
177
|
+
status: str,
|
|
178
|
+
chaos: str,
|
|
179
|
+
task: dict[str, Any],
|
|
180
|
+
trace: list[TraceEvent],
|
|
181
|
+
vendors: tuple[VendorQuote, ...],
|
|
182
|
+
decisionTree: dict[str, Any],
|
|
183
|
+
mandate: dict[str, Any],
|
|
184
|
+
*,
|
|
185
|
+
receipt: dict[str, Any] | None = None,
|
|
186
|
+
freezeReceipt: dict[str, Any] | None = None,
|
|
187
|
+
) -> dict[str, Any]:
|
|
188
|
+
return SandboxResult(
|
|
189
|
+
ok=ok,
|
|
190
|
+
scenario="midnightArbitrage",
|
|
191
|
+
status=status,
|
|
192
|
+
chaos=chaos,
|
|
193
|
+
task=task,
|
|
194
|
+
trace=tuple(trace),
|
|
195
|
+
vendors=vendors,
|
|
196
|
+
decisionTree=decisionTree,
|
|
197
|
+
mandate=mandate,
|
|
198
|
+
receipt=receipt,
|
|
199
|
+
freezeReceipt=freezeReceipt,
|
|
200
|
+
).toMap()
|
|
201
|
+
|
|
202
|
+
def _event(
|
|
203
|
+
self,
|
|
204
|
+
trace: list[TraceEvent],
|
|
205
|
+
phase: str,
|
|
206
|
+
name: str,
|
|
207
|
+
status: str,
|
|
208
|
+
msg: str,
|
|
209
|
+
data: dict[str, Any] | None = None,
|
|
210
|
+
) -> None:
|
|
211
|
+
trace.append(TraceEvent(len(trace) + 1, phase, name, status, msg, len(trace) * 37, data or {}))
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def _verifyMandate(payload: dict[str, Any], signature: str, publicKey: str) -> bool:
|
|
215
|
+
return payload.get("mandateId") == "mandate_midnight_arbitrage" and signature == "sandboxSig" and publicKey == "sandboxPub"
|
picux/sandbox/models.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class VendorQuote:
|
|
9
|
+
vendorId: str
|
|
10
|
+
listUsd: float
|
|
11
|
+
offerUsd: float
|
|
12
|
+
hiddenFeeUsd: float
|
|
13
|
+
stock: int
|
|
14
|
+
region: str
|
|
15
|
+
latencyMs: int = 0
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def netUsd(self) -> float:
|
|
19
|
+
return round(max(0.0, float(self.offerUsd) + float(self.hiddenFeeUsd)), 2)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def discountPct(self) -> float:
|
|
23
|
+
if self.listUsd <= 0:
|
|
24
|
+
return 0.0
|
|
25
|
+
return round(((self.listUsd - self.offerUsd) / self.listUsd) * 100, 2)
|
|
26
|
+
|
|
27
|
+
def toMap(self) -> dict[str, Any]:
|
|
28
|
+
return {
|
|
29
|
+
"vendorId": self.vendorId,
|
|
30
|
+
"listUsd": round(self.listUsd, 2),
|
|
31
|
+
"offerUsd": round(self.offerUsd, 2),
|
|
32
|
+
"hiddenFeeUsd": round(self.hiddenFeeUsd, 2),
|
|
33
|
+
"netUsd": self.netUsd,
|
|
34
|
+
"discountPct": self.discountPct,
|
|
35
|
+
"stock": self.stock,
|
|
36
|
+
"region": self.region,
|
|
37
|
+
"latencyMs": self.latencyMs,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class TraceEvent:
|
|
43
|
+
step: int
|
|
44
|
+
phase: str
|
|
45
|
+
name: str
|
|
46
|
+
status: str
|
|
47
|
+
msg: str
|
|
48
|
+
atMs: int
|
|
49
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
50
|
+
|
|
51
|
+
def toMap(self) -> dict[str, Any]:
|
|
52
|
+
return {
|
|
53
|
+
"step": self.step,
|
|
54
|
+
"phase": self.phase,
|
|
55
|
+
"name": self.name,
|
|
56
|
+
"status": self.status,
|
|
57
|
+
"msg": self.msg,
|
|
58
|
+
"atMs": self.atMs,
|
|
59
|
+
"data": self.data,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class SandboxResult:
|
|
65
|
+
ok: bool
|
|
66
|
+
scenario: str
|
|
67
|
+
status: str
|
|
68
|
+
chaos: str
|
|
69
|
+
task: dict[str, Any]
|
|
70
|
+
trace: tuple[TraceEvent, ...]
|
|
71
|
+
vendors: tuple[VendorQuote, ...]
|
|
72
|
+
decisionTree: dict[str, Any]
|
|
73
|
+
mandate: dict[str, Any]
|
|
74
|
+
receipt: dict[str, Any] | None = None
|
|
75
|
+
freezeReceipt: dict[str, Any] | None = None
|
|
76
|
+
|
|
77
|
+
def toMap(self) -> dict[str, Any]:
|
|
78
|
+
return {
|
|
79
|
+
"ok": self.ok,
|
|
80
|
+
"scenario": self.scenario,
|
|
81
|
+
"status": self.status,
|
|
82
|
+
"chaos": self.chaos,
|
|
83
|
+
"task": self.task,
|
|
84
|
+
"trace": [event.toMap() for event in self.trace],
|
|
85
|
+
"vendors": [vendor.toMap() for vendor in self.vendors],
|
|
86
|
+
"decisionTree": self.decisionTree,
|
|
87
|
+
"mandate": self.mandate,
|
|
88
|
+
"receipt": self.receipt,
|
|
89
|
+
"freezeReceipt": self.freezeReceipt,
|
|
90
|
+
}
|
picux/sdk/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Picux SDK."""
|
|
2
|
+
|
|
3
|
+
from .client import HttpTransport, InProcessTransport, PicuxApiError, PicuxClient
|
|
4
|
+
from .external import ExternalAppClient, ExternalPricing
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ExternalAppClient",
|
|
8
|
+
"ExternalPricing",
|
|
9
|
+
"HttpTransport",
|
|
10
|
+
"InProcessTransport",
|
|
11
|
+
"PicuxApiError",
|
|
12
|
+
"PicuxClient",
|
|
13
|
+
]
|