Skip to content

Commit 2ed72e1

Browse files
committed
Initial commit
0 parents  commit 2ed72e1

17 files changed

Lines changed: 910 additions & 0 deletions

.envrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
watch_file .env uv.lock pyproject.toml
2+
3+
dotenv_if_exists
4+
5+
uv sync
6+
source .venv/bin/activate

.github/workflows/publish.yaml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Publish Python Package
2+
3+
on:
4+
push:
5+
branches: [main, wip]
6+
workflow_dispatch:
7+
8+
jobs:
9+
test:
10+
uses: ./.github/workflows/test.yaml
11+
12+
build:
13+
runs-on: ubuntu-24.04
14+
needs: [test]
15+
steps:
16+
- uses: actions/checkout@v4
17+
- name: Install uv
18+
uses: astral-sh/setup-uv@v5
19+
with:
20+
enable-cache: true
21+
cache-dependency-glob: "uv.lock"
22+
- name: Create venv
23+
run: uv venv
24+
- name: Install build dependencies
25+
run: uv pip install setuptools wheel build
26+
- name: Build the package
27+
run: uv run python -m build
28+
- name: Store the distribution packages
29+
uses: actions/upload-artifact@v4
30+
with:
31+
name: python-packages
32+
path: dist/
33+
34+
publish-to-testpypi:
35+
name: Publish to PyPI-Test
36+
runs-on: ubuntu-24.04
37+
needs: [build]
38+
environment:
39+
name: testpypi
40+
url: https://test.pypi.org/p/simplefin-python
41+
permissions:
42+
id-token: write
43+
steps:
44+
- name: Download distribution packages
45+
uses: actions/download-artifact@v4
46+
with:
47+
name: python-packages
48+
path: dist/
49+
- name: Publish to PyPI
50+
uses: pypa/gh-action-pypi-publish@release/v1
51+
with:
52+
repository-url: https://test.pypi.org/legacy/
53+
verbose: true

.github/workflows/test.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [main, wip]
6+
pull_request:
7+
workflow_dispatch:
8+
workflow_call:
9+
10+
jobs:
11+
test:
12+
name: Run Tests
13+
runs-on: ubuntu-24.04
14+
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- name: Install uv
19+
uses: astral-sh/setup-uv@v5
20+
with:
21+
enable-cache: true
22+
cache-dependency-glob: "uv.lock"
23+
24+
- name: Install the project
25+
run: uv sync --all-extras --dev
26+
27+
- name: Run tests
28+
# For example, using `pytest`
29+
run: uv run pytest tests

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.env
2+
3+
# Python-generated files
4+
__pycache__/
5+
*.py[oc]
6+
build/
7+
dist/
8+
wheels/
9+
*.egg-info
10+
11+
# Virtual environments
12+
.venv

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

.vscode/settings.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"[python]": {
3+
"editor.formatOnSave": true,
4+
"editor.codeActionsOnSave": {
5+
"source.fixAll": "explicit",
6+
"source.organizeImports": "explicit"
7+
},
8+
"editor.defaultFormatter": "charliermarsh.ruff"
9+
},
10+
"ruff.path": [
11+
".venv/bin/ruff"
12+
]
13+
}

README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# SimpleFIN Python Library
2+
3+
## Installation
4+
5+
`pip install simplefin-python`
6+
7+
## Command line interface
8+
9+
### Setup
10+
11+
You will first need to get a setup token and convert it to an access URL.
12+
13+
1. Create a new application connection in you SimpleFIN Bridge account.
14+
2. Copy the provided setup key to your clipboard.
15+
3. Run `simplefin setup` and paste the setup key from above.
16+
4. Securely store the provided Access URL as it is required for future calls.
17+
18+
See [#1](https://github.com/chrishas35/simplefin-python/issues/1) for discussion on securely storing this in future releases.
19+
20+
### Usage
21+
22+
Your Access URL will need to be stored in an environment variable called `SIMPLEFIN_ACCESS_URL` for future CLI calls.
23+
24+
Examples below leverage the SimpleFIN Bridge Demo Access URL of `https://demo:demo@beta-bridge.simplefin.org/simplefin`. Real world Account IDs will be in the format of `ACT-[guid]`.
25+
26+
#### Get accounts
27+
28+
❯ simplefin accounts
29+
SimpleFIN Accounts
30+
┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
31+
┃ Institution ┃ Account ┃ Balance ┃ Account ID ┃
32+
┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
33+
│ SimpleFIN Demo │ SimpleFIN Savings │ 114125.50 │ Demo Savings │
34+
│ SimpleFIN Demo │ SimpleFIN Checking │ 24302.22 │ Demo Checking │
35+
└────────────────┴────────────────────┴───────────┴───────────────┘
36+
37+
#### Get transactions for an account
38+
39+
`simplefin transactions ACCOUNT_ID [--format FORMAT]`
40+
41+
❯ simplefin transactions "Demo Savings"
42+
Transactions for Demo Savings
43+
┏━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┓
44+
┃ Date ┃ Payee ┃ Amount ┃
45+
┡━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━┩
46+
│ 10 Jan 2025 │ John's Fishin Shack │ -50.00 │
47+
│ 10 Jan 2025 │ Grocery store │ -90.00 │
48+
│ 11 Jan 2025 │ John's Fishin Shack │ -55.50 │
49+
│ 11 Jan 2025 │ Grocery store │ -85.50 │
50+
└─────────────┴─────────────────────┴────────┘
51+
52+
##### JSON output
53+
54+
We convert the posted and transacted_at, if provided, values into ISO strings.
55+
56+
❯ simplefin transactions "Demo Savings" --format json
57+
[
58+
{
59+
"id": "1736496000",
60+
"posted": "2025-01-10T08:00:00+00:00",
61+
"amount": "-50.00",
62+
"description": "Fishing bait",
63+
"payee": "John's Fishin Shack",
64+
"memo": "JOHNS FISHIN SHACK BAIT"
65+
},
66+
{
67+
"id": "1736524800",
68+
"posted": "2025-01-10T16:00:00+00:00",
69+
"amount": "-90.00",
70+
"description": "Grocery store",
71+
"payee": "Grocery store",
72+
"memo": "LOCAL GROCER STORE #1133"
73+
},
74+
{
75+
"id": "1736582400",
76+
"posted": "2025-01-11T08:00:00+00:00",
77+
"amount": "-55.50",
78+
"description": "Fishing bait",
79+
"payee": "John's Fishin Shack",
80+
"memo": "JOHNS FISHIN SHACK BAIT"
81+
},
82+
{
83+
"id": "1736611200",
84+
"posted": "2025-01-11T16:00:00+00:00",
85+
"amount": "-85.50",
86+
"description": "Grocery store",
87+
"payee": "Grocery store",
88+
"memo": "LOCAL GROCER STORE #1133"
89+
}
90+
]

pyproject.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[project]
2+
name = "simplefin"
3+
version = "0.1.0"
4+
description = "Add your description here"
5+
readme = "README.md"
6+
authors = [
7+
{ name = "Chris Hasenpflug", email = "git@chris.hasenpflug.net" }
8+
]
9+
requires-python = ">=3.13"
10+
dependencies = [
11+
"click>=8.1.8",
12+
"httpx>=0.28.1",
13+
"rich>=13.9.4",
14+
]
15+
16+
[project.scripts]
17+
simplefin = "simplefin:cli"
18+
19+
[build-system]
20+
requires = ["hatchling"]
21+
build-backend = "hatchling.build"
22+
23+
[dependency-groups]
24+
dev = [
25+
"pytest>=8.3.4",
26+
"pytest-httpx>=0.35.0",
27+
"ruff>=0.9.0",
28+
]

src/simplefin/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from simplefin.cli import cli
2+
from simplefin.client import SimpleFINClient
3+
4+
__all__ = ["cli", "SimpleFINClient"]

src/simplefin/cli/__init__.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import datetime
2+
import json
3+
import os
4+
from datetime import date
5+
6+
import click
7+
from rich.console import Console
8+
from rich.pretty import pprint
9+
from rich.table import Table
10+
11+
from simplefin.client import SimpleFINClient
12+
13+
14+
class DateTimeEncoder(json.JSONEncoder):
15+
def default(self, obj):
16+
if isinstance(obj, datetime.datetime):
17+
return obj.isoformat()
18+
return super().default(obj)
19+
20+
21+
@click.group()
22+
def cli():
23+
pass
24+
25+
26+
@cli.command()
27+
def setup() -> None:
28+
setup_token = click.prompt("Please provide your setup token", type=str)
29+
access_url = SimpleFINClient.get_access_url(setup_token)
30+
31+
console = Console()
32+
console.print(f"\nAccess URL: {access_url}\n")
33+
console.print(
34+
"For security reasons we do not store the access_url on disk for you."
35+
)
36+
console.print(
37+
"Please securely store for future usage of simplefin as setup tokens are not reusable."
38+
)
39+
40+
41+
@cli.command()
42+
def accounts() -> None:
43+
c = SimpleFINClient(access_url=os.getenv("SIMPLEFIN_ACCESS_URL"))
44+
accounts = c.get_accounts()
45+
table = Table(title="SimpleFIN Accounts")
46+
table.add_column("Institution")
47+
table.add_column("Account")
48+
table.add_column("Balance")
49+
table.add_column("Account ID")
50+
51+
for account in accounts:
52+
table.add_row(
53+
account["org"]["name"],
54+
account["name"],
55+
str(account["balance"]),
56+
account["id"],
57+
)
58+
59+
console = Console()
60+
console.print(table)
61+
62+
63+
# TODO: Add date range option
64+
@cli.command()
65+
@click.argument("account_id", type=str)
66+
@click.option(
67+
"lookback_days",
68+
"--lookback-days",
69+
type=int,
70+
default=7,
71+
help="Number of days to look back for transactions",
72+
)
73+
@click.option(
74+
"--format",
75+
type=click.Choice(["json", "table"], case_sensitive=False),
76+
default="table",
77+
help="Specify output format",
78+
)
79+
def transactions(account_id: str, format: str, lookback_days: int) -> None:
80+
c = SimpleFINClient(access_url=os.getenv("SIMPLEFIN_ACCESS_URL"))
81+
start_dt = date.today() - datetime.timedelta(days=lookback_days)
82+
transactions = c.get_transactions(account_id, start_dt)
83+
84+
if format == "json":
85+
console = Console()
86+
console.print(json.dumps(transactions, indent=4, cls=DateTimeEncoder))
87+
else:
88+
table = Table(title=f"Transactions for {account_id}")
89+
table.add_column("Date")
90+
table.add_column("Payee")
91+
table.add_column("Amount")
92+
93+
for txn in transactions:
94+
table.add_row(
95+
txn["posted"].strftime("%d %b %Y"), txn["payee"], str(txn["amount"])
96+
)
97+
98+
console = Console()
99+
console.print(table)
100+
101+
102+
@cli.command()
103+
def info() -> None:
104+
c = SimpleFINClient(access_url=os.getenv("SIMPLEFIN_ACCESS_URL"))
105+
info = c.get_info()
106+
pprint(info)
107+
108+
109+
if __name__ == "__main__":
110+
cli()

0 commit comments

Comments
 (0)