isitcredible 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,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: isitcredible
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the isitcredible.com academic peer-review API
5
+ Author: isitcredible.com
6
+ License: MIT
7
+ Project-URL: Homepage, https://isitcredible.com
8
+ Project-URL: Documentation, https://isitcredible.com/docs/api
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Topic :: Scientific/Engineering
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.8
14
+ Requires-Dist: requests>=2.28
@@ -0,0 +1,173 @@
1
+ # isitcredible — Python SDK
2
+
3
+ Python client for the [isitcredible.com](https://isitcredible.com) automated peer-review API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install isitcredible
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ from isitcredible import Client
15
+
16
+ client = Client("iic_your_api_key")
17
+ report = client.analyze("paper.pdf")
18
+ report.save("review.pdf")
19
+ ```
20
+
21
+ `analyze()` submits the document and polls until the report is ready, then returns a `Report` object. **Jobs take at least 20 minutes and may run longer** — the default timeout is 60 minutes.
22
+
23
+ ## API key
24
+
25
+ Generate a key from your [account page](https://isitcredible.com/account). Keys start with `iic_` and require a verified email address.
26
+
27
+ ```python
28
+ client = Client("iic_your_api_key")
29
+ ```
30
+
31
+ You can also read the key from an environment variable:
32
+
33
+ ```python
34
+ import os
35
+ from isitcredible import Client
36
+
37
+ client = Client(os.environ["IIC_API_KEY"])
38
+ ```
39
+
40
+ ## Report types
41
+
42
+ | Mode | Price | Description |
43
+ |------|-------|-------------|
44
+ | `standard` | $5.00 | Full peer review: methodological critique, identified issues, directions for future research. |
45
+ | `extended` | $7.00 | Everything in *standard*, plus an editorial response note and a proofreading pass. |
46
+
47
+ ```python
48
+ report = client.analyze("paper.pdf", mode="extended")
49
+ ```
50
+
51
+ ## Saving the report
52
+
53
+ ```python
54
+ report.save("review.pdf") # PDF (default)
55
+ report.save("review.txt", format="txt") # plain text
56
+ ```
57
+
58
+ Or get the raw bytes:
59
+
60
+ ```python
61
+ pdf_bytes = report.download()
62
+ ```
63
+
64
+ ## Handling timeouts
65
+
66
+ Because jobs are long-running, `analyze()` (and `wait()`) will raise `JobTimeoutError` if the job doesn't finish within the timeout. The job **keeps running on the server** — you can resume polling using the `job_id` attached to the exception:
67
+
68
+ ```python
69
+ from isitcredible import Client, JobTimeoutError
70
+
71
+ client = Client("iic_your_api_key")
72
+
73
+ try:
74
+ report = client.analyze("paper.pdf", timeout=3600) # 60 min
75
+ except JobTimeoutError as e:
76
+ print(f"Timed out. Job is still running: {e.job_id}")
77
+ # Resume later:
78
+ report = client.wait(e.job_id, timeout=3600)
79
+ report.save("review.pdf")
80
+ ```
81
+
82
+ If you lose track of a job ID, use `list_jobs()` to find it:
83
+
84
+ ```python
85
+ jobs = client.list_jobs()
86
+ # Each entry has 'uuid', 'status', 'title', 'mode', 'audit_date'
87
+ ```
88
+
89
+ ## Advanced: submit and poll separately
90
+
91
+ Use `submit()` and `wait()` directly if you need to fire-and-forget or manage polling yourself:
92
+
93
+ ```python
94
+ # Submit without blocking
95
+ job = client.submit("paper.pdf", mode="standard")
96
+ job_id = job["job_id"]
97
+ print(f"Submitted: {job_id}")
98
+
99
+ # ... do other things ...
100
+
101
+ # Resume polling later
102
+ report = client.wait(job_id, poll_interval=60, timeout=7200)
103
+ report.save("review.pdf")
104
+ ```
105
+
106
+ ## Checking your balance
107
+
108
+ ```python
109
+ balance = client.get_credits()
110
+ print(f"${balance['balance_usd']:.2f} remaining")
111
+ ```
112
+
113
+ ## Webhooks
114
+
115
+ Register a webhook to receive a POST when a job completes, instead of polling:
116
+
117
+ ```python
118
+ # Register (secret is shown once — save it)
119
+ hook = client.create_webhook("https://yourapp.com/webhook")
120
+ print(hook["secret"])
121
+
122
+ # Or pass a webhook URL per job
123
+ client.submit("paper.pdf", webhook_url="https://yourapp.com/webhook")
124
+
125
+ # Manage webhooks
126
+ hooks = client.list_webhooks()
127
+ client.delete_webhook(hooks[0]["id"])
128
+ ```
129
+
130
+ ## Full method reference
131
+
132
+ | Method | Description |
133
+ |--------|-------------|
134
+ | `analyze(file_path, mode, note, webhook_url, poll_interval, timeout)` | Submit + poll + return `Report`. Blocks until complete. |
135
+ | `submit(file_path, mode, note, webhook_url)` | Submit without waiting. Returns `dict` with `job_id`. |
136
+ | `wait(job_id, poll_interval, timeout)` | Poll an existing job to completion. Returns `Report`. |
137
+ | `get_job(job_id)` | Get current status and metadata for a job. |
138
+ | `list_jobs()` | List your last 50 jobs. |
139
+ | `get_credits()` | Get your current credit balance. |
140
+ | `create_webhook(url, event)` | Register a webhook. Returns signing secret (shown once). |
141
+ | `list_webhooks()` | List registered webhooks. |
142
+ | `delete_webhook(id)` | Delete a webhook. |
143
+
144
+ ### Report object
145
+
146
+ | Attribute | Description |
147
+ |-----------|-------------|
148
+ | `job_id` | Job UUID. |
149
+ | `status` | `completed` or `published`. |
150
+ | `mode` | `standard` or `extended`. |
151
+ | `title` | Paper title (extracted from document). |
152
+ | `authors` | Authors (extracted from document). |
153
+ | `discipline` | Academic discipline (extracted from document). |
154
+ | `report_url` | Direct URL to download the report. |
155
+ | `.download(format)` | Return report as bytes (`"pdf"` or `"txt"`). |
156
+ | `.save(path, format)` | Write report to a file. |
157
+
158
+ ## Errors
159
+
160
+ | Exception | Description |
161
+ |-----------|-------------|
162
+ | `IsItCredibleError` | Base class. Has `.status_code` and `.detail`. |
163
+ | `JobTimeoutError` | Polling timed out. Has `.job_id` — use it to resume with `wait()`. |
164
+
165
+ HTTP errors map to `IsItCredibleError` with the relevant status code:
166
+
167
+ | Code | Meaning |
168
+ |------|---------|
169
+ | `401` | Missing or invalid API key. |
170
+ | `402` | Insufficient balance. |
171
+ | `403` | Email not verified. |
172
+ | `404` | Job not found. |
173
+ | `400` | Invalid input (file too large, wrong format, etc.). |
@@ -0,0 +1,4 @@
1
+ from .client import Client, IsItCredibleError, JobTimeoutError
2
+
3
+ __all__ = ["Client", "IsItCredibleError", "JobTimeoutError"]
4
+ __version__ = "0.1.0"
@@ -0,0 +1,259 @@
1
+ """
2
+ isitcredible — Python SDK
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
4
+
5
+ Usage:
6
+ from isitcredible import Client
7
+
8
+ client = Client("iic_your_api_key")
9
+ report = client.analyze("paper.pdf")
10
+ report.save("review.pdf")
11
+ """
12
+ import os
13
+ import time
14
+ from pathlib import Path
15
+ from typing import Optional, Union
16
+
17
+ import requests
18
+
19
+
20
+ API_BASE = "https://isitcredible.com/api/v1"
21
+
22
+
23
+ class IsItCredibleError(Exception):
24
+ """Base exception for API errors."""
25
+ def __init__(self, message: str, status_code: int = None, detail: str = None):
26
+ self.status_code = status_code
27
+ self.detail = detail or message
28
+ super().__init__(message)
29
+
30
+
31
+ class JobTimeoutError(IsItCredibleError):
32
+ """Raised when polling exceeds the timeout."""
33
+ pass
34
+
35
+
36
+ class Report:
37
+ """Represents a completed analysis report."""
38
+
39
+ def __init__(self, job: dict, client: "Client"):
40
+ self.job_id: str = job["uuid"]
41
+ self.status: str = job["status"]
42
+ self.mode: str = job.get("mode", "standard")
43
+ self.title: str = job.get("title")
44
+ self.authors: str = job.get("authors")
45
+ self.discipline: str = job.get("discipline")
46
+ self.report_url: str = job.get("report_url")
47
+ self._client = client
48
+
49
+ def download(self, format: str = "pdf") -> bytes:
50
+ """Download the report as bytes."""
51
+ if not self.report_url:
52
+ raise IsItCredibleError("No report available (job may not be completed).")
53
+ resp = self._client._get(f"/jobs/{self.job_id}/report", params={"format": format},
54
+ stream=True)
55
+ return resp.content
56
+
57
+ def save(self, path: Union[str, Path], format: str = "pdf"):
58
+ """Download and save the report to a file."""
59
+ data = self.download(format=format)
60
+ Path(path).write_bytes(data)
61
+
62
+ def __repr__(self):
63
+ return f"<Report job_id={self.job_id!r} status={self.status!r} title={self.title!r}>"
64
+
65
+
66
+ class Client:
67
+ """
68
+ isitcredible.com API client.
69
+
70
+ Args:
71
+ api_key: Your API key (starts with ``iic_``).
72
+ base_url: Override the API base URL (for testing).
73
+ timeout: HTTP request timeout in seconds.
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ api_key: str,
79
+ base_url: str = None,
80
+ timeout: int = 30,
81
+ ):
82
+ if not api_key:
83
+ raise IsItCredibleError("API key is required.")
84
+ self._api_key = api_key
85
+ self._base_url = (base_url or API_BASE).rstrip("/")
86
+ self._timeout = timeout
87
+ self._session = requests.Session()
88
+ self._session.headers.update({
89
+ "Authorization": f"Bearer {api_key}",
90
+ "User-Agent": "isitcredible-python/0.1.0",
91
+ })
92
+
93
+ # -----------------------------------------------------------------
94
+ # Core high-level method
95
+ # -----------------------------------------------------------------
96
+
97
+ def analyze(
98
+ self,
99
+ file_path: Union[str, Path],
100
+ mode: str = "standard",
101
+ note: str = "",
102
+ webhook_url: str = "",
103
+ poll_interval: int = 15,
104
+ timeout: int = 3600,
105
+ ) -> Report:
106
+ """
107
+ Submit a document for analysis and wait for the result.
108
+
109
+ Args:
110
+ file_path: Path to the PDF, TXT, or MD file.
111
+ mode: ``"standard"`` or ``"extended"``.
112
+ note: Optional note for the reviewer.
113
+ webhook_url: Optional URL to receive a POST when the job completes.
114
+ poll_interval: Seconds between status checks (default 15).
115
+ timeout: Maximum seconds to wait (default 3600 = 60 min).
116
+
117
+ Returns:
118
+ A :class:`Report` object with the completed analysis.
119
+
120
+ Raises:
121
+ IsItCredibleError: On API errors.
122
+ JobTimeoutError: If the job doesn't finish within ``timeout``.
123
+ FileNotFoundError: If ``file_path`` doesn't exist.
124
+ """
125
+ job = self.submit(file_path, mode=mode, note=note, webhook_url=webhook_url)
126
+ job_id = job["job_id"]
127
+ return self.wait(job_id, poll_interval=poll_interval, timeout=timeout)
128
+
129
+ # -----------------------------------------------------------------
130
+ # Low-level methods (for advanced users / async pipelines)
131
+ # -----------------------------------------------------------------
132
+
133
+ def submit(
134
+ self,
135
+ file_path: Union[str, Path],
136
+ mode: str = "standard",
137
+ note: str = "",
138
+ webhook_url: str = "",
139
+ ) -> dict:
140
+ """
141
+ Submit a file for analysis without waiting.
142
+
143
+ Returns:
144
+ dict with ``job_id``, ``status``, ``mode``, ``message``.
145
+ """
146
+ file_path = Path(file_path)
147
+ if not file_path.exists():
148
+ raise FileNotFoundError(f"File not found: {file_path}")
149
+
150
+ with open(file_path, "rb") as f:
151
+ data = {"mode": mode, "user_note": note}
152
+ if webhook_url:
153
+ data["webhook_url"] = webhook_url
154
+ resp = self._post(
155
+ "/jobs",
156
+ files={"main_file": (file_path.name, f)},
157
+ data=data,
158
+ )
159
+ return resp.json()
160
+
161
+ def wait(self, job_id: str, poll_interval: int = 15, timeout: int = 3600) -> Report:
162
+ """
163
+ Poll a job until completion.
164
+
165
+ Args:
166
+ job_id: The job UUID returned by :meth:`submit`.
167
+ poll_interval: Seconds between polls.
168
+ timeout: Maximum wait time in seconds.
169
+
170
+ Returns:
171
+ A :class:`Report` object.
172
+ """
173
+ deadline = time.time() + timeout
174
+
175
+ while True:
176
+ job = self.get_job(job_id)
177
+ status = job.get("status", "")
178
+
179
+ if status in ("completed", "published"):
180
+ return Report(job, self)
181
+
182
+ if status == "failed":
183
+ raise IsItCredibleError(
184
+ f"Job {job_id} failed.",
185
+ detail=job.get("admin_notes", ""),
186
+ )
187
+
188
+ if time.time() > deadline:
189
+ err = JobTimeoutError(
190
+ f"Job {job_id} did not complete within {timeout}s (status: {status})."
191
+ )
192
+ err.job_id = job_id
193
+ raise err
194
+
195
+ time.sleep(poll_interval)
196
+
197
+ def get_job(self, job_id: str) -> dict:
198
+ """Get status and metadata for a single job."""
199
+ return self._get(f"/jobs/{job_id}").json()
200
+
201
+ def list_jobs(self) -> list:
202
+ """List your recent jobs."""
203
+ return self._get("/jobs").json()
204
+
205
+ def get_credits(self) -> dict:
206
+ """Get your current credit balance."""
207
+ return self._get("/credits").json()
208
+
209
+ # -- Webhook management --
210
+
211
+ def create_webhook(self, url: str, event: str = "job.completed") -> dict:
212
+ """Register a default webhook URL. Returns the signing secret (save it!)."""
213
+ return self._post("/webhooks", data={"url": url, "event": event}).json()
214
+
215
+ def list_webhooks(self) -> list:
216
+ """List your registered webhooks."""
217
+ return self._get("/webhooks").json()
218
+
219
+ def delete_webhook(self, webhook_id: int) -> dict:
220
+ """Delete a webhook by ID."""
221
+ return self._delete(f"/webhooks/{webhook_id}").json()
222
+
223
+ # -----------------------------------------------------------------
224
+ # HTTP helpers
225
+ # -----------------------------------------------------------------
226
+
227
+ def _get(self, path: str, params: dict = None, stream: bool = False) -> requests.Response:
228
+ resp = self._session.get(
229
+ f"{self._base_url}{path}",
230
+ params=params,
231
+ timeout=self._timeout,
232
+ stream=stream,
233
+ )
234
+ self._check(resp)
235
+ return resp
236
+
237
+ def _post(self, path: str, **kwargs) -> requests.Response:
238
+ kwargs.setdefault("timeout", self._timeout)
239
+ resp = self._session.post(f"{self._base_url}{path}", **kwargs)
240
+ self._check(resp)
241
+ return resp
242
+
243
+ def _delete(self, path: str) -> requests.Response:
244
+ resp = self._session.delete(f"{self._base_url}{path}", timeout=self._timeout)
245
+ self._check(resp)
246
+ return resp
247
+
248
+ @staticmethod
249
+ def _check(resp: requests.Response):
250
+ if resp.status_code >= 400:
251
+ try:
252
+ detail = resp.json().get("detail", resp.text)
253
+ except Exception:
254
+ detail = resp.text
255
+ raise IsItCredibleError(
256
+ f"HTTP {resp.status_code}: {detail}",
257
+ status_code=resp.status_code,
258
+ detail=detail,
259
+ )
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: isitcredible
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the isitcredible.com academic peer-review API
5
+ Author: isitcredible.com
6
+ License: MIT
7
+ Project-URL: Homepage, https://isitcredible.com
8
+ Project-URL: Documentation, https://isitcredible.com/docs/api
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Topic :: Scientific/Engineering
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.8
14
+ Requires-Dist: requests>=2.28
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ isitcredible/__init__.py
4
+ isitcredible/client.py
5
+ isitcredible.egg-info/PKG-INFO
6
+ isitcredible.egg-info/SOURCES.txt
7
+ isitcredible.egg-info/dependency_links.txt
8
+ isitcredible.egg-info/requires.txt
9
+ isitcredible.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ requests>=2.28
@@ -0,0 +1,2 @@
1
+ dist
2
+ isitcredible
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "isitcredible"
7
+ version = "0.1.0"
8
+ description = "Python SDK for the isitcredible.com academic peer-review API"
9
+ requires-python = ">=3.8"
10
+ license = {text = "MIT"}
11
+ authors = [{name = "isitcredible.com"}]
12
+ dependencies = ["requests>=2.28"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Science/Research",
16
+ "Topic :: Scientific/Engineering",
17
+ "Programming Language :: Python :: 3",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://isitcredible.com"
22
+ Documentation = "https://isitcredible.com/docs/api"
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["."]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+