statement-etl-ph 0.1.0__tar.gz

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.
Files changed (37) hide show
  1. statement_etl_ph-0.1.0/.gitignore +17 -0
  2. statement_etl_ph-0.1.0/LICENSE +21 -0
  3. statement_etl_ph-0.1.0/PKG-INFO +155 -0
  4. statement_etl_ph-0.1.0/README.md +125 -0
  5. statement_etl_ph-0.1.0/docs/add-a-bank.md +82 -0
  6. statement_etl_ph-0.1.0/docs/run-locally.md +102 -0
  7. statement_etl_ph-0.1.0/pyproject.toml +50 -0
  8. statement_etl_ph-0.1.0/src/statement_etl_ph/__init__.py +11 -0
  9. statement_etl_ph-0.1.0/src/statement_etl_ph/__main__.py +3 -0
  10. statement_etl_ph-0.1.0/src/statement_etl_ph/banks/__init__.py +35 -0
  11. statement_etl_ph-0.1.0/src/statement_etl_ph/banks/base.py +50 -0
  12. statement_etl_ph-0.1.0/src/statement_etl_ph/banks/gcash.py +234 -0
  13. statement_etl_ph-0.1.0/src/statement_etl_ph/banks/gotyme.py +262 -0
  14. statement_etl_ph-0.1.0/src/statement_etl_ph/banks/grab.py +237 -0
  15. statement_etl_ph-0.1.0/src/statement_etl_ph/cli.py +349 -0
  16. statement_etl_ph-0.1.0/src/statement_etl_ph/config.py +93 -0
  17. statement_etl_ph-0.1.0/src/statement_etl_ph/gmail/__init__.py +0 -0
  18. statement_etl_ph-0.1.0/src/statement_etl_ph/gmail/auth.py +135 -0
  19. statement_etl_ph-0.1.0/src/statement_etl_ph/gmail/fetch.py +492 -0
  20. statement_etl_ph-0.1.0/src/statement_etl_ph/models.py +451 -0
  21. statement_etl_ph-0.1.0/src/statement_etl_ph/pdf_geometry.py +127 -0
  22. statement_etl_ph-0.1.0/src/statement_etl_ph/sinks/__init__.py +65 -0
  23. statement_etl_ph-0.1.0/src/statement_etl_ph/sinks/export.py +84 -0
  24. statement_etl_ph-0.1.0/src/statement_etl_ph/sinks/money_manager.py +117 -0
  25. statement_etl_ph-0.1.0/tests/conftest.py +42 -0
  26. statement_etl_ph-0.1.0/tests/fixtures/make_fixture.py +143 -0
  27. statement_etl_ph-0.1.0/tests/fixtures/make_gotyme_fixture.py +155 -0
  28. statement_etl_ph-0.1.0/tests/fixtures/synthetic_gotyme_statement.pdf +0 -0
  29. statement_etl_ph-0.1.0/tests/fixtures/synthetic_statement.pdf +0 -0
  30. statement_etl_ph-0.1.0/tests/test_auth_and_config.py +117 -0
  31. statement_etl_ph-0.1.0/tests/test_cli.py +199 -0
  32. statement_etl_ph-0.1.0/tests/test_gmail_fetch.py +356 -0
  33. statement_etl_ph-0.1.0/tests/test_models.py +287 -0
  34. statement_etl_ph-0.1.0/tests/test_parse_gcash.py +166 -0
  35. statement_etl_ph-0.1.0/tests/test_parse_gotyme.py +143 -0
  36. statement_etl_ph-0.1.0/tests/test_parse_grab.py +279 -0
  37. statement_etl_ph-0.1.0/tests/test_sinks.py +158 -0
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ venv/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+
10
+ # Never commit a real statement, a token or a config with passwords in it.
11
+ *.pdf
12
+ !tests/fixtures/synthetic_*.pdf
13
+ token.json
14
+ client_secret*.json
15
+ statement-etl-ph.json
16
+ *_moneymanager.csv
17
+ .env
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 statement-etl-ph contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.5
2
+ Name: statement-etl-ph
3
+ Version: 0.1.0
4
+ Summary: Read GCash, GoTyme and Grab statements into checked transactions, a CSV, or a Money Manager import.
5
+ Project-URL: Issues, https://github.com/Jomaries777/statement-etl-ph/issues
6
+ Project-URL: Source, https://github.com/Jomaries777/statement-etl-ph
7
+ Author: statement-etl-ph contributors
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: bank statement,budget,gcash,gotyme,grab,pdf,philippines
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Office/Business :: Financial :: Accounting
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: pdfplumber>=0.11.4
20
+ Provides-Extra: dev
21
+ Requires-Dist: pikepdf>=9.0.0; extra == 'dev'
22
+ Requires-Dist: pytest>=8.3.0; extra == 'dev'
23
+ Requires-Dist: reportlab>=4.2.0; extra == 'dev'
24
+ Provides-Extra: gmail
25
+ Requires-Dist: google-api-python-client>=2.140.0; extra == 'gmail'
26
+ Requires-Dist: google-auth-httplib2>=0.2.0; extra == 'gmail'
27
+ Requires-Dist: google-auth-oauthlib>=1.2.1; extra == 'gmail'
28
+ Requires-Dist: google-auth>=2.34.0; extra == 'gmail'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # statement-etl-ph
32
+
33
+ Turn your **GCash**, **GoTyme** and **GrabPay** statements into a list of transactions you can actually use: a CSV for a spreadsheet, or a file you import straight into the **Money Manager** app.
34
+
35
+ Every statement is checked before anything is written. The rows it reads must add up to the totals the bank printed, so a misread statement stops instead of quietly putting wrong numbers into your budget.
36
+
37
+ > **Only trust the official source.** The package is `statement-etl-ph` on PyPI and the code lives at <https://github.com/Jomaries777/statement-etl-ph>. This tool runs on your own computer. It never sends your statements, passwords or transactions to anyone. Be suspicious of any copy, fork or "helper" site that asks for them.
38
+
39
+ ## Quick start: one statement, no setup
40
+
41
+ You need Python 3.10 or newer.
42
+
43
+ 1. **Get your statement.** In the GCash app, open your transaction history and request it by email. It arrives as a password-protected PDF; the email tells you what the password is.
44
+ 2. **Install:**
45
+ ```bash
46
+ pipx install statement-etl-ph
47
+ ```
48
+ (Or `pip install statement-etl-ph` inside a virtual environment.)
49
+ 3. **Convert it:**
50
+ ```bash
51
+ statement-etl-ph statement.pdf --bank gcash --to moneymanager --password YOUR-PDF-PASSWORD
52
+ ```
53
+ It prints every row and the check, then writes `gcash_<from>_to_<to>_moneymanager.csv`. To keep the password out of your shell history, set `GCASH_PDF_PASSWORD` instead of passing `--password`.
54
+ 4. **Import that file into Money Manager.** The Category column is left blank on purpose, so you can sort rows in the app.
55
+
56
+ Add `--dry-run` to see the rows without writing anything, and `--to csv` for a plain spreadsheet instead.
57
+
58
+ ### What the check catches
59
+
60
+ GCash prints some inbound bank transfers with **no description at all**. A parser that matches a list of descriptions to a list of amounts by position shifts every later amount onto the wrong row, then drops the last one, and reports no error. This package reads the page as a grid, keeps the blank row, and then proves nothing moved: the debits and credits it read must equal the statement's printed Total Debit and Total Credit. If they don't, nothing is written (unless you pass `--force`).
61
+
62
+ It also reports what the bank's own arithmetic leaves out. GCash sometimes refunds a cancelled booking without printing a row, and the running balance is the only evidence. Such refunds, and any other gap the balance proves but nothing explains, are listed in the output rather than hidden. They're never written as rows: add them yourself if you want them.
63
+
64
+ ## Supported banks and e-wallets
65
+
66
+ | `--bank` | What it reads | Password variable |
67
+ |---|---|---|
68
+ | `gcash` | GCash transaction history PDF | `GCASH_PDF_PASSWORD` |
69
+ | `gotyme` | GoTyme Bank statement of account PDF (the Everyday deposit account by default) | `GOTYME_PDF_PASSWORD` |
70
+ | `grab` | Grab e-receipt emails; only GrabPay Wallet payments are imported, because card and GCash payments already appear on those statements | none |
71
+
72
+ Want another bank? See [Adding a bank](docs/add-a-bank.md). It's one file.
73
+
74
+ ## Output formats
75
+
76
+ **`--to moneymanager`**: Money Manager's bulk import, with these columns:
77
+
78
+ | Column | Value |
79
+ |---|---|
80
+ | Date | the transaction date on the statement (default `2026-08-26`; change it with `--date-format`) |
81
+ | Account | `GCash`, `GoTyme` or `GrabPay`; use `--account-name` to match an account you already have |
82
+ | Category, Subcategory, Note | blank |
83
+ | Amount | always positive, e.g. `1250.00` |
84
+ | Income/Expense | `Income` for money in, `Expense` for money out |
85
+ | Description | the merchant and the bank's reference, e.g. `Example Apps (ref 0000100000066)` |
86
+
87
+ Money Manager doesn't detect duplicates, so importing the same file twice imports every row twice. Each file is named after its bank and statement period to make that easy to notice.
88
+
89
+ **`--to csv`**: one row per transaction with the signed amount, debit, credit, balance, reference number and a `duplicate_key` column. Writing into a CSV that already exists only appends rows whose key isn't there yet.
90
+
91
+ ## Level up: fetch statements from Gmail automatically
92
+
93
+ Instead of downloading each PDF, let the tool find statement emails in your Gmail, check they really came from the bank, and convert them.
94
+
95
+ ```bash
96
+ pipx install "statement-etl-ph[gmail]"
97
+ statement-etl-ph auth client_secret.json # once
98
+ statement-etl-ph init # once: a config file for your passwords
99
+ statement-etl-ph fetch --bank gcash --to moneymanager --out ~/Statements/
100
+ ```
101
+
102
+ **Only mail the bank actually sent is accepted.** A `From:` address is easy to fake, so each message must carry a passing DKIM signature from the bank's own domain (`gcash.com`, `gotyme.com.ph` or `grab.com`), as verified by Gmail. Anything else is refused and left alone.
103
+
104
+ After a statement is converted, its email gets the label `statement-etl-ph/<bank>`, so the next run skips it.
105
+
106
+ ### Set up your own Gmail access (about 10 minutes, free)
107
+
108
+ This tool has no shared Google login. You create your own, so nobody else's app ever gets access to your mail. Nothing is deployed, and no billing is needed.
109
+
110
+ 1. Go to [console.cloud.google.com](https://console.cloud.google.com), create a project (any name), and enable the **Gmail API** for it.
111
+ 2. Open **Google Auth Platform** and set up the consent screen. Choose **External**, and add your own Gmail address as a test user.
112
+ 3. Under **Clients**, create an OAuth client of type **Desktop app** and download its JSON file.
113
+ 4. **Publish the app:** on the **Audience** page, click **Publish app** to move it from *Testing* to *In production*. Without this, Google expires your sign-in after **7 days** and fetching stops working.
114
+ 5. Run `statement-etl-ph auth path/to/client_secret.json`. A browser opens. Google warns that it "hasn't verified this app": that's expected, because the app is yours. Continue, and allow access.
115
+
116
+ The token is saved to `~/.config/statement-etl-ph/token.json`, readable only by you. Revoke it any time at [myaccount.google.com/permissions](https://myaccount.google.com/permissions).
117
+
118
+ **Read-only option:** `statement-etl-ph auth client_secret.json --readonly` never changes your mailbox. Processed emails aren't labelled, so each run sees them again. For CSV output, the duplicate key stops rows being added twice; a Money Manager file is simply rewritten.
119
+
120
+ ### Your bank mail goes somewhere other than Gmail?
121
+
122
+ Create a free Gmail account just for statements, and forward the bank emails to it. A dedicated account also means the token can only ever see bank mail.
123
+
124
+ - **Forward with a rule that keeps the original email intact:** a Gmail filter's forwarding, or Outlook's **Redirect to**. "Forward"-style rules and forwarding by hand change the sender, add "FW:" and can wrap the PDF inside another message; those won't match and are rejected.
125
+ - **In the receiving Gmail account,** add a filter for the bank senders set to **Never send it to Spam**. The tool doesn't search Spam.
126
+ - **Known limits:**
127
+ - Microsoft 365 work accounts often block forwarding outside the company. Where it does forward, Safe Links and "External sender" banners break the bank's DKIM signature, so those messages are rejected.
128
+ - Some providers, such as Yahoo and Proton, may need a paid plan for automatic forwarding.
129
+ - Forwarding adds a few seconds to a minute of delay.
130
+
131
+ ## Run it on a schedule
132
+
133
+ [docs/run-locally.md](docs/run-locally.md) has ready-to-edit schedules for macOS (`launchd`), Linux (`cron` or a systemd timer) and Windows (Task Scheduler), so `fetch` runs hourly or daily on its own.
134
+
135
+ ## Use it as a library
136
+
137
+ ```python
138
+ from statement_etl_ph.banks import get_bank
139
+
140
+ statement = get_bank("gcash").parser("statement.pdf", "password")
141
+ result = statement.reconcile()
142
+ assert result.ok, result.errors
143
+ for txn in statement.transactions:
144
+ print(txn.value_date, txn.signed_amount, txn.merchant, txn.reference_no)
145
+ ```
146
+
147
+ Amounts are `Decimal`, never floats.
148
+
149
+ ## Never share a real statement
150
+
151
+ When reporting a problem, **never attach a real statement, a screenshot of one, or a real reference number**, and never post your PDF password. The [issue template](.github/ISSUE_TEMPLATE/parse-bug.yml) explains how to describe a layout problem using `-v` output and invented values instead.
152
+
153
+ ## License
154
+
155
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,125 @@
1
+ # statement-etl-ph
2
+
3
+ Turn your **GCash**, **GoTyme** and **GrabPay** statements into a list of transactions you can actually use: a CSV for a spreadsheet, or a file you import straight into the **Money Manager** app.
4
+
5
+ Every statement is checked before anything is written. The rows it reads must add up to the totals the bank printed, so a misread statement stops instead of quietly putting wrong numbers into your budget.
6
+
7
+ > **Only trust the official source.** The package is `statement-etl-ph` on PyPI and the code lives at <https://github.com/Jomaries777/statement-etl-ph>. This tool runs on your own computer. It never sends your statements, passwords or transactions to anyone. Be suspicious of any copy, fork or "helper" site that asks for them.
8
+
9
+ ## Quick start: one statement, no setup
10
+
11
+ You need Python 3.10 or newer.
12
+
13
+ 1. **Get your statement.** In the GCash app, open your transaction history and request it by email. It arrives as a password-protected PDF; the email tells you what the password is.
14
+ 2. **Install:**
15
+ ```bash
16
+ pipx install statement-etl-ph
17
+ ```
18
+ (Or `pip install statement-etl-ph` inside a virtual environment.)
19
+ 3. **Convert it:**
20
+ ```bash
21
+ statement-etl-ph statement.pdf --bank gcash --to moneymanager --password YOUR-PDF-PASSWORD
22
+ ```
23
+ It prints every row and the check, then writes `gcash_<from>_to_<to>_moneymanager.csv`. To keep the password out of your shell history, set `GCASH_PDF_PASSWORD` instead of passing `--password`.
24
+ 4. **Import that file into Money Manager.** The Category column is left blank on purpose, so you can sort rows in the app.
25
+
26
+ Add `--dry-run` to see the rows without writing anything, and `--to csv` for a plain spreadsheet instead.
27
+
28
+ ### What the check catches
29
+
30
+ GCash prints some inbound bank transfers with **no description at all**. A parser that matches a list of descriptions to a list of amounts by position shifts every later amount onto the wrong row, then drops the last one, and reports no error. This package reads the page as a grid, keeps the blank row, and then proves nothing moved: the debits and credits it read must equal the statement's printed Total Debit and Total Credit. If they don't, nothing is written (unless you pass `--force`).
31
+
32
+ It also reports what the bank's own arithmetic leaves out. GCash sometimes refunds a cancelled booking without printing a row, and the running balance is the only evidence. Such refunds, and any other gap the balance proves but nothing explains, are listed in the output rather than hidden. They're never written as rows: add them yourself if you want them.
33
+
34
+ ## Supported banks and e-wallets
35
+
36
+ | `--bank` | What it reads | Password variable |
37
+ |---|---|---|
38
+ | `gcash` | GCash transaction history PDF | `GCASH_PDF_PASSWORD` |
39
+ | `gotyme` | GoTyme Bank statement of account PDF (the Everyday deposit account by default) | `GOTYME_PDF_PASSWORD` |
40
+ | `grab` | Grab e-receipt emails; only GrabPay Wallet payments are imported, because card and GCash payments already appear on those statements | none |
41
+
42
+ Want another bank? See [Adding a bank](docs/add-a-bank.md). It's one file.
43
+
44
+ ## Output formats
45
+
46
+ **`--to moneymanager`**: Money Manager's bulk import, with these columns:
47
+
48
+ | Column | Value |
49
+ |---|---|
50
+ | Date | the transaction date on the statement (default `2026-08-26`; change it with `--date-format`) |
51
+ | Account | `GCash`, `GoTyme` or `GrabPay`; use `--account-name` to match an account you already have |
52
+ | Category, Subcategory, Note | blank |
53
+ | Amount | always positive, e.g. `1250.00` |
54
+ | Income/Expense | `Income` for money in, `Expense` for money out |
55
+ | Description | the merchant and the bank's reference, e.g. `Example Apps (ref 0000100000066)` |
56
+
57
+ Money Manager doesn't detect duplicates, so importing the same file twice imports every row twice. Each file is named after its bank and statement period to make that easy to notice.
58
+
59
+ **`--to csv`**: one row per transaction with the signed amount, debit, credit, balance, reference number and a `duplicate_key` column. Writing into a CSV that already exists only appends rows whose key isn't there yet.
60
+
61
+ ## Level up: fetch statements from Gmail automatically
62
+
63
+ Instead of downloading each PDF, let the tool find statement emails in your Gmail, check they really came from the bank, and convert them.
64
+
65
+ ```bash
66
+ pipx install "statement-etl-ph[gmail]"
67
+ statement-etl-ph auth client_secret.json # once
68
+ statement-etl-ph init # once: a config file for your passwords
69
+ statement-etl-ph fetch --bank gcash --to moneymanager --out ~/Statements/
70
+ ```
71
+
72
+ **Only mail the bank actually sent is accepted.** A `From:` address is easy to fake, so each message must carry a passing DKIM signature from the bank's own domain (`gcash.com`, `gotyme.com.ph` or `grab.com`), as verified by Gmail. Anything else is refused and left alone.
73
+
74
+ After a statement is converted, its email gets the label `statement-etl-ph/<bank>`, so the next run skips it.
75
+
76
+ ### Set up your own Gmail access (about 10 minutes, free)
77
+
78
+ This tool has no shared Google login. You create your own, so nobody else's app ever gets access to your mail. Nothing is deployed, and no billing is needed.
79
+
80
+ 1. Go to [console.cloud.google.com](https://console.cloud.google.com), create a project (any name), and enable the **Gmail API** for it.
81
+ 2. Open **Google Auth Platform** and set up the consent screen. Choose **External**, and add your own Gmail address as a test user.
82
+ 3. Under **Clients**, create an OAuth client of type **Desktop app** and download its JSON file.
83
+ 4. **Publish the app:** on the **Audience** page, click **Publish app** to move it from *Testing* to *In production*. Without this, Google expires your sign-in after **7 days** and fetching stops working.
84
+ 5. Run `statement-etl-ph auth path/to/client_secret.json`. A browser opens. Google warns that it "hasn't verified this app": that's expected, because the app is yours. Continue, and allow access.
85
+
86
+ The token is saved to `~/.config/statement-etl-ph/token.json`, readable only by you. Revoke it any time at [myaccount.google.com/permissions](https://myaccount.google.com/permissions).
87
+
88
+ **Read-only option:** `statement-etl-ph auth client_secret.json --readonly` never changes your mailbox. Processed emails aren't labelled, so each run sees them again. For CSV output, the duplicate key stops rows being added twice; a Money Manager file is simply rewritten.
89
+
90
+ ### Your bank mail goes somewhere other than Gmail?
91
+
92
+ Create a free Gmail account just for statements, and forward the bank emails to it. A dedicated account also means the token can only ever see bank mail.
93
+
94
+ - **Forward with a rule that keeps the original email intact:** a Gmail filter's forwarding, or Outlook's **Redirect to**. "Forward"-style rules and forwarding by hand change the sender, add "FW:" and can wrap the PDF inside another message; those won't match and are rejected.
95
+ - **In the receiving Gmail account,** add a filter for the bank senders set to **Never send it to Spam**. The tool doesn't search Spam.
96
+ - **Known limits:**
97
+ - Microsoft 365 work accounts often block forwarding outside the company. Where it does forward, Safe Links and "External sender" banners break the bank's DKIM signature, so those messages are rejected.
98
+ - Some providers, such as Yahoo and Proton, may need a paid plan for automatic forwarding.
99
+ - Forwarding adds a few seconds to a minute of delay.
100
+
101
+ ## Run it on a schedule
102
+
103
+ [docs/run-locally.md](docs/run-locally.md) has ready-to-edit schedules for macOS (`launchd`), Linux (`cron` or a systemd timer) and Windows (Task Scheduler), so `fetch` runs hourly or daily on its own.
104
+
105
+ ## Use it as a library
106
+
107
+ ```python
108
+ from statement_etl_ph.banks import get_bank
109
+
110
+ statement = get_bank("gcash").parser("statement.pdf", "password")
111
+ result = statement.reconcile()
112
+ assert result.ok, result.errors
113
+ for txn in statement.transactions:
114
+ print(txn.value_date, txn.signed_amount, txn.merchant, txn.reference_no)
115
+ ```
116
+
117
+ Amounts are `Decimal`, never floats.
118
+
119
+ ## Never share a real statement
120
+
121
+ When reporting a problem, **never attach a real statement, a screenshot of one, or a real reference number**, and never post your PDF password. The [issue template](.github/ISSUE_TEMPLATE/parse-bug.yml) explains how to describe a layout problem using `-v` output and invented values instead.
122
+
123
+ ## License
124
+
125
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,82 @@
1
+ # Adding a bank
2
+
3
+ A bank is one module in `src/statement_etl_ph/banks/`: a parser and a `BANK` definition. You never need, and must never share, a real statement to add one.
4
+
5
+ ## 1. Learn the layout without sharing it
6
+
7
+ On your own machine, run an existing parser over your statement with `-v` to see how the PDF is laid out:
8
+
9
+ ```bash
10
+ statement-etl-ph your-statement.pdf --bank gcash --dry-run -v
11
+ ```
12
+
13
+ It fails for a new bank, but the debug lines show what pdfplumber sees. For a closer look, print the words and their positions yourself:
14
+
15
+ ```python
16
+ import pdfplumber
17
+
18
+ with pdfplumber.open("your-statement.pdf", password="...") as pdf:
19
+ for word in pdf.pages[0].extract_words():
20
+ print(round(word["x0"]), round(word["x1"]), round(word["top"]), word["text"])
21
+ ```
22
+
23
+ Note the **positions** of the column headers and of a few amounts, never the values themselves. Those positions are all the rest of the work needs.
24
+
25
+ ## 2. Write the parser
26
+
27
+ Most statements are a borderless table, and `pdf_geometry` already does the hard part:
28
+
29
+ - `cluster_rows(words, tolerance, sort_key)` groups words into visual rows.
30
+ - `find_money_columns(rows, headers)` finds where the money columns sit from their header row.
31
+ - `assign_money_columns(row, columns, tolerance)` assigns each amount on a row to the nearest column by its right edge.
32
+
33
+ What differs per bank is the vocabulary: the header names, how the period is printed, what the totals rows are called, and how forgiving the tolerances need to be. Pass yours in; don't change another bank's.
34
+
35
+ The parser has one shape, `parse_statement(path, password) -> Statement`. Fill in the printed totals (`starting_balance`, `ending_balance`, `total_debit`, `total_credit`) whenever the statement prints them: they're what `reconcile()` checks the rows against.
36
+
37
+ **Reference numbers.** If the bank prints one per row, use it. It becomes the duplicate key. If not, build a stable one from the date, description, amounts and running balance, as `banks/gotyme.py` does, so two identical payments on one day stay two rows.
38
+
39
+ ## 3. Declare the bank
40
+
41
+ At the bottom of your module:
42
+
43
+ ```python
44
+ BANK = BankDefinition(
45
+ name="examplebank",
46
+ display_name="Example Bank",
47
+ parser=parse_statement,
48
+ gmail_query='from:statements@examplebank.com.ph subject:"Statement" has:attachment',
49
+ dkim_domains=("examplebank.com.ph",),
50
+ password_env="EXAMPLEBANK_PDF_PASSWORD",
51
+ )
52
+ ```
53
+
54
+ Then add it to the tuple in `banks/__init__.py`.
55
+
56
+ **`dkim_domains`:** in Gmail, open one of the bank's emails, choose **Show original**, and read the `Authentication-Results` line. Use the domain on the `dkim=pass` result that belongs to the bank (`header.i=@…` or `header.d=…`). Never add an email provider's domain such as `amazonses.com` or `sendgrid.net`: they sign mail for anyone who pays them.
57
+
58
+ ## 4. Build a synthetic statement
59
+
60
+ Copy `tests/fixtures/make_fixture.py` into `tests/fixtures/make_<bank>_fixture.py` and change it to draw your bank's layout:
61
+
62
+ - the same header words, at the same x positions you measured
63
+ - **invented** rows: made-up merchants, made-up reference numbers, round amounts
64
+ - a running balance and printed totals that genuinely add up
65
+ - encrypted with a made-up password, using pikepdf
66
+
67
+ Cover the cases that break parsers: a credit row, a thousands separator, a long description, a row with an empty description, and a second page if real statements have one.
68
+
69
+ ## 5. Test it
70
+
71
+ In `tests/test_parse_<bank>.py`, at least:
72
+
73
+ - every row is extracted, and every reference and description matches the generator's rows exactly
74
+ - `reconcile().ok` is true
75
+ - a wrong password is rejected
76
+ - a row with an empty description keeps every later amount on its own row
77
+
78
+ ## 6. Before you open the pull request
79
+
80
+ - [ ] No real names, account or mobile numbers, card digits, reference numbers or addresses anywhere: code, tests, comments or commit messages.
81
+ - [ ] No real PDFs, screenshots or `Authentication-Results` headers. Invent values with the same shape.
82
+ - [ ] `pytest` passes.
@@ -0,0 +1,102 @@
1
+ # Run it on your own computer
2
+
3
+ Everything here runs on your machine. The only Google Cloud step is the free project that holds your Gmail OAuth client ([README](../README.md#set-up-your-own-gmail-access-about-10-minutes-free)); nothing is deployed.
4
+
5
+ ## 1. Install
6
+
7
+ Python 3.10 or newer, on macOS, Linux or Windows.
8
+
9
+ ```bash
10
+ pipx install "statement-etl-ph[gmail]"
11
+ ```
12
+
13
+ Drop `[gmail]` if you'll only convert PDFs you download yourself.
14
+
15
+ ## 2. Sign in to Gmail once
16
+
17
+ ```bash
18
+ statement-etl-ph auth path/to/client_secret.json
19
+ ```
20
+
21
+ A browser opens for consent. The token is saved to `~/.config/statement-etl-ph/token.json` (`%APPDATA%\statement-etl-ph\token.json` on Windows), readable only by you. Add `--readonly` if the tool must never change your mailbox.
22
+
23
+ ## 3. Write your config
24
+
25
+ ```bash
26
+ statement-etl-ph init
27
+ ```
28
+
29
+ This writes `~/.config/statement-etl-ph/statement-etl-ph.json`, also readable only by you. Fill in:
30
+
31
+ - each bank's `password` (or leave it empty and set `GCASH_PDF_PASSWORD` / `GOTYME_PDF_PASSWORD` instead)
32
+ - `account_name`, if your Money Manager account is named differently
33
+ - `output.to` (`moneymanager` or `csv`) and `output.dir`
34
+ - `gmail.since`: how far back to search (`30d`, `2w`, `yesterday`, `all`)
35
+
36
+ Remove the banks you don't use: `fetch` with no `--bank` reads every bank listed.
37
+
38
+ ## 4. Run it
39
+
40
+ ```bash
41
+ # A PDF you already have
42
+ statement-etl-ph statement.pdf --bank gcash --to moneymanager
43
+
44
+ # Everything new in Gmail, for every bank in your config
45
+ statement-etl-ph fetch
46
+
47
+ # See what would happen, write nothing, label nothing
48
+ statement-etl-ph fetch --dry-run
49
+
50
+ # Show how a PDF was read, column positions included
51
+ statement-etl-ph statement.pdf --bank gcash --dry-run -v
52
+ ```
53
+
54
+ A statement whose rows don't match its printed totals is never written, and its email isn't labelled, so the next run tries it again.
55
+
56
+ ## 5. Run it on a schedule (optional)
57
+
58
+ Each schedule runs `statement-etl-ph fetch`. Because converted emails are labelled, running often is harmless: a run with nothing new does nothing.
59
+
60
+ First find the full path of the command, since schedulers don't load your shell's `PATH`:
61
+
62
+ ```bash
63
+ which statement-etl-ph
64
+ ```
65
+
66
+ ### macOS (launchd)
67
+
68
+ 1. Copy [`deploy/local/com.statement-etl-ph.fetch.plist`](../deploy/local/com.statement-etl-ph.fetch.plist) to `~/Library/LaunchAgents/`.
69
+ 2. Edit the command path and the log paths in it.
70
+ 3. Load it:
71
+ ```bash
72
+ launchctl load ~/Library/LaunchAgents/com.statement-etl-ph.fetch.plist
73
+ ```
74
+
75
+ It runs every hour while you're logged in, and once on login.
76
+
77
+ ### Linux (systemd timer)
78
+
79
+ 1. Copy [`deploy/local/statement-etl-ph.service`](../deploy/local/statement-etl-ph.service) and [`statement-etl-ph.timer`](../deploy/local/statement-etl-ph.timer) to `~/.config/systemd/user/`.
80
+ 2. Edit the command path in the `.service` file.
81
+ 3. Enable it:
82
+ ```bash
83
+ systemctl --user enable --now statement-etl-ph.timer
84
+ ```
85
+
86
+ Or with cron (`crontab -e`), hourly:
87
+
88
+ ```
89
+ 0 * * * * /home/you/.local/bin/statement-etl-ph fetch >> /home/you/statement-etl-ph.log 2>&1
90
+ ```
91
+
92
+ ### Windows (Task Scheduler)
93
+
94
+ In PowerShell, using the path from `where.exe statement-etl-ph`:
95
+
96
+ ```powershell
97
+ schtasks /Create /SC HOURLY /TN "statement-etl-ph fetch" /TR "C:\Users\you\.local\bin\statement-etl-ph.exe fetch"
98
+ ```
99
+
100
+ ## What a local run can't do
101
+
102
+ An import happens when the command runs, not the moment the email arrives. Reacting instantly needs a public address for Gmail's notifications to reach, which a laptop doesn't have.
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "statement-etl-ph"
7
+ version = "0.1.0"
8
+ description = "Read GCash, GoTyme and Grab statements into checked transactions, a CSV, or a Money Manager import."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "statement-etl-ph contributors" }]
14
+ keywords = ["gcash", "gotyme", "grab", "philippines", "bank statement", "pdf", "budget"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Operating System :: OS Independent",
22
+ "Topic :: Office/Business :: Financial :: Accounting",
23
+ ]
24
+ # Parsing a statement you already have needs nothing else.
25
+ dependencies = ["pdfplumber>=0.11.4"]
26
+
27
+ [project.optional-dependencies]
28
+ gmail = [
29
+ "google-api-python-client>=2.140.0",
30
+ "google-auth>=2.34.0",
31
+ "google-auth-oauthlib>=1.2.1",
32
+ "google-auth-httplib2>=0.2.0",
33
+ ]
34
+ dev = ["pytest>=8.3.0", "reportlab>=4.2.0", "pikepdf>=9.0.0"]
35
+
36
+ [project.scripts]
37
+ statement-etl-ph = "statement_etl_ph.cli:main"
38
+
39
+ [project.urls]
40
+ Issues = "https://github.com/Jomaries777/statement-etl-ph/issues"
41
+ Source = "https://github.com/Jomaries777/statement-etl-ph"
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["src/statement_etl_ph"]
45
+
46
+ [tool.hatch.build.targets.sdist]
47
+ include = ["src/", "tests/", "README.md", "LICENSE", "docs/"]
48
+
49
+ [tool.pytest.ini_options]
50
+ testpaths = ["tests"]
@@ -0,0 +1,11 @@
1
+ """Read Philippine bank and e-wallet statements into plain transactions.
2
+
3
+ from statement_etl_ph.banks import get_bank
4
+
5
+ statement = get_bank("gcash").parser("statement.pdf", "password")
6
+ assert statement.reconcile().ok
7
+ for txn in statement.transactions:
8
+ print(txn.value_date, txn.signed_amount, txn.merchant)
9
+ """
10
+
11
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,35 @@
1
+ """The banks and e-wallets this package can read, one module each.
2
+
3
+ The registry is a fixed list on purpose. Nothing is discovered from other
4
+ installed packages: a statement is financial data, and which code reads it
5
+ should be decided here, not by whatever else happens to be installed.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .base import BankDefinition
11
+ from .gcash import BANK as GCASH
12
+ from .gotyme import BANK as GOTYME
13
+ from .grab import BANK as GRAB
14
+
15
+ __all__ = ["BankDefinition", "available", "get_bank", "register"]
16
+
17
+ _REGISTRY: dict[str, BankDefinition] = {bank.name: bank for bank in (GCASH, GOTYME, GRAB)}
18
+
19
+
20
+ def register(bank: BankDefinition) -> None:
21
+ """Add a bank from your own code, e.g. while developing a new parser."""
22
+ _REGISTRY[bank.name] = bank
23
+
24
+
25
+ def available() -> list[str]:
26
+ return sorted(_REGISTRY)
27
+
28
+
29
+ def get_bank(name: str) -> BankDefinition:
30
+ try:
31
+ return _REGISTRY[name.lower()]
32
+ except KeyError:
33
+ raise KeyError(
34
+ f"no bank called {name!r}. Available: {', '.join(available())}"
35
+ ) from None
@@ -0,0 +1,50 @@
1
+ """What a bank is to this package: how to find its mail and how to read it."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Callable
7
+
8
+ from ..models import Statement
9
+
10
+ # (path, password) -> Statement. A provider whose statement is the email body
11
+ # rather than a PDF keeps the same shape and ignores the password.
12
+ Parser = Callable[..., Statement]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class BankDefinition:
17
+ """Everything that differs between banks, in one place.
18
+
19
+ Adding a bank means writing one module in this package that defines a
20
+ parser and a ``BANK`` of this type, then listing it in ``banks/__init__``.
21
+ """
22
+
23
+ name: str
24
+ # How the provider is named to a person: the Account column of a Money
25
+ # Manager import, a memo, a report.
26
+ display_name: str
27
+ parser: Parser
28
+ # Gmail search for the statement email. The processed label and date
29
+ # window are added at fetch time, not written here.
30
+ gmail_query: str
31
+ # The domains the provider DKIM-signs its mail with. A message no one of
32
+ # them signed is refused, whatever its From header says.
33
+ dkim_domains: tuple[str, ...]
34
+ # The environment variable holding the PDF password. Empty for a provider
35
+ # whose statement is not encrypted.
36
+ password_env: str = ""
37
+ # "pdf" for an attachment; "email_body" when the email *is* the receipt.
38
+ content: str = "pdf"
39
+ # Filename fragments that pick the right PDF when a message carries
40
+ # several, tried in order. A pattern that matches nothing fails the
41
+ # message rather than falling back to the first PDF.
42
+ attachment_patterns: tuple[str, ...] = field(default_factory=tuple)
43
+
44
+ @property
45
+ def reads_body(self) -> bool:
46
+ return self.content == "email_body"
47
+
48
+ @property
49
+ def processed_label(self) -> str:
50
+ return f"statement-etl-ph/{self.name}"