PaperNest: Architecture and Runtime Guide
PaperNest: Architecture and Runtime Guide
This document explains how this project is structured and how it behaves at runtime. After reading it, you should be able to follow the full path from app startup to user actions, persistence, OCR, and exports.
Companion versions:
- Function-by-function call graph: FUNCTION_BY_FUNCTION_CALL_GRAPH.md
- Diagram-first sequence guide: DIAGRAM_FIRST_SEQUENCE_GUIDE.md
1) What the app is
PaperNest is a KivyMD app that lets a user:
- capture or select receipt images,
- auto-detect date and amount using OCR,
- save receipts in SQLite,
- view timeline totals,
- edit/delete records,
- export timeline CSV and receipt images.
Primary runtime stack:
- UI framework: Kivy + KivyMD
- Business logic: pure Python service layer
- Persistence: sqlite3
- OCR engines:
- Desktop/non-Android: Tesseract via pytesseract + Pillow
- Android: Google ML Kit Text Recognition via pyjnius
2) High-level architecture
The codebase follows a layered approach:
- Entry/App layer
- main.py
- receipt_app/views/app.py
- UI layer (screens + components)
- receipt_app/views/screens/*
- receipt_app/views/components/*
- receipt_app/views/shared/*
- Business layer
- receipt_app/business/receipt_service.py
- receipt_app/business/ocr_service.py
- receipt_app/business/image_storage_service.py
- receipt_app/business/csv_export_service.py
- Data layer
- receipt_app/data/receipt_repository.py
- Core shared types/errors
- receipt_app/core/models.py
- receipt_app/core/exceptions.py
- Compatibility facade modules (legacy import paths)
- receipt_app/models.py, exceptions.py, repository.py, services.py, ui.py
Important design idea:
- UI talks to
ReceiptService(not directly to SQLite). ReceiptServiceorchestrates repository + storage + OCR + export services.ReceiptRepositoryonly handles SQL operations.
3) Startup sequence (exact runtime path)
main.pyrunsReceiptTrackerApp().run().- Kivy calls
ReceiptTrackerApp.build()in receipt_app/views/app.py. build()does dependency wiring:- creates data directory under Kivy
user_data_dir - instantiates:
ReceiptRepositoryImageStorageServiceCsvExportServiceOCRServiceReceiptService(repository=..., image_storage=..., ocr_service=..., csv_export_service=...)
- creates data directory under Kivy
build()loads KV files in_load_kv_files()withBuilder.load_file(...).build()creates root widget:ReceiptRoot(receipt_service=receipt_service, image_storage=image_storage)
ReceiptRoot.on_kv_post()configures screens:- AddReceiptScreen gets
receipt_service+image_storage - TimelineScreen gets
receipt_service - GalleryScreen gets
receipt_service
- AddReceiptScreen gets
This is constructor injection at app startup: dependencies are created once and passed down.
4) Python language features used in this project
4.1 Inheritance
Examples:
ReceiptTrackerApp(MDApp)AddReceiptScreen(Screen),TimelineScreen(Screen),GalleryScreen(Screen)OutlinedCard(MDCard)TappableOutlinedCard(OutlinedCard)TapToEditTextInput(TextInput)
Why it is used:
- to extend framework classes with app-specific behavior while reusing Kivy/KivyMD features.
4.2 Dependency injection
Main example:
ReceiptServiceis initialized with dependencies inReceiptTrackerApp.build():- repository
- image storage
- OCR service
- CSV export service
Why this matters:
- business logic is not tightly coupled to concrete global singletons.
- easier to test and replace pieces.
4.3 Dataclasses
In receipt_app/core/models.py:
ReceiptandOCRSuggestionare@dataclass(frozen=True, slots=True).
What that gives:
- concise immutable-style data objects (
frozen=True), - lower memory footprint (
slots=True), - clear typing between layers.
4.4 Properties and descriptors (Kivy)
Screens define Kivy properties such as StringProperty, NumericProperty, BooleanProperty.
Example: AddReceiptScreen.status_message.
Why:
- KV bindings auto-refresh UI when these values change.
4.5 Package-level __init__.py
Used for:
- package markers,
- re-exporting selected symbols via
__all__.
There are also compatibility __init__ and facade modules preserving old import paths.
5) Data model and persistence
5.1 Receipt model
Receipt fields:
- id
- receipt_date
- amount
- image_path
- created_at
Receipt.from_row() converts sqlite rows to typed objects.
5.2 Database schema
In ReceiptRepository._initialize_schema():
- table
receipts - index on
receipt_date
Schema choices:
receipt_dateandcreated_atstored as ISO text- amount stored as REAL with non-negative CHECK
5.3 Repository behavior
ReceiptRepository opens a fresh sqlite connection per operation (_connect).
This simplifies thread use, because workers do not share one sqlite connection object.
Main operations:
- add/get/update/delete receipt
- count by image path (for image cleanup safety)
- query date range and totals
- list all receipts (gallery)
6) Business layer behavior
6.1 ReceiptService is the orchestrator
ReceiptService validates inputs and coordinates lower-level services.
Key methods:
add_receipt()- parse date/amount
- validate/copy image
- insert DB row
- rollback copied file if DB insert fails
suggest_fields_from_image()- validates image path
- delegates OCR
update_receipt()delete_receipt()- delete DB row
- remove image file only if no other record references it
get_timeline_summary()export_timeline_csv()export_receipt_images()get_gallery_grouped_by_year_month()
6.2 OCRService engine selection
At initialization (OCRService.__init__ -> _setup_engine):
- If Android env detected, tries ML Kit setup first.
- Otherwise tries pytesseract/Pillow and checks
tesseractbinary availability.
available and unavailable_reason are exposed to UI.
OCR output pipeline:
- extract raw text (ML Kit or pytesseract)
- parse date candidates
- parse and rank amount candidates
- choose top suggestions
- return
OCRSuggestion
Android OCR uses a fast-first path: it scans the original sampled bitmap, stops when date/amount confidence is sufficient, and prepares enhanced fallback variants only for weak or partial results. Temporary variants are dimension-bounded before filtering, and the path includes GC hints to reduce memory pressure from large phone photos.
7) UI architecture and KV setup
7.1 Root layout
ReceiptRoot in root_screen.kv contains:
ScreenManagerwith three active screens:- add_receipt
- timeline
- gallery
- bottom navigation bar to switch current screen
- global busy indicator card bound to app-level busy state
Busy indicator data source:
ReceiptTrackerApp.begin_busy()/end_busy()busy_task_count,busy_message
7.2 KV loading strategy
All KV files are loaded explicitly in ReceiptTrackerApp._load_kv_files().
This avoids implicit loading surprises and ensures custom widgets/rules are available.
7.3 Shared reusable UI pieces
OutlinedCard,TappableOutlinedCard: custom card visuals/tap handlingpopup_utils.py: themed popup factory + button stylingfile_picker_helper.py: reusable file/folder picker popupsamount_numpad_popup.py: numeric keypad popup for amount entryreceipt_editor.py: shared edit popup used by timeline and gallery
8) Main user flows
8.1 Add Receipt flow
Screen: receipt_app/views/screens/add_receipt/add_receipt_screen.py
Flow:
- User captures photo or chooses existing image(s).
- Selected images become
ReceiptDraftobjects inReceiptDraftManager. - Screen auto-triggers OCR scan for one or all selected drafts.
- OCR suggestions populate date/amount and candidate lists.
- User can edit using date picker and numpad.
- User taps Save.
- Save runs in worker thread.
- On success:
- draft removed from in-memory batch
- timeline and gallery are refreshed
Special details:
- supports multiple images in one selection batch
- supports swipe left/right preview navigation
- uses
_scan_generationto ignore stale OCR results from old scans
8.2 Timeline flow
Screen: timeline_screen.py
Flow:
- User sets start/end date.
refresh_summary()queries service for total + receipts.- Results rendered as row cards.
- User can edit/delete a row.
- User can export:
- CSV report
- receipt image files grouped by year/month-name
8.3 Gallery flow
Screen: gallery_screen.py
Flow:
- Worker refresh loads grouped hierarchy year -> month -> receipts.
- UI shows year list, then month list, then receipt cards.
- User opens options popup on a receipt card.
- Edit/delete actions call shared editor/service methods.
- Timeline screen is notified and refreshed after changes.
9) Threading model: when threads start and end
Only a few places use Python threads, mostly to keep UI responsive.
All worker threads are daemon threads and hand results back with Clock.schedule_once(...).
9.1 AddReceiptScreen OCR scan thread
Start:
AddReceiptScreen.run_ocr()createsthreading.Thread(target=_scan_receipts_worker, daemon=True).start().
Thread work:
- loops selected image batch
- calls
receipt_service.suggest_fields_from_image(...) - collects results/errors
- schedules
_apply_scan_results_safely(...)on main thread
End:
- thread exits when
_scan_receipts_workerreturns. finallyblock detaches jnius thread on Android.- UI loading state is ended by main-thread callbacks (
s(False)).
9.2 AddReceiptScreen save thread
Start:
save_receipt()startsThread(target=_save_receipt_worker, daemon=True).
Thread work:
- calls
receipt_service.add_receipt(...) - schedules
_apply_saved_receipt(...)or_handle_save_error(...)on main thread
End:
- exits after one save attempt.
- loading state cleared in scheduled UI callback.
9.3 GalleryScreen refresh thread
Start:
refresh_gallery()startsThread(target=_refresh_gallery_worker, daemon=True).
Thread work:
- fetches grouped data from service
- schedules
_apply_refreshed_gallery(...)or error handler
End:
- exits after one refresh attempt.
- loading popup + busy token cleared in UI callback.
9.4 GalleryScreen delete thread
Start:
delete_receipt()startsThread(target=_delete_receipt_worker, daemon=True).
Thread work:
- calls service delete
- schedules success/failure callback
End:
- exits after delete attempt.
- loading UI cleared on callback.
9.5 Thread safety notes
- UI updates are correctly marshaled to Kivy main thread with
Clock.schedule_once. - Repository opens separate sqlite connection per call, reducing shared-connection thread issues.
- Android pyjnius attach/detach is explicitly handled in OCR worker thread.
10) Purpose of __init__ methods in this project
10.1 Class __init__ methods
They mostly do one of these:
- establish default runtime state (indices, flags, caches),
- store injected dependencies,
- prepare helper objects (draft manager, editor, popups),
- create directories for persistence services.
Examples:
ReceiptTrackerApp.__init__: initializes global busy token bookkeeping.ReceiptRepository.__init__: stores DB path, ensures parent dir, initializes schema.ImageStorageService.__init__: ensures receipt image directory exists.AddReceiptScreen.__init__: initializes draft manager, Android capture state, scan generation.GalleryScreen.__init__: initializes hierarchy cache and loading state.
10.2 Package __init__.py files
They define import surfaces with __all__ and re-export selected classes.
This keeps imports tidy and supports stable API boundaries across modules.
11) Error handling strategy
Error classes in core/exceptions.py are domain-specific:
ValidationErrorRepositoryErrorImageStorageErrorOCRServiceErrorExportErrorNotFoundError
Pattern:
- lower layers raise typed exceptions,
- screen layer catches and shows user-friendly popups,
- unexpected exceptions are logged and surfaced with safe generic messages.
12) Android-specific behavior
Android-specific paths include:
- runtime permission requests before camera/media actions,
- native camera intent result handling,
- content URI copy into app-owned local files,
- OCR through ML Kit via pyjnius,
- explicit jnius
attach()anddetach()in OCR worker thread.
Build settings (buildozer.spec) include:
- Android permissions,
- ML Kit Gradle dependency,
- target/min API settings,
- architecture targets.
13) Compatibility and non-primary modules
13.1 Compatibility facade modules
receipt_app/models.py, exceptions.py, repository.py, services.py, ui.py re-export modern modules.
Purpose: keep old import paths working while project moved to layered structure.
13.2 Batch review screen status
BatchReviewScreen exists but is not currently wired into root ScreenManager and not exported by views/screens/__init__.py.
It is available as a feature module but not part of current runtime navigation.
13.3 Image processing test utility
image_processing_test_ui.py and business/image_processing_service.py provide OCR preprocessing experimentation tools.
They are useful for tuning/diagnostics and are not required for normal app runtime.
14) Mental model summary
If you want one compact model of this app, use this:
- App startup builds one dependency graph in
ReceiptTrackerApp. - Screens are stateful controllers over Kivy properties + KV views.
- Screens call
ReceiptServicefor all business actions. ReceiptServicecoordinates repository/OCR/storage/export services.- Long operations run in daemon threads.
- Worker threads never mutate UI directly; they schedule main-thread callbacks.
- Persistence is SQLite + file system paths under Kivy user data dir.
- Domain exceptions flow upward and are shown to users as popups.
That is the core architecture this project follows.