@@ -127,5 +127,87 @@ def info() -> None:
127127 pprint (info )
128128
129129
130+ @cli .command ()
131+ @click .option (
132+ "--output-dir" ,
133+ type = click .Path (file_okay = False , dir_okay = True ),
134+ required = True ,
135+ help = "Directory to output per-account JSON files" ,
136+ )
137+ @click .option (
138+ "lookback_days" ,
139+ "--lookback-days" ,
140+ type = int ,
141+ default = 30 ,
142+ help = "Number of days to look back for transactions (default: 30)" ,
143+ )
144+ def fetch (output_dir : str , lookback_days : int ) -> None :
145+ """Fetch all accounts with transactions to separate JSON files.
146+
147+ Creates one JSON file per account in the output directory, organized by
148+ institution and account name:
149+
150+ <output-dir>/<institution-domain>/<account-name>/<account-id>_<date>.json
151+
152+ This structure is human-navigable and preserves history. It's useful for
153+ integration with tools like beangulp that expect one file per account.
154+ """
155+ import pathlib
156+
157+ def sanitize_path (name : str ) -> str :
158+ """Sanitize a string for use as a directory/file name."""
159+ safe = "" .join (ch if ch .isalnum () or ch in "-_." else "-" for ch in name )
160+ # Collapse multiple dashes and strip leading/trailing dashes
161+ while "--" in safe :
162+ safe = safe .replace ("--" , "-" )
163+ return safe .strip ("-" )
164+
165+ c = SimpleFINClient (access_url = os .getenv ("SIMPLEFIN_ACCESS_URL" ))
166+ console = Console ()
167+
168+ # Get list of accounts (with balance info but no transactions)
169+ accounts = c .get_accounts ()
170+ console .print (f"Found { len (accounts )} accounts" )
171+
172+ # Create output directory
173+ output_path = pathlib .Path (output_dir )
174+ output_path .mkdir (parents = True , exist_ok = True )
175+
176+ # Fetch transactions for each account
177+ start_dt = datetime .date .today () - datetime .timedelta (days = lookback_days )
178+ today_str = datetime .date .today ().isoformat ()
179+
180+ for account in accounts :
181+ account_id = account ["id" ]
182+ account_name = account ["name" ]
183+ org_domain = account .get ("org" , {}).get ("domain" , "unknown" )
184+
185+ # Fetch transactions for this account
186+ # get_transactions returns a list of transaction dicts
187+ transactions = c .get_transactions (account_id , start_dt )
188+
189+ # Merge account metadata with transactions
190+ account_data = account .copy ()
191+ account_data ["transactions" ] = transactions if isinstance (transactions , list ) else []
192+
193+ # Build directory structure: <institution>/<account-name>/
194+ inst_dir = output_path / sanitize_path (org_domain )
195+ acct_dir = inst_dir / sanitize_path (account_name )
196+ acct_dir .mkdir (parents = True , exist_ok = True )
197+
198+ # Filename: <account-id>_<date>.json
199+ filename = f"{ account_id } _{ today_str } .json"
200+ filepath = acct_dir / filename
201+
202+ with open (filepath , "w" ) as f :
203+ json .dump (account_data , f , indent = 2 , cls = DateTimeEncoder )
204+
205+ txn_count = len (account_data .get ("transactions" , []))
206+ rel_path = filepath .relative_to (output_path )
207+ console .print (f" { account_name } : { txn_count } transactions -> { rel_path } " )
208+
209+ console .print (f"\n Wrote { len (accounts )} account files to { output_dir } " )
210+
211+
130212if __name__ == "__main__" :
131213 cli ()
0 commit comments