From 7e5c63d02a5cac1958550cc9f1abef7ce3e40c23 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 06:08:19 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`dev`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @yusuke0610. * https://github.com/yusuke0610/devforge/pull/270#issuecomment-4540817056 The following files were modified: * `backend/app/main.py` * `backend/app/routers/github_link.py` * `backend/app/routers/internal.py` * `backend/app/services/markdown/generators/resume_generator.py` * `backend/app/services/pdf/generators/resume_generator.py` * `backend/app/services/shared/resume_format.py` * `backend/tests/test_worker/_helpers.py` * `backend/tests/test_worker/test_execute_task.py` * `backend/tests/test_worker/test_github_link.py` --- backend/app/main.py | 12 +++++ backend/app/routers/github_link.py | 20 ++++++-- backend/app/routers/internal.py | 18 ++++++- .../markdown/generators/resume_generator.py | 13 +++++ .../pdf/generators/resume_generator.py | 27 ++++++++++- backend/app/services/shared/resume_format.py | 47 +++++++++++++----- backend/tests/test_worker/_helpers.py | 48 ++++++++++++++++--- .../tests/test_worker/test_execute_task.py | 24 ++++++---- backend/tests/test_worker/test_github_link.py | 6 ++- 9 files changed, 178 insertions(+), 37 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index dbcf8172..ffa4edf6 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -77,6 +77,18 @@ async def _rate_limit_handler(request: Request, exc: RateLimitExceeded): @app.exception_handler(RequestValidationError) async def _validation_exception_handler(request: Request, exc: RequestValidationError): + """ + Handle request validation errors and produce a standardized 422 error response. + + Parameters: + request (Request): The incoming request that triggered validation. + exc (RequestValidationError): The validation error details. + + Returns: + JSONResponse: HTTP 422 response containing an application error payload with + `code` set to `ErrorCode.VALIDATION_ERROR`, a localized `message`, an `action` + prompting input review, and an `error_id`. + """ error_id = generate_error_id() logger.warning( "リクエストバリデーションエラー", diff --git a/backend/app/routers/github_link.py b/backend/app/routers/github_link.py index d31de433..5fc999cd 100644 --- a/backend/app/routers/github_link.py +++ b/backend/app/routers/github_link.py @@ -149,10 +149,22 @@ async def retry_github_link( user: User = Depends(get_current_user), db: Session = Depends(get_db), ): - """失敗した GitHub 連携タスクを手動で再実行する。 - - ``dead_letter`` 状態のキャッシュのみ再実行可能。 - ``retry_count`` を 0 にリセットし、ステータスを ``pending`` に戻して再ディスパッチする。 + """ + Manually re-dispatch a previously failed GitHub link task for the current user. + + Only a cache in the "dead_letter" terminal state can be retried. This resets the cache's retry count to 0, sets its status to "pending", and dispatches the background task to re-run the GitHub linking process. + + Parameters: + payload (GitHubLinkRequest | None): Optional request body. If provided, its `include_forks` value is used; otherwise `include_forks` defaults to False. + + Returns: + dict: {"status": "pending"} when the task has been queued. + + Raises: + HTTPException: 403 if the current user is not authenticated via GitHub. + HTTPException: 404 if there is no existing GitHub link cache for the user. + HTTPException: 409 if the cache is not in a retryable terminal state or a concurrent retry prevented resetting to pending. + HTTPException: 500 if dispatching the background task fails. """ if not user.username.startswith("github:"): raise_app_error( diff --git a/backend/app/routers/internal.py b/backend/app/routers/internal.py index 7f1b7a6b..eaa868c9 100644 --- a/backend/app/routers/internal.py +++ b/backend/app/routers/internal.py @@ -49,7 +49,23 @@ def _get_max_attempts() -> int: @router.post("/{task_type}") async def handle_task(task_type: str, request: Request): - """Cloud Tasks コールバックまたはローカルテスト用エンドポイント。""" + """ + Handle a task callback request from Cloud Tasks or a local/test invocation and dispatch execution for the specified task type. + + Parameters: + task_type (str): The task type identifier extracted from the URL path. + request (Request): The incoming FastAPI request containing headers and JSON payload. + + Returns: + dict: JSON-serializable response. On success returns {"status": "ok"}; for a non-retryable failure returns {"status": "non_retryable", "error": ""}. + + Raises: + HTTPException: + - 403 when the request is not authorized to invoke internal tasks. + - 400 when the provided task_type is unrecognized. + - 429 or 503 for retryable task failures (429 is used when a `Retry-After` value is provided). + - 500 for unexpected execution errors (generic error detail to avoid leaking internals). + """ if not _verify_request(request): raise HTTPException( status_code=403, diff --git a/backend/app/services/markdown/generators/resume_generator.py b/backend/app/services/markdown/generators/resume_generator.py index 9962da03..34a594f8 100644 --- a/backend/app/services/markdown/generators/resume_generator.py +++ b/backend/app/services/markdown/generators/resume_generator.py @@ -12,6 +12,19 @@ def build_resume_markdown(payload: dict[str, Any]) -> str: + """ + Builds a Japanese resume as a single Markdown-formatted string from the provided payload. + + The payload may include keys such as: + - full_name: applicant's full name + - qualifications: list of {name, acquired_date} + - career_summary: free-form career summary text + - experiences: list of experience objects; each experience may contain company, start_date, end_date, is_current, business_description, employee_count, capital, and normalized client/project data (projects include project-level fields, team, phases, technology_stacks) + - self_pr: free-form self-promotion text + + Returns: + A single string containing the resume formatted in Markdown (Japanese labels and section structure). + """ lines: list[str] = [] lines.append(tpl.TITLE) lines.append("") diff --git a/backend/app/services/pdf/generators/resume_generator.py b/backend/app/services/pdf/generators/resume_generator.py index 8a7e1a42..889b7a95 100644 --- a/backend/app/services/pdf/generators/resume_generator.py +++ b/backend/app/services/pdf/generators/resume_generator.py @@ -46,7 +46,20 @@ def _format_period( def _build_project_html(project) -> str: - """プロジェクト1件分のHTMLを組み立てる""" + """ + Builds the HTML fragment for a single project section of the resume. + + Parameters: + project (dict): Project data following the resume schema. Expected keys include + 'name', 'start_date', 'end_date', 'is_current', 'role', 'phases', + 'challenge', 'action', 'result', 'technology_stacks' and fields used by + team normalization helpers. + + Returns: + str: HTML string representing the project, containing a header (period/name/role/phases), + a left column for work details (challenge/action/result), a right column for grouped + technology stacks, and a team column (total and members). + """ # ヘッダー(3行構成: 期間/プロジェクト名、役割、工程) name = _a(project, "name") start = _a(project, "start_date") @@ -127,7 +140,17 @@ def _build_project_html(project) -> str: def _build_html(resume: dict) -> str: - """職務経歴書データからHTML文字列を組み立てる""" + """ + Assemble an HTML document body representing the provided resume data. + + Builds the HTML for name, current date (rendered in Japan Standard Time), career summary, experiences (companies, clients, projects), qualifications, and self-promotion using the module's escaping, Markdown rendering, and normalization helpers. + + Parameters: + resume (dict): Structured resume data expected to contain keys like "full_name", "career_summary", "experiences", "qualifications", and "self_pr". + + Returns: + html (str): The assembled HTML string for the resume. + """ parts: list[str] = [] # タイトル diff --git a/backend/app/services/shared/resume_format.py b/backend/app/services/shared/resume_format.py index 7f2d38fd..88f64d98 100644 --- a/backend/app/services/shared/resume_format.py +++ b/backend/app/services/shared/resume_format.py @@ -38,17 +38,32 @@ def attr(obj: Any, key: str, default: Any = "") -> Any: - """dict / ORM オブジェクト両対応の属性アクセスヘルパ。""" + """ + Access a value from a mapping or an attribute from an object using a single call. + + If `obj` is a dict-like mapping, returns `obj.get(key, default)`; otherwise returns `getattr(obj, key, default)`. + + Parameters: + obj (Any): A mapping (e.g., dict) or an object with attributes. + key (str): The key or attribute name to retrieve. + default (Any): Value returned when the key/attribute is missing (defaults to empty string). + + Returns: + Any: The retrieved value if present, otherwise `default`. + """ if isinstance(obj, dict): return obj.get(key, default) return getattr(obj, key, default) def normalize_clients(experience: Any) -> list[Any]: - """職歴から取引先(clients)リストを取り出す(dict / ORM 両対応)。 - - 旧スキーマ後方互換: ``clients`` が無く ``projects`` だけがある場合は、 - 無名取引先 1 件(``{"name": "", "projects": [...]}``)に畳んで返す。 + """ + Extract the list of clients from an experience object, supporting both dict and ORM-style access. + + If the experience has no `clients` but has `projects`, returns a single unnamed client `{"name": "", "projects": [...]}` for backward compatibility with the old schema. + + Returns: + list[Any]: A list of client objects (possibly a single synthesized unnamed client). """ clients = attr(experience, "clients", []) if not clients and attr(experience, "projects", None): @@ -57,11 +72,13 @@ def normalize_clients(experience: Any) -> list[Any]: def normalize_team(project: Any) -> Any | None: - """プロジェクトから体制(team)情報を取り出す(dict / ORM 両対応)。 - - 旧スキーマ後方互換: ``team`` が無く ``scale`` だけがある場合は - ``{"total": scale, "members": []}`` に変換して返す。 - team も scale も無ければ ``None`` を返す。 + """ + Extract team information from a project object, supporting both dict and attribute-style access. + + If `team` is missing but `scale` exists, a compatible team object `{"total": scale, "members": []}` is returned. If neither `team` nor `scale` is present, `None` is returned. + + Returns: + dict | None: A team object with keys `"total"` (number) and `"members"` (list) when available, otherwise `None`. """ team = attr(project, "team", None) if not team and attr(project, "scale", None): @@ -70,9 +87,13 @@ def normalize_team(project: Any) -> Any | None: def group_stacks_by_category(stacks: Iterable[Any]) -> dict[str, list[str]]: - """technology_stacks を ``{category: [name, ...]}`` にグルーピングする(dict / ORM 両対応)。 - - 最初に出現した category の順序を保持する。表示用ラベル付けは呼び出し側が行う。 + """ + Group technology stack entries into a mapping from category to a list of names. + + Preserves the order in which categories first appear; display labeling is the caller's responsibility. + + Returns: + dict[str, list[str]]: Mapping from each category to a list of stack names. Categories appear in insertion order (first-seen first). """ grouped: dict[str, list[str]] = {} for st in stacks: diff --git a/backend/tests/test_worker/_helpers.py b/backend/tests/test_worker/_helpers.py index 72056437..fdcaac71 100644 --- a/backend/tests/test_worker/_helpers.py +++ b/backend/tests/test_worker/_helpers.py @@ -6,7 +6,18 @@ def run_sync(coro): - """async 関数を同期的に実行するヘルパー。""" + """ + Run a coroutine to completion on a new event loop and return its result. + + Parameters: + coro: A coroutine or awaitable to execute. + + Returns: + The value produced by the coroutine. + + Notes: + The function creates a fresh event loop and always closes it after completion or if an exception occurs. + """ loop = asyncio.new_event_loop() try: return loop.run_until_complete(coro) @@ -15,21 +26,46 @@ def run_sync(coro): def keep_open_session(db: Session): - """worker が finally で呼ぶ ``db.close()`` を no-op 化するプロキシを返す。 - - worker は終端処理ごとに ``SessionLocal()`` を開いて finally で close するが、 - テストでは同じ ``db_session`` を検証側でも使い続けたいため close を握りつぶす。 - close 時は ``expire_all()`` のみ行い、次回 refresh で最新の DB 状態を読めるようにする。 + """ + Provide a Session proxy whose close() does not close the underlying session. + + Used in tests to prevent worker code from closing a shared Session: attribute access is delegated to the original Session, and calling close() calls `expire_all()` on the real session instead of closing it so the test can continue to use the same Session. + + Parameters: + db (Session): The real SQLAlchemy Session to wrap. + + Returns: + A proxy object exposing the same attributes as `db`; its `close()` method calls `db.expire_all()` rather than closing `db`. """ class _Proxy: def __init__(self, real: Session) -> None: + """ + Initialize the proxy with the underlying SQLAlchemy Session. + + Parameters: + real (Session): The actual Session instance to which attribute access and operations are delegated. + """ self._real = real def __getattr__(self, name): + """ + Delegate attribute access to the underlying real Session. + + Parameters: + name (str): Attribute name being accessed on the proxy. + + Returns: + The attribute or method with the given name from the proxied `self._real`. + """ return getattr(self._real, name) def close(self) -> None: + """ + Mark all ORM objects in the proxied Session as expired without closing the underlying Session. + + Invokes `expire_all()` on the wrapped `Session` to expire loaded instances while keeping the session open for reuse by callers. + """ self._real.expire_all() return _Proxy(db) diff --git a/backend/tests/test_worker/test_execute_task.py b/backend/tests/test_worker/test_execute_task.py index e65317d7..1644d29e 100644 --- a/backend/tests/test_worker/test_execute_task.py +++ b/backend/tests/test_worker/test_execute_task.py @@ -38,10 +38,10 @@ def test_known_task_type_routes_to_correct_handler(self, db_session: Session): mock_gh.assert_called_once() def test_all_task_types_have_dispatch_branch(self): - """TaskType に列挙された全種別に execute_task のディスパッチ分岐があること。 - - 新しい TaskType を追加した際に worker.execute_task への分岐追加を - 忘れて「黙って completed になる」事故を防ぐためのガード。 + """ + Ensure every TaskType has a corresponding dispatch branch in worker.execute_task. + + Acts as a guard so adding a new TaskType without updating execute_task does not silently treat the task as completed; the test fails listing missing TaskType members. """ import inspect @@ -55,11 +55,11 @@ def test_all_task_types_have_dispatch_branch(self): ) def test_execute_task_marks_dead_letter_on_error(self, db_session: Session): - """予期しない例外が発生した場合(max_attempts=1)、例外が再 raise され、 - キャッシュが dead_letter へ遷移し error_message が永続化されること。 - - 内部関数 _mark_dead_letter の呼び出し引数ではなく結果 DB state を検証する - (test_retry_flow.py と同じ契約を、実装詳細に結合しない形で守る)。""" + """ + Verify that when a task handler raises an unexpected exception (max_attempts=1), the exception is re-raised and the corresponding GitHubLinkCache row transitions to the "dead_letter" state with the error message persisted and completed_at set. + + Asserts the post-failure database state (cache.status == "dead_letter", cache.error_message contains the exception text, and cache.completed_at is not None) rather than relying on internal helper call observations. + """ user = UserRepository(db_session).create( "github:dead-letter-user", hashed_password=None, email="dl@test.com", ) @@ -93,7 +93,11 @@ def test_execute_task_marks_dead_letter_on_error(self, db_session: Session): assert cache.completed_at is not None def test_execute_task_creates_notification_on_success(self, db_session: Session): - """タスク成功時に _create_notification が呼ばれること。""" + """ + Verifies that a notification is created when a GitHub link task completes successfully. + + Ensures `_create_notification` is invoked once with the database session provided by `SessionLocal`, the `TaskType.GITHUB_LINK`, the task's `user_id`, and the `"completed"` status. + """ mock_db = MagicMock() mock_session_local = MagicMock(return_value=mock_db) diff --git a/backend/tests/test_worker/test_github_link.py b/backend/tests/test_worker/test_github_link.py index 66f68558..b0135416 100644 --- a/backend/tests/test_worker/test_github_link.py +++ b/backend/tests/test_worker/test_github_link.py @@ -47,7 +47,11 @@ def _sample_repos(self): ] def test_status_transitions_to_completed(self, db_session: Session, session_factory): - """正常系: status が completed に遷移すること。""" + """ + Verify that a pending GitHub link cache transitions to completed on successful processing. + + This test mocks repository collection and token decryption, runs the GitHub link worker, and asserts the cache is marked as "completed", a result is persisted, and `completed_at` is populated. + """ user, cache = self._make_user_and_cache(db_session) repos = self._sample_repos()