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.
Files changed (116) hide show
  1. apps/__init__.py +2 -0
  2. apps/api/__init__.py +2 -0
  3. apps/api/main.py +652 -0
  4. apps/benchmarks/__init__.py +1 -0
  5. apps/benchmarks/main.py +20 -0
  6. apps/sandbox/__init__.py +1 -0
  7. apps/sandbox/main.py +20 -0
  8. apps/worker/__init__.py +2 -0
  9. apps/worker/main.py +15 -0
  10. apps/worker/verify.py +14 -0
  11. patchr/__init__.py +12 -0
  12. patchr/sdk/__init__.py +20 -0
  13. patchr/sdk/client.py +12 -0
  14. patchr-0.1.0.dist-info/METADATA +137 -0
  15. patchr-0.1.0.dist-info/RECORD +116 -0
  16. patchr-0.1.0.dist-info/WHEEL +5 -0
  17. patchr-0.1.0.dist-info/entry_points.txt +5 -0
  18. patchr-0.1.0.dist-info/licenses/LICENSE +17 -0
  19. patchr-0.1.0.dist-info/top_level.txt +3 -0
  20. picux/__init__.py +6 -0
  21. picux/agents/__init__.py +5 -0
  22. picux/agents/registry.py +204 -0
  23. picux/api/__init__.py +5 -0
  24. picux/api/service.py +5075 -0
  25. picux/audit/__init__.py +31 -0
  26. picux/audit/activity.py +97 -0
  27. picux/audit/observability.py +55 -0
  28. picux/audit/verification/__init__.py +21 -0
  29. picux/audit/verification/ledger.py +633 -0
  30. picux/benchmarks/__init__.py +5 -0
  31. picux/benchmarks/local.py +286 -0
  32. picux/config.py +140 -0
  33. picux/contracts/__init__.py +22 -0
  34. picux/contracts/handshake.py +122 -0
  35. picux/contracts/integration.py +385 -0
  36. picux/contracts/openapi.py +187 -0
  37. picux/contracts/protocol_map.py +152 -0
  38. picux/contracts/routes.py +980 -0
  39. picux/contracts/schema_catalog.py +125 -0
  40. picux/core/__init__.py +17 -0
  41. picux/core/models.py +148 -0
  42. picux/core/router.py +131 -0
  43. picux/core/runtime.py +42 -0
  44. picux/core/state_machine.py +38 -0
  45. picux/domains/__init__.py +2 -0
  46. picux/domains/bridge/HostRun.py +1104 -0
  47. picux/domains/bridge/__init__.py +6 -0
  48. picux/domains/bridge/engine.py +345 -0
  49. picux/domains/hunt/__init__.py +6 -0
  50. picux/domains/hunt/engine.py +307 -0
  51. picux/domains/hunt/models.py +88 -0
  52. picux/domains/pay/__init__.py +16 -0
  53. picux/domains/pay/adapters.py +607 -0
  54. picux/domains/pay/engine.py +950 -0
  55. picux/domains/pay/models.py +95 -0
  56. picux/domains/proxy/__init__.py +5 -0
  57. picux/domains/proxy/engine.py +466 -0
  58. picux/domains/resolve/__init__.py +5 -0
  59. picux/domains/resolve/engine.py +546 -0
  60. picux/orchestrator/__init__.py +3 -0
  61. picux/orchestrator/engine.py +2840 -0
  62. picux/portals/__init__.py +17 -0
  63. picux/portals/templates.py +272 -0
  64. picux/protocols/__init__.py +1 -0
  65. picux/protocols/a2a/__init__.py +6 -0
  66. picux/protocols/a2a/client.py +51 -0
  67. picux/protocols/a2a/envelope.py +132 -0
  68. picux/protocols/mcp/__init__.py +7 -0
  69. picux/protocols/mcp/client.py +69 -0
  70. picux/protocols/mcp/contract.py +67 -0
  71. picux/protocols/mcp/server.py +76 -0
  72. picux/sandbox/__init__.py +6 -0
  73. picux/sandbox/midnight_arbitrage.py +215 -0
  74. picux/sandbox/models.py +90 -0
  75. picux/sdk/__init__.py +13 -0
  76. picux/sdk/client.py +768 -0
  77. picux/sdk/external.py +245 -0
  78. picux/security/__init__.py +18 -0
  79. picux/security/auth.py +86 -0
  80. picux/security/config_validator.py +58 -0
  81. picux/security/policy.py +158 -0
  82. picux/security/secrets.py +144 -0
  83. picux/signals/__init__.py +1 -0
  84. picux/signals/community/__init__.py +24 -0
  85. picux/signals/community/adapters/__init__.py +7 -0
  86. picux/signals/community/adapters/reddit.py +37 -0
  87. picux/signals/community/adapters/shopify.py +23 -0
  88. picux/signals/community/adapters/web.py +23 -0
  89. picux/signals/community/disambiguation.py +51 -0
  90. picux/signals/community/intake.py +227 -0
  91. picux/signals/community/models.py +102 -0
  92. picux/signals/community/rules.py +91 -0
  93. picux/signals/community/scoring.py +64 -0
  94. picux/storage/__init__.py +41 -0
  95. picux/storage/agents.py +50 -0
  96. picux/storage/cases.py +440 -0
  97. picux/storage/channels.py +476 -0
  98. picux/storage/connectors.py +411 -0
  99. picux/storage/envelopes.py +137 -0
  100. picux/storage/escrows.py +168 -0
  101. picux/storage/events.py +989 -0
  102. picux/storage/keyspace.py +60 -0
  103. picux/storage/mandates.py +107 -0
  104. picux/storage/portals.py +222 -0
  105. picux/storage/postgres.py +2049 -0
  106. picux/storage/providers.py +148 -0
  107. picux/storage/proxy.py +231 -0
  108. picux/storage/receipts.py +131 -0
  109. picux/storage/signals.py +147 -0
  110. picux/storage/tasks.py +179 -0
  111. picux/tools/__init__.py +11 -0
  112. picux/tools/shared.py +2048 -0
  113. picux/verification/__init__.py +5 -0
  114. picux/verification/rollout.py +183 -0
  115. picux/workflows/__init__.py +5 -0
  116. picux/workflows/templates.py +74 -0
@@ -0,0 +1,307 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from typing import Any
6
+
7
+ from .models import HuntCriteria, HuntOffer
8
+
9
+
10
+ class HuntDomain:
11
+ """Deterministic discovery and ranking for HUNT telemetry."""
12
+
13
+ def discover(self, payload: dict[str, Any]) -> dict[str, Any]:
14
+ criteria = HuntCriteria.fromObj(payload.get("criteria") if isinstance(payload.get("criteria"), dict) else payload)
15
+ offers = [
16
+ HuntOffer.fromObj(item)
17
+ for item in payload.get("offers", payload.get("telemetry", []))
18
+ if isinstance(item, dict)
19
+ ]
20
+ candidates = [self.scoreOffer(offer, criteria) for offer in offers]
21
+ candidates.sort(key=lambda item: (item["eligible"], item["score"], -item["netUsd"]), reverse=True)
22
+ selected = next((item for item in candidates if item["eligible"]), None)
23
+ sourceResponse = self.sourceResponse(payload, offers, candidates, selected)
24
+ handoff = self.handoff(payload)
25
+ aggregate = self.sourceAggregate(payload, offers, candidates, selected, sourceResponse, handoff)
26
+ stream = self.streamEvents(aggregate)
27
+ return {
28
+ "ok": bool(selected),
29
+ "criteria": criteria.toMap(),
30
+ "selected": selected or {},
31
+ "candidates": candidates,
32
+ "sourceResponse": sourceResponse,
33
+ "sourceAggregate": aggregate,
34
+ "aggregate": aggregate,
35
+ "stream": stream,
36
+ "handoff": handoff,
37
+ "response": {
38
+ "status": "selected" if selected else sourceResponse["status"],
39
+ "message": self.responseMessage(sourceResponse, selected),
40
+ "nextAction": "returnSelectedOption" if selected else "requestBetterSourceTelemetry",
41
+ "delivery": aggregate["delivery"],
42
+ },
43
+ "trace": self.trace(candidates),
44
+ }
45
+
46
+ def scoreOffer(self, offer: HuntOffer | dict[str, Any], criteria: HuntCriteria | dict[str, Any]) -> dict[str, Any]:
47
+ item = offer if isinstance(offer, HuntOffer) else HuntOffer.fromObj(offer)
48
+ crit = criteria if isinstance(criteria, HuntCriteria) else HuntCriteria.fromObj(criteria)
49
+ reasons = self.reasons(item, crit)
50
+ latencyPenalty = min(20.0, item.latencyMs / 50.0) if item.latencyMs > 0 else 0.0
51
+ feePenalty = min(30.0, item.feesUsd)
52
+ stockBoost = min(10.0, max(0, item.stock) / 10.0)
53
+ score = round(max(0.0, item.discountPct + stockBoost - latencyPenalty - feePenalty), 3)
54
+ return {
55
+ **item.toMap(),
56
+ "eligible": not reasons,
57
+ "reasons": reasons,
58
+ "score": score,
59
+ "povRules": self.povRules(item, crit, reasons),
60
+ }
61
+
62
+ def reasons(self, offer: HuntOffer, criteria: HuntCriteria) -> list[str]:
63
+ reasons: list[str] = []
64
+ if criteria.itemId and offer.itemId != criteria.itemId:
65
+ reasons.append("itemMismatch")
66
+ if offer.discountPct < criteria.minDiscountPct:
67
+ reasons.append("discountBelowThreshold")
68
+ if criteria.maxNetUsd > 0 and offer.netUsd > criteria.maxNetUsd:
69
+ reasons.append("overBudget")
70
+ if offer.stock < criteria.minStock:
71
+ reasons.append("stockLimit")
72
+ if criteria.region and offer.region != criteria.region:
73
+ reasons.append("regionMismatch")
74
+ if criteria.maxLatencyMs > 0 and offer.latencyMs > criteria.maxLatencyMs:
75
+ reasons.append("latencyExceeded")
76
+ if offer.feesUsd > 0:
77
+ reasons.append("feeLeak")
78
+ return reasons
79
+
80
+ @staticmethod
81
+ def povRules(offer: HuntOffer, criteria: HuntCriteria, reasons: list[str]) -> list[str]:
82
+ rules = []
83
+ if offer.stock >= criteria.minStock:
84
+ rules.append("inventoryVerified")
85
+ if criteria.minDiscountPct > 0 and offer.discountPct >= criteria.minDiscountPct:
86
+ rules.append(f"priceArbitrage{int(criteria.minDiscountPct)}")
87
+ if criteria.region and offer.region == criteria.region:
88
+ rules.append("regionMatched")
89
+ if offer.feesUsd <= 0:
90
+ rules.append("hiddenFeeClear")
91
+ if not reasons:
92
+ rules.append("huntEligible")
93
+ return rules
94
+
95
+ @staticmethod
96
+ def trace(candidates: list[dict[str, Any]]) -> list[dict[str, Any]]:
97
+ events = []
98
+ for idx, candidate in enumerate(candidates, start=1):
99
+ events.append(
100
+ {
101
+ "step": idx,
102
+ "phase": "hunt",
103
+ "name": "candidateRanked" if candidate["eligible"] else "candidatePruned",
104
+ "status": "selected" if idx == 1 and candidate["eligible"] else "observed",
105
+ "sourceId": candidate["sourceId"],
106
+ "score": candidate["score"],
107
+ "reasons": candidate["reasons"],
108
+ }
109
+ )
110
+ return events
111
+
112
+ @staticmethod
113
+ def sourceResponse(payload: dict[str, Any], offers: list[HuntOffer], candidates: list[dict[str, Any]], selected: dict[str, Any] | None) -> dict[str, Any]:
114
+ sourceTelemetry = payload.get("sourceTelemetry", {}) if isinstance(payload.get("sourceTelemetry"), dict) else {}
115
+ attempts = sourceTelemetry.get("attempts", []) if isinstance(sourceTelemetry.get("attempts"), list) else []
116
+ targetSources = [str(item) for item in sourceTelemetry.get("targetSources", []) if str(item)]
117
+ attemptedSources = [str(item.get("source", "")) for item in attempts if isinstance(item, dict) and item.get("source")]
118
+ failedSources = [str(item.get("source", "")) for item in attempts if isinstance(item, dict) and item.get("source") and not item.get("ok")]
119
+ successfulSources = [str(item.get("source", "")) for item in attempts if isinstance(item, dict) and item.get("source") and item.get("ok")]
120
+ listingSources = [str(item.get("source", "")) for item in attempts if isinstance(item, dict) and item.get("source") and int(item.get("listingCount", 0) or 0) > 0]
121
+ resultSources = [str(offer.meta.get("source", offer.sourceId) or offer.sourceId) for offer in offers]
122
+ listingCount = sum(int(item.get("listingCount", 0) or 0) for item in attempts if isinstance(item, dict))
123
+ filteredListingCount = sum(int(item.get("filteredListingCount", 0) or 0) for item in attempts if isinstance(item, dict))
124
+ marketplaceSet = sourceTelemetry.get("marketplaceSet", {}) if isinstance(sourceTelemetry.get("marketplaceSet"), dict) else {}
125
+ budget = sourceTelemetry.get("budget", {}) if isinstance(sourceTelemetry.get("budget"), dict) else {}
126
+ fx = sourceTelemetry.get("fx", {}) if isinstance(sourceTelemetry.get("fx"), dict) else {}
127
+ if selected:
128
+ status = "selected"
129
+ elif offers or candidates:
130
+ status = "noEligibleCandidate"
131
+ elif sourceTelemetry.get("networkAttempted"):
132
+ status = "searchedNoSourceBoundOffer"
133
+ else:
134
+ status = "needsSourceTelemetry"
135
+ return {
136
+ "status": status,
137
+ "adapter": sourceTelemetry.get("adapter", ""),
138
+ "rendered": sourceTelemetry.get("rendered", {}),
139
+ "marketplaceSet": marketplaceSet,
140
+ "budget": budget,
141
+ "fx": fx,
142
+ "targetSources": targetSources,
143
+ "attemptedSources": list(dict.fromkeys(attemptedSources)),
144
+ "successfulSources": list(dict.fromkeys(successfulSources)),
145
+ "failedSources": list(dict.fromkeys(failedSources)),
146
+ "listingSources": list(dict.fromkeys(listingSources)),
147
+ "resultSources": list(dict.fromkeys(resultSources)),
148
+ "networkAttempted": bool(sourceTelemetry.get("networkAttempted")),
149
+ "offerCount": int(sourceTelemetry.get("offerCount", len(offers)) or 0),
150
+ "listingCount": listingCount,
151
+ "filteredListingCount": filteredListingCount,
152
+ "candidateCount": len(candidates),
153
+ "selectedSourceId": str(selected.get("sourceId", "") if selected else ""),
154
+ }
155
+
156
+ @staticmethod
157
+ def responseMessage(sourceResponse: dict[str, Any], selected: dict[str, Any] | None) -> str:
158
+ if selected:
159
+ return f"HUNT selected {selected.get('sourceId', '')} from source-bound telemetry."
160
+ targets = ", ".join(sourceResponse.get("targetSources", [])[:6])
161
+ if sourceResponse.get("networkAttempted"):
162
+ return f"HUNT searched {targets} and returned no eligible source-bound offer."
163
+ return f"HUNT prepared source targets {targets}; live source telemetry is required before ranking."
164
+
165
+ @staticmethod
166
+ def handoff(payload: dict[str, Any]) -> dict[str, str]:
167
+ raw = payload.get("handoff", payload.get("handsoff", {}))
168
+ raw = raw if isinstance(raw, dict) else {}
169
+ explicitSource = raw.get("source") or payload.get("handoffSource") or payload.get("handsoffSource")
170
+ source = str(explicitSource or payload.get("clientId", "orchestrator") or "orchestrator")
171
+ explicitTarget = raw.get("target") or payload.get("handoffTarget") or payload.get("handsoffTarget")
172
+ target = str(explicitTarget or (source if explicitSource else "orchestrator") or "orchestrator")
173
+ if target in {"source", "handoffSource", "handsoffSource"}:
174
+ target = source
175
+ return {
176
+ "source": source,
177
+ "target": target,
178
+ "channel": str(raw.get("channel", payload.get("handoffChannel", payload.get("channel", "api"))) or "api"),
179
+ "mode": str(raw.get("mode", payload.get("handoffMode", "return")) or "return"),
180
+ }
181
+
182
+ @staticmethod
183
+ def sourceAggregate(
184
+ payload: dict[str, Any],
185
+ offers: list[HuntOffer],
186
+ candidates: list[dict[str, Any]],
187
+ selected: dict[str, Any] | None,
188
+ sourceResponse: dict[str, Any],
189
+ handoff: dict[str, str],
190
+ ) -> dict[str, Any]:
191
+ sourceTelemetry = payload.get("sourceTelemetry", {}) if isinstance(payload.get("sourceTelemetry"), dict) else {}
192
+ attempts = sourceTelemetry.get("attempts", []) if isinstance(sourceTelemetry.get("attempts"), list) else []
193
+ observations = sourceTelemetry.get("observations", []) if isinstance(sourceTelemetry.get("observations"), list) else []
194
+ sources = HuntDomain.aggregateSources(sourceResponse, attempts, offers)
195
+ deliveryMode = handoff.get("mode", "return")
196
+ aggregate = {
197
+ "kind": "huntSourceAggregate",
198
+ "aggregateId": "",
199
+ "status": sourceResponse.get("status", ""),
200
+ "handoff": handoff,
201
+ "delivery": {
202
+ "mode": deliveryMode,
203
+ "target": handoff.get("target", "orchestrator"),
204
+ "channel": handoff.get("channel", "api"),
205
+ "status": "streamReady" if deliveryMode == "stream" else "returned",
206
+ },
207
+ "marketplaceSet": sourceResponse.get("marketplaceSet", {}),
208
+ "budget": sourceResponse.get("budget", {}),
209
+ "fx": sourceResponse.get("fx", {}),
210
+ "adapter": sourceResponse.get("adapter", ""),
211
+ "rendered": sourceResponse.get("rendered", {}),
212
+ "sources": sources,
213
+ "observations": observations[:24],
214
+ "offers": [offer.toMap() for offer in offers[:24]],
215
+ "candidates": candidates[:24],
216
+ "result": {
217
+ "selected": selected or {},
218
+ "selectedSourceId": sourceResponse.get("selectedSourceId", ""),
219
+ "candidateCount": len(candidates),
220
+ "offerCount": sourceResponse.get("offerCount", 0),
221
+ "nextAction": "returnSelectedOption" if selected else "requestBetterSourceTelemetry",
222
+ },
223
+ }
224
+ aggregate["aggregateId"] = "huntAgg_" + hashlib.sha256(json.dumps(aggregate, sort_keys=True, default=str).encode("utf-8")).hexdigest()[:24]
225
+ return aggregate
226
+
227
+ @staticmethod
228
+ def aggregateSources(sourceResponse: dict[str, Any], attempts: list[Any], offers: list[HuntOffer]) -> list[dict[str, Any]]:
229
+ records: dict[str, dict[str, Any]] = {}
230
+ for source in sourceResponse.get("targetSources", []):
231
+ sourceId = str(source)
232
+ if sourceId:
233
+ records[sourceId] = {"source": sourceId, "status": "targeted", "ok": False, "attempted": False}
234
+ for attempt in attempts:
235
+ if not isinstance(attempt, dict):
236
+ continue
237
+ sourceId = str(attempt.get("source", "") or "")
238
+ if not sourceId:
239
+ continue
240
+ records[sourceId] = {
241
+ "source": sourceId,
242
+ "status": "read" if attempt.get("ok") else "failed",
243
+ "ok": bool(attempt.get("ok")),
244
+ "attempted": True,
245
+ "url": attempt.get("url", ""),
246
+ "statusCode": attempt.get("statusCode", 0),
247
+ "title": attempt.get("title", ""),
248
+ "matched": bool(attempt.get("matched")),
249
+ "listingCount": attempt.get("listingCount", 0),
250
+ "search": attempt.get("search", {}) if isinstance(attempt.get("search"), dict) else {},
251
+ "error": attempt.get("error", ""),
252
+ }
253
+ for offer in offers:
254
+ sourceId = str(offer.meta.get("source", offer.sourceId) or offer.sourceId)
255
+ if sourceId and sourceId not in records:
256
+ records[sourceId] = {"source": sourceId, "status": "candidateOnly", "ok": True, "attempted": False}
257
+ if sourceId:
258
+ records[sourceId]["offerCount"] = int(records[sourceId].get("offerCount", 0) or 0) + 1
259
+ records[sourceId]["ok"] = True
260
+ return list(records.values())
261
+
262
+ @staticmethod
263
+ def streamEvents(aggregate: dict[str, Any]) -> list[dict[str, Any]]:
264
+ events = [
265
+ {
266
+ "seq": 1,
267
+ "phase": "hunt",
268
+ "name": "sourceAggregate.started",
269
+ "status": "started",
270
+ "target": aggregate["delivery"]["target"],
271
+ "data": {"aggregateId": aggregate["aggregateId"], "sourceCount": len(aggregate.get("sources", []))},
272
+ }
273
+ ]
274
+ for source in aggregate.get("sources", []):
275
+ name = "source.read" if source.get("ok") else ("source.failed" if source.get("attempted") else "source.targeted")
276
+ events.append(
277
+ {
278
+ "seq": len(events) + 1,
279
+ "phase": "hunt",
280
+ "name": name,
281
+ "status": source.get("status", ""),
282
+ "target": aggregate["delivery"]["target"],
283
+ "data": source,
284
+ }
285
+ )
286
+ for candidate in aggregate.get("candidates", []):
287
+ events.append(
288
+ {
289
+ "seq": len(events) + 1,
290
+ "phase": "hunt",
291
+ "name": "candidate.selected" if candidate.get("eligible") else "candidate.pruned",
292
+ "status": "selected" if candidate.get("eligible") and candidate.get("sourceId") == aggregate["result"].get("selectedSourceId") else "observed",
293
+ "target": aggregate["delivery"]["target"],
294
+ "data": {"sourceId": candidate.get("sourceId", ""), "score": candidate.get("score", 0), "reasons": candidate.get("reasons", [])},
295
+ }
296
+ )
297
+ events.append(
298
+ {
299
+ "seq": len(events) + 1,
300
+ "phase": "hunt",
301
+ "name": "sourceAggregate.delivered",
302
+ "status": aggregate["delivery"]["status"],
303
+ "target": aggregate["delivery"]["target"],
304
+ "data": {"aggregateId": aggregate["aggregateId"], "result": aggregate["result"], "delivery": aggregate["delivery"]},
305
+ }
306
+ )
307
+ return events
@@ -0,0 +1,88 @@
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 HuntOffer:
9
+ sourceId: str
10
+ itemId: str
11
+ listUsd: float
12
+ offerUsd: float
13
+ feesUsd: float = 0.0
14
+ stock: int = 0
15
+ region: str = ""
16
+ latencyMs: int = 0
17
+ meta: dict[str, Any] = field(default_factory=dict)
18
+
19
+ @classmethod
20
+ def fromObj(cls, value: dict[str, Any]) -> "HuntOffer":
21
+ return cls(
22
+ sourceId=str(value.get("sourceId", value.get("vendorId", "")) or ""),
23
+ itemId=str(value.get("itemId", value.get("sku", "")) or ""),
24
+ listUsd=float(value.get("listUsd", value.get("listPriceUsd", 0.0)) or 0.0),
25
+ offerUsd=float(value.get("offerUsd", value.get("priceUsd", 0.0)) or 0.0),
26
+ feesUsd=float(value.get("feesUsd", value.get("hiddenFeeUsd", 0.0)) or 0.0),
27
+ stock=int(value.get("stock", value.get("qty", 0)) or 0),
28
+ region=str(value.get("region", "") or ""),
29
+ latencyMs=int(value.get("latencyMs", 0) or 0),
30
+ meta=value.get("meta", {}) if isinstance(value.get("meta", {}), dict) else {},
31
+ )
32
+
33
+ @property
34
+ def netUsd(self) -> float:
35
+ return round(max(0.0, self.offerUsd + self.feesUsd), 2)
36
+
37
+ @property
38
+ def discountPct(self) -> float:
39
+ if self.listUsd <= 0:
40
+ return 0.0
41
+ return round(((self.listUsd - self.offerUsd) / self.listUsd) * 100, 2)
42
+
43
+ def toMap(self) -> dict[str, Any]:
44
+ return {
45
+ "sourceId": self.sourceId,
46
+ "itemId": self.itemId,
47
+ "listUsd": round(self.listUsd, 2),
48
+ "offerUsd": round(self.offerUsd, 2),
49
+ "feesUsd": round(self.feesUsd, 2),
50
+ "netUsd": self.netUsd,
51
+ "discountPct": self.discountPct,
52
+ "stock": self.stock,
53
+ "region": self.region,
54
+ "latencyMs": self.latencyMs,
55
+ "meta": self.meta,
56
+ }
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class HuntCriteria:
61
+ itemId: str = ""
62
+ minDiscountPct: float = 0.0
63
+ maxNetUsd: float = 0.0
64
+ minStock: int = 1
65
+ region: str = ""
66
+ maxLatencyMs: int = 0
67
+
68
+ @classmethod
69
+ def fromObj(cls, value: dict[str, Any] | None) -> "HuntCriteria":
70
+ value = value or {}
71
+ return cls(
72
+ itemId=str(value.get("itemId", value.get("sku", "")) or ""),
73
+ minDiscountPct=float(value.get("minDiscountPct", 0.0) or 0.0),
74
+ maxNetUsd=float(value.get("maxNetUsd", 0.0) or 0.0),
75
+ minStock=int(value.get("minStock", 1) or 1),
76
+ region=str(value.get("region", "") or ""),
77
+ maxLatencyMs=int(value.get("maxLatencyMs", 0) or 0),
78
+ )
79
+
80
+ def toMap(self) -> dict[str, Any]:
81
+ return {
82
+ "itemId": self.itemId,
83
+ "minDiscountPct": round(self.minDiscountPct, 2),
84
+ "maxNetUsd": round(self.maxNetUsd, 2),
85
+ "minStock": self.minStock,
86
+ "region": self.region,
87
+ "maxLatencyMs": self.maxLatencyMs,
88
+ }
@@ -0,0 +1,16 @@
1
+ """PAY domain primitives."""
2
+
3
+ from .adapters import PayRailAdapter, PayRailAdapterContext
4
+ from .engine import PayDomain
5
+ from .models import Money, ProofOfValue, SettlementAdapter, SettlementAdapterSpec, SettlementRequest
6
+
7
+ __all__ = [
8
+ "Money",
9
+ "PayDomain",
10
+ "PayRailAdapter",
11
+ "PayRailAdapterContext",
12
+ "ProofOfValue",
13
+ "SettlementAdapter",
14
+ "SettlementAdapterSpec",
15
+ "SettlementRequest",
16
+ ]