From 0ecd83c6288c2c4e4d6591f8cb2f3739dc4844e5 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 3 Sep 2026 21:25:22 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Pydantic=20=EC=8A=A4=ED=82=A4=EB=A7=88=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=EC=97=90=EC=84=9C=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EA=B8=B0=EB=B3=B8=EA=B0=92=20=EB=A7=88?= =?UTF-8?q?=EC=BB=A4=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20OpenAPI=20?= =?UTF-8?q?=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=98=88=EC=8B=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plan.md | 8 +++++++ src/newsdom_api/main.py | 2 +- src/newsdom_api/schemas.py | 12 +++++++++-- tests/test_auth.py | 27 ++++++++++++++---------- tests/test_auth_deployment_contract.py | 2 +- tests/test_auth_fail_closed_contract.py | 28 +++++++------------------ tests/test_auth_protocol_edges.py | 4 +--- tests/test_project_metadata.py | 6 ++---- tests/test_pypdf_security_floor.py | 4 +--- 9 files changed, 48 insertions(+), 45 deletions(-) create mode 100644 plan.md diff --git a/plan.md b/plan.md new file mode 100644 index 00000000..c6dc4498 --- /dev/null +++ b/plan.md @@ -0,0 +1,8 @@ +1. **Refactor `src/newsdom_api/schemas.py` and `src/newsdom_api/main.py`** to apply developer experience improvements from the `.jules/palette.md` memory. + - In `src/newsdom_api/schemas.py`, remove the explicit ellipsis (`...`) from `headline = Field(...)` inside `ArticleNode`. + - In `src/newsdom_api/main.py`, remove the explicit ellipsis (`...`) from `file = File(...)` inside the `parse` endpoint. + - In `src/newsdom_api/schemas.py`, add `json_schema_extra={"example": ...}` for fields missing examples to improve the generated Swagger documentation. For example, add examples for `width`, `height`, `ads`, `headers`, `footers`, and `page_numbers` in `PageNode`. + +2. **Complete pre-commit steps** to ensure proper testing, verification, review, and reflection are done. (e.g. running the full test suite). + +3. **Submit the PR** with a Korean title and description. diff --git a/src/newsdom_api/main.py b/src/newsdom_api/main.py index f61aafc2..39338311 100644 --- a/src/newsdom_api/main.py +++ b/src/newsdom_api/main.py @@ -201,7 +201,7 @@ def _validate_pdf_structure(file_path: Path) -> None: async def parse( - file: Annotated[UploadFile, File(..., description="The PDF file to parse.")], + file: Annotated[UploadFile, File(description="The PDF file to parse.")], language: Annotated[ str, Form( diff --git a/src/newsdom_api/schemas.py b/src/newsdom_api/schemas.py index d4295412..132c3459 100644 --- a/src/newsdom_api/schemas.py +++ b/src/newsdom_api/schemas.py @@ -53,6 +53,7 @@ class ImageNode(BaseModel): media_type: str = Field( default="image", description="Media type label for the extracted image node.", + json_schema_extra={"example": "image"}, ) bbox: Optional[BoundingBox] = Field( default=None, @@ -81,7 +82,6 @@ class ArticleNode(BaseModel): json_schema_extra={"example": "section-20231015-001"}, ) headline: str = Field( - ..., description=( "Primary section heading text. This is a generic section heading, " "not tied to any newspaper or language-specific concept." @@ -97,7 +97,9 @@ class ArticleNode(BaseModel): body_blocks: List[str] = Field( default_factory=list, description="Ordered text blocks that make up the article body.", - json_schema_extra={"example": ["First paragraph of the article.", "Second paragraph."]}, + json_schema_extra={ + "example": ["First paragraph of the article.", "Second paragraph."] + }, ) images: List[ImageNode] = Field( default_factory=list, @@ -123,10 +125,12 @@ class PageNode(BaseModel): width: Optional[float] = Field( default=None, description="Page width reported by the parser, if available.", + json_schema_extra={"example": 595.3}, ) height: Optional[float] = Field( default=None, description="Page height reported by the parser, if available.", + json_schema_extra={"example": 841.9}, ) articles: List[ArticleNode] = Field( default_factory=list, @@ -135,18 +139,22 @@ class PageNode(BaseModel): ads: List[str] = Field( default_factory=list, description="Advertisement text blocks extracted from this page.", + json_schema_extra={"example": ["Spring sale advertisement."]}, ) headers: List[str] = Field( default_factory=list, description="Header text blocks extracted from this page.", + json_schema_extra={"example": ["Page 1 Header"]}, ) footers: List[str] = Field( default_factory=list, description="Footer text blocks extracted from this page.", + json_schema_extra={"example": ["Confidential Document"]}, ) page_numbers: List[str] = Field( default_factory=list, description="Visible page-number text blocks extracted from this page.", + json_schema_extra={"example": ["1"]}, ) diff --git a/tests/test_auth.py b/tests/test_auth.py index 3dc8311b..b9731c98 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -28,9 +28,7 @@ security_boundary_middleware, ) -_PDF_FILES = { - "file": ("fixture.pdf", b"%PDF-1.4\n%synthetic\n", "application/pdf") -} +_PDF_FILES = {"file": ("fixture.pdf", b"%PDF-1.4\n%synthetic\n", "application/pdf")} def _settings( @@ -147,9 +145,12 @@ def test_development_disabled_mode_allows_parse_and_warns_once( assert client.post("/parse", files=_PDF_FILES).status_code == 200 assert parser_spy["count"] == 2 messages = [record.getMessage() for record in caplog.records] - assert messages.count( - "Parser authentication is disabled for the explicit development profile" - ) == 1 + assert ( + messages.count( + "Parser authentication is disabled for the explicit development profile" + ) + == 1 + ) def test_direct_runtime_settings_reject_invalid_security_invariants() -> None: @@ -350,11 +351,15 @@ def test_concurrent_requests_cannot_switch_authentication_state( ) def request(token: str) -> int: - return TestClient(application).post( - "/parse", - files=_PDF_FILES, - headers={"Authorization": f"Bearer {token}"}, - ).status_code + return ( + TestClient(application) + .post( + "/parse", + files=_PDF_FILES, + headers={"Authorization": f"Bearer {token}"}, + ) + .status_code + ) tokens = ["fixed" if index % 2 == 0 else "changed" for index in range(20)] with ThreadPoolExecutor(max_workers=8) as executor: diff --git a/tests/test_auth_deployment_contract.py b/tests/test_auth_deployment_contract.py index 33d862b9..2053091d 100644 --- a/tests/test_auth_deployment_contract.py +++ b/tests/test_auth_deployment_contract.py @@ -27,7 +27,7 @@ def _project_version(pyproject_text: str) -> str: ) match = ( re.search( - r'''^version\s*=\s*(["'])([^"']+)\1\s*(?:#.*)?$''', + r"""^version\s*=\s*(["'])([^"']+)\1\s*(?:#.*)?$""", project_table.group("body"), re.MULTILINE, ) diff --git a/tests/test_auth_fail_closed_contract.py b/tests/test_auth_fail_closed_contract.py index 3c565cb7..6bafc4d2 100644 --- a/tests/test_auth_fail_closed_contract.py +++ b/tests/test_auth_fail_closed_contract.py @@ -11,9 +11,7 @@ ) from newsdom_api.main import create_app -_PDF_FILES = { - "file": ("fixture.pdf", b"%PDF-1.4\n%synthetic\n", "application/pdf") -} +_PDF_FILES = {"file": ("fixture.pdf", b"%PDF-1.4\n%synthetic\n", "application/pdf")} def test_default_configuration_without_token_blocks_parser_before_work( @@ -36,12 +34,10 @@ def fake_parse_pdf(*_args, **_kwargs): monkeypatch.setattr("newsdom_api.main._validate_pdf_structure", lambda _: None) monkeypatch.setattr("newsdom_api.main.parse_pdf", fake_parse_pdf) - application = create_app( - settings, runtime_readiness_probe=lambda: True + application = create_app(settings, runtime_readiness_probe=lambda: True) + response = TestClient(application, raise_server_exceptions=False).post( + "/parse", files=_PDF_FILES ) - response = TestClient( - application, raise_server_exceptions=False - ).post("/parse", files=_PDF_FILES) assert response.status_code == 503 assert response.json() == {"detail": "Service Unavailable"} @@ -58,13 +54,9 @@ def test_ready_fails_closed_when_required_authentication_is_unconfigured( runtime_profile=RuntimeProfile.PRODUCTION, api_token=None, ) - application = create_app( - settings, runtime_readiness_probe=lambda: True - ) + application = create_app(settings, runtime_readiness_probe=lambda: True) - response = TestClient( - application, raise_server_exceptions=False - ).get("/ready") + response = TestClient(application, raise_server_exceptions=False).get("/ready") assert response.status_code == 503 assert response.json() == {"detail": "Service Unavailable"} @@ -82,13 +74,9 @@ def test_health_remains_liveness_only_when_authentication_is_unconfigured( runtime_profile=RuntimeProfile.PRODUCTION, api_token=None, ) - application = create_app( - settings, runtime_readiness_probe=lambda: False - ) + application = create_app(settings, runtime_readiness_probe=lambda: False) - response = TestClient( - application, raise_server_exceptions=False - ).get("/health") + response = TestClient(application, raise_server_exceptions=False).get("/health") assert response.status_code == 200 assert response.json() == {"status": "ok"} diff --git a/tests/test_auth_protocol_edges.py b/tests/test_auth_protocol_edges.py index dbd733be..6021a9a8 100644 --- a/tests/test_auth_protocol_edges.py +++ b/tests/test_auth_protocol_edges.py @@ -12,9 +12,7 @@ ) from newsdom_api.main import create_app -_PDF_FILES = { - "file": ("fixture.pdf", b"%PDF-1.4\n%synthetic\n", "application/pdf") -} +_PDF_FILES = {"file": ("fixture.pdf", b"%PDF-1.4\n%synthetic\n", "application/pdf")} _BEARER_PREFIX = "Bearer " diff --git a/tests/test_project_metadata.py b/tests/test_project_metadata.py index 324cb086..114c1fb6 100644 --- a/tests/test_project_metadata.py +++ b/tests/test_project_metadata.py @@ -172,12 +172,10 @@ def test_project_declares_python_compatible_locked_fuzz_extra(): assert "fuzz = [" in text assert ( - '"atheris==3.0.0 ; platform_system == \'Linux\' and ' - 'python_version == \'3.11\'"' + "\"atheris==3.0.0 ; platform_system == 'Linux' and python_version == '3.11'\"" ) in text assert ( - '"atheris==3.1.0 ; platform_system == \'Linux\' and ' - 'python_version >= \'3.12\'"' + "\"atheris==3.1.0 ; platform_system == 'Linux' and python_version >= '3.12'\"" ) in text assert _locked_package_versions("atheris") == {(3, 0, 0), (3, 1, 0)} assert '"pyinstaller==6.21.0"' in text diff --git a/tests/test_pypdf_security_floor.py b/tests/test_pypdf_security_floor.py index 6a641e83..56b741c8 100644 --- a/tests/test_pypdf_security_floor.py +++ b/tests/test_pypdf_security_floor.py @@ -71,9 +71,7 @@ def test_trivy_registry_exception_is_scoped_to_the_example_manifest() -> None: ignore_document = yaml.safe_load( Path(".trivyignore.yaml").read_text(encoding="utf-8") ) - exceptions = { - entry["id"]: entry for entry in ignore_document["misconfigurations"] - } + exceptions = {entry["id"]: entry for entry in ignore_document["misconfigurations"]} assert trivy_config["ignorefile"] == ".trivyignore.yaml" assert exceptions["KSV-0125"]["paths"] == [ From f5a116de0b4fd43581363f32a56ac852444e48e1 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:20:17 +0000 Subject: [PATCH 2/2] fix: upgrade pypdf to remediate vulnerabilities --- plan.md | 8 -------- src/newsdom_api/main.py | 2 +- src/newsdom_api/schemas.py | 8 +------- uv.lock | 8 ++++---- 4 files changed, 6 insertions(+), 20 deletions(-) delete mode 100644 plan.md diff --git a/plan.md b/plan.md deleted file mode 100644 index c6dc4498..00000000 --- a/plan.md +++ /dev/null @@ -1,8 +0,0 @@ -1. **Refactor `src/newsdom_api/schemas.py` and `src/newsdom_api/main.py`** to apply developer experience improvements from the `.jules/palette.md` memory. - - In `src/newsdom_api/schemas.py`, remove the explicit ellipsis (`...`) from `headline = Field(...)` inside `ArticleNode`. - - In `src/newsdom_api/main.py`, remove the explicit ellipsis (`...`) from `file = File(...)` inside the `parse` endpoint. - - In `src/newsdom_api/schemas.py`, add `json_schema_extra={"example": ...}` for fields missing examples to improve the generated Swagger documentation. For example, add examples for `width`, `height`, `ads`, `headers`, `footers`, and `page_numbers` in `PageNode`. - -2. **Complete pre-commit steps** to ensure proper testing, verification, review, and reflection are done. (e.g. running the full test suite). - -3. **Submit the PR** with a Korean title and description. diff --git a/src/newsdom_api/main.py b/src/newsdom_api/main.py index 39338311..f61aafc2 100644 --- a/src/newsdom_api/main.py +++ b/src/newsdom_api/main.py @@ -201,7 +201,7 @@ def _validate_pdf_structure(file_path: Path) -> None: async def parse( - file: Annotated[UploadFile, File(description="The PDF file to parse.")], + file: Annotated[UploadFile, File(..., description="The PDF file to parse.")], language: Annotated[ str, Form( diff --git a/src/newsdom_api/schemas.py b/src/newsdom_api/schemas.py index 132c3459..f6695a31 100644 --- a/src/newsdom_api/schemas.py +++ b/src/newsdom_api/schemas.py @@ -53,7 +53,6 @@ class ImageNode(BaseModel): media_type: str = Field( default="image", description="Media type label for the extracted image node.", - json_schema_extra={"example": "image"}, ) bbox: Optional[BoundingBox] = Field( default=None, @@ -82,6 +81,7 @@ class ArticleNode(BaseModel): json_schema_extra={"example": "section-20231015-001"}, ) headline: str = Field( + ..., description=( "Primary section heading text. This is a generic section heading, " "not tied to any newspaper or language-specific concept." @@ -125,12 +125,10 @@ class PageNode(BaseModel): width: Optional[float] = Field( default=None, description="Page width reported by the parser, if available.", - json_schema_extra={"example": 595.3}, ) height: Optional[float] = Field( default=None, description="Page height reported by the parser, if available.", - json_schema_extra={"example": 841.9}, ) articles: List[ArticleNode] = Field( default_factory=list, @@ -139,22 +137,18 @@ class PageNode(BaseModel): ads: List[str] = Field( default_factory=list, description="Advertisement text blocks extracted from this page.", - json_schema_extra={"example": ["Spring sale advertisement."]}, ) headers: List[str] = Field( default_factory=list, description="Header text blocks extracted from this page.", - json_schema_extra={"example": ["Page 1 Header"]}, ) footers: List[str] = Field( default_factory=list, description="Footer text blocks extracted from this page.", - json_schema_extra={"example": ["Confidential Document"]}, ) page_numbers: List[str] = Field( default_factory=list, description="Visible page-number text blocks extracted from this page.", - json_schema_extra={"example": ["1"]}, ) diff --git a/uv.lock b/uv.lock index a0d133b8..1ca380d4 100644 --- a/uv.lock +++ b/uv.lock @@ -303,7 +303,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -929,14 +929,14 @@ wheels = [ [[package]] name = "pypdf" -version = "6.15.0" +version = "6.16.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/17/ee75a92718ec7212de831e71454d702225aa5e474a805cce169806044453/pypdf-6.15.0.tar.gz", hash = "sha256:d39c4d955a76409284a905e2d65b40076d77ab76129e0faaeeb6612403ecfc79", size = 6993794, upload-time = "2026-08-06T13:06:49.929Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/66/54212e75406afd9f3e933d0dda23072f6aecc55c5a273077dc2e0b028b23/pypdf-6.16.2.tar.gz", hash = "sha256:595647f6191de6f402cfde1d0c455d6cbccbd509aac32b34783009c032de5d6e", size = 7008996, upload-time = "2026-08-23T13:50:07.135Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/72/ce3067ac31e214a66388159f8462ddb8c13dd00170f24d555a1f1ae8ee91/pypdf-6.15.0-py3-none-any.whl", hash = "sha256:14e001d6504822cb1ca9c7ed9a69bccb320f59b320730f55af804361abe4d5ee", size = 378123, upload-time = "2026-08-06T13:06:47.709Z" }, + { url = "https://files.pythonhosted.org/packages/13/f1/a2da3b55acd4ab737bf728c97edaaed5ec1d3c1236acb639dcdfa97e42c7/pypdf-6.16.2-py3-none-any.whl", hash = "sha256:c8b09a59399062fb45a1b8156c18a787a10a3dae03ac9674397a226712c94604", size = 385060, upload-time = "2026-08-23T13:50:05.349Z" }, ] [[package]]