corent 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.
corent/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """Corent Python SDK — one API for AI image, video, and voice.
2
+
3
+ from corent import Corent
4
+
5
+ client = Corent("co_live_...")
6
+ image = client.images.generate("a lighthouse at dusk", tier="premium")
7
+ print(image.url, image.width, image.height, image.cost_cents)
8
+ """
9
+
10
+ from .client import Corent
11
+ from .errors import (
12
+ CorentError,
13
+ InsufficientBalanceError,
14
+ InvalidRequestError,
15
+ RateLimitedError,
16
+ )
17
+ from .types import GeneratedImage, GeneratedSpeech, GeneratedVideo, Job
18
+
19
+ __all__ = [
20
+ "Corent",
21
+ "CorentError",
22
+ "InsufficientBalanceError",
23
+ "InvalidRequestError",
24
+ "RateLimitedError",
25
+ "GeneratedImage",
26
+ "GeneratedVideo",
27
+ "GeneratedSpeech",
28
+ "Job",
29
+ ]
30
+
31
+ __version__ = "0.1.0"
corent/client.py ADDED
@@ -0,0 +1,236 @@
1
+ from __future__ import annotations
2
+
3
+ """The Corent client. Hides everything the raw HTTP caller has to hand-roll:
4
+ auth headers, idempotency keys, 429/5xx retries with backoff, and job polling.
5
+
6
+ Design choices worth knowing:
7
+
8
+ - Image generation submits with async=true and polls the job. A finished
9
+ render can then never be lost to a client-side timeout — the job id exists
10
+ from the first response, and the SDK just resumes polling.
11
+ - Every generate call sends an auto-generated Idempotency-Key, so the SDK's
12
+ own retries (and yours) can never double-charge.
13
+ - Failed generations are never billed by the API; the SDK surfaces them as
14
+ GenerationFailedError with the API's sanitized error code.
15
+ """
16
+
17
+ import time
18
+ import uuid
19
+
20
+ import httpx
21
+
22
+ from .errors import (
23
+ CorentError,
24
+ GenerationFailedError,
25
+ InsufficientBalanceError,
26
+ InvalidRequestError,
27
+ RateLimitedError,
28
+ )
29
+ from .types import GeneratedImage, GeneratedSpeech, GeneratedVideo, Job
30
+
31
+ DEFAULT_BASE_URL = "https://api.corent.tech"
32
+ MAX_RETRIES = 3
33
+ POLL_INTERVAL_S = 3.0
34
+ DEFAULT_WAIT_TIMEOUT_S = 900 # videos can take minutes; images far less
35
+
36
+
37
+ class Corent:
38
+ def __init__(
39
+ self,
40
+ api_key: str,
41
+ base_url: str = DEFAULT_BASE_URL,
42
+ timeout: float = 60.0,
43
+ ):
44
+ self._http = httpx.Client(
45
+ base_url=base_url,
46
+ timeout=timeout,
47
+ headers={
48
+ "Authorization": f"Bearer {api_key}",
49
+ "User-Agent": "corent-python/0.1.0",
50
+ },
51
+ )
52
+ self.images = _Images(self)
53
+ self.videos = _Videos(self)
54
+ self.speech = _Speech(self)
55
+ self.jobs = _Jobs(self)
56
+
57
+ # -- account & discovery -------------------------------------------------
58
+
59
+ def balance_cents(self) -> int:
60
+ return self._request("GET", "/v1/account/balance")["balance_cents"]
61
+
62
+ def tiers(self) -> list[dict]:
63
+ """The live tier catalog with honest price ranges. Public endpoint."""
64
+ return self._request("GET", "/v1/tiers")["tiers"]
65
+
66
+ def close(self) -> None:
67
+ self._http.close()
68
+
69
+ def __enter__(self):
70
+ return self
71
+
72
+ def __exit__(self, *exc):
73
+ self.close()
74
+
75
+ # -- plumbing ------------------------------------------------------------
76
+
77
+ def _request(self, method: str, path: str, json: dict | None = None,
78
+ idempotent: bool = False) -> dict:
79
+ headers = {}
80
+ if idempotent:
81
+ headers["Idempotency-Key"] = str(uuid.uuid4())
82
+ last_exc: Exception | None = None
83
+ for attempt in range(MAX_RETRIES + 1):
84
+ try:
85
+ resp = self._http.request(method, path, json=json, headers=headers)
86
+ except httpx.TransportError as exc:
87
+ last_exc = exc
88
+ time.sleep(min(2**attempt, 8))
89
+ continue
90
+ if resp.status_code < 400:
91
+ return resp.json()
92
+ if resp.status_code == 429:
93
+ retry_after = float(resp.headers.get("Retry-After", 2 ** (attempt + 1)))
94
+ if attempt < MAX_RETRIES:
95
+ time.sleep(min(retry_after, 30))
96
+ continue
97
+ raise RateLimitedError("rate limited", retry_after_s=retry_after)
98
+ if resp.status_code in (502, 503) and attempt < MAX_RETRIES:
99
+ time.sleep(min(2 ** (attempt + 1), 8))
100
+ continue
101
+ self._raise_for(resp)
102
+ raise CorentError(f"request failed after {MAX_RETRIES} retries: {last_exc}")
103
+
104
+ @staticmethod
105
+ def _raise_for(resp: httpx.Response) -> None:
106
+ try:
107
+ detail = resp.json().get("detail", resp.text)
108
+ except Exception:
109
+ detail = resp.text
110
+ message = detail if isinstance(detail, str) else str(detail)
111
+ if resp.status_code == 402:
112
+ raise InsufficientBalanceError(message, 402)
113
+ if resp.status_code in (400, 422):
114
+ raise InvalidRequestError(message, resp.status_code)
115
+ raise CorentError(message, resp.status_code)
116
+
117
+ def _wait(self, job_id: str, timeout_s: float) -> dict:
118
+ deadline = time.monotonic() + timeout_s
119
+ while time.monotonic() < deadline:
120
+ job = self._request("GET", f"/v1/jobs/{job_id}")
121
+ if job["status"] == "completed":
122
+ return job
123
+ if job["status"] == "failed":
124
+ raise GenerationFailedError(job.get("error") or "generation_failed")
125
+ time.sleep(POLL_INTERVAL_S)
126
+ raise CorentError(f"job {job_id} still processing after {timeout_s:.0f}s; "
127
+ f"poll client.jobs.get('{job_id}') to resume")
128
+
129
+
130
+ class _Images:
131
+ def __init__(self, client: Corent):
132
+ self._c = client
133
+
134
+ def generate(
135
+ self,
136
+ prompt: str,
137
+ tier: str | None = None,
138
+ style: str | None = None,
139
+ aspect_ratio: str = "1:1",
140
+ wait: bool = True,
141
+ timeout_s: float = DEFAULT_WAIT_TIMEOUT_S,
142
+ ) -> "GeneratedImage | Job":
143
+ """Render an image. tier: air | lite | premium | pro | max_pro.
144
+ wait=False returns the Job immediately; poll with client.jobs."""
145
+ body = {"prompt": prompt, "aspect_ratio": aspect_ratio, "async": True}
146
+ if tier:
147
+ body["tier"] = tier
148
+ if style:
149
+ body["style"] = style
150
+ submitted = self._c._request("POST", "/v1/images/generate", json=body, idempotent=True)
151
+ if submitted["status"] == "completed": # idempotent replay of a finished job
152
+ return _image_from_job(submitted)
153
+ if not wait:
154
+ return Job(id=submitted["id"], status=submitted["status"], raw=submitted)
155
+ return _image_from_job(self._c._wait(submitted["id"], timeout_s))
156
+
157
+
158
+ class _Videos:
159
+ def __init__(self, client: Corent):
160
+ self._c = client
161
+
162
+ def generate(
163
+ self,
164
+ prompt: str,
165
+ tier: str | None = None,
166
+ aspect_ratio: str = "16:9",
167
+ duration_s: int | None = None,
168
+ resolution: str | None = None,
169
+ image_url: str | None = None,
170
+ wait: bool = True,
171
+ timeout_s: float = DEFAULT_WAIT_TIMEOUT_S,
172
+ ) -> "GeneratedVideo | Job":
173
+ """Render a video (1-5 minutes typical). resolution: 720p | 1080p | 4k
174
+ (tier-capped; clamped down, never rejected). image_url animates an
175
+ existing image instead of starting from text."""
176
+ body: dict = {"prompt": prompt, "aspect_ratio": aspect_ratio}
177
+ if tier:
178
+ body["tier"] = tier
179
+ if duration_s is not None:
180
+ body["duration_s"] = duration_s
181
+ if resolution:
182
+ body["resolution"] = resolution
183
+ if image_url:
184
+ body["image_url"] = image_url
185
+ submitted = self._c._request("POST", "/v1/videos/generate", json=body, idempotent=True)
186
+ if not wait:
187
+ return Job(id=submitted["id"], status=submitted["status"], raw=submitted)
188
+ return _video_from_job(self._c._wait(submitted["id"], timeout_s))
189
+
190
+
191
+ class _Speech:
192
+ def __init__(self, client: Corent):
193
+ self._c = client
194
+
195
+ def generate(self, text: str, voice_id: str | None = None) -> GeneratedSpeech:
196
+ """Text to speech; synchronous, returns the finished audio."""
197
+ body: dict = {"text": text}
198
+ if voice_id:
199
+ body["voice_id"] = voice_id
200
+ r = self._c._request("POST", "/v1/audio/speech", json=body, idempotent=True)
201
+ meta = r.get("meta") or {}
202
+ return GeneratedSpeech(id=r["id"], url=r["audio_url"],
203
+ model=meta.get("model"), cost_cents=meta.get("cost_cents"))
204
+
205
+
206
+ class _Jobs:
207
+ def __init__(self, client: Corent):
208
+ self._c = client
209
+
210
+ def get(self, job_id: str) -> Job:
211
+ raw = self._c._request("GET", f"/v1/jobs/{job_id}")
212
+ return Job(id=raw["id"], status=raw["status"], raw=raw)
213
+
214
+ def wait(self, job_id: str, timeout_s: float = DEFAULT_WAIT_TIMEOUT_S) -> Job:
215
+ raw = self._c._wait(job_id, timeout_s)
216
+ return Job(id=raw["id"], status=raw["status"], raw=raw)
217
+
218
+
219
+ def _image_from_job(job: dict) -> GeneratedImage:
220
+ image = (job.get("images") or [{}])[0]
221
+ meta = job.get("meta") or {}
222
+ return GeneratedImage(
223
+ id=job["id"], url=image.get("url"), width=image.get("width"),
224
+ height=image.get("height"), model=meta.get("model"), cost_cents=meta.get("cost_cents"),
225
+ )
226
+
227
+
228
+ def _video_from_job(job: dict) -> GeneratedVideo:
229
+ video = (job.get("videos") or [{}])[0]
230
+ meta = job.get("meta") or {}
231
+ return GeneratedVideo(
232
+ id=job["id"], url=video.get("url"), width=video.get("width"),
233
+ height=video.get("height"), resolution=video.get("resolution"),
234
+ duration_s=video.get("duration_s"), model=meta.get("model"),
235
+ cost_cents=meta.get("cost_cents"),
236
+ )
corent/errors.py ADDED
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class CorentError(Exception):
5
+ """Base error: carries the HTTP status and the API's error detail."""
6
+
7
+ def __init__(self, message: str, status_code: int | None = None):
8
+ self.status_code = status_code
9
+ super().__init__(message)
10
+
11
+
12
+ class InvalidRequestError(CorentError):
13
+ """400/422: fix the highlighted field and resend."""
14
+
15
+
16
+ class InsufficientBalanceError(CorentError):
17
+ """402: add funds at https://corent.tech/dashboard."""
18
+
19
+
20
+ class RateLimitedError(CorentError):
21
+ """429 after retries were exhausted: slow down or raise your key limits."""
22
+
23
+ def __init__(self, message: str, retry_after_s: float | None = None):
24
+ self.retry_after_s = retry_after_s
25
+ super().__init__(message, 429)
26
+
27
+
28
+ class GenerationFailedError(CorentError):
29
+ """The job reached a terminal failed state. You were not charged."""
corent/types.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class GeneratedImage:
8
+ """A finished image. width/height are the MEASURED pixels of the delivered
9
+ file (Corent parses the file itself, never echoes your request)."""
10
+
11
+ id: str
12
+ url: str
13
+ width: int | None
14
+ height: int | None
15
+ model: str | None # Corent tier that served it, e.g. "image-premium"
16
+ cost_cents: int | None
17
+
18
+
19
+ @dataclass
20
+ class GeneratedVideo:
21
+ id: str
22
+ url: str
23
+ width: int | None
24
+ height: int | None
25
+ resolution: str | None
26
+ duration_s: int | None
27
+ model: str | None
28
+ cost_cents: int | None
29
+
30
+
31
+ @dataclass
32
+ class GeneratedSpeech:
33
+ id: str
34
+ url: str
35
+ model: str | None
36
+ cost_cents: int | None
37
+
38
+
39
+ @dataclass
40
+ class Job:
41
+ """Raw job state for callers driving their own polling."""
42
+
43
+ id: str
44
+ status: str # "processing" | "completed" | "failed"
45
+ raw: dict
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: corent
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for Corent — one API for AI image, video, and voice generation with built-in routing, failover, and exact receipts.
5
+ Project-URL: Homepage, https://corent.tech
6
+ Project-URL: Documentation, https://corent.tech/docs
7
+ Author-email: Corent <krrocicrypto@gmail.com>
8
+ License-Expression: MIT
9
+ Keywords: ai,api,corent,image-generation,text-to-speech,video-generation
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Requires-Python: >=3.9
15
+ Requires-Dist: httpx>=0.24
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Corent Python SDK
19
+
20
+ One API for AI **image, video, and voice** generation. You pick a quality tier; Corent's router picks the best live model, reroutes failures, verifies the output, and returns the exact charge on every response. Failed generations are never billed.
21
+
22
+ ```bash
23
+ pip install corent
24
+ ```
25
+
26
+ ```python
27
+ from corent import Corent
28
+
29
+ client = Corent("co_live_...") # get a key at https://corent.tech
30
+
31
+ image = client.images.generate("a lighthouse at dusk", tier="premium", aspect_ratio="9:16")
32
+ print(image.url, image.width, image.height, image.cost_cents)
33
+
34
+ video = client.videos.generate("a paper boat drifting across a puddle",
35
+ tier="premium", duration_s=5, resolution="1080p")
36
+ print(video.url, video.resolution, video.cost_cents)
37
+
38
+ speech = client.speech.generate("Welcome to Corent.")
39
+ print(speech.url, speech.cost_cents)
40
+ ```
41
+
42
+ ## What the SDK handles for you
43
+
44
+ - **Timeout-safe renders** — images submit as background jobs and are polled; a network hiccup can never lose a finished (and billed) result.
45
+ - **Safe retries** — every generate call carries an auto idempotency key; retries can never double-charge.
46
+ - **Backoff** — 429/5xx are retried with `Retry-After` respected.
47
+ - **Honest receipts** — `width`/`height` are the *measured* pixels of the delivered file, and `cost_cents` is the exact charge.
48
+
49
+ ## Fine-grained control
50
+
51
+ ```python
52
+ job = client.images.generate("...", tier="max_pro", wait=False) # returns immediately
53
+ job = client.jobs.wait(job.id) # resume any time
54
+
55
+ client.tiers() # live catalog with honest min-max price ranges
56
+ client.balance_cents() # prepaid balance
57
+ ```
58
+
59
+ Tiers: `air` | `lite` | `premium` | `pro` | `max_pro` — see [corent.tech/pricing](https://corent.tech/pricing). Docs: [corent.tech/docs](https://corent.tech/docs).
@@ -0,0 +1,7 @@
1
+ corent/__init__.py,sha256=Y5CPC8Ztqq6g9y1IuL9XU9OfOKNbKCHeimYcMr0st0g,727
2
+ corent/client.py,sha256=_o5QPkIUAxhdDFCpuf4ictRhofK-0jlnneiJ3JNeLzI,8760
3
+ corent/errors.py,sha256=9Cno3lxQ9mP-Vt7UDyueB12G8CBCM2RArf-Nc40Y3rQ,883
4
+ corent/types.py,sha256=-sYESRx53h7O3pahjUl9dYKpbSrMA7AZkVQLRFr1RBk,917
5
+ corent-0.1.0.dist-info/METADATA,sha256=c3aywH2Pnaa92BLPtt8uk5zTu3m4r24QEd3TjczPpbM,2577
6
+ corent-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
7
+ corent-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any