Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Offline tests and build

on:
pull_request:
push:
branches: [master]

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test-build:
name: OCR response contracts and package build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: "0.13.4"
python-version: "3.13"
enable-cache: false
- name: Run complete mocked HTTP suite
run: uv run --isolated --no-project --with requests python -B -m unittest discover -v
- name: Build wheel and source distribution
run: uv build
- name: Require both package artifacts
run: |
test -s dist/ocrspace-0.1-py3-none-any.whl
test -s dist/ocrspace-0.1.tar.gz
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# OCRSpace

## Development

Run the complete offline response-contract suite from the repository root:

```sh
uv run --isolated --no-project --with requests python -B -m unittest discover -v
```

All HTTP calls are mocked and image inputs are in-memory fixtures. The tests
exercise the real wrapper without uploading files, retrieving URLs or validating
service credentials.

Build the existing setuptools package without publishing it:

```sh
uv build
```

CI runs the complete suite and build on Linux with CPython 3.13, then requires
both the wheel and source distribution.
25 changes: 25 additions & 0 deletions agent-notes/modernize/task6-ocrspace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Report provider processing errors through APIError

## Goal

Handle documented top-level OCR error responses with absent or empty ParsedResults before indexing that collection. Smallest slice: read the JSON payload once and check IsErroredOnProcessing only when page results are absent/empty in each existing method (approximately eight source lines), a local mocked response regression file, and this note. No endpoint, credentials, upload, OCR engine, dependency or public return-shape change.

## State

- Fresh default/base: `3d13e393a7d050613467b9c1fea9c25c4c89704a`; isolated branch modernize/task6-ocrspace in task-6/worktrees/ocrspace. Canonical git-conversions checkout and same-name non-Git user directory remain untouched.
- Read complete ocrspace.py/setup.py (version0.1; requests dependency), actual Chicken Nugget OCR initializer and interface.py:1253-1296 consumer/error handling.
- No local guidance, notes, existing tests, hosted CI, lock, hooks configuration or documented full-check command exists at base.
- Primary schema: https://ocr.space/ocrapi documents top-level IsErroredOnProcessing/ErrorMessage separately from per-page ParsedResults. Provider support evidence https://forum.ui.vision/t/internal-processing-error/5180 contains an actual processing-error response without ParsedResults. Tests use that response shape, not a live service.
- All HTTP calls are patched; file inputs are local BytesIO fixtures. No file upload, URL retrieval, API-key validation or OCR request is performed.
- Red: `python -B -m unittest -v test_ocrspace.py` ran the entire sole suite on CPython3.13.6. Both documented provider-error subcases raised KeyError('ParsedResults') instead of APIError; existing success-shape and per-page-error characterization cases passed (3 tests,2 errors).
- Green: the same complete sole-suite command passed all3 tests in0.002s after the two method checks. No dependency resolver, build or live API operation was run.
- Revision red: the new partial-result characterization failed with APIError under the initial unconditional guard. Read-only inspection after an editing-tool context error confirmed both narrowed guards and the new partial/empty-result tests were already applied; they were not applied again. Run the revised four-test sole suite next.
- Revision green: the complete sole suite now passes all4 tests in0.002s, covering missing/empty error page collections for both methods and preserving successful/partial-result/per-page-error contracts.

## Decisions and why

Use the existing APIError with the provider's original ErrorMessage only when an explicit processing error has no page results. A processing-error flag can accompany partial results; checking it unconditionally would replace the existing page-result contract. Preserve that path, including successful first-page URL results and file lists containing later page errors. Do not introduce a shared response abstraction or broaden per-page error policy.

## Next step

Coordinator review of initial commit a6617ba found the unconditional flag check changed partial-result behavior. Publication was not authorized. The requested characterization is red/green and both guards are narrowed to absent/empty page results. Commit this revision and return the complete base-to-head diff for review before publication. Focused mocked tests do not claim live OCR/provider interoperability.
27 changes: 27 additions & 0 deletions agent-notes/repair-task6-ocrspace-ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Host the existing offline response contracts and package build

## Goal

Add minimal hosted full-test/build coverage to feature PR #1. No source, dependency, provider policy or runtime/support changes. Approximately 35 workflow lines plus command documentation and this living note.

## State

- Verified origin git@github.com:accessibleapps/ocrspace.git; fresh master 3d13e393a7d050613467b9c1fea9c25c4c89704a contains only ocrspace.py/setup.py, with no tests/workflow.
- Original local and published feature heads both 66ce14cc8aaa5e0c476ce46a35ff818f00f87a83. Named backup refs backup/task6-ocrspace-local-before-ci and backup/task6-ocrspace-published-before-ci retain both. Canonical and original feature worktrees clean and unchanged.
- Isolated task-root worktrees/ocrspace-ci on repair/task6-ocrspace-ci starts at the published feature tip. Actual existing tests live only on that feature; no empty baseline prerequisite is appropriate.
- Read global instructions, repair prompts, repository-ci, complete source/setup.py/test and original branch note. No repository/nested guidance/.agents skills found. Actions enabled verified read-only; no settings changed.

## Decisions and why

- Original full four-method unittest suite patches requests.post and uses BytesIO fixture inputs. It exercises actual wrapper behavior for successful/partial pages, missing/empty top-level errors, and existing page errors without any real HTTP operation or credential.
- Use isolated Requests environment from existing setup.py's sole declared runtime dependency. No new lock, manifest version, major dependency update or mock-test rewrite.
- Coordinator explicitly settled narrow build authority: uv build the existing setuptools setup.py package, require exact wheel/source artifacts. This is a nonpublishing package gate, no new build system.
- One Linux/Python 3.13 job on PR/master push, read-only permission, no persisted checkout credentials, no cache/secrets, timeout and concurrency. Immutable action pins and uv 0.13.4 verified from official release metadata/manifests during this session.

## Surprises

Missing CI is a verification gap, not a demonstrated default source/test failure. No new behavioral red regression claimed; no live provider checks.

## Next step

Final new YAML passed actionlint 1.7.12 syntax/expression audit and zizmor 1.31.1 --offline --pedantic with zero findings; optional actionlint shellcheck/pyflakes integrations disabled. Complete added workflow/README/note inspected; source/setup.py/test unchanged from original feature. Commit named files and return exact head for coordinator review before any existing-feature leased publication. Prior full 4-test green results are in the original feature note; no unnecessary local full rerun. Hosted final-head test/build execution awaits publication authority. Actual Claude review is separately coordinated. No push/merge/rebase/settings/release action here.
10 changes: 8 additions & 2 deletions ocrspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ def OCR_URL(self, url, overlay=False):
}
response = requests.post(self.url, data=payload)
response.raise_for_status()
result = response.json()['ParsedResults'][0]
response_data = response.json()
if response_data.get('IsErroredOnProcessing') and not response_data.get('ParsedResults'):
raise APIError(response_data.get('ErrorMessage'))
result = response_data['ParsedResults'][0]
if result['ErrorMessage']:
raise APIError(result['ErrorMessage'])
return result
Expand All @@ -30,7 +33,10 @@ def OCR_file(self, fileobj, overlay=False):
}
response = requests.post(self.url, data=payload, files={'file': fileobj})
response.raise_for_status()
results = response.json()['ParsedResults']
response_data = response.json()
if response_data.get('IsErroredOnProcessing') and not response_data.get('ParsedResults'):
raise APIError(response_data.get('ErrorMessage'))
results = response_data['ParsedResults']
if results[0]['ErrorMessage']:
raise APIError(results[0]['ErrorMessage'])
return results
Expand Down
84 changes: 84 additions & 0 deletions test_ocrspace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Check provider response contracts with mocked HTTP and local fixtures."""

from io import BytesIO
import unittest
from unittest.mock import Mock, patch

import ocrspace


class OCRSpaceResponseTests(unittest.TestCase):
def setUp(self):
self.api = ocrspace.OCRSpaceAPI("example-key")
self.response = Mock()
post = patch.object(ocrspace.requests, "post", return_value=self.response)
post.start()
self.addCleanup(post.stop)

def test_provider_error_without_page_results_raises_api_error(self):
# Provider-documented error schema, also reported in its support forum:
# https://forum.ui.vision/t/internal-processing-error/5180
messages = ["Some internal processing error occurred."]
self.response.json.return_value = {
"OCRExitCode": 99,
"IsErroredOnProcessing": True,
"ErrorMessage": messages,
"ErrorDetails": "",
}
for empty_results in (False, True):
if empty_results:
self.response.json.return_value["ParsedResults"] = []
with BytesIO(b"local fixture") as image:
calls = (
(self.api.OCR_URL, "https://example.test/image.png"),
(self.api.OCR_file, image),
)
for method, argument in calls:
with self.subTest(method=method.__name__, empty_results=empty_results):
with self.assertRaises(ocrspace.APIError) as caught:
method(argument)
self.assertEqual(caught.exception.args[0], messages)

def test_success_retains_url_and_file_return_shapes(self):
pages = [
{"ErrorMessage": "", "ParsedText": "first page"},
{"ErrorMessage": "", "ParsedText": "second page"},
]
self.response.json.return_value = {
"IsErroredOnProcessing": False, "ParsedResults": pages
}

self.assertEqual(self.api.OCR_URL("https://example.test/image.png"), pages[0])
with BytesIO(b"local fixture") as image:
self.assertEqual(self.api.OCR_file(image), pages)

def test_partial_results_keep_existing_page_result_contract(self):
pages = [
{"ErrorMessage": "", "ParsedText": "first page"},
{"ErrorMessage": "page parse failed", "ParsedText": None},
]
self.response.json.return_value = {
"OCRExitCode": 2,
"IsErroredOnProcessing": True,
"ErrorMessage": ["A page could not be parsed"],
"ParsedResults": pages,
}

self.assertEqual(self.api.OCR_URL("https://example.test/image.png"), pages[0])
with BytesIO(b"local fixture") as image:
self.assertEqual(self.api.OCR_file(image), pages)

def test_page_error_still_raises_api_error(self):
self.response.json.return_value = {
"IsErroredOnProcessing": False,
"ParsedResults": [{"ErrorMessage": "page parse failed"}],
}
with self.assertRaises(ocrspace.APIError):
self.api.OCR_URL("https://example.test/image.png")
with BytesIO(b"local fixture") as image:
with self.assertRaises(ocrspace.APIError):
self.api.OCR_file(image)


if __name__ == "__main__":
unittest.main()
Loading