shield-python 0.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,266 @@
1
+ Metadata-Version: 2.4
2
+ Name: shield-python
3
+ Version: 0.1.0
4
+ Summary: Official Shield SDK for Python
5
+ License: MIT
6
+ Project-URL: Homepage, https://getshield.dev
7
+ Project-URL: Documentation, https://docs.getshield.dev
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: requests>=2.28.0
11
+
12
+ # Shield Python SDK
13
+
14
+ Official Python SDK for [Shield](https://getshield.dev) — tamper-evident session recording for real estate transactions.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install shield-python
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```python
25
+ import shield
26
+
27
+ client = shield.Client(api_key="sk_live_your_api_key")
28
+
29
+ # Create a session
30
+ session = client.sessions.create(title="123 Main St Closing")
31
+ session_id = session["id"]
32
+
33
+ # Record events
34
+ client.events.create(
35
+ session_id=session_id,
36
+ event_type="shield.party.joined",
37
+ actor="agent@example.com",
38
+ data={"role": "listing_agent", "name": "Jane Smith"},
39
+ )
40
+
41
+ client.events.create(
42
+ session_id=session_id,
43
+ event_type="shield.content.uploaded",
44
+ actor="agent@example.com",
45
+ data={"filename": "purchase_agreement.pdf", "hash": "sha256:abc123..."},
46
+ )
47
+
48
+ client.events.create(
49
+ session_id=session_id,
50
+ event_type="shield.agreement.signed",
51
+ actor="buyer@example.com",
52
+ data={"document": "purchase_agreement.pdf"},
53
+ )
54
+
55
+ # Verify session integrity
56
+ result = client.verify.session(session_id)
57
+ print(result["intact"]) # True
58
+
59
+ # Export session
60
+ pdf_bytes = client.sessions.export(session_id, format="pdf")
61
+ with open("audit_trail.pdf", "wb") as f:
62
+ f.write(pdf_bytes)
63
+ ```
64
+
65
+ ## HMAC Authentication
66
+
67
+ For enhanced security, provide an HMAC secret to sign every request:
68
+
69
+ ```python
70
+ client = shield.Client(
71
+ api_key="sk_live_your_api_key",
72
+ hmac_secret="your_hmac_secret",
73
+ )
74
+ ```
75
+
76
+ When an HMAC secret is configured, the SDK computes a signature for each request:
77
+
78
+ - `X-Shield-Timestamp` — Unix timestamp of the request
79
+ - `X-Shield-Signature` — HMAC-SHA256 of `{timestamp}.{METHOD}.{path}.{SHA256(body)}`
80
+
81
+ The server validates these headers to ensure requests have not been tampered with or replayed.
82
+
83
+ ## Flask Integration
84
+
85
+ ```python
86
+ from flask import Flask, request, jsonify
87
+ import shield
88
+
89
+ app = Flask(__name__)
90
+ client = shield.Client(api_key="sk_live_your_api_key")
91
+
92
+ @app.route("/sessions", methods=["POST"])
93
+ def create_session():
94
+ data = request.get_json()
95
+ session = client.sessions.create(title=data["title"])
96
+
97
+ # Record who created the session
98
+ client.events.create(
99
+ session_id=session["id"],
100
+ event_type="shield.session.created",
101
+ actor=data["user_email"],
102
+ )
103
+ return jsonify(session), 201
104
+
105
+ @app.route("/sessions/<session_id>/upload", methods=["POST"])
106
+ def upload_document(session_id):
107
+ file = request.files["file"]
108
+ # ... save file logic ...
109
+
110
+ client.events.create(
111
+ session_id=session_id,
112
+ event_type="shield.content.uploaded",
113
+ actor=request.headers.get("X-User-Email"),
114
+ data={"filename": file.filename},
115
+ )
116
+ return jsonify({"status": "uploaded"}), 200
117
+
118
+ @app.route("/sessions/<session_id>/sign", methods=["POST"])
119
+ def sign_agreement(session_id):
120
+ data = request.get_json()
121
+
122
+ client.events.create(
123
+ session_id=session_id,
124
+ event_type="shield.agreement.signed",
125
+ actor=data["signer_email"],
126
+ data={"document": data["document_name"], "ip": request.remote_addr},
127
+ )
128
+ return jsonify({"status": "signed"}), 200
129
+ ```
130
+
131
+ ## FastAPI Integration
132
+
133
+ ```python
134
+ from fastapi import FastAPI, UploadFile, Header
135
+ from pydantic import BaseModel
136
+ import shield
137
+
138
+ app = FastAPI()
139
+ client = shield.Client(
140
+ api_key="sk_live_your_api_key",
141
+ hmac_secret="your_hmac_secret",
142
+ )
143
+
144
+ class CreateSessionRequest(BaseModel):
145
+ title: str
146
+ user_email: str
147
+
148
+ class SignRequest(BaseModel):
149
+ signer_email: str
150
+ document_name: str
151
+
152
+ @app.post("/sessions")
153
+ async def create_session(body: CreateSessionRequest):
154
+ session = client.sessions.create(title=body.title)
155
+ client.events.create(
156
+ session_id=session["id"],
157
+ event_type="shield.session.created",
158
+ actor=body.user_email,
159
+ )
160
+ return session
161
+
162
+ @app.post("/sessions/{session_id}/upload")
163
+ async def upload_document(
164
+ session_id: str,
165
+ file: UploadFile,
166
+ x_user_email: str = Header(...),
167
+ ):
168
+ # ... save file logic ...
169
+
170
+ client.events.create(
171
+ session_id=session_id,
172
+ event_type="shield.content.uploaded",
173
+ actor=x_user_email,
174
+ data={"filename": file.filename},
175
+ )
176
+ return {"status": "uploaded"}
177
+
178
+ @app.post("/sessions/{session_id}/sign")
179
+ async def sign_agreement(session_id: str, body: SignRequest):
180
+ client.events.create(
181
+ session_id=session_id,
182
+ event_type="shield.agreement.signed",
183
+ actor=body.signer_email,
184
+ data={"document": body.document_name},
185
+ )
186
+ return {"status": "signed"}
187
+
188
+ @app.get("/sessions/{session_id}/verify")
189
+ async def verify_session(session_id: str):
190
+ return client.verify.session(session_id)
191
+ ```
192
+
193
+ ## Event Types Reference
194
+
195
+ Shield Standard Event Taxonomy v1.0 — 37 event types across 7 categories:
196
+
197
+ ### Party Events
198
+ | Event Type | Description |
199
+ |---|---|
200
+ | `shield.party.joined` | A party joined the session |
201
+ | `shield.party.left` | A party left the session |
202
+ | `shield.party.identity.verified` | Party identity was verified |
203
+ | `shield.party.identity.failed` | Party identity verification failed |
204
+ | `shield.party.role.assigned` | A role was assigned to a party |
205
+
206
+ ### Session Events
207
+ | Event Type | Description |
208
+ |---|---|
209
+ | `shield.session.created` | Session was created |
210
+ | `shield.session.opened` | Session was opened |
211
+ | `shield.session.closed` | Session was closed |
212
+ | `shield.session.expired` | Session expired |
213
+ | `shield.session.archived` | Session was archived |
214
+
215
+ ### Content Events
216
+ | Event Type | Description |
217
+ |---|---|
218
+ | `shield.content.uploaded` | Content was uploaded |
219
+ | `shield.content.viewed` | Content was viewed |
220
+ | `shield.content.downloaded` | Content was downloaded |
221
+ | `shield.content.deleted` | Content was deleted |
222
+ | `shield.content.hash.verified` | Content hash was verified |
223
+
224
+ ### Negotiation Events
225
+ | Event Type | Description |
226
+ |---|---|
227
+ | `shield.negotiation.terms.proposed` | Terms were proposed |
228
+ | `shield.negotiation.terms.accepted` | Terms were accepted |
229
+ | `shield.negotiation.terms.rejected` | Terms were rejected |
230
+ | `shield.negotiation.terms.modified` | Terms were modified |
231
+ | `shield.negotiation.terms.expired` | Terms expired |
232
+ | `shield.negotiation.message.sent` | Negotiation message sent |
233
+ | `shield.negotiation.message.read` | Negotiation message read |
234
+
235
+ ### Agreement Events
236
+ | Event Type | Description |
237
+ |---|---|
238
+ | `shield.agreement.drafted` | Agreement was drafted |
239
+ | `shield.agreement.reviewed` | Agreement was reviewed |
240
+ | `shield.agreement.approved` | Agreement was approved |
241
+ | `shield.agreement.signed` | Agreement was signed |
242
+ | `shield.agreement.countersigned` | Agreement was countersigned |
243
+ | `shield.agreement.voided` | Agreement was voided |
244
+ | `shield.agreement.reached` | Agreement was reached |
245
+
246
+ ### Access Events
247
+ | Event Type | Description |
248
+ |---|---|
249
+ | `shield.access.granted` | Access was granted |
250
+ | `shield.access.revoked` | Access was revoked |
251
+ | `shield.access.attempted` | Access was attempted |
252
+ | `shield.access.denied` | Access was denied |
253
+
254
+ ### Disclosure Events
255
+ | Event Type | Description |
256
+ |---|---|
257
+ | `shield.disclosure.presented` | Disclosure was presented |
258
+ | `shield.disclosure.acknowledged` | Disclosure was acknowledged |
259
+ | `shield.disclosure.declined` | Disclosure was declined |
260
+
261
+ ### Evidence Events
262
+ | Event Type | Description |
263
+ |---|---|
264
+ | `shield.evidence.exported` | Evidence was exported |
265
+ | `shield.evidence.verified` | Evidence was verified |
266
+ | `shield.evidence.tampered_detected` | Evidence tampering was detected |
@@ -0,0 +1,255 @@
1
+ # Shield Python SDK
2
+
3
+ Official Python SDK for [Shield](https://getshield.dev) — tamper-evident session recording for real estate transactions.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install shield-python
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ import shield
15
+
16
+ client = shield.Client(api_key="sk_live_your_api_key")
17
+
18
+ # Create a session
19
+ session = client.sessions.create(title="123 Main St Closing")
20
+ session_id = session["id"]
21
+
22
+ # Record events
23
+ client.events.create(
24
+ session_id=session_id,
25
+ event_type="shield.party.joined",
26
+ actor="agent@example.com",
27
+ data={"role": "listing_agent", "name": "Jane Smith"},
28
+ )
29
+
30
+ client.events.create(
31
+ session_id=session_id,
32
+ event_type="shield.content.uploaded",
33
+ actor="agent@example.com",
34
+ data={"filename": "purchase_agreement.pdf", "hash": "sha256:abc123..."},
35
+ )
36
+
37
+ client.events.create(
38
+ session_id=session_id,
39
+ event_type="shield.agreement.signed",
40
+ actor="buyer@example.com",
41
+ data={"document": "purchase_agreement.pdf"},
42
+ )
43
+
44
+ # Verify session integrity
45
+ result = client.verify.session(session_id)
46
+ print(result["intact"]) # True
47
+
48
+ # Export session
49
+ pdf_bytes = client.sessions.export(session_id, format="pdf")
50
+ with open("audit_trail.pdf", "wb") as f:
51
+ f.write(pdf_bytes)
52
+ ```
53
+
54
+ ## HMAC Authentication
55
+
56
+ For enhanced security, provide an HMAC secret to sign every request:
57
+
58
+ ```python
59
+ client = shield.Client(
60
+ api_key="sk_live_your_api_key",
61
+ hmac_secret="your_hmac_secret",
62
+ )
63
+ ```
64
+
65
+ When an HMAC secret is configured, the SDK computes a signature for each request:
66
+
67
+ - `X-Shield-Timestamp` — Unix timestamp of the request
68
+ - `X-Shield-Signature` — HMAC-SHA256 of `{timestamp}.{METHOD}.{path}.{SHA256(body)}`
69
+
70
+ The server validates these headers to ensure requests have not been tampered with or replayed.
71
+
72
+ ## Flask Integration
73
+
74
+ ```python
75
+ from flask import Flask, request, jsonify
76
+ import shield
77
+
78
+ app = Flask(__name__)
79
+ client = shield.Client(api_key="sk_live_your_api_key")
80
+
81
+ @app.route("/sessions", methods=["POST"])
82
+ def create_session():
83
+ data = request.get_json()
84
+ session = client.sessions.create(title=data["title"])
85
+
86
+ # Record who created the session
87
+ client.events.create(
88
+ session_id=session["id"],
89
+ event_type="shield.session.created",
90
+ actor=data["user_email"],
91
+ )
92
+ return jsonify(session), 201
93
+
94
+ @app.route("/sessions/<session_id>/upload", methods=["POST"])
95
+ def upload_document(session_id):
96
+ file = request.files["file"]
97
+ # ... save file logic ...
98
+
99
+ client.events.create(
100
+ session_id=session_id,
101
+ event_type="shield.content.uploaded",
102
+ actor=request.headers.get("X-User-Email"),
103
+ data={"filename": file.filename},
104
+ )
105
+ return jsonify({"status": "uploaded"}), 200
106
+
107
+ @app.route("/sessions/<session_id>/sign", methods=["POST"])
108
+ def sign_agreement(session_id):
109
+ data = request.get_json()
110
+
111
+ client.events.create(
112
+ session_id=session_id,
113
+ event_type="shield.agreement.signed",
114
+ actor=data["signer_email"],
115
+ data={"document": data["document_name"], "ip": request.remote_addr},
116
+ )
117
+ return jsonify({"status": "signed"}), 200
118
+ ```
119
+
120
+ ## FastAPI Integration
121
+
122
+ ```python
123
+ from fastapi import FastAPI, UploadFile, Header
124
+ from pydantic import BaseModel
125
+ import shield
126
+
127
+ app = FastAPI()
128
+ client = shield.Client(
129
+ api_key="sk_live_your_api_key",
130
+ hmac_secret="your_hmac_secret",
131
+ )
132
+
133
+ class CreateSessionRequest(BaseModel):
134
+ title: str
135
+ user_email: str
136
+
137
+ class SignRequest(BaseModel):
138
+ signer_email: str
139
+ document_name: str
140
+
141
+ @app.post("/sessions")
142
+ async def create_session(body: CreateSessionRequest):
143
+ session = client.sessions.create(title=body.title)
144
+ client.events.create(
145
+ session_id=session["id"],
146
+ event_type="shield.session.created",
147
+ actor=body.user_email,
148
+ )
149
+ return session
150
+
151
+ @app.post("/sessions/{session_id}/upload")
152
+ async def upload_document(
153
+ session_id: str,
154
+ file: UploadFile,
155
+ x_user_email: str = Header(...),
156
+ ):
157
+ # ... save file logic ...
158
+
159
+ client.events.create(
160
+ session_id=session_id,
161
+ event_type="shield.content.uploaded",
162
+ actor=x_user_email,
163
+ data={"filename": file.filename},
164
+ )
165
+ return {"status": "uploaded"}
166
+
167
+ @app.post("/sessions/{session_id}/sign")
168
+ async def sign_agreement(session_id: str, body: SignRequest):
169
+ client.events.create(
170
+ session_id=session_id,
171
+ event_type="shield.agreement.signed",
172
+ actor=body.signer_email,
173
+ data={"document": body.document_name},
174
+ )
175
+ return {"status": "signed"}
176
+
177
+ @app.get("/sessions/{session_id}/verify")
178
+ async def verify_session(session_id: str):
179
+ return client.verify.session(session_id)
180
+ ```
181
+
182
+ ## Event Types Reference
183
+
184
+ Shield Standard Event Taxonomy v1.0 — 37 event types across 7 categories:
185
+
186
+ ### Party Events
187
+ | Event Type | Description |
188
+ |---|---|
189
+ | `shield.party.joined` | A party joined the session |
190
+ | `shield.party.left` | A party left the session |
191
+ | `shield.party.identity.verified` | Party identity was verified |
192
+ | `shield.party.identity.failed` | Party identity verification failed |
193
+ | `shield.party.role.assigned` | A role was assigned to a party |
194
+
195
+ ### Session Events
196
+ | Event Type | Description |
197
+ |---|---|
198
+ | `shield.session.created` | Session was created |
199
+ | `shield.session.opened` | Session was opened |
200
+ | `shield.session.closed` | Session was closed |
201
+ | `shield.session.expired` | Session expired |
202
+ | `shield.session.archived` | Session was archived |
203
+
204
+ ### Content Events
205
+ | Event Type | Description |
206
+ |---|---|
207
+ | `shield.content.uploaded` | Content was uploaded |
208
+ | `shield.content.viewed` | Content was viewed |
209
+ | `shield.content.downloaded` | Content was downloaded |
210
+ | `shield.content.deleted` | Content was deleted |
211
+ | `shield.content.hash.verified` | Content hash was verified |
212
+
213
+ ### Negotiation Events
214
+ | Event Type | Description |
215
+ |---|---|
216
+ | `shield.negotiation.terms.proposed` | Terms were proposed |
217
+ | `shield.negotiation.terms.accepted` | Terms were accepted |
218
+ | `shield.negotiation.terms.rejected` | Terms were rejected |
219
+ | `shield.negotiation.terms.modified` | Terms were modified |
220
+ | `shield.negotiation.terms.expired` | Terms expired |
221
+ | `shield.negotiation.message.sent` | Negotiation message sent |
222
+ | `shield.negotiation.message.read` | Negotiation message read |
223
+
224
+ ### Agreement Events
225
+ | Event Type | Description |
226
+ |---|---|
227
+ | `shield.agreement.drafted` | Agreement was drafted |
228
+ | `shield.agreement.reviewed` | Agreement was reviewed |
229
+ | `shield.agreement.approved` | Agreement was approved |
230
+ | `shield.agreement.signed` | Agreement was signed |
231
+ | `shield.agreement.countersigned` | Agreement was countersigned |
232
+ | `shield.agreement.voided` | Agreement was voided |
233
+ | `shield.agreement.reached` | Agreement was reached |
234
+
235
+ ### Access Events
236
+ | Event Type | Description |
237
+ |---|---|
238
+ | `shield.access.granted` | Access was granted |
239
+ | `shield.access.revoked` | Access was revoked |
240
+ | `shield.access.attempted` | Access was attempted |
241
+ | `shield.access.denied` | Access was denied |
242
+
243
+ ### Disclosure Events
244
+ | Event Type | Description |
245
+ |---|---|
246
+ | `shield.disclosure.presented` | Disclosure was presented |
247
+ | `shield.disclosure.acknowledged` | Disclosure was acknowledged |
248
+ | `shield.disclosure.declined` | Disclosure was declined |
249
+
250
+ ### Evidence Events
251
+ | Event Type | Description |
252
+ |---|---|
253
+ | `shield.evidence.exported` | Evidence was exported |
254
+ | `shield.evidence.verified` | Evidence was verified |
255
+ | `shield.evidence.tampered_detected` | Evidence tampering was detected |
@@ -0,0 +1,16 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "shield-python"
7
+ version = "0.1.0"
8
+ description = "Official Shield SDK for Python"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.8"
12
+ dependencies = ["requests>=2.28.0"]
13
+
14
+ [project.urls]
15
+ Homepage = "https://getshield.dev"
16
+ Documentation = "https://docs.getshield.dev"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="shield-python",
5
+ version="0.1.0",
6
+ packages=find_packages(),
7
+ install_requires=["requests>=2.28.0"],
8
+ )
@@ -0,0 +1,5 @@
1
+ from .client import Client
2
+ from .exceptions import ShieldError
3
+
4
+ __all__ = ["Client", "ShieldError"]
5
+ __version__ = "0.1.0"
@@ -0,0 +1,130 @@
1
+ import hashlib
2
+ import hmac as hmac_mod
3
+ import json
4
+ import time
5
+ from typing import Any, Dict, Optional
6
+ from urllib.parse import urlparse
7
+
8
+ import requests
9
+
10
+ from .exceptions import ShieldError
11
+ from .resources.sessions import Sessions
12
+ from .resources.events import Events
13
+ from .resources.verify import Verify
14
+
15
+
16
+ class Client:
17
+ """Official Shield Python SDK client.
18
+
19
+ Args:
20
+ api_key: Your Shield API key.
21
+ base_url: Base URL for the Shield API. Defaults to https://getshield.dev/api/v1.
22
+ hmac_secret: Optional HMAC secret for request signing.
23
+ timeout: Request timeout in seconds. Defaults to 30.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ api_key: str,
29
+ base_url: str = "https://getshield.dev/api/v1",
30
+ hmac_secret: Optional[str] = None,
31
+ timeout: int = 30,
32
+ ):
33
+ self.api_key = api_key
34
+ self.base_url = base_url.rstrip("/")
35
+ self.hmac_secret = hmac_secret
36
+ self.timeout = timeout
37
+ self._session = requests.Session()
38
+
39
+ # Resource instances
40
+ self.sessions = Sessions(self)
41
+ self.events = Events(self)
42
+ self.verify = Verify(self)
43
+
44
+ def _request(
45
+ self,
46
+ method: str,
47
+ path: str,
48
+ json_data: Optional[Dict[str, Any]] = None,
49
+ params: Optional[Dict[str, Any]] = None,
50
+ raw_response: bool = False,
51
+ ) -> Any:
52
+ """Make an authenticated request to the Shield API.
53
+
54
+ Args:
55
+ method: HTTP method (GET, POST, PUT, DELETE).
56
+ path: API path (e.g. /sessions).
57
+ json_data: JSON body for POST/PUT requests.
58
+ params: Query parameters.
59
+ raw_response: If True, return raw response content (bytes).
60
+
61
+ Returns:
62
+ Parsed JSON response as dict, or bytes if raw_response is True.
63
+
64
+ Raises:
65
+ ShieldError: On non-2xx responses or request failures.
66
+ """
67
+ url = f"{self.base_url}{path}"
68
+ method = method.upper()
69
+
70
+ headers = {
71
+ "X-Shield-Key": self.api_key,
72
+ "Content-Type": "application/json",
73
+ }
74
+
75
+ # Serialize body
76
+ body = b""
77
+ if json_data is not None:
78
+ body = json.dumps(json_data, separators=(",", ":")).encode("utf-8")
79
+
80
+ # HMAC signing
81
+ if self.hmac_secret:
82
+ timestamp = str(int(time.time()))
83
+ body_hash = hashlib.sha256(body).hexdigest()
84
+ message = f"{timestamp}.{method}.{path}.{body_hash}"
85
+ signature = hmac_mod.new(
86
+ self.hmac_secret.encode("utf-8"),
87
+ message.encode("utf-8"),
88
+ hashlib.sha256,
89
+ ).hexdigest()
90
+ headers["X-Shield-Signature"] = signature
91
+ headers["X-Shield-Timestamp"] = timestamp
92
+
93
+ try:
94
+ response = self._session.request(
95
+ method=method,
96
+ url=url,
97
+ headers=headers,
98
+ data=body if body else None,
99
+ params=params,
100
+ timeout=self.timeout,
101
+ )
102
+ except requests.RequestException as e:
103
+ raise ShieldError(
104
+ message=f"Request failed: {str(e)}",
105
+ status_code=0,
106
+ code="request_error",
107
+ )
108
+
109
+ if not (200 <= response.status_code < 300):
110
+ error_message = response.text
111
+ error_code = "api_error"
112
+ try:
113
+ error_body = response.json()
114
+ error_message = error_body.get("error", error_message)
115
+ error_code = error_body.get("code", error_code)
116
+ except (ValueError, KeyError):
117
+ pass
118
+ raise ShieldError(
119
+ message=error_message,
120
+ status_code=response.status_code,
121
+ code=error_code,
122
+ )
123
+
124
+ if raw_response:
125
+ return response.content
126
+
127
+ if not response.content:
128
+ return {}
129
+
130
+ return response.json()
@@ -0,0 +1,23 @@
1
+ class ShieldError(Exception):
2
+ """Base exception for Shield SDK errors."""
3
+
4
+ def __init__(self, message: str, status_code: int = None, code: str = None):
5
+ super().__init__(message)
6
+ self.message = message
7
+ self.status_code = status_code
8
+ self.code = code
9
+
10
+ def __str__(self):
11
+ parts = []
12
+ if self.status_code:
13
+ parts.append(f"[{self.status_code}]")
14
+ if self.code:
15
+ parts.append(f"({self.code})")
16
+ parts.append(self.message)
17
+ return " ".join(parts)
18
+
19
+ def __repr__(self):
20
+ return (
21
+ f"ShieldError(message={self.message!r}, "
22
+ f"status_code={self.status_code!r}, code={self.code!r})"
23
+ )
File without changes
@@ -0,0 +1,40 @@
1
+ from typing import Any, Dict, Optional
2
+
3
+
4
+ class Events:
5
+ """Record events within Shield sessions."""
6
+
7
+ def __init__(self, client):
8
+ self._client = client
9
+
10
+ def create(
11
+ self,
12
+ session_id: str,
13
+ event_type: str,
14
+ actor: str,
15
+ data: Optional[Dict[str, Any]] = None,
16
+ ) -> Dict[str, Any]:
17
+ """Create a new event in a session.
18
+
19
+ Args:
20
+ session_id: The session ID to record the event in.
21
+ event_type: Event type from the Shield Standard Event Taxonomy
22
+ (e.g. "shield.party.joined").
23
+ actor: Identifier for the actor performing the action.
24
+ data: Optional additional event data.
25
+
26
+ Returns:
27
+ Created event object.
28
+ """
29
+ payload: Dict[str, Any] = {
30
+ "event_type": event_type,
31
+ "actor": actor,
32
+ }
33
+ if data is not None:
34
+ payload["data"] = data
35
+
36
+ return self._client._request(
37
+ "POST",
38
+ f"/sessions/{session_id}/events",
39
+ json_data=payload,
40
+ )
@@ -0,0 +1,47 @@
1
+ from typing import Any, Dict
2
+
3
+
4
+ class Sessions:
5
+ """Manage Shield sessions."""
6
+
7
+ def __init__(self, client):
8
+ self._client = client
9
+
10
+ def create(self, title: str) -> Dict[str, Any]:
11
+ """Create a new session.
12
+
13
+ Args:
14
+ title: Human-readable title for the session.
15
+
16
+ Returns:
17
+ Created session object.
18
+ """
19
+ return self._client._request("POST", "/sessions", json_data={"title": title})
20
+
21
+ def retrieve(self, session_id: str) -> Dict[str, Any]:
22
+ """Retrieve a session by ID.
23
+
24
+ Args:
25
+ session_id: The session ID.
26
+
27
+ Returns:
28
+ Session object.
29
+ """
30
+ return self._client._request("GET", f"/sessions/{session_id}")
31
+
32
+ def export(self, session_id: str, format: str = "json"):
33
+ """Export a session in the specified format.
34
+
35
+ Args:
36
+ session_id: The session ID.
37
+ format: Export format — "json" or "pdf". Defaults to "json".
38
+
39
+ Returns:
40
+ dict if format is "json", bytes for binary formats like "pdf".
41
+ """
42
+ raw = format != "json"
43
+ return self._client._request(
44
+ "GET",
45
+ f"/sessions/{session_id}/export/{format}",
46
+ raw_response=raw,
47
+ )
@@ -0,0 +1,19 @@
1
+ from typing import Any, Dict
2
+
3
+
4
+ class Verify:
5
+ """Verify Shield session integrity."""
6
+
7
+ def __init__(self, client):
8
+ self._client = client
9
+
10
+ def session(self, session_id: str) -> Dict[str, Any]:
11
+ """Verify the integrity of a session's event chain.
12
+
13
+ Args:
14
+ session_id: The session ID to verify.
15
+
16
+ Returns:
17
+ Verification result with integrity status.
18
+ """
19
+ return self._client._request("GET", f"/sessions/{session_id}/verify")
@@ -0,0 +1,266 @@
1
+ Metadata-Version: 2.4
2
+ Name: shield-python
3
+ Version: 0.1.0
4
+ Summary: Official Shield SDK for Python
5
+ License: MIT
6
+ Project-URL: Homepage, https://getshield.dev
7
+ Project-URL: Documentation, https://docs.getshield.dev
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: requests>=2.28.0
11
+
12
+ # Shield Python SDK
13
+
14
+ Official Python SDK for [Shield](https://getshield.dev) — tamper-evident session recording for real estate transactions.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install shield-python
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```python
25
+ import shield
26
+
27
+ client = shield.Client(api_key="sk_live_your_api_key")
28
+
29
+ # Create a session
30
+ session = client.sessions.create(title="123 Main St Closing")
31
+ session_id = session["id"]
32
+
33
+ # Record events
34
+ client.events.create(
35
+ session_id=session_id,
36
+ event_type="shield.party.joined",
37
+ actor="agent@example.com",
38
+ data={"role": "listing_agent", "name": "Jane Smith"},
39
+ )
40
+
41
+ client.events.create(
42
+ session_id=session_id,
43
+ event_type="shield.content.uploaded",
44
+ actor="agent@example.com",
45
+ data={"filename": "purchase_agreement.pdf", "hash": "sha256:abc123..."},
46
+ )
47
+
48
+ client.events.create(
49
+ session_id=session_id,
50
+ event_type="shield.agreement.signed",
51
+ actor="buyer@example.com",
52
+ data={"document": "purchase_agreement.pdf"},
53
+ )
54
+
55
+ # Verify session integrity
56
+ result = client.verify.session(session_id)
57
+ print(result["intact"]) # True
58
+
59
+ # Export session
60
+ pdf_bytes = client.sessions.export(session_id, format="pdf")
61
+ with open("audit_trail.pdf", "wb") as f:
62
+ f.write(pdf_bytes)
63
+ ```
64
+
65
+ ## HMAC Authentication
66
+
67
+ For enhanced security, provide an HMAC secret to sign every request:
68
+
69
+ ```python
70
+ client = shield.Client(
71
+ api_key="sk_live_your_api_key",
72
+ hmac_secret="your_hmac_secret",
73
+ )
74
+ ```
75
+
76
+ When an HMAC secret is configured, the SDK computes a signature for each request:
77
+
78
+ - `X-Shield-Timestamp` — Unix timestamp of the request
79
+ - `X-Shield-Signature` — HMAC-SHA256 of `{timestamp}.{METHOD}.{path}.{SHA256(body)}`
80
+
81
+ The server validates these headers to ensure requests have not been tampered with or replayed.
82
+
83
+ ## Flask Integration
84
+
85
+ ```python
86
+ from flask import Flask, request, jsonify
87
+ import shield
88
+
89
+ app = Flask(__name__)
90
+ client = shield.Client(api_key="sk_live_your_api_key")
91
+
92
+ @app.route("/sessions", methods=["POST"])
93
+ def create_session():
94
+ data = request.get_json()
95
+ session = client.sessions.create(title=data["title"])
96
+
97
+ # Record who created the session
98
+ client.events.create(
99
+ session_id=session["id"],
100
+ event_type="shield.session.created",
101
+ actor=data["user_email"],
102
+ )
103
+ return jsonify(session), 201
104
+
105
+ @app.route("/sessions/<session_id>/upload", methods=["POST"])
106
+ def upload_document(session_id):
107
+ file = request.files["file"]
108
+ # ... save file logic ...
109
+
110
+ client.events.create(
111
+ session_id=session_id,
112
+ event_type="shield.content.uploaded",
113
+ actor=request.headers.get("X-User-Email"),
114
+ data={"filename": file.filename},
115
+ )
116
+ return jsonify({"status": "uploaded"}), 200
117
+
118
+ @app.route("/sessions/<session_id>/sign", methods=["POST"])
119
+ def sign_agreement(session_id):
120
+ data = request.get_json()
121
+
122
+ client.events.create(
123
+ session_id=session_id,
124
+ event_type="shield.agreement.signed",
125
+ actor=data["signer_email"],
126
+ data={"document": data["document_name"], "ip": request.remote_addr},
127
+ )
128
+ return jsonify({"status": "signed"}), 200
129
+ ```
130
+
131
+ ## FastAPI Integration
132
+
133
+ ```python
134
+ from fastapi import FastAPI, UploadFile, Header
135
+ from pydantic import BaseModel
136
+ import shield
137
+
138
+ app = FastAPI()
139
+ client = shield.Client(
140
+ api_key="sk_live_your_api_key",
141
+ hmac_secret="your_hmac_secret",
142
+ )
143
+
144
+ class CreateSessionRequest(BaseModel):
145
+ title: str
146
+ user_email: str
147
+
148
+ class SignRequest(BaseModel):
149
+ signer_email: str
150
+ document_name: str
151
+
152
+ @app.post("/sessions")
153
+ async def create_session(body: CreateSessionRequest):
154
+ session = client.sessions.create(title=body.title)
155
+ client.events.create(
156
+ session_id=session["id"],
157
+ event_type="shield.session.created",
158
+ actor=body.user_email,
159
+ )
160
+ return session
161
+
162
+ @app.post("/sessions/{session_id}/upload")
163
+ async def upload_document(
164
+ session_id: str,
165
+ file: UploadFile,
166
+ x_user_email: str = Header(...),
167
+ ):
168
+ # ... save file logic ...
169
+
170
+ client.events.create(
171
+ session_id=session_id,
172
+ event_type="shield.content.uploaded",
173
+ actor=x_user_email,
174
+ data={"filename": file.filename},
175
+ )
176
+ return {"status": "uploaded"}
177
+
178
+ @app.post("/sessions/{session_id}/sign")
179
+ async def sign_agreement(session_id: str, body: SignRequest):
180
+ client.events.create(
181
+ session_id=session_id,
182
+ event_type="shield.agreement.signed",
183
+ actor=body.signer_email,
184
+ data={"document": body.document_name},
185
+ )
186
+ return {"status": "signed"}
187
+
188
+ @app.get("/sessions/{session_id}/verify")
189
+ async def verify_session(session_id: str):
190
+ return client.verify.session(session_id)
191
+ ```
192
+
193
+ ## Event Types Reference
194
+
195
+ Shield Standard Event Taxonomy v1.0 — 37 event types across 7 categories:
196
+
197
+ ### Party Events
198
+ | Event Type | Description |
199
+ |---|---|
200
+ | `shield.party.joined` | A party joined the session |
201
+ | `shield.party.left` | A party left the session |
202
+ | `shield.party.identity.verified` | Party identity was verified |
203
+ | `shield.party.identity.failed` | Party identity verification failed |
204
+ | `shield.party.role.assigned` | A role was assigned to a party |
205
+
206
+ ### Session Events
207
+ | Event Type | Description |
208
+ |---|---|
209
+ | `shield.session.created` | Session was created |
210
+ | `shield.session.opened` | Session was opened |
211
+ | `shield.session.closed` | Session was closed |
212
+ | `shield.session.expired` | Session expired |
213
+ | `shield.session.archived` | Session was archived |
214
+
215
+ ### Content Events
216
+ | Event Type | Description |
217
+ |---|---|
218
+ | `shield.content.uploaded` | Content was uploaded |
219
+ | `shield.content.viewed` | Content was viewed |
220
+ | `shield.content.downloaded` | Content was downloaded |
221
+ | `shield.content.deleted` | Content was deleted |
222
+ | `shield.content.hash.verified` | Content hash was verified |
223
+
224
+ ### Negotiation Events
225
+ | Event Type | Description |
226
+ |---|---|
227
+ | `shield.negotiation.terms.proposed` | Terms were proposed |
228
+ | `shield.negotiation.terms.accepted` | Terms were accepted |
229
+ | `shield.negotiation.terms.rejected` | Terms were rejected |
230
+ | `shield.negotiation.terms.modified` | Terms were modified |
231
+ | `shield.negotiation.terms.expired` | Terms expired |
232
+ | `shield.negotiation.message.sent` | Negotiation message sent |
233
+ | `shield.negotiation.message.read` | Negotiation message read |
234
+
235
+ ### Agreement Events
236
+ | Event Type | Description |
237
+ |---|---|
238
+ | `shield.agreement.drafted` | Agreement was drafted |
239
+ | `shield.agreement.reviewed` | Agreement was reviewed |
240
+ | `shield.agreement.approved` | Agreement was approved |
241
+ | `shield.agreement.signed` | Agreement was signed |
242
+ | `shield.agreement.countersigned` | Agreement was countersigned |
243
+ | `shield.agreement.voided` | Agreement was voided |
244
+ | `shield.agreement.reached` | Agreement was reached |
245
+
246
+ ### Access Events
247
+ | Event Type | Description |
248
+ |---|---|
249
+ | `shield.access.granted` | Access was granted |
250
+ | `shield.access.revoked` | Access was revoked |
251
+ | `shield.access.attempted` | Access was attempted |
252
+ | `shield.access.denied` | Access was denied |
253
+
254
+ ### Disclosure Events
255
+ | Event Type | Description |
256
+ |---|---|
257
+ | `shield.disclosure.presented` | Disclosure was presented |
258
+ | `shield.disclosure.acknowledged` | Disclosure was acknowledged |
259
+ | `shield.disclosure.declined` | Disclosure was declined |
260
+
261
+ ### Evidence Events
262
+ | Event Type | Description |
263
+ |---|---|
264
+ | `shield.evidence.exported` | Evidence was exported |
265
+ | `shield.evidence.verified` | Evidence was verified |
266
+ | `shield.evidence.tampered_detected` | Evidence tampering was detected |
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ shield/__init__.py
5
+ shield/client.py
6
+ shield/exceptions.py
7
+ shield/resources/__init__.py
8
+ shield/resources/events.py
9
+ shield/resources/sessions.py
10
+ shield/resources/verify.py
11
+ shield_python.egg-info/PKG-INFO
12
+ shield_python.egg-info/SOURCES.txt
13
+ shield_python.egg-info/dependency_links.txt
14
+ shield_python.egg-info/requires.txt
15
+ shield_python.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ requests>=2.28.0