notex-python 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.
notex_sdk/client.py ADDED
@@ -0,0 +1,496 @@
1
+ """Synchronous, dependency-free client for API-key-enabled NoteX endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Any, BinaryIO, Dict, Mapping, Optional, Sequence, Union
9
+ from urllib.parse import quote, urlencode
10
+
11
+ from .config import (
12
+ CREDITS_ME_ENDPOINT,
13
+ CREDITS_QUOTA_ENDPOINT,
14
+ DEFAULT_BASE_URL,
15
+ FLASHCARDS_CREATE_ENDPOINT,
16
+ MINDMAP_CREATE_ENDPOINT,
17
+ NOTE_CREATE_ENDPOINT,
18
+ NOTE_VALIDATE_ENDPOINT,
19
+ PODCAST_CREATE_ENDPOINT,
20
+ PRESIGNED_URL_ENDPOINT,
21
+ QUIZ_CREATE_ENDPOINT,
22
+ QUIZ_VIDEO_CREATE_ENDPOINT,
23
+ SHORTS_CREATE_ENDPOINT,
24
+ SLIDE_CREATE_ENDPOINT,
25
+ TASK_RESULT_ENDPOINT,
26
+ TRANSLATE_CREATE_ENDPOINT,
27
+ USER_NOTES_ENDPOINT,
28
+ USER_PROFILE_ENDPOINT,
29
+ )
30
+ from .errors import (
31
+ NotexAPIError,
32
+ NotexAuthenticationError,
33
+ NotexTaskError,
34
+ NotexUploadError,
35
+ )
36
+ from .models import (
37
+ Credits,
38
+ NotesPage,
39
+ PresignedUpload,
40
+ Quota,
41
+ SourceValidation,
42
+ TaskResult,
43
+ TaskSubmission,
44
+ UserProfile,
45
+ )
46
+ from .transport import RetryPolicy, UrllibTransport
47
+
48
+ Headers = Optional[Mapping[str, str]]
49
+
50
+
51
+ class _RedactedHeaders(dict):
52
+ """Request headers whose repr never exposes authentication credentials."""
53
+
54
+ _SENSITIVE_NAMES = {"authorization", "x-api-key"}
55
+
56
+ def __repr__(self) -> str:
57
+ redacted = {
58
+ key: "<redacted>" if key.lower() in self._SENSITIVE_NAMES else value
59
+ for key, value in self.items()
60
+ }
61
+ return repr(redacted)
62
+
63
+
64
+ class NotexClient:
65
+ """A request-scoped client for one NoteX API key.
66
+
67
+ In a multi-user application, create this client from the credential
68
+ resolved for the current request or background job. The SDK never stores,
69
+ creates, lists, or deletes user API keys.
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ api_key: Optional[str] = None,
75
+ *,
76
+ base_url: str = DEFAULT_BASE_URL,
77
+ auth_header: str = "X-API-Key",
78
+ timeout: float = 30.0,
79
+ retry_policy: Optional[RetryPolicy] = None,
80
+ transport: Optional[UrllibTransport] = None,
81
+ ) -> None:
82
+ self.api_key = api_key
83
+ self.base_url = base_url.rstrip("/")
84
+ self.auth_header = auth_header
85
+ self.timeout = timeout
86
+
87
+ if self.auth_header not in {"X-API-Key", "Authorization"}:
88
+ raise ValueError("auth_header must be 'X-API-Key' or 'Authorization'")
89
+ if self.api_key and not self.api_key.startswith("ntx_"):
90
+ raise ValueError("NoteX API keys must start with 'ntx_'")
91
+ self._transport = transport or UrllibTransport(timeout=timeout, retry_policy=retry_policy)
92
+
93
+ def get_credits(self, *, endpoint: str = CREDITS_ME_ENDPOINT, base_url: Optional[str] = None) -> Credits:
94
+ return Credits.from_response(self.request("GET", endpoint, base_url=base_url))
95
+
96
+ def get_quota(self, *, endpoint: str = CREDITS_QUOTA_ENDPOINT, base_url: Optional[str] = None) -> Quota:
97
+ return Quota.from_response(self.request("GET", endpoint, base_url=base_url))
98
+
99
+ def get_profile(self, *, endpoint: str = USER_PROFILE_ENDPOINT, base_url: Optional[str] = None) -> UserProfile:
100
+ return UserProfile.from_response(self.request("GET", endpoint, base_url=base_url))
101
+
102
+ def list_notes(
103
+ self,
104
+ *,
105
+ note_type: Optional[str] = None,
106
+ tab: Optional[str] = None,
107
+ limit: Optional[int] = None,
108
+ cursor: Optional[str] = None,
109
+ page: Optional[int] = None,
110
+ sort_field: Optional[str] = None,
111
+ sort_order: Optional[int] = None,
112
+ endpoint: str = USER_NOTES_ENDPOINT,
113
+ base_url: Optional[str] = None,
114
+ ) -> NotesPage:
115
+ if sort_order is not None and sort_order not in {-1, 1}:
116
+ raise ValueError("sort_order must be 1 (ascending) or -1 (descending)")
117
+ if limit is not None and limit < 1:
118
+ raise ValueError("limit must be greater than 0")
119
+ if page is not None and page < 1:
120
+ raise ValueError("page must be greater than 0")
121
+ return NotesPage.from_response(
122
+ self.request(
123
+ "GET",
124
+ endpoint,
125
+ params={
126
+ key: value
127
+ for key, value in {
128
+ "type": note_type,
129
+ "tab": tab,
130
+ "limit": limit,
131
+ "cursor": cursor,
132
+ "page": page,
133
+ "sort_field": sort_field,
134
+ "sort_order": sort_order,
135
+ }.items()
136
+ if value is not None
137
+ },
138
+ base_url=base_url,
139
+ )
140
+ )
141
+
142
+ def _request_presigned_upload(
143
+ self,
144
+ file_name: str,
145
+ *,
146
+ is_public: Optional[bool] = None,
147
+ endpoint: str = PRESIGNED_URL_ENDPOINT,
148
+ base_url: Optional[str] = None,
149
+ ) -> PresignedUpload:
150
+ self._require_text(file_name, "file_name")
151
+ params: Dict[str, Any] = {"file_name": file_name}
152
+ if is_public is not None:
153
+ params["is_public"] = is_public
154
+ return PresignedUpload.from_response(
155
+ self.request("GET", endpoint, params=params, base_url=base_url)
156
+ )
157
+
158
+ def _upload_file(
159
+ self,
160
+ upload: PresignedUpload,
161
+ file_path: Union[str, Path],
162
+ *,
163
+ content_type: Optional[str] = None,
164
+ ) -> None:
165
+ path = Path(file_path)
166
+ self._require_text(upload.upload_url, "upload_url")
167
+ try:
168
+ size = path.stat().st_size
169
+ headers = dict(upload.headers)
170
+ signed_content_type = self._get_header(headers, "Content-Type")
171
+ if content_type and signed_content_type and content_type != signed_content_type:
172
+ raise NotexUploadError(
173
+ "content_type does not match the Content-Type returned by NoteX presign"
174
+ )
175
+ if content_type and not signed_content_type:
176
+ headers["Content-Type"] = content_type
177
+ if not self._get_header(headers, "Content-Length"):
178
+ headers["Content-Length"] = str(size)
179
+ with path.open("rb") as file_handle:
180
+ self._send(
181
+ url=upload.upload_url,
182
+ method=upload.method,
183
+ headers=headers,
184
+ content=file_handle,
185
+ )
186
+ except OSError as exc:
187
+ raise NotexUploadError(f"Could not read file '{path.name}' for NoteX upload") from exc
188
+
189
+ def create_note_from_file(
190
+ self,
191
+ file_path: Union[str, Path],
192
+ *,
193
+ upload_file_name: Optional[str] = None,
194
+ is_public: Optional[bool] = None,
195
+ language_hints: Optional[Sequence[str]] = None,
196
+ use_ocr: Optional[bool] = None,
197
+ summary_style: Optional[str] = None,
198
+ writing_style: Optional[str] = None,
199
+ human_style: Optional[str] = None,
200
+ is_record: Optional[bool] = None,
201
+ bot_id: Optional[str] = None,
202
+ record_session_id: Optional[str] = None,
203
+ device_meeting_id: Optional[str] = None,
204
+ target_language: Optional[str] = None,
205
+ content_type: Optional[str] = None,
206
+ presigned_endpoint: str = PRESIGNED_URL_ENDPOINT,
207
+ create_endpoint: str = NOTE_CREATE_ENDPOINT,
208
+ base_url: Optional[str] = None,
209
+ headers: Headers = None,
210
+ ) -> TaskSubmission:
211
+ """Upload a local file internally and submit asynchronous note creation.
212
+
213
+ This method returns as soon as NoteX returns a task id. It never polls
214
+ the task; call it from an application worker rather than a web handler.
215
+ """
216
+
217
+ path = Path(file_path)
218
+ if not path.is_file():
219
+ raise NotexUploadError(f"File does not exist or is not a file: {path}")
220
+ if upload_file_name is not None:
221
+ self._require_text(upload_file_name, "upload_file_name")
222
+ try:
223
+ upload = self._request_presigned_upload(
224
+ upload_file_name or path.name,
225
+ is_public=is_public,
226
+ endpoint=presigned_endpoint,
227
+ base_url=base_url,
228
+ )
229
+ self._upload_file(upload, path, content_type=content_type)
230
+ return self.create_note(
231
+ file_url=upload.file_url,
232
+ language_hints=language_hints,
233
+ use_ocr=use_ocr,
234
+ summary_style=summary_style,
235
+ writing_style=writing_style,
236
+ human_style=human_style,
237
+ is_record=is_record,
238
+ bot_id=bot_id,
239
+ record_session_id=record_session_id,
240
+ device_meeting_id=device_meeting_id,
241
+ target_language=target_language,
242
+ endpoint=create_endpoint,
243
+ base_url=base_url,
244
+ headers=headers,
245
+ )
246
+ except NotexAPIError:
247
+ raise
248
+ except OSError as exc:
249
+ raise NotexUploadError(f"Could not prepare '{path.name}' for NoteX upload") from exc
250
+
251
+ def validate_source(
252
+ self,
253
+ *,
254
+ web_url: Optional[str] = None,
255
+ file_url: Optional[str] = None,
256
+ endpoint: str = NOTE_VALIDATE_ENDPOINT,
257
+ base_url: Optional[str] = None,
258
+ headers: Headers = None,
259
+ ) -> SourceValidation:
260
+ return SourceValidation.from_response(
261
+ self.request(
262
+ "POST",
263
+ endpoint,
264
+ body=self._source_body(web_url=web_url, file_url=file_url),
265
+ base_url=base_url,
266
+ headers=headers,
267
+ )
268
+ )
269
+
270
+ def create_note(
271
+ self,
272
+ *,
273
+ web_url: Optional[str] = None,
274
+ file_url: Optional[str] = None,
275
+ language_hints: Optional[Sequence[str]] = None,
276
+ use_ocr: Optional[bool] = None,
277
+ summary_style: Optional[str] = None,
278
+ writing_style: Optional[str] = None,
279
+ human_style: Optional[str] = None,
280
+ is_record: Optional[bool] = None,
281
+ bot_id: Optional[str] = None,
282
+ record_session_id: Optional[str] = None,
283
+ device_meeting_id: Optional[str] = None,
284
+ target_language: Optional[str] = None,
285
+ endpoint: str = NOTE_CREATE_ENDPOINT,
286
+ base_url: Optional[str] = None,
287
+ headers: Headers = None,
288
+ ) -> TaskSubmission:
289
+ body = self._source_body(web_url=web_url, file_url=file_url)
290
+ optional_values = {
291
+ "language_hints": list(language_hints) if language_hints is not None else None,
292
+ "use_ocr": use_ocr,
293
+ "summary_style": summary_style,
294
+ "writing_style": writing_style,
295
+ "human_style": human_style,
296
+ "is_record": is_record,
297
+ "bot_id": bot_id,
298
+ "record_session_id": record_session_id,
299
+ "device_meeting_id": device_meeting_id,
300
+ "target_language": target_language,
301
+ }
302
+ body.update({key: value for key, value in optional_values.items() if value is not None})
303
+ return TaskSubmission.from_response(
304
+ self.request("POST", endpoint, body=body, base_url=base_url, headers=headers)
305
+ )
306
+
307
+ def create_note_from_url(self, web_url: str, **options: Any) -> TaskSubmission:
308
+ return self.create_note(web_url=web_url, **options)
309
+
310
+ def create_note_from_file_url(self, file_url: str, **options: Any) -> TaskSubmission:
311
+ return self.create_note(file_url=file_url, **options)
312
+
313
+ def create_flashcards(
314
+ self,
315
+ note_id: str,
316
+ *,
317
+ num_cards: Optional[Union[int, str]] = None,
318
+ set_name: Optional[str] = None,
319
+ difficulty: Optional[str] = None,
320
+ custom_prompt: Optional[str] = None,
321
+ community: Optional[bool] = None,
322
+ endpoint: str = FLASHCARDS_CREATE_ENDPOINT,
323
+ base_url: Optional[str] = None,
324
+ headers: Headers = None,
325
+ ) -> TaskSubmission:
326
+ return self._submit_form_task(endpoint, {"note_id": note_id, "num_cards": num_cards, "set_name": set_name, "difficulty": difficulty, "custom_prompt": custom_prompt, "community": community}, base_url, headers)
327
+
328
+ def create_mindmap(self, note_id: str, *, community: Optional[bool] = None, endpoint: str = MINDMAP_CREATE_ENDPOINT, base_url: Optional[str] = None, headers: Headers = None) -> TaskSubmission:
329
+ return self._submit_form_task(endpoint, {"note_id": note_id, "community": community}, base_url, headers)
330
+
331
+ def create_podcast(
332
+ self,
333
+ note_id: str,
334
+ duration: int,
335
+ *,
336
+ speakers: Optional[int] = None,
337
+ voice: Optional[str] = None,
338
+ voice1: Optional[str] = None,
339
+ voice2: Optional[str] = None,
340
+ community: Optional[bool] = None,
341
+ endpoint: str = PODCAST_CREATE_ENDPOINT,
342
+ base_url: Optional[str] = None,
343
+ headers: Headers = None,
344
+ ) -> TaskSubmission:
345
+ if duration <= 0:
346
+ raise ValueError("duration must be greater than 0")
347
+ return self._submit_form_task(endpoint, {"note_id": note_id, "duration": duration, "speakers": speakers, "voice": voice, "voice1": voice1, "voice2": voice2, "community": community}, base_url, headers)
348
+
349
+ def create_quiz(self, note_id: str, *, num_questions: Optional[Union[int, str]] = None, set_name: Optional[str] = None, difficulty: Optional[str] = None, custom_prompt: Optional[str] = None, community: Optional[bool] = None, endpoint: str = QUIZ_CREATE_ENDPOINT, base_url: Optional[str] = None, headers: Headers = None) -> TaskSubmission:
350
+ return self._submit_form_task(endpoint, {"note_id": note_id, "num_questions": num_questions, "set_name": set_name, "difficulty": difficulty, "custom_prompt": custom_prompt, "community": community}, base_url, headers)
351
+
352
+ def create_shorts(self, note_id: str, voice_id: str, *, clip_duration: Optional[int] = None, is_audio_only: Optional[bool] = None, bg_id: Optional[str] = None, endpoint: str = SHORTS_CREATE_ENDPOINT, base_url: Optional[str] = None, headers: Headers = None) -> TaskSubmission:
353
+ return self._submit_form_task(endpoint, {"note_id": note_id, "voice_id": voice_id, "clip_duration": clip_duration, "is_audio_only": is_audio_only, "bg_id": bg_id}, base_url, headers)
354
+
355
+ def create_quiz_video(self, note_id: str, voice_id: str, *, bg_id: Optional[str] = None, template_id: Optional[str] = None, endpoint: str = QUIZ_VIDEO_CREATE_ENDPOINT, base_url: Optional[str] = None, headers: Headers = None) -> TaskSubmission:
356
+ return self._submit_form_task(endpoint, {"note_id": note_id, "voice_id": voice_id, "bg_id": bg_id, "template_id": template_id}, base_url, headers)
357
+
358
+ def create_slide(self, note_id: str, template_id: str, *, language: Optional[str] = None, slide_count_range: Optional[str] = None, community: Optional[bool] = None, endpoint: str = SLIDE_CREATE_ENDPOINT, base_url: Optional[str] = None, headers: Headers = None) -> TaskSubmission:
359
+ return self._submit_form_task(endpoint, {"note_id": note_id, "template_id": template_id, "language": language, "slide_count_range": slide_count_range, "community": community}, base_url, headers)
360
+
361
+ def create_translation(self, note_id: str, language: str, *, only_summary: Optional[bool] = None, community: Optional[bool] = None, endpoint: str = TRANSLATE_CREATE_ENDPOINT, base_url: Optional[str] = None, headers: Headers = None) -> TaskSubmission:
362
+ return self._submit_form_task(endpoint, {"note_id": note_id, "language": language, "only_summary": only_summary, "community": community}, base_url, headers)
363
+
364
+ def get_task_result(self, task_id: str, *, endpoint: str = TASK_RESULT_ENDPOINT, base_url: Optional[str] = None) -> TaskResult:
365
+ self._require_text(task_id, "task_id")
366
+ return TaskResult.from_response(
367
+ self.request("GET", endpoint.format(task_id=quote(task_id.strip(), safe="")), base_url=base_url)
368
+ )
369
+
370
+ def wait_for_task(self, task_id: str, *, poll_interval: float = 5.0, timeout: float = 120.0, sleep_fn=time.sleep, endpoint: str = TASK_RESULT_ENDPOINT, base_url: Optional[str] = None) -> TaskResult:
371
+ if poll_interval < 0:
372
+ raise ValueError("poll_interval cannot be negative")
373
+ if timeout <= 0:
374
+ raise ValueError("timeout must be greater than 0")
375
+ started_at = time.monotonic()
376
+ while True:
377
+ result = self.get_task_result(task_id, endpoint=endpoint, base_url=base_url)
378
+ if result.status == "SUCCESS":
379
+ return result
380
+ if result.status == "FAILURE":
381
+ raise NotexTaskError(result.error or "NoteX task failed", error_key="task_failure", body=self._task_body(result))
382
+ if time.monotonic() - started_at >= timeout:
383
+ raise NotexTaskError(f"NoteX task timed out after {timeout:g} seconds", error_key="task_timeout", body=self._task_body(result))
384
+ sleep_fn(poll_interval)
385
+
386
+ def request(
387
+ self,
388
+ method: str,
389
+ path: str,
390
+ *,
391
+ params: Optional[Mapping[str, Any]] = None,
392
+ body: Optional[Mapping[str, Any]] = None,
393
+ form: Optional[Mapping[str, Any]] = None,
394
+ content: Optional[bytes] = None,
395
+ headers: Headers = None,
396
+ base_url: Optional[str] = None,
397
+ retryable: Optional[bool] = None,
398
+ ) -> Dict[str, Any]:
399
+ """Call a NoteX API endpoint with JSON, form, or binary request data."""
400
+
401
+ if not self.api_key:
402
+ raise NotexAuthenticationError("No NoteX API key supplied", status_code=401, error_key="not_authenticated")
403
+ supplied_bodies = sum(value is not None for value in (body, form, content))
404
+ if supplied_bodies > 1:
405
+ raise ValueError("use only one of body, form, or content")
406
+ base = (base_url or self.base_url).rstrip("/")
407
+ url = f"{base}/{path.lstrip('/')}"
408
+ if params:
409
+ url = f"{url}?{urlencode(self._normalize_values(params), doseq=True)}"
410
+ request_headers = self._auth_headers(headers)
411
+ encoded_content: Optional[bytes] = content
412
+ if body is not None:
413
+ request_headers.setdefault("Content-Type", "application/json")
414
+ encoded_content = json.dumps(body).encode("utf-8")
415
+ elif form is not None:
416
+ request_headers.setdefault("Content-Type", "application/x-www-form-urlencoded")
417
+ encoded_content = urlencode(self._normalize_values(form), doseq=True).encode("utf-8")
418
+ return self._send(
419
+ url=url,
420
+ method=method,
421
+ headers=request_headers,
422
+ content=encoded_content,
423
+ retryable=retryable,
424
+ )
425
+
426
+ def _submit_form_task(self, endpoint: str, form: Mapping[str, Any], base_url: Optional[str], headers: Headers) -> TaskSubmission:
427
+ self._require_text(form.get("note_id"), "note_id")
428
+ return TaskSubmission.from_response(
429
+ self.request(
430
+ "POST",
431
+ endpoint,
432
+ form={key: value for key, value in form.items() if value is not None},
433
+ base_url=base_url,
434
+ headers=headers,
435
+ )
436
+ )
437
+
438
+ @staticmethod
439
+ def _source_body(*, web_url: Optional[str], file_url: Optional[str]) -> Dict[str, Any]:
440
+ supplied = [value for value in (web_url, file_url) if value and value.strip()]
441
+ if len(supplied) != 1:
442
+ raise ValueError("provide exactly one of web_url or file_url")
443
+ return {"web_url": web_url} if web_url and web_url.strip() else {"file_url": file_url}
444
+
445
+ @staticmethod
446
+ def _require_text(value: Any, name: str) -> None:
447
+ if not isinstance(value, str) or not value.strip():
448
+ raise ValueError(f"{name} is required")
449
+
450
+ @staticmethod
451
+ def _normalize_values(values: Mapping[str, Any]) -> Dict[str, Any]:
452
+ normalized: Dict[str, Any] = {}
453
+ for key, value in values.items():
454
+ if isinstance(value, bool):
455
+ normalized[key] = "true" if value else "false"
456
+ elif isinstance(value, (list, tuple)):
457
+ normalized[key] = ["true" if item is True else "false" if item is False else item for item in value]
458
+ else:
459
+ normalized[key] = value
460
+ return normalized
461
+
462
+ def _auth_headers(self, extra_headers: Headers) -> Dict[str, str]:
463
+ headers: Dict[str, str] = _RedactedHeaders({"Accept": "application/json"})
464
+ if self.auth_header == "Authorization":
465
+ headers["Authorization"] = f"Bearer {self.api_key}"
466
+ else:
467
+ headers["X-API-Key"] = str(self.api_key)
468
+ if extra_headers:
469
+ headers.update(extra_headers)
470
+ return headers
471
+
472
+ @staticmethod
473
+ def _get_header(headers: Mapping[str, str], name: str) -> Optional[str]:
474
+ target = name.lower()
475
+ return next((value for key, value in headers.items() if key.lower() == target), None)
476
+
477
+ def _send(
478
+ self,
479
+ *,
480
+ url: str,
481
+ method: str,
482
+ headers: Mapping[str, str],
483
+ content: Optional[Union[bytes, BinaryIO]],
484
+ retryable: Optional[bool] = None,
485
+ ) -> Dict[str, Any]:
486
+ return self._transport.send(
487
+ url=url,
488
+ method=method,
489
+ headers=headers,
490
+ content=content,
491
+ retryable=retryable,
492
+ )
493
+
494
+ @staticmethod
495
+ def _task_body(result: TaskResult) -> Dict[str, Any]:
496
+ return {"status": result.status, "data": result.data, "error": result.error}
notex_sdk/config.py ADDED
@@ -0,0 +1,25 @@
1
+ """Centralized NoteX API defaults."""
2
+
3
+ DEFAULT_BASE_URL = "https://api.notexapp.com"
4
+
5
+ API_V1 = "/v1"
6
+ API_V2 = "/v2"
7
+
8
+ CREDITS_ME_ENDPOINT = f"{API_V2}/credits/me"
9
+ CREDITS_QUOTA_ENDPOINT = f"{API_V2}/credits/quota"
10
+ USER_PROFILE_ENDPOINT = f"{API_V1}/user/profile"
11
+ USER_NOTES_ENDPOINT = f"{API_V2}/user/notes"
12
+ TASK_RESULT_ENDPOINT = f"{API_V1}/create/tasks/{{task_id}}/result"
13
+
14
+ PRESIGNED_URL_ENDPOINT = f"{API_V1}/presigned-url"
15
+ NOTE_CREATE_ENDPOINT = "/v9/create/note"
16
+ NOTE_VALIDATE_ENDPOINT = "/v9/create/validate"
17
+
18
+ FLASHCARDS_CREATE_ENDPOINT = "/v6/create/flashcards"
19
+ MINDMAP_CREATE_ENDPOINT = f"{API_V2}/create/mindmap"
20
+ PODCAST_CREATE_ENDPOINT = f"{API_V1}/create/podcast"
21
+ QUIZ_CREATE_ENDPOINT = "/v6/create/quiz"
22
+ SHORTS_CREATE_ENDPOINT = f"{API_V2}/create/shorts"
23
+ QUIZ_VIDEO_CREATE_ENDPOINT = f"{API_V2}/create/quiz-video"
24
+ SLIDE_CREATE_ENDPOINT = f"{API_V2}/create/slide"
25
+ TRANSLATE_CREATE_ENDPOINT = f"{API_V2}/create/translate"
notex_sdk/errors.py ADDED
@@ -0,0 +1,45 @@
1
+ """Error types raised by the NoteX SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping, Optional
6
+
7
+
8
+ class NotexAPIError(Exception):
9
+ """An error returned by the NoteX API or the transport layer."""
10
+
11
+ def __init__(
12
+ self,
13
+ message: str,
14
+ *,
15
+ status_code: Optional[int] = None,
16
+ error_key: Optional[str] = None,
17
+ body: Optional[Mapping[str, Any]] = None,
18
+ retry_after: Optional[float] = None,
19
+ ) -> None:
20
+ super().__init__(message)
21
+ self.message = message
22
+ self.status_code = status_code
23
+ self.error_key = error_key
24
+ self.body = dict(body or {})
25
+ self.retry_after = retry_after
26
+
27
+
28
+ class NotexAuthenticationError(NotexAPIError):
29
+ """The API key is missing, invalid, revoked, or expired."""
30
+
31
+
32
+ class NotexPermissionError(NotexAPIError):
33
+ """The authenticated account cannot perform the requested operation."""
34
+
35
+
36
+ class NotexUploadError(NotexAPIError):
37
+ """A local-file or presigned-upload operation could not be completed."""
38
+
39
+
40
+ class NotexRateLimitError(NotexAPIError):
41
+ """The NoteX API rate limited the request."""
42
+
43
+
44
+ class NotexTaskError(NotexAPIError):
45
+ """An asynchronous NoteX task failed or returned an invalid status."""