xproof 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.
xproof/__init__.py ADDED
@@ -0,0 +1,44 @@
1
+ """xProof Python SDK — blockchain proof-of-existence on MultiversX."""
2
+
3
+ from .client import XProofClient
4
+ from .exceptions import (
5
+ AuthenticationError,
6
+ ConflictError,
7
+ NotFoundError,
8
+ RateLimitError,
9
+ ServerError,
10
+ ValidationError,
11
+ XProofError,
12
+ )
13
+ from .models import (
14
+ BatchResult,
15
+ BatchResultSummary,
16
+ Certification,
17
+ PricingInfo,
18
+ PricingTier,
19
+ RegistrationResult,
20
+ TrialInfo,
21
+ )
22
+ from .utils import hash_bytes, hash_file
23
+
24
+ __version__ = "0.1.0"
25
+
26
+ __all__ = [
27
+ "XProofClient",
28
+ "hash_file",
29
+ "hash_bytes",
30
+ "Certification",
31
+ "BatchResult",
32
+ "BatchResultSummary",
33
+ "PricingInfo",
34
+ "PricingTier",
35
+ "RegistrationResult",
36
+ "TrialInfo",
37
+ "XProofError",
38
+ "AuthenticationError",
39
+ "ConflictError",
40
+ "NotFoundError",
41
+ "RateLimitError",
42
+ "ServerError",
43
+ "ValidationError",
44
+ ]
xproof/client.py ADDED
@@ -0,0 +1,384 @@
1
+ """Main client for the xProof API."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Dict, List, Optional, Union
5
+
6
+ import requests
7
+
8
+ from .exceptions import (
9
+ AuthenticationError,
10
+ ConflictError,
11
+ NotFoundError,
12
+ RateLimitError,
13
+ ServerError,
14
+ ValidationError,
15
+ XProofError,
16
+ )
17
+ from .models import BatchResult, Certification, PricingInfo, RegistrationResult
18
+ from .utils import hash_file
19
+
20
+ __version__ = "0.1.0"
21
+
22
+ DEFAULT_BASE_URL = "https://xproof.app"
23
+ DEFAULT_TIMEOUT = 30
24
+
25
+
26
+ class XProofClient:
27
+ """Client for interacting with the xProof API.
28
+
29
+ Args:
30
+ api_key: Your xProof API key (starts with ``pm_``).
31
+ Pass an empty string or omit if you plan to call
32
+ :meth:`register` first.
33
+ base_url: Override the API base URL (default: ``https://xproof.app``).
34
+ timeout: Request timeout in seconds (default: 30).
35
+
36
+ Example::
37
+
38
+ from xproof import XProofClient
39
+
40
+ client = XProofClient(api_key="pm_...")
41
+ cert = client.certify("report.pdf", author="Alice")
42
+ print(cert.transaction_url)
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ api_key: str = "",
48
+ base_url: str = DEFAULT_BASE_URL,
49
+ timeout: int = DEFAULT_TIMEOUT,
50
+ ) -> None:
51
+ self.api_key = api_key
52
+ self.base_url = base_url.rstrip("/")
53
+ self.timeout = timeout
54
+
55
+ self._session = requests.Session()
56
+ self._session.headers.update(
57
+ {
58
+ "Content-Type": "application/json",
59
+ "User-Agent": f"xproof-python/{__version__}",
60
+ }
61
+ )
62
+ if api_key:
63
+ self._session.headers["Authorization"] = f"Bearer {api_key}"
64
+
65
+ def _request(
66
+ self,
67
+ method: str,
68
+ path: str,
69
+ json: Optional[Dict[str, Any]] = None,
70
+ params: Optional[Dict[str, Any]] = None,
71
+ auth_required: bool = True,
72
+ ) -> Dict[str, Any]:
73
+ """Send an HTTP request to the xProof API and return parsed JSON."""
74
+ url = f"{self.base_url}{path}"
75
+
76
+ headers: Optional[Dict[str, str]] = None
77
+ if not auth_required:
78
+ headers = {"Authorization": ""}
79
+
80
+ try:
81
+ resp = self._session.request(
82
+ method,
83
+ url,
84
+ json=json,
85
+ params=params,
86
+ timeout=self.timeout,
87
+ headers=headers,
88
+ )
89
+ except requests.RequestException as exc:
90
+ raise XProofError(f"Request failed: {exc}") from exc
91
+
92
+ if resp.status_code in (200, 201):
93
+ try:
94
+ return resp.json() # type: ignore[no-any-return]
95
+ except ValueError as exc:
96
+ raise XProofError(
97
+ f"Unexpected non-JSON response from {method} {url}: "
98
+ f"{resp.text[:200]}"
99
+ ) from exc
100
+
101
+ self._handle_error(resp)
102
+ return {}
103
+
104
+ @staticmethod
105
+ def _handle_error(resp: requests.Response) -> None:
106
+ """Raise an appropriate exception based on the HTTP response."""
107
+ try:
108
+ body = resp.json()
109
+ except ValueError:
110
+ body = {"message": resp.text}
111
+
112
+ message = body.get("message", body.get("error", resp.text))
113
+ status = resp.status_code
114
+
115
+ if status == 400:
116
+ raise ValidationError(message, response=body)
117
+ if status in (401, 403):
118
+ raise AuthenticationError(message, response=body)
119
+ if status == 404:
120
+ raise NotFoundError(message, response=body)
121
+ if status == 409:
122
+ raise ConflictError(
123
+ message,
124
+ certification_id=body.get("certificationId", ""),
125
+ response=body,
126
+ )
127
+ if status == 429:
128
+ raise RateLimitError(message, response=body)
129
+ if status >= 500:
130
+ raise ServerError(message, status_code=status, response=body)
131
+
132
+ raise XProofError(message, status_code=status, response=body)
133
+
134
+ @classmethod
135
+ def register(
136
+ cls,
137
+ agent_name: str,
138
+ base_url: str = DEFAULT_BASE_URL,
139
+ timeout: int = DEFAULT_TIMEOUT,
140
+ ) -> "XProofClient":
141
+ """Register a new agent and return a configured client.
142
+
143
+ This is the zero-friction entry point: no wallet, no payment, no
144
+ browser. The returned client is already authenticated with the
145
+ trial API key and ready to certify.
146
+
147
+ Args:
148
+ agent_name: A human-readable name for the agent.
149
+ base_url: Override the API base URL.
150
+ timeout: Request timeout in seconds.
151
+
152
+ Returns:
153
+ A new :class:`XProofClient` already configured with the
154
+ trial API key. Access registration details via
155
+ ``client.registration``.
156
+
157
+ Example::
158
+
159
+ client = XProofClient.register("my-research-agent")
160
+ print(client.registration.trial.remaining) # 10
161
+ cert = client.certify_hash(...)
162
+ """
163
+ temp = cls(api_key="", base_url=base_url, timeout=timeout)
164
+ data = temp._request(
165
+ "POST",
166
+ "/api/agent/register",
167
+ json={"agent_name": agent_name},
168
+ auth_required=False,
169
+ )
170
+ result = RegistrationResult.from_dict(data)
171
+ new_client = cls(api_key=result.api_key, base_url=base_url, timeout=timeout)
172
+ new_client.registration = result
173
+ return new_client
174
+
175
+ registration: Optional[RegistrationResult] = None
176
+
177
+ def certify(
178
+ self,
179
+ path: Union[str, Path],
180
+ author: str,
181
+ file_name: Optional[str] = None,
182
+ *,
183
+ who: Optional[str] = None,
184
+ what: Optional[str] = None,
185
+ when: Optional[str] = None,
186
+ why: Optional[str] = None,
187
+ metadata: Optional[Dict[str, Any]] = None,
188
+ ) -> Certification:
189
+ """Certify a file by path. The file is hashed locally (SHA-256).
190
+
191
+ Supports the xProof 4W framework via optional keyword arguments.
192
+
193
+ Args:
194
+ path: Path to the file to certify.
195
+ author: Author / owner name for the certification.
196
+ file_name: Override the file name recorded on-chain
197
+ (defaults to the basename of *path*).
198
+ who: 4W — agent identity.
199
+ what: 4W — action hash or description.
200
+ when: 4W — ISO-8601 timestamp.
201
+ why: 4W — instruction or reason.
202
+ metadata: Arbitrary key-value metadata stored alongside the proof.
203
+
204
+ Returns:
205
+ A :class:`Certification` with the on-chain proof details.
206
+ """
207
+ if not self.api_key:
208
+ raise ValueError("api_key is required — call register() or pass an api_key")
209
+
210
+ p = Path(path)
211
+ file_hash = hash_file(p)
212
+ resolved_name = file_name or p.name
213
+
214
+ return self.certify_hash(
215
+ file_hash=file_hash,
216
+ file_name=resolved_name,
217
+ author=author,
218
+ who=who,
219
+ what=what,
220
+ when=when,
221
+ why=why,
222
+ metadata=metadata,
223
+ )
224
+
225
+ def certify_hash(
226
+ self,
227
+ file_hash: str,
228
+ file_name: str,
229
+ author: str,
230
+ *,
231
+ who: Optional[str] = None,
232
+ what: Optional[str] = None,
233
+ when: Optional[str] = None,
234
+ why: Optional[str] = None,
235
+ metadata: Optional[Dict[str, Any]] = None,
236
+ ) -> Certification:
237
+ """Certify a file using a pre-computed SHA-256 hash.
238
+
239
+ Supports the xProof 4W framework via optional keyword arguments:
240
+
241
+ - **who**: Agent identity (wallet, name, or ID)
242
+ - **what**: Action hash or description being certified
243
+ - **when**: ISO-8601 timestamp of the action
244
+ - **why**: Instruction hash, goal, or reason for the action
245
+
246
+ Any additional key-value pairs can be passed via *metadata*.
247
+
248
+ Args:
249
+ file_hash: 64-character lowercase hex SHA-256 digest.
250
+ file_name: Original file name.
251
+ author: Author / owner name.
252
+ who: 4W — agent identity.
253
+ what: 4W — action hash or description.
254
+ when: 4W — ISO-8601 timestamp.
255
+ why: 4W — instruction or reason.
256
+ metadata: Arbitrary key-value metadata stored alongside the proof.
257
+
258
+ Returns:
259
+ A :class:`Certification` with the on-chain proof details.
260
+ """
261
+ if not self.api_key:
262
+ raise ValueError("api_key is required — call register() or pass an api_key")
263
+
264
+ proof_metadata: Dict[str, Any] = dict(metadata) if metadata else {}
265
+ if who is not None:
266
+ proof_metadata["who"] = who
267
+ if what is not None:
268
+ proof_metadata["what"] = what
269
+ if when is not None:
270
+ proof_metadata["when"] = when
271
+ if why is not None:
272
+ proof_metadata["why"] = why
273
+
274
+ payload: Dict[str, Any] = {
275
+ "filename": file_name,
276
+ "file_hash": file_hash,
277
+ "author_name": author,
278
+ }
279
+ if proof_metadata:
280
+ payload["metadata"] = proof_metadata
281
+ data = self._request("POST", "/api/proof", json=payload)
282
+ return Certification.from_dict(data)
283
+
284
+ def batch_certify(
285
+ self,
286
+ files: List[Dict[str, Any]],
287
+ ) -> BatchResult:
288
+ """Certify multiple files in a single API call (up to 50).
289
+
290
+ Each entry in *files* must contain:
291
+
292
+ - ``path`` (str): Path to the file **or**
293
+ - ``file_hash`` (str): Pre-computed SHA-256 hex hash
294
+ - ``file_name`` (str): File name (required when using ``file_hash``)
295
+ - ``author`` (str): Author name (applied to the entire batch)
296
+
297
+ Args:
298
+ files: List of file descriptors.
299
+
300
+ Returns:
301
+ A :class:`BatchResult` with individual results and a summary.
302
+
303
+ Raises:
304
+ ValueError: If more than 50 files are provided.
305
+ """
306
+ if not self.api_key:
307
+ raise ValueError("api_key is required — call register() or pass an api_key")
308
+
309
+ if len(files) > 50:
310
+ raise ValueError("Batch certification supports a maximum of 50 files")
311
+
312
+ entries: List[Dict[str, Any]] = []
313
+ for f in files:
314
+ if "path" in f:
315
+ p = Path(f["path"])
316
+ fhash = hash_file(p)
317
+ fname = f.get("file_name", p.name)
318
+ elif "file_hash" in f:
319
+ fhash = f["file_hash"]
320
+ fname = f.get("file_name", "unknown")
321
+ else:
322
+ raise ValueError("Each file entry must contain either 'path' or 'file_hash'")
323
+
324
+ entry: Dict[str, Any] = {
325
+ "filename": fname,
326
+ "file_hash": fhash,
327
+ }
328
+ if f.get("metadata"):
329
+ entry["metadata"] = f["metadata"]
330
+ entries.append(entry)
331
+
332
+ payload: Dict[str, Any] = {"files": entries}
333
+ author = None
334
+ for f in files:
335
+ if f.get("author"):
336
+ author = f["author"]
337
+ break
338
+ if author:
339
+ payload["author_name"] = author
340
+
341
+ data = self._request("POST", "/api/batch", json=payload)
342
+ return BatchResult.from_dict(data)
343
+
344
+ def verify(self, proof_id: str) -> Certification:
345
+ """Retrieve a certification by its proof ID.
346
+
347
+ This is a public endpoint and does not require authentication.
348
+
349
+ Args:
350
+ proof_id: The certification / proof UUID.
351
+
352
+ Returns:
353
+ A :class:`Certification` with the proof details.
354
+ """
355
+ data = self._request("GET", f"/api/proof/{proof_id}", auth_required=False)
356
+ return Certification.from_dict(data)
357
+
358
+ def verify_hash(self, file_hash: str) -> Certification:
359
+ """Look up a certification by its SHA-256 file hash.
360
+
361
+ This is a public endpoint and does not require authentication.
362
+
363
+ Args:
364
+ file_hash: The 64-character hex SHA-256 digest.
365
+
366
+ Returns:
367
+ A :class:`Certification` with the proof details.
368
+
369
+ Raises:
370
+ NotFoundError: If no certification exists for this hash.
371
+ """
372
+ data = self._request("GET", f"/api/proof/hash/{file_hash}", auth_required=False)
373
+ return Certification.from_dict(data)
374
+
375
+ def get_pricing(self) -> PricingInfo:
376
+ """Retrieve current pricing information.
377
+
378
+ This is a public endpoint and does not require authentication.
379
+
380
+ Returns:
381
+ A :class:`PricingInfo` with tier details and payment methods.
382
+ """
383
+ data = self._request("GET", "/api/pricing", auth_required=False)
384
+ return PricingInfo.from_dict(data)
xproof/exceptions.py ADDED
@@ -0,0 +1,54 @@
1
+ """Exception classes for the xProof SDK."""
2
+
3
+
4
+ class XProofError(Exception):
5
+ """Base exception for all xProof SDK errors."""
6
+
7
+ def __init__(self, message: str, status_code: int = 0, response: object = None) -> None:
8
+ super().__init__(message)
9
+ self.message = message
10
+ self.status_code = status_code
11
+ self.response = response
12
+
13
+
14
+ class AuthenticationError(XProofError):
15
+ """Raised when API key authentication fails (401/403)."""
16
+
17
+ def __init__(self, message: str = "Invalid or missing API key", response: object = None) -> None:
18
+ super().__init__(message, status_code=401, response=response)
19
+
20
+
21
+ class NotFoundError(XProofError):
22
+ """Raised when a requested resource is not found (404)."""
23
+
24
+ def __init__(self, message: str = "Resource not found", response: object = None) -> None:
25
+ super().__init__(message, status_code=404, response=response)
26
+
27
+
28
+ class ValidationError(XProofError):
29
+ """Raised when the request body fails server-side validation (400)."""
30
+
31
+ def __init__(self, message: str = "Invalid request data", response: object = None) -> None:
32
+ super().__init__(message, status_code=400, response=response)
33
+
34
+
35
+ class ConflictError(XProofError):
36
+ """Raised when the file hash has already been certified (409)."""
37
+
38
+ def __init__(self, message: str = "File already certified", certification_id: str = "", response: object = None) -> None:
39
+ super().__init__(message, status_code=409, response=response)
40
+ self.certification_id = certification_id
41
+
42
+
43
+ class RateLimitError(XProofError):
44
+ """Raised when rate limits are exceeded (429)."""
45
+
46
+ def __init__(self, message: str = "Rate limit exceeded", response: object = None) -> None:
47
+ super().__init__(message, status_code=429, response=response)
48
+
49
+
50
+ class ServerError(XProofError):
51
+ """Raised when the server returns a 5xx error."""
52
+
53
+ def __init__(self, message: str = "Internal server error", status_code: int = 500, response: object = None) -> None:
54
+ super().__init__(message, status_code=status_code, response=response)
@@ -0,0 +1,13 @@
1
+ try:
2
+ from .langchain import XProofCallbackHandler
3
+ except ImportError:
4
+ pass
5
+
6
+ try:
7
+ from .crewai import XProofCertifyTool, XProofTool, XProofCrewCallback
8
+ try:
9
+ from .crewai import XProofCrewTool
10
+ except ImportError:
11
+ pass
12
+ except ImportError:
13
+ pass