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,148 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import hashlib
5
+ import json
6
+ import re
7
+ import time
8
+ from typing import Any
9
+
10
+
11
+ class LocalProviderBook:
12
+ """Private local-service contact cache for HUNT -> PROXY workflows."""
13
+
14
+ def __init__(self, *, backing: Any | None = None) -> None:
15
+ self.backing = backing
16
+ self._records: dict[str, dict[str, Any]] = {}
17
+
18
+ def saveLead(self, payload: dict[str, Any]) -> dict[str, Any]:
19
+ return self._save(payload, status=str(payload.get("status", "lead") or "lead"))
20
+
21
+ def saveVerified(self, payload: dict[str, Any]) -> dict[str, Any]:
22
+ return self._save(payload, status="verified")
23
+
24
+ def getProvider(self, providerId: str) -> dict[str, Any]:
25
+ providerId = str(providerId or "").strip()
26
+ record = self._records.get(providerId)
27
+ if record is None and self._backingEnabled() and hasattr(self.backing, "fetchLocalProvider"):
28
+ record = self.backing.fetchLocalProvider(providerId)
29
+ if record:
30
+ self._records[providerId] = copy.deepcopy(record)
31
+ if not record:
32
+ return {"ok": False, "error": "providerNotFound", "providerId": providerId}
33
+ return {"ok": True, "provider": copy.deepcopy(record)}
34
+
35
+ def find(self, *, service: str = "", location: str = "", verifiedOnly: bool = True, limit: int = 10) -> dict[str, Any]:
36
+ self._loadBacking()
37
+ serviceKey = _norm(service)
38
+ locationKey = _norm(location)
39
+ records = list(self._records.values())
40
+ if verifiedOnly:
41
+ records = [item for item in records if item.get("status") == "verified"]
42
+ if serviceKey:
43
+ records = [item for item in records if serviceKey in _norm(item.get("service", "")) or serviceKey in _norm(item.get("entity", ""))]
44
+ if locationKey:
45
+ records = [item for item in records if _locationMatch(locationKey, _norm(" ".join(str(item.get(key, "")) for key in ("location", "address", "area"))))]
46
+ records.sort(key=lambda item: (item.get("status") == "verified", float(item.get("rating", 0.0) or 0.0), int(item.get("updatedAt", 0) or 0)), reverse=True)
47
+ limited = records[: max(1, min(int(limit or 10), 50))]
48
+ return {"ok": True, "providers": [copy.deepcopy(record) for record in limited], "count": len(limited)}
49
+
50
+ def toPlaces(self, *, service: str = "", location: str = "", limit: int = 10) -> list[dict[str, Any]]:
51
+ found = self.find(service=service, location=location, verifiedOnly=True, limit=limit)
52
+ places = []
53
+ for record in found.get("providers", []):
54
+ places.append(
55
+ {
56
+ "placeId": str(record.get("providerId", "") or record.get("contactRef", "")),
57
+ "name": str(record.get("name", "") or ""),
58
+ "category": str(record.get("category", service) or service),
59
+ "address": str(record.get("address", "") or ""),
60
+ "phone": str(record.get("phone", "") or ""),
61
+ "website": str(record.get("website", "") or ""),
62
+ "rating": record.get("rating", 0),
63
+ "lat": record.get("lat", 0),
64
+ "lng": record.get("lng", 0),
65
+ "distanceKm": record.get("distanceKm", 0),
66
+ "source": "verifiedProviders",
67
+ "sourceUrl": str(record.get("sourceUrl", "") or ""),
68
+ "sourceBound": True,
69
+ "entity": str(record.get("service", service) or service),
70
+ "contactRef": str(record.get("contactRef", record.get("providerId", "")) or ""),
71
+ "verified": True,
72
+ "availability": record.get("availability", {}) if isinstance(record.get("availability"), dict) else {},
73
+ }
74
+ )
75
+ return places
76
+
77
+ def _save(self, payload: dict[str, Any], *, status: str) -> dict[str, Any]:
78
+ if not isinstance(payload, dict):
79
+ return {"ok": False, "error": "invalid:provider"}
80
+ providerId = str(payload.get("providerId", payload.get("contactRef", "")) or "").strip()
81
+ if not providerId:
82
+ providerId = "provider_" + hashlib.sha256(json.dumps(payload, sort_keys=True, default=str).encode("utf-8")).hexdigest()[:24]
83
+ now = int(time.time())
84
+ current = self._records.get(providerId, {})
85
+ createdAt = int(current.get("createdAt", now) or now)
86
+ record = {
87
+ **copy.deepcopy(current),
88
+ "providerId": providerId,
89
+ "contactRef": str(payload.get("contactRef", current.get("contactRef", providerId)) or providerId),
90
+ "status": status,
91
+ "service": str(payload.get("service", payload.get("entity", current.get("service", ""))) or ""),
92
+ "entity": str(payload.get("entity", payload.get("service", current.get("entity", ""))) or ""),
93
+ "location": str(payload.get("location", payload.get("area", current.get("location", ""))) or ""),
94
+ "area": str(payload.get("area", payload.get("location", current.get("area", ""))) or ""),
95
+ "name": str(payload.get("name", current.get("name", "")) or ""),
96
+ "category": str(payload.get("category", current.get("category", "")) or ""),
97
+ "address": str(payload.get("address", current.get("address", "")) or ""),
98
+ "phone": str(payload.get("phone", current.get("phone", "")) or ""),
99
+ "website": str(payload.get("website", current.get("website", "")) or ""),
100
+ "rating": _float(payload.get("rating", current.get("rating", 0))),
101
+ "lat": _float(payload.get("lat", current.get("lat", 0))),
102
+ "lng": _float(payload.get("lng", current.get("lng", 0))),
103
+ "distanceKm": _float(payload.get("distanceKm", current.get("distanceKm", 0))),
104
+ "source": str(payload.get("source", current.get("source", "")) or ""),
105
+ "sourceUrl": str(payload.get("sourceUrl", current.get("sourceUrl", "")) or ""),
106
+ "availability": payload.get("availability", current.get("availability", {})) if isinstance(payload.get("availability", current.get("availability", {})), dict) else {},
107
+ "proof": payload.get("proof", current.get("proof", {})) if isinstance(payload.get("proof", current.get("proof", {})), dict) else {},
108
+ "meta": payload.get("meta", current.get("meta", {})) if isinstance(payload.get("meta", current.get("meta", {})), dict) else {},
109
+ "createdAt": createdAt,
110
+ "updatedAt": now,
111
+ }
112
+ self._records[providerId] = copy.deepcopy(record)
113
+ if self._backingEnabled() and hasattr(self.backing, "upsertLocalProvider"):
114
+ self.backing.upsertLocalProvider(record)
115
+ return {"ok": True, "provider": copy.deepcopy(record)}
116
+
117
+ def _loadBacking(self) -> None:
118
+ if not self._backingEnabled() or not hasattr(self.backing, "listLocalProviders"):
119
+ return
120
+ for record in self.backing.listLocalProviders():
121
+ if isinstance(record, dict) and str(record.get("providerId", "") or ""):
122
+ self._records[str(record["providerId"])] = copy.deepcopy(record)
123
+
124
+ def _backingEnabled(self) -> bool:
125
+ if self.backing is None:
126
+ return False
127
+ return bool(getattr(self.backing, "enabled", True))
128
+
129
+
130
+ def _norm(value: Any) -> str:
131
+ text = re.sub(r"[^a-z0-9]+", " ", str(value or "").lower())
132
+ return re.sub(r"\s+", " ", text).strip()
133
+
134
+
135
+ def _locationMatch(needle: str, haystack: str) -> bool:
136
+ if not needle:
137
+ return True
138
+ if needle in haystack:
139
+ return True
140
+ needleParts = [part for part in needle.split() if len(part) > 2]
141
+ return bool(needleParts) and all(part in haystack for part in needleParts[:3])
142
+
143
+
144
+ def _float(value: Any) -> float:
145
+ try:
146
+ return float(value or 0.0)
147
+ except Exception:
148
+ return 0.0
picux/storage/proxy.py ADDED
@@ -0,0 +1,231 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import time
5
+ from typing import Any
6
+
7
+
8
+ PROXY_STATUSES = {
9
+ "requested",
10
+ "awaitingProxy",
11
+ "notified",
12
+ "inProgress",
13
+ "submitted",
14
+ "pending",
15
+ "confirmedSuccess",
16
+ "confirmedFailure",
17
+ "canceled",
18
+ "overdue",
19
+ }
20
+ PROXY_KINDS = {"review", "logistics", "call", "message", "approval", "custom"}
21
+
22
+
23
+ class ProxyBook:
24
+ """Proxy mission storage with callback reference indexes."""
25
+
26
+ def __init__(self, *, backing: Any | None = None) -> None:
27
+ self.backing = backing
28
+ self._records: dict[str, dict[str, Any]] = {}
29
+ self._refs: dict[str, str] = {}
30
+
31
+ def saveMission(self, payload: dict[str, Any], *, status: str = "awaitingProxy") -> dict[str, Any]:
32
+ proxyId = str(payload.get("proxyId", payload.get("missionId", "")) or "")
33
+ if not proxyId:
34
+ return {"ok": False, "error": "missing:proxyId"}
35
+ errors = _missionErrors(payload)
36
+ if errors:
37
+ return {"ok": False, "error": "invalid:proxyMission", "errors": errors}
38
+ current = self.getMission(proxyId)
39
+ now = int(time.time())
40
+ createdAt = current.get("mission", {}).get("createdAt", now) if current.get("ok") else now
41
+ record = _recordFromPayload({**payload, "proxyId": proxyId}, status=status, createdAt=createdAt, updatedAt=now)
42
+ self._saveRecord(record)
43
+ return {"ok": True, "mission": copy.deepcopy(record)}
44
+
45
+ def getMission(self, proxyId: str) -> dict[str, Any]:
46
+ proxyId = str(proxyId or "")
47
+ record = self._records.get(proxyId)
48
+ if record is None and self._backingEnabled() and hasattr(self.backing, "fetchProxyMission"):
49
+ record = self.backing.fetchProxyMission(proxyId)
50
+ if record:
51
+ self._saveRecord(record, persist=False)
52
+ if not record:
53
+ return {"ok": False, "error": "proxyMissionNotFound", "proxyId": proxyId}
54
+ return {"ok": True, "mission": copy.deepcopy(record)}
55
+
56
+ def getMissionByRef(self, payload: dict[str, Any]) -> dict[str, Any]:
57
+ proxyId = self.resolveProxyId(payload)
58
+ return self.getMission(proxyId) if proxyId else {"ok": False, "error": "proxyMissionNotFound", "proxyId": ""}
59
+
60
+ def resolveProxyId(self, payload: dict[str, Any]) -> str:
61
+ for key in ("proxyId", "missionId"):
62
+ value = str(payload.get(key, "") or "")
63
+ if value:
64
+ return value
65
+ refs = payload.get("refs", {}) if isinstance(payload.get("refs"), dict) else {}
66
+ for key in ("requestId", "providerTaskId", "externalRef", "connectorRunId"):
67
+ value = str(payload.get(key, refs.get(key, "")) or "")
68
+ if not value:
69
+ continue
70
+ found = self._refs.get(f"{key}:{value}")
71
+ if found:
72
+ return found
73
+ if self._backingEnabled() and hasattr(self.backing, "fetchProxyMissionByRef"):
74
+ record = self.backing.fetchProxyMissionByRef(key, value)
75
+ if record:
76
+ self._saveRecord(record, persist=False)
77
+ proxyId = str(record.get("proxyId", "") or "")
78
+ if proxyId:
79
+ return proxyId
80
+ return ""
81
+
82
+ def listMissions(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
83
+ self._loadBacking()
84
+ filters = filters or {}
85
+ records = list(self._records.values())
86
+ for key in ("taskId", "userId", "status", "kind"):
87
+ value = str(filters.get(key, "") or "")
88
+ if value:
89
+ records = [record for record in records if str(record.get(key, "") or "") == value]
90
+ records.sort(key=lambda item: (int(item.get("createdAt", 0) or 0), str(item.get("proxyId", ""))), reverse=True)
91
+ limit = _limit(filters.get("limit", 100))
92
+ return {"ok": True, "missions": [copy.deepcopy(record) for record in records[:limit]], "count": min(len(records), limit)}
93
+
94
+ def submitOutcome(self, proxyId: str, payload: dict[str, Any], outcome: dict[str, Any]) -> dict[str, Any]:
95
+ current = self.getMission(proxyId)
96
+ if not current.get("ok"):
97
+ return current
98
+ now = int(time.time())
99
+ mission = current["mission"]
100
+ status = _status(outcome.get("status", payload.get("status", "submitted")))
101
+ merged = {
102
+ **mission,
103
+ "status": status,
104
+ "outcome": copy.deepcopy(outcome),
105
+ "resume": copy.deepcopy(outcome.get("resume", {}) if isinstance(outcome.get("resume"), dict) else {}),
106
+ "updatedAt": int(payload.get("updatedAt", now) or now),
107
+ }
108
+ refs = _refs({**mission, **payload})
109
+ if refs:
110
+ merged["refs"] = refs
111
+ self._saveRecord(merged)
112
+ return {"ok": True, "mission": copy.deepcopy(merged), "outcome": copy.deepcopy(outcome)}
113
+
114
+ def submitOutcomeByRef(self, payload: dict[str, Any], outcome: dict[str, Any]) -> dict[str, Any]:
115
+ proxyId = self.resolveProxyId(payload)
116
+ if not proxyId:
117
+ return {"ok": False, "error": "proxyMissionNotFound", "proxyId": ""}
118
+ return self.submitOutcome(proxyId, payload, outcome)
119
+
120
+ def _saveRecord(self, record: dict[str, Any], *, persist: bool = True) -> None:
121
+ proxyId = str(record.get("proxyId", "") or "")
122
+ if not proxyId:
123
+ return
124
+ self._records[proxyId] = copy.deepcopy(record)
125
+ for key, value in record.get("refs", {}).items():
126
+ clean = str(value or "")
127
+ if clean:
128
+ self._refs[f"{key}:{clean}"] = proxyId
129
+ if persist and self._backingEnabled() and hasattr(self.backing, "upsertProxyMission"):
130
+ self.backing.upsertProxyMission(record)
131
+
132
+ def _loadBacking(self) -> None:
133
+ if self.backing is None or not self._backingEnabled() or not hasattr(self.backing, "listProxyMissions"):
134
+ return
135
+ for record in self.backing.listProxyMissions():
136
+ proxyId = str(record.get("proxyId", "") or "")
137
+ if proxyId:
138
+ self._saveRecord(record, persist=False)
139
+
140
+ def _backingEnabled(self) -> bool:
141
+ if self.backing is None:
142
+ return False
143
+ return bool(getattr(self.backing, "enabled", True))
144
+
145
+
146
+ def _recordFromPayload(payload: dict[str, Any], *, status: str, createdAt: int, updatedAt: int) -> dict[str, Any]:
147
+ brief = _dict(payload.get("brief", {}))
148
+ context = _dict(payload.get("context", {}))
149
+ return {
150
+ "proxyId": str(payload.get("proxyId", "") or ""),
151
+ "status": _status(payload.get("status", status)),
152
+ "domain": "proxy",
153
+ "kind": _kind(payload.get("kind", payload.get("missionType", brief.get("missionType", "custom")))),
154
+ "taskId": str(payload.get("taskId", context.get("taskId", "")) or ""),
155
+ "userId": str(payload.get("userId", context.get("userId", "")) or ""),
156
+ "resolveId": str(payload.get("resolveId", context.get("resolveId", "")) or ""),
157
+ "reason": str(payload.get("reason", brief.get("reason", "humanIntervention")) or "humanIntervention"),
158
+ "brief": brief,
159
+ "context": context,
160
+ "decisionFork": _dict(payload.get("decisionFork", brief.get("decisionFork", {}))),
161
+ "logistics": _dict(payload.get("logistics", brief.get("logistics", {}))),
162
+ "proofReq": _cleanList(payload.get("proofReq", brief.get("proofReq", []))),
163
+ "caseOpsTimeline": _cleanDicts(payload.get("caseOpsTimeline", brief.get("caseOpsTimeline", []))),
164
+ "route": _dict(payload.get("route", {})),
165
+ "channelMsg": _dict(payload.get("channelMsg", brief.get("channelMsg", {}))),
166
+ "voiceCall": _dict(payload.get("voiceCall", brief.get("voiceCall", {}))),
167
+ "refs": _refs(payload),
168
+ "bridgeAction": _dict(payload.get("bridgeAction", {})),
169
+ "bridgeRun": _dict(payload.get("bridgeRun", {})),
170
+ "outcome": _dict(payload.get("outcome", {})),
171
+ "resume": _dict(payload.get("resume", {})),
172
+ "createdAt": int(createdAt or updatedAt),
173
+ "updatedAt": int(updatedAt or createdAt),
174
+ }
175
+
176
+
177
+ def _missionErrors(payload: dict[str, Any]) -> list[str]:
178
+ errors: list[str] = []
179
+ if _kind(payload.get("kind", payload.get("missionType", "custom"))) not in PROXY_KINDS:
180
+ errors.append("invalid:kind")
181
+ if not _dict(payload.get("brief", {})) and not _dict(payload.get("context", {})) and not str(payload.get("task", payload.get("goal", "")) or ""):
182
+ errors.append("missing:context")
183
+ return errors
184
+
185
+
186
+ def _refs(payload: dict[str, Any]) -> dict[str, str]:
187
+ raw = payload.get("refs", {}) if isinstance(payload.get("refs"), dict) else {}
188
+ refs: dict[str, str] = {}
189
+ for key in ("requestId", "providerTaskId", "externalRef", "connectorRunId"):
190
+ value = str(payload.get(key, raw.get(key, "")) or "")
191
+ if value:
192
+ refs[key] = value
193
+ return refs
194
+
195
+
196
+ def _dict(value: Any) -> dict[str, Any]:
197
+ return copy.deepcopy(value) if isinstance(value, dict) else {}
198
+
199
+
200
+ def _cleanList(value: Any) -> list[str]:
201
+ if not isinstance(value, list):
202
+ return []
203
+ out = []
204
+ for item in value:
205
+ clean = str(item or "").strip()
206
+ if clean and clean not in out:
207
+ out.append(clean)
208
+ return out
209
+
210
+
211
+ def _cleanDicts(value: Any) -> list[dict[str, Any]]:
212
+ if not isinstance(value, list):
213
+ return []
214
+ return [copy.deepcopy(item) for item in value if isinstance(item, dict)]
215
+
216
+
217
+ def _kind(value: Any) -> str:
218
+ kind = str(value or "custom")
219
+ return kind if kind in PROXY_KINDS else kind
220
+
221
+
222
+ def _status(value: Any) -> str:
223
+ status = str(value or "awaitingProxy")
224
+ return status if status in PROXY_STATUSES else "submitted"
225
+
226
+
227
+ def _limit(value: Any) -> int:
228
+ try:
229
+ return max(1, min(500, int(value)))
230
+ except Exception:
231
+ return 100
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ from typing import Any
5
+
6
+
7
+ class ReceiptBook:
8
+ """Receipt and proof export boundary with optional durable backing."""
9
+
10
+ def __init__(self, *, backing: Any | None = None) -> None:
11
+ self.backing = backing
12
+ self._receipts: dict[str, dict[str, Any]] = {}
13
+ self._pov: dict[str, dict[str, Any]] = {}
14
+
15
+ def saveReceipt(self, receipt: dict[str, Any]) -> dict[str, Any]:
16
+ receiptId = str(receipt.get("receiptId", "") or "")
17
+ if not receiptId:
18
+ return {"ok": False, "error": "missing:receiptId"}
19
+ stored = copy.deepcopy(receipt)
20
+ self._receipts[receiptId] = stored
21
+ if self._backingEnabled() and hasattr(self.backing, "insertReceipt"):
22
+ self.backing.insertReceipt(stored)
23
+ return {"ok": True, "receiptId": receiptId}
24
+
25
+ def getReceipt(self, receiptId: str) -> dict[str, Any]:
26
+ receiptId = str(receiptId or "")
27
+ receipt = self._receipts.get(receiptId)
28
+ if receipt is None and self._backingEnabled() and hasattr(self.backing, "fetchReceipt"):
29
+ receipt = self.backing.fetchReceipt(receiptId)
30
+ if receipt:
31
+ self._receipts[receiptId] = copy.deepcopy(receipt)
32
+ if not receipt:
33
+ return {"ok": False, "error": "receiptNotFound", "receiptId": receiptId}
34
+ return {"ok": True, "receipt": copy.deepcopy(receipt)}
35
+
36
+ def listReceipts(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
37
+ self._loadReceipts()
38
+ filters = filters or {}
39
+ status = str(filters.get("status", "") or "")
40
+ mandateId = str(filters.get("mandateId", "") or "")
41
+ taskId = str(filters.get("taskId", "") or "")
42
+ records = list(self._receipts.values())
43
+ if status:
44
+ records = [record for record in records if record.get("status") == status]
45
+ if mandateId:
46
+ records = [record for record in records if record.get("mandateId") == mandateId]
47
+ if taskId:
48
+ records = [record for record in records if record.get("taskId") == taskId]
49
+ records.sort(key=lambda item: (str(item.get("createdAt", "")), str(item.get("receiptId", ""))), reverse=True)
50
+ limit = _limit(filters.get("limit", 100))
51
+ return {
52
+ "ok": True,
53
+ "receipts": [copy.deepcopy(record) for record in records[:limit]],
54
+ "count": min(len(records), limit),
55
+ }
56
+
57
+ def savePov(self, pov: dict[str, Any]) -> dict[str, Any]:
58
+ povId = str(pov.get("povId", "") or "")
59
+ if not povId:
60
+ return {"ok": False, "error": "missing:povId"}
61
+ stored = copy.deepcopy(pov)
62
+ self._pov[povId] = stored
63
+ if self._backingEnabled() and hasattr(self.backing, "insertPov"):
64
+ self.backing.insertPov(stored)
65
+ return {"ok": True, "povId": povId}
66
+
67
+ def getPov(self, povId: str) -> dict[str, Any]:
68
+ povId = str(povId or "")
69
+ pov = self._pov.get(povId)
70
+ if pov is None and self._backingEnabled() and hasattr(self.backing, "fetchPov"):
71
+ pov = self.backing.fetchPov(povId)
72
+ if pov:
73
+ self._pov[povId] = copy.deepcopy(pov)
74
+ if not pov:
75
+ return {"ok": False, "error": "povNotFound", "povId": povId}
76
+ return {"ok": True, "pov": copy.deepcopy(pov)}
77
+
78
+ def listPov(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
79
+ self._loadPov()
80
+ filters = filters or {}
81
+ taskId = str(filters.get("taskId", "") or "")
82
+ domain = str(filters.get("domain", "") or "")
83
+ userId = str(filters.get("userId", "") or "")
84
+ records = list(self._pov.values())
85
+ if taskId:
86
+ records = [record for record in records if record.get("taskId") == taskId]
87
+ if domain:
88
+ records = [record for record in records if record.get("domain") == domain]
89
+ if userId:
90
+ records = [record for record in records if _povUserId(record) == userId]
91
+ records.sort(key=lambda item: (str(item.get("createdAt", "")), str(item.get("povId", ""))), reverse=True)
92
+ limit = _limit(filters.get("limit", 100))
93
+ return {
94
+ "ok": True,
95
+ "pov": [copy.deepcopy(record) for record in records[:limit]],
96
+ "count": min(len(records), limit),
97
+ }
98
+
99
+ def _loadReceipts(self) -> None:
100
+ if self.backing is None or not self._backingEnabled() or not hasattr(self.backing, "listReceipts"):
101
+ return
102
+ for receipt in self.backing.listReceipts():
103
+ receiptId = str(receipt.get("receiptId", "") or "")
104
+ if receiptId:
105
+ self._receipts[receiptId] = copy.deepcopy(receipt)
106
+
107
+ def _loadPov(self) -> None:
108
+ if self.backing is None or not self._backingEnabled() or not hasattr(self.backing, "listPov"):
109
+ return
110
+ for pov in self.backing.listPov():
111
+ povId = str(pov.get("povId", "") or "")
112
+ if povId:
113
+ self._pov[povId] = copy.deepcopy(pov)
114
+
115
+ def _backingEnabled(self) -> bool:
116
+ if self.backing is None:
117
+ return False
118
+ return bool(getattr(self.backing, "enabled", True))
119
+
120
+
121
+ def _povUserId(record: dict[str, Any]) -> str:
122
+ meta = record.get("meta", {}) if isinstance(record.get("meta"), dict) else {}
123
+ return str(record.get("userId", meta.get("userId", "")) or "")
124
+
125
+
126
+ def _limit(value: Any) -> int:
127
+ try:
128
+ parsed = int(value)
129
+ except (TypeError, ValueError):
130
+ return 100
131
+ return max(1, min(parsed, 500))
@@ -0,0 +1,147 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import time
5
+ from datetime import datetime, timezone
6
+ from typing import Any
7
+
8
+
9
+ class SignalBook:
10
+ """Community signal records with optional durable backing."""
11
+
12
+ def __init__(self, *, backing: Any | None = None) -> None:
13
+ self.backing = backing
14
+ self._records: dict[str, dict[str, Any]] = {}
15
+
16
+ def saveSignal(self, signal: Any, analysis: Any) -> dict[str, Any]:
17
+ signalMap = _toMap(signal)
18
+ analysisMap = _toMap(analysis)
19
+ signalId = str(signalMap.get("signalId", analysisMap.get("signalId", "")) or "")
20
+ if not signalId:
21
+ return {"ok": False, "error": "missing:signalId"}
22
+
23
+ current = self.getSignal(signalId)
24
+ observedAt = str(signalMap.get("observedAt", "") or "")
25
+ now = _epoch(observedAt) or int(time.time())
26
+ createdAt = current.get("record", {}).get("createdAt", now) if current.get("ok") else now
27
+ record = {
28
+ "signalId": signalId,
29
+ "entity": str(analysisMap.get("entity", signalMap.get("entity", "unknown")) or "unknown"),
30
+ "confidence": _confidence(analysisMap.get("confidence", signalMap.get("confidence", 0.0))),
31
+ "sourcePlatform": _sourcePlatform(signalMap, analysisMap),
32
+ "query": str(analysisMap.get("query", signalMap.get("query", "")) or ""),
33
+ "signal": copy.deepcopy(signalMap),
34
+ "analysis": copy.deepcopy(analysisMap),
35
+ "createdAt": int(createdAt or now),
36
+ "updatedAt": now,
37
+ }
38
+ self._saveRecord(record)
39
+ return {"ok": True, "record": copy.deepcopy(record)}
40
+
41
+ def getSignal(self, signalId: str) -> dict[str, Any]:
42
+ signalId = str(signalId or "")
43
+ record = self._records.get(signalId)
44
+ if record is None and self._backingEnabled() and hasattr(self.backing, "fetchSignal"):
45
+ record = self.backing.fetchSignal(signalId)
46
+ if record:
47
+ self._records[signalId] = copy.deepcopy(record)
48
+ if not record:
49
+ return {"ok": False, "error": "signalNotFound", "signalId": signalId}
50
+ return {"ok": True, "record": copy.deepcopy(record)}
51
+
52
+ def listSignals(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
53
+ self._loadBacking()
54
+ filters = filters or {}
55
+ entity = str(filters.get("entity", "") or "")
56
+ sourcePlatform = str(filters.get("sourcePlatform", "") or "")
57
+ query = str(filters.get("query", "") or "")
58
+ minConfidence = _optionalFloat(filters.get("minConfidence"))
59
+ records = list(self._records.values())
60
+ if entity:
61
+ records = [record for record in records if record.get("entity") == entity]
62
+ if sourcePlatform:
63
+ records = [record for record in records if record.get("sourcePlatform") == sourcePlatform]
64
+ if query:
65
+ records = [record for record in records if record.get("query") == query]
66
+ if minConfidence is not None:
67
+ records = [record for record in records if _confidence(record.get("confidence", 0.0)) >= minConfidence]
68
+ records.sort(key=lambda item: (int(item.get("createdAt", 0) or 0), str(item.get("signalId", ""))), reverse=True)
69
+ limit = _limit(filters.get("limit", 100))
70
+ return {
71
+ "ok": True,
72
+ "signals": [copy.deepcopy(record) for record in records[:limit]],
73
+ "count": min(len(records), limit),
74
+ }
75
+
76
+ def _saveRecord(self, record: dict[str, Any]) -> None:
77
+ signalId = str(record.get("signalId", "") or "")
78
+ if not signalId:
79
+ return
80
+ self._records[signalId] = copy.deepcopy(record)
81
+ if self._backingEnabled() and hasattr(self.backing, "upsertSignal"):
82
+ self.backing.upsertSignal(record)
83
+
84
+ def _loadBacking(self) -> None:
85
+ if self.backing is None or not self._backingEnabled() or not hasattr(self.backing, "listSignals"):
86
+ return
87
+ for record in self.backing.listSignals():
88
+ signalId = str(record.get("signalId", "") or "")
89
+ if signalId:
90
+ self._records[signalId] = copy.deepcopy(record)
91
+
92
+ def _backingEnabled(self) -> bool:
93
+ if self.backing is None:
94
+ return False
95
+ return bool(getattr(self.backing, "enabled", True))
96
+
97
+
98
+ def _toMap(value: Any) -> dict[str, Any]:
99
+ if hasattr(value, "toMap"):
100
+ mapped = value.toMap()
101
+ return mapped if isinstance(mapped, dict) else {}
102
+ return copy.deepcopy(value) if isinstance(value, dict) else {}
103
+
104
+
105
+ def _sourcePlatform(signal: dict[str, Any], analysis: dict[str, Any]) -> str:
106
+ source = signal.get("source", {}) if isinstance(signal.get("source"), dict) else {}
107
+ analysisSource = analysis.get("source", {}) if isinstance(analysis.get("source"), dict) else {}
108
+ return str(source.get("platform", analysisSource.get("platform", "")) or "")
109
+
110
+
111
+ def _epoch(value: Any) -> int:
112
+ if isinstance(value, (int, float)):
113
+ return int(value or 0)
114
+ raw = str(value or "").strip()
115
+ if not raw:
116
+ return 0
117
+ if raw.endswith("Z"):
118
+ raw = raw[:-1] + "+00:00"
119
+ try:
120
+ parsed = datetime.fromisoformat(raw)
121
+ except Exception:
122
+ return 0
123
+ if parsed.tzinfo is None:
124
+ parsed = parsed.replace(tzinfo=timezone.utc)
125
+ return int(parsed.timestamp())
126
+
127
+
128
+ def _confidence(value: Any) -> float:
129
+ try:
130
+ parsed = float(value or 0.0)
131
+ except (TypeError, ValueError):
132
+ return 0.0
133
+ return max(0.0, min(parsed, 1.0))
134
+
135
+
136
+ def _optionalFloat(value: Any) -> float | None:
137
+ if value is None or value == "":
138
+ return None
139
+ return _confidence(value)
140
+
141
+
142
+ def _limit(value: Any) -> int:
143
+ try:
144
+ parsed = int(value)
145
+ except (TypeError, ValueError):
146
+ return 100
147
+ return max(1, min(parsed, 500))