note-connector 0.2.5 → 0.2.6
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.
- package/dist/paths.js +4 -0
- package/dist/setup-dependencies.js +56 -13
- package/package.json +3 -2
- package/py/pyproject.toml +86 -0
- package/py/src/note_mcp/__init__.py +7 -0
- package/py/src/note_mcp/__main__.py +65 -0
- package/py/src/note_mcp/api/__init__.py +31 -0
- package/py/src/note_mcp/api/articles.py +1395 -0
- package/py/src/note_mcp/api/client.py +318 -0
- package/py/src/note_mcp/api/embeds.py +482 -0
- package/py/src/note_mcp/api/images.py +456 -0
- package/py/src/note_mcp/api/preview.py +142 -0
- package/py/src/note_mcp/api/public_notes.py +150 -0
- package/py/src/note_mcp/auth/__init__.py +9 -0
- package/py/src/note_mcp/auth/browser.py +574 -0
- package/py/src/note_mcp/auth/file_session.py +145 -0
- package/py/src/note_mcp/auth/session.py +240 -0
- package/py/src/note_mcp/browser/__init__.py +10 -0
- package/py/src/note_mcp/browser/config.py +21 -0
- package/py/src/note_mcp/browser/manager.py +182 -0
- package/py/src/note_mcp/browser/preview.py +68 -0
- package/py/src/note_mcp/browser/url_helpers.py +18 -0
- package/py/src/note_mcp/chatgpt/__init__.py +1 -0
- package/py/src/note_mcp/chatgpt/__main__.py +63 -0
- package/py/src/note_mcp/chatgpt/access_log.py +25 -0
- package/py/src/note_mcp/chatgpt/auth.py +52 -0
- package/py/src/note_mcp/chatgpt/images.py +92 -0
- package/py/src/note_mcp/chatgpt/login_once.py +26 -0
- package/py/src/note_mcp/chatgpt/middleware.py +31 -0
- package/py/src/note_mcp/chatgpt/tools.py +255 -0
- package/py/src/note_mcp/chatgpt/widgets.py +121 -0
- package/py/src/note_mcp/decorators.py +113 -0
- package/py/src/note_mcp/investigator/__init__.py +33 -0
- package/py/src/note_mcp/investigator/__main__.py +11 -0
- package/py/src/note_mcp/investigator/cli.py +313 -0
- package/py/src/note_mcp/investigator/core.py +653 -0
- package/py/src/note_mcp/investigator/mcp_tools.py +225 -0
- package/py/src/note_mcp/models.py +557 -0
- package/py/src/note_mcp/py.typed +0 -0
- package/py/src/note_mcp/server.py +905 -0
- package/py/src/note_mcp/utils/__init__.py +7 -0
- package/py/src/note_mcp/utils/file_parser.py +314 -0
- package/py/src/note_mcp/utils/html_to_markdown.py +477 -0
- package/py/src/note_mcp/utils/logging.py +119 -0
- package/py/src/note_mcp/utils/markdown.py +12 -0
- package/py/src/note_mcp/utils/markdown_to_html.py +826 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
"""Pydantic data models for note-mcp.
|
|
2
|
+
|
|
3
|
+
This module defines all data models used throughout the note-mcp server,
|
|
4
|
+
including session management, article handling, and error types.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Session(BaseModel):
|
|
16
|
+
"""User authentication session.
|
|
17
|
+
|
|
18
|
+
Stores authentication state including cookies, user information,
|
|
19
|
+
and session expiration.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
cookies: note.com authentication cookies (note_gql_auth_token, _note_session_v5)
|
|
23
|
+
user_id: note.com user ID
|
|
24
|
+
username: note.com username (used in URL paths)
|
|
25
|
+
expires_at: Session expiration timestamp (Unix timestamp), None if no expiry
|
|
26
|
+
created_at: Session creation timestamp (Unix timestamp)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
cookies: dict[str, str]
|
|
30
|
+
user_id: str
|
|
31
|
+
username: str
|
|
32
|
+
expires_at: int | None = None
|
|
33
|
+
created_at: int
|
|
34
|
+
|
|
35
|
+
def is_expired(self) -> bool:
|
|
36
|
+
"""Check if the session has expired.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
True if session has expired, False otherwise.
|
|
40
|
+
Returns False if expires_at is None (no expiry set).
|
|
41
|
+
"""
|
|
42
|
+
if self.expires_at is None:
|
|
43
|
+
return False
|
|
44
|
+
return time.time() > self.expires_at
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ArticleStatus(str, Enum):
|
|
48
|
+
"""Article publication status."""
|
|
49
|
+
|
|
50
|
+
DRAFT = "draft"
|
|
51
|
+
PUBLISHED = "published"
|
|
52
|
+
PRIVATE = "private"
|
|
53
|
+
DELETED = "deleted"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ImageType(str, Enum):
|
|
57
|
+
"""Image upload type.
|
|
58
|
+
|
|
59
|
+
Determines which note.com API endpoint to use for image upload.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
EYECATCH = "eyecatch" # Header/eyecatch image (見出し画像)
|
|
63
|
+
BODY = "body" # Inline/body image (記事内埋め込み画像)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Article(BaseModel):
|
|
67
|
+
"""A note.com article.
|
|
68
|
+
|
|
69
|
+
Represents an article with all its metadata as stored on note.com.
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
id: Article ID (note.com internal ID)
|
|
73
|
+
key: Article key (used in URL path)
|
|
74
|
+
title: Article title
|
|
75
|
+
body: Article body content (HTML format)
|
|
76
|
+
status: Publication status
|
|
77
|
+
tags: List of hashtags (without # prefix)
|
|
78
|
+
eyecatch_image_key: Eyecatch image key (if set)
|
|
79
|
+
prev_access_key: Preview access key for draft articles
|
|
80
|
+
created_at: Creation timestamp (ISO 8601)
|
|
81
|
+
updated_at: Last update timestamp (ISO 8601)
|
|
82
|
+
published_at: Publication timestamp (ISO 8601)
|
|
83
|
+
url: Full article URL
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
id: str
|
|
87
|
+
key: str
|
|
88
|
+
title: str
|
|
89
|
+
body: str
|
|
90
|
+
status: ArticleStatus
|
|
91
|
+
tags: list[str] = []
|
|
92
|
+
eyecatch_image_key: str | None = None
|
|
93
|
+
prev_access_key: str | None = None
|
|
94
|
+
created_at: str | None = None
|
|
95
|
+
updated_at: str | None = None
|
|
96
|
+
published_at: str | None = None
|
|
97
|
+
url: str | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ArticleInput(BaseModel):
|
|
101
|
+
"""Input data for creating or updating an article.
|
|
102
|
+
|
|
103
|
+
Attributes:
|
|
104
|
+
title: Article title
|
|
105
|
+
body: Article body content (Markdown format)
|
|
106
|
+
tags: List of hashtags (# prefix optional, will be normalized)
|
|
107
|
+
eyecatch_image_path: Local path to eyecatch image (optional)
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
title: str
|
|
111
|
+
body: str
|
|
112
|
+
tags: list[str] = []
|
|
113
|
+
eyecatch_image_path: str | None = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Image(BaseModel):
|
|
117
|
+
"""An uploaded image.
|
|
118
|
+
|
|
119
|
+
Attributes:
|
|
120
|
+
key: note.com image key (None for eyecatch images as API doesn't return it)
|
|
121
|
+
url: Image URL on note.com
|
|
122
|
+
original_path: Original local file path
|
|
123
|
+
size_bytes: File size in bytes (optional)
|
|
124
|
+
uploaded_at: Upload timestamp (Unix timestamp)
|
|
125
|
+
image_type: Type of image (eyecatch or body)
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
key: str | None = None
|
|
129
|
+
url: str
|
|
130
|
+
original_path: str
|
|
131
|
+
size_bytes: int | None = None
|
|
132
|
+
uploaded_at: int
|
|
133
|
+
image_type: ImageType = ImageType.EYECATCH
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class Tag(BaseModel):
|
|
137
|
+
"""A hashtag for articles.
|
|
138
|
+
|
|
139
|
+
Attributes:
|
|
140
|
+
name: Tag name (without # prefix)
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
name: str
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def normalize(cls, tag: str) -> str:
|
|
147
|
+
"""Normalize a tag by removing leading # characters.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
tag: Tag string, possibly with # prefix
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Tag string without # prefix
|
|
154
|
+
"""
|
|
155
|
+
return tag.lstrip("#")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ArticleListResult(BaseModel):
|
|
159
|
+
"""Result of listing articles.
|
|
160
|
+
|
|
161
|
+
Attributes:
|
|
162
|
+
articles: List of articles
|
|
163
|
+
total: Total number of articles matching the query
|
|
164
|
+
page: Current page number (1-indexed)
|
|
165
|
+
has_more: Whether there are more articles to fetch
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
articles: list[Article]
|
|
169
|
+
total: int
|
|
170
|
+
page: int
|
|
171
|
+
has_more: bool
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class BrowserArticleResult(BaseModel):
|
|
175
|
+
"""Result of browser-based article creation/update.
|
|
176
|
+
|
|
177
|
+
Includes the article and optional TOC/alignment/embed/image insertion results for user notification.
|
|
178
|
+
|
|
179
|
+
Attributes:
|
|
180
|
+
article: The created/updated article
|
|
181
|
+
toc_inserted: True if TOC was successfully inserted, False if failed, None if not attempted
|
|
182
|
+
toc_error: Error message if TOC insertion failed
|
|
183
|
+
alignments_applied: Number of text alignments successfully applied, None if not attempted
|
|
184
|
+
alignment_error: Error message if text alignment application failed
|
|
185
|
+
embeds_inserted: Number of embeds successfully inserted, None if not attempted
|
|
186
|
+
embed_error: Error message if embed insertion failed
|
|
187
|
+
images_inserted: Number of images successfully inserted, None if not attempted
|
|
188
|
+
image_error: Error message if image insertion failed
|
|
189
|
+
debug_info: Debug information for troubleshooting (temporary)
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
article: Article
|
|
193
|
+
toc_inserted: bool | None = None
|
|
194
|
+
toc_error: str | None = None
|
|
195
|
+
alignments_applied: int | None = None
|
|
196
|
+
alignment_error: str | None = None
|
|
197
|
+
embeds_inserted: int | None = None
|
|
198
|
+
embed_error: str | None = None
|
|
199
|
+
images_inserted: int | None = None
|
|
200
|
+
image_error: str | None = None
|
|
201
|
+
debug_info: str | None = None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ErrorCode(str, Enum):
|
|
205
|
+
"""Error codes for note-mcp API errors."""
|
|
206
|
+
|
|
207
|
+
NOT_AUTHENTICATED = "not_authenticated"
|
|
208
|
+
SESSION_EXPIRED = "session_expired"
|
|
209
|
+
ARTICLE_NOT_FOUND = "article_not_found"
|
|
210
|
+
RATE_LIMITED = "rate_limited"
|
|
211
|
+
API_ERROR = "api_error"
|
|
212
|
+
UPLOAD_FAILED = "upload_failed"
|
|
213
|
+
INVALID_INPUT = "invalid_input"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class NoteAPIError(Exception):
|
|
217
|
+
"""Exception for note.com API errors.
|
|
218
|
+
|
|
219
|
+
Attributes:
|
|
220
|
+
code: Error code
|
|
221
|
+
message: Human-readable error message
|
|
222
|
+
details: Additional error details
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
def __init__(
|
|
226
|
+
self,
|
|
227
|
+
code: ErrorCode,
|
|
228
|
+
message: str,
|
|
229
|
+
details: dict[str, object] | None = None,
|
|
230
|
+
) -> None:
|
|
231
|
+
"""Initialize the error.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
code: Error code from ErrorCode enum
|
|
235
|
+
message: Human-readable error message
|
|
236
|
+
details: Additional context (e.g., status code, response body)
|
|
237
|
+
"""
|
|
238
|
+
self.code = code
|
|
239
|
+
self.message = message
|
|
240
|
+
self.details = details or {}
|
|
241
|
+
super().__init__(message)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class LoginError(Exception):
|
|
245
|
+
"""ログイン処理でのエラー。
|
|
246
|
+
|
|
247
|
+
reCAPTCHA検出、2FA要求、認証情報エラー時に送出される。
|
|
248
|
+
手動ログインへのフォールバックは行わず、明確なエラーで通知する。
|
|
249
|
+
|
|
250
|
+
Attributes:
|
|
251
|
+
code: エラーコード(RECAPTCHA_DETECTED, TWO_FACTOR_REQUIRED,
|
|
252
|
+
INVALID_CREDENTIALS, LOGIN_TIMEOUT)
|
|
253
|
+
message: エラーメッセージ
|
|
254
|
+
resolution: 推奨される対処法
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
def __init__(
|
|
258
|
+
self,
|
|
259
|
+
code: str,
|
|
260
|
+
message: str,
|
|
261
|
+
resolution: str | None = None,
|
|
262
|
+
) -> None:
|
|
263
|
+
"""Initialize the error.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
code: エラーコード
|
|
267
|
+
message: エラーメッセージ
|
|
268
|
+
resolution: 推奨される対処法(オプション)
|
|
269
|
+
"""
|
|
270
|
+
self.code = code
|
|
271
|
+
self.message = message
|
|
272
|
+
self.resolution = resolution
|
|
273
|
+
super().__init__(message)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# =============================================================================
|
|
277
|
+
# Issue #141: Delete Draft Models
|
|
278
|
+
# =============================================================================
|
|
279
|
+
|
|
280
|
+
# Delete operation error messages (T020)
|
|
281
|
+
DELETE_ERROR_PUBLISHED_ARTICLE = "公開済み記事は削除できません。下書きのみ削除可能です。"
|
|
282
|
+
DELETE_ERROR_NO_ACCESS = "この記事へのアクセス権がありません。"
|
|
283
|
+
DELETE_ERROR_NOT_FOUND = "記事が見つかりません。キー: {article_key}"
|
|
284
|
+
DELETE_CONFIRM_REQUIRED = "削除を実行するには confirm=True を指定してください。"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class ArticleSummary(BaseModel):
|
|
288
|
+
"""Summary information of an article.
|
|
289
|
+
|
|
290
|
+
Used in bulk delete preview/result to show article information
|
|
291
|
+
without the full body content.
|
|
292
|
+
|
|
293
|
+
Attributes:
|
|
294
|
+
article_id: Article ID (note.com internal ID)
|
|
295
|
+
article_key: Article key (used in URL path)
|
|
296
|
+
title: Article title
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
article_id: str
|
|
300
|
+
article_key: str
|
|
301
|
+
title: str
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class FailedArticle(BaseModel):
|
|
305
|
+
"""Information about a failed deletion.
|
|
306
|
+
|
|
307
|
+
Used in BulkDeleteResult to provide details about articles
|
|
308
|
+
that could not be deleted and why.
|
|
309
|
+
|
|
310
|
+
Attributes:
|
|
311
|
+
article_id: Article ID
|
|
312
|
+
article_key: Article key
|
|
313
|
+
title: Article title
|
|
314
|
+
error: Error message explaining the failure
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
article_id: str
|
|
318
|
+
article_key: str
|
|
319
|
+
title: str
|
|
320
|
+
error: str
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class DeleteResult(BaseModel):
|
|
324
|
+
"""Result of a single delete operation.
|
|
325
|
+
|
|
326
|
+
Returned when a delete operation completes (success or failure).
|
|
327
|
+
|
|
328
|
+
Attributes:
|
|
329
|
+
success: Whether the deletion was successful
|
|
330
|
+
article_id: ID of the deleted article
|
|
331
|
+
article_key: Key of the deleted article
|
|
332
|
+
article_title: Title of the deleted article
|
|
333
|
+
message: Result message for the user
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
success: bool
|
|
337
|
+
article_id: str
|
|
338
|
+
article_key: str
|
|
339
|
+
article_title: str
|
|
340
|
+
message: str
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class DeletePreview(BaseModel):
|
|
344
|
+
"""Preview information before deletion.
|
|
345
|
+
|
|
346
|
+
Returned when confirm=False to show what will be deleted
|
|
347
|
+
and prompt for confirmation.
|
|
348
|
+
|
|
349
|
+
Attributes:
|
|
350
|
+
article_id: ID of the article to be deleted
|
|
351
|
+
article_key: Key of the article to be deleted
|
|
352
|
+
article_title: Title of the article to be deleted
|
|
353
|
+
status: Current status of the article
|
|
354
|
+
message: Confirmation prompt message
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
article_id: str
|
|
358
|
+
article_key: str
|
|
359
|
+
article_title: str
|
|
360
|
+
status: ArticleStatus
|
|
361
|
+
message: str
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class BulkDeletePreview(BaseModel):
|
|
365
|
+
"""Preview information for bulk deletion.
|
|
366
|
+
|
|
367
|
+
Returned when confirm=False to show all drafts that will be deleted.
|
|
368
|
+
|
|
369
|
+
Attributes:
|
|
370
|
+
total_count: Total number of drafts to be deleted
|
|
371
|
+
articles: List of articles to be deleted
|
|
372
|
+
message: Confirmation prompt message
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
total_count: int
|
|
376
|
+
articles: list[ArticleSummary]
|
|
377
|
+
message: str
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class BulkDeleteResult(BaseModel):
|
|
381
|
+
"""Result of bulk delete operation.
|
|
382
|
+
|
|
383
|
+
Provides detailed information about the bulk deletion,
|
|
384
|
+
including counts and lists of successful/failed deletions.
|
|
385
|
+
|
|
386
|
+
Attributes:
|
|
387
|
+
success: Whether all deletions were successful
|
|
388
|
+
total_count: Total number of articles targeted
|
|
389
|
+
deleted_count: Number of successfully deleted articles
|
|
390
|
+
failed_count: Number of failed deletions
|
|
391
|
+
deleted_articles: List of successfully deleted articles
|
|
392
|
+
failed_articles: List of articles that failed to delete
|
|
393
|
+
message: Summary message
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
success: bool
|
|
397
|
+
total_count: int
|
|
398
|
+
deleted_count: int
|
|
399
|
+
failed_count: int
|
|
400
|
+
deleted_articles: list[ArticleSummary]
|
|
401
|
+
failed_articles: list[FailedArticle]
|
|
402
|
+
message: str
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class DeleteDraftInput(BaseModel):
|
|
406
|
+
"""Input for note_delete_draft MCP tool.
|
|
407
|
+
|
|
408
|
+
Attributes:
|
|
409
|
+
article_key: Key of the article to delete (format: nXXXXXXXXXXXX)
|
|
410
|
+
confirm: Confirmation flag (must be True to execute deletion)
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
article_key: str
|
|
414
|
+
confirm: bool = False
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class PublicArticleSummary(BaseModel):
|
|
418
|
+
"""Summary of a public note.com article in search results."""
|
|
419
|
+
|
|
420
|
+
key: str
|
|
421
|
+
title: str
|
|
422
|
+
author_username: str
|
|
423
|
+
author_nickname: str | None = None
|
|
424
|
+
url: str
|
|
425
|
+
published_at: str | None = None
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class PublicSearchResult(BaseModel):
|
|
429
|
+
"""Public note search results."""
|
|
430
|
+
|
|
431
|
+
items: list[PublicArticleSummary]
|
|
432
|
+
query: str
|
|
433
|
+
is_last_page: bool | None = None
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class PublicArticle(BaseModel):
|
|
437
|
+
"""A fetched public note.com article."""
|
|
438
|
+
|
|
439
|
+
key: str
|
|
440
|
+
title: str
|
|
441
|
+
body_markdown: str
|
|
442
|
+
author_username: str
|
|
443
|
+
author_nickname: str | None = None
|
|
444
|
+
url: str
|
|
445
|
+
status: str
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class DeleteAllDraftsInput(BaseModel):
|
|
449
|
+
"""Input for note_delete_all_drafts MCP tool.
|
|
450
|
+
|
|
451
|
+
Attributes:
|
|
452
|
+
confirm: Confirmation flag (must be True to execute deletion)
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
confirm: bool = False
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def from_api_response(data: dict[str, object]) -> Article:
|
|
459
|
+
"""Create an Article from note.com API response.
|
|
460
|
+
|
|
461
|
+
Article 6 (Data Accuracy Mandate) compliant:
|
|
462
|
+
- Required fields (id, key, status) must be present, no implicit fallbacks
|
|
463
|
+
- Missing required fields raise NoteAPIError
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
data: Raw API response dictionary
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
Article instance
|
|
470
|
+
|
|
471
|
+
Raises:
|
|
472
|
+
NoteAPIError: If required fields (id, key, status) are missing or invalid
|
|
473
|
+
"""
|
|
474
|
+
# Article 6: Validate required field 'id' - no fallback
|
|
475
|
+
article_id = data.get("id")
|
|
476
|
+
if not article_id:
|
|
477
|
+
raise NoteAPIError(
|
|
478
|
+
code=ErrorCode.API_ERROR,
|
|
479
|
+
message="API response missing required field: id",
|
|
480
|
+
details={"response": data},
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# Article 6: Validate required field 'key' - no fallback
|
|
484
|
+
article_key = data.get("key")
|
|
485
|
+
if not article_key:
|
|
486
|
+
raise NoteAPIError(
|
|
487
|
+
code=ErrorCode.API_ERROR,
|
|
488
|
+
message="API response missing required field: key",
|
|
489
|
+
details={"response": data},
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# Article 6: Validate required field 'status' - no fallback or guessing
|
|
493
|
+
status_str = data.get("status")
|
|
494
|
+
if not isinstance(status_str, str) or not status_str:
|
|
495
|
+
raise NoteAPIError(
|
|
496
|
+
code=ErrorCode.API_ERROR,
|
|
497
|
+
message="API response missing or invalid required field: status",
|
|
498
|
+
details={"response": data, "status_value": status_str},
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Extract hashtag names from the hashtags array
|
|
502
|
+
# Empty hashtags list is valid - this is not a fallback
|
|
503
|
+
hashtags = data.get("hashtags", [])
|
|
504
|
+
tags: list[str] = []
|
|
505
|
+
if isinstance(hashtags, list):
|
|
506
|
+
for ht in hashtags:
|
|
507
|
+
if isinstance(ht, dict):
|
|
508
|
+
hashtag_obj = ht.get("hashtag", {})
|
|
509
|
+
if isinstance(hashtag_obj, dict):
|
|
510
|
+
# Skip hashtags without name - no fallback to empty string
|
|
511
|
+
name = hashtag_obj.get("name")
|
|
512
|
+
if name:
|
|
513
|
+
tags.append(str(name))
|
|
514
|
+
|
|
515
|
+
# Extract title: use "name" field, fallback to "noteDraft.name" for drafts
|
|
516
|
+
title = data.get("name")
|
|
517
|
+
if not title:
|
|
518
|
+
note_draft = data.get("noteDraft")
|
|
519
|
+
if isinstance(note_draft, dict):
|
|
520
|
+
title = note_draft.get("name")
|
|
521
|
+
title_str = str(title) if title else ""
|
|
522
|
+
|
|
523
|
+
# body can be empty string - this is a valid value, not a missing field
|
|
524
|
+
body = data.get("body")
|
|
525
|
+
body_str = str(body) if body is not None else ""
|
|
526
|
+
|
|
527
|
+
return Article(
|
|
528
|
+
id=str(article_id),
|
|
529
|
+
key=str(article_key),
|
|
530
|
+
title=title_str,
|
|
531
|
+
body=body_str,
|
|
532
|
+
status=ArticleStatus(status_str),
|
|
533
|
+
tags=tags,
|
|
534
|
+
eyecatch_image_key=str(data.get("eyecatch_image_key")) if data.get("eyecatch_image_key") else None,
|
|
535
|
+
prev_access_key=str(data.get("prev_access_key")) if data.get("prev_access_key") else None,
|
|
536
|
+
created_at=str(data.get("created_at")) if data.get("created_at") else None,
|
|
537
|
+
updated_at=str(data.get("updated_at")) if data.get("updated_at") else None,
|
|
538
|
+
published_at=str(data.get("publish_at")) if data.get("publish_at") else None,
|
|
539
|
+
url=str(data.get("noteUrl")) if data.get("noteUrl") else None,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def to_api_request(article_input: ArticleInput, html_body: str) -> dict[str, object]:
|
|
544
|
+
"""Convert ArticleInput to note.com API request format.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
article_input: Input data from user
|
|
548
|
+
html_body: HTML-converted body content
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
Dictionary suitable for note.com API request
|
|
552
|
+
"""
|
|
553
|
+
return {
|
|
554
|
+
"name": article_input.title,
|
|
555
|
+
"body": html_body,
|
|
556
|
+
"status": "draft",
|
|
557
|
+
}
|
|
File without changes
|