1- from datetime import date
1+ import csv
2+ import io
23import json
34import logging
45import os
56import re
7+ from datetime import date
8+
69
710import backoff
811import boto3
1619_mip_url_login = "https://login.mip.com/api/v1/sso/mipadv/login"
1720_mip_url_coa_segments = "https://api.mip.com/api/coa/segments"
1821_mip_url_coa_accounts = "https://api.mip.com/api/coa/segments/accounts"
19- # _mip_url_current_balance = (
20- # "https://api.mip.com/api/v1/operations/balances/accounts/documents"
21- # )
2222_mip_url_current_balance = (
2323 "https://api.mip.com/api/model/CBODispBal/methods/GetAccountBalances"
2424)
@@ -84,6 +84,21 @@ def _param_priority_list(params):
8484 return None
8585
8686
87+ # helper functions to encapsulate the body, headers, and status code
88+ def _build_return_json (code , body ):
89+ return {
90+ "statusCode" : code ,
91+ "body" : json .dumps (body , indent = 2 ),
92+ }
93+
94+
95+ def _build_return_text (code , body ):
96+ return {
97+ "statusCode" : code ,
98+ "body" : body ,
99+ }
100+
101+
87102def collect_secrets (ssm_path ):
88103 """Collect secure parameters from SSM"""
89104
@@ -215,29 +230,6 @@ def _request_balance(access_token, period_from, period_to):
215230 timeout = 4
216231 LOG .info ("Getting balances" )
217232
218- # api_response = requests.get(
219- # _mip_url_balance_template,
220- # headers={"Authorization-Token": access_token},
221- # timeout=timeout,
222- # )
223- # api_response.raise_for_status()
224- # json_response = api_response.json()
225- # LOG.debug(f"Raw balance template json: {json_response}")
226-
227- # body = {
228- # "dateFrom": "03/01/2025",
229- # "dateTo": "04/01/2025",
230- # "segmentInformation": [
231- # {
232- # "filterSelected": True,
233- # "filterItem": "PRG",
234- # "filterOperator": "<>",
235- # "filterCriteria1": "<Blank>",
236- # "filterCriteria2": "",
237- # },
238- # ],
239- # }
240-
241233 # copied from chrome dev tools while clicking through the web ui
242234 body = {
243235 "BOInformation" : {
@@ -357,11 +349,11 @@ def _balance_requests(org_name, secrets):
357349
358350 end = today .replace (day = 1 ) # first of this month
359351 end_str = end .strftime ("%m/%d/%Y" )
360- LOG .info (f"End day is { end } " )
352+ LOG .info (f"End day is { end_str } " )
361353
362354 start = end .replace (month = end .month - 1 )
363355 start_str = start .strftime ("%m/%d/%Y" ) # first day of last month
364- LOG .info (f"Start day is { start } " )
356+ LOG .info (f"Start day is { start_str } " )
365357
366358 try :
367359 # get mip access token
@@ -377,6 +369,11 @@ def _balance_requests(org_name, secrets):
377369 except Exception as exc :
378370 LOG .exception ("Error logging out" )
379371
372+ bal_dict ["period_from" ] = start_str
373+ bal_dict ["period_to" ] = end_str
374+
375+ LOG .debug (f"Balance dict: { bal_dict } " )
376+
380377 return bal_dict
381378
382379
@@ -457,17 +454,17 @@ def s3_cache(src_dict, bucket, path):
457454
458455def chart_cache (org_name , secrets , bucket , path , inactive ):
459456 """
460- Access the Chart of Accounts from MIP Cloud, and implement a write-through
461- cache of successful responses to tolerate long-term faults in the upstream
462- API.
457+ Access the Chart of Accounts from MIP Cloud, and implement a
458+ write-through cache of successful responses to tolerate long-term
459+ faults in the upstream API.
463460
464- A successful API response will be stored in S3 indefinitely, to be retrieved
465- and used in the case of an API failure.
461+ A successful API response will be stored in S3 indefinitely, to be
462+ retrieved and used in the case of an API failure.
466463
467- The S3 bucket has versioning enabled for disaster recovery, but this means
468- that every PUT request will create a new S3 object. In order to minimize
469- the number of objects in the bucket, read the cache value on every run and
470- only update the S3 object if it changes.
464+ The S3 bucket has versioning enabled for disaster recovery, but this
465+ means that every PUT request will create a new S3 object. In order
466+ to minimize the number of objects in the bucket, read the cache
467+ value on every run and only update the S3 object if it changes.
471468 """
472469
473470 # get the upstream API response
@@ -539,11 +536,12 @@ def process_chart(
539536
540537 if priority_codes is not None :
541538 if short in priority_codes :
542- # Since Python 3.7, python dictionaries preserve insertion
543- # order, so to prepend an item to the top of the dictionary,
544- # we create a new dictionary inserting the target code first,
545- # then add the previous output, and finally save the new
546- # dictionary as our output dictionary.
539+ # Since Python 3.7, python dictionaries preserve
540+ # insertion order, so to prepend an item to the top
541+ # of the dictionary, we create a new dictionary
542+ # inserting the target code first, then add the
543+ # previous output, and finally save the new
544+ # dictionary as our output.
547545 new_chart = {short : name }
548546 new_chart .update (out_chart )
549547 out_chart = new_chart
@@ -613,12 +611,17 @@ def list_tags(chart_dict, limit):
613611 return tags
614612
615613
616- def format_balance (bal_dict , coa_dict ):
614+ def process_balance (bal_dict , coa_dict ):
615+
617616 # check for success
617+ if "executionResult" not in bal_dict :
618+ LOG .error (f"No execution result found: '{ bal_dict } '" )
619+ raise KeyError ("No 'executionResult' found" )
620+
618621 result = bal_dict ["executionResult" ]
619622 if result != "SUCCESS" :
620623 LOG .error (f"Execution result is not 'SUCCESS': '{ result } '" )
621- raise ValueError
624+ raise ValueError ( "Execution result is not 'SUCCESS'" )
622625
623626 # collate api response into a dict
624627 _data = {}
@@ -647,38 +650,52 @@ def format_balance(bal_dict, coa_dict):
647650 LOG .debug (f"Raw internal balance dict: { _data } " )
648651
649652 # List of rows in CSV
650- csv_out = []
653+ out_rows = []
651654
652655 # Add header row
653656 headers = [
654657 "AccountNumber" ,
655658 "AccountName" ,
656- "Period" ,
659+ "PeriodStart" ,
660+ "PeriodEnd" ,
657661 "StartBalance" ,
658662 "Activity" ,
659663 "EndBalance" ,
660664 ]
661- csv_out .append (headers )
665+ out_rows .append (headers )
662666
663667 # Generate rows from input dict
664668 for k , v in _data .items ():
665669 name = None
666670 if k not in coa_dict :
667- LOG .error (f"Key { k } not found in coa dict " )
671+ LOG .error (f"Key { k } not found in chart of accounts " )
668672 name = k
669673 else :
670674 name = coa_dict [k ]
671675
672676 row = [
673677 k ,
674678 name ,
679+ bal_dict ["period_from" ],
680+ bal_dict ["period_to" ],
675681 v ["balance_start" ],
676682 v ["activity" ],
677683 v ["balance_end" ],
678684 ]
679- csv_out .append (row )
685+ out_rows .append (row )
680686
681- return csv_out
687+ return out_rows
688+
689+
690+ def format_balance (bal_dict , coa_dict ):
691+ csv_out = io .StringIO ()
692+ csv_writer = csv .writer (csv_out )
693+
694+ csv_rows = process_balance (bal_dict , coa_dict )
695+ for row in csv_rows :
696+ csv_writer .writerow (row )
697+
698+ return csv_out .getvalue ()
682699
683700
684701def lambda_handler (event , context ):
@@ -703,13 +720,6 @@ def lambda_handler(event, context):
703720 Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
704721 """
705722
706- # helper functions to encapsulate the body, headers, and status code
707- def _build_return (code , body ):
708- return {
709- "statusCode" : code ,
710- "body" : json .dumps (body , indent = 2 ),
711- }
712-
713723 try :
714724 # collect environment variables
715725 mip_org = _get_os_var ("MipsOrg" )
@@ -776,26 +786,26 @@ def _build_return(code, body):
776786 )
777787 bal_csv = format_balance (raw_bal , coa_chart )
778788
779- return _build_return (200 , bal_csv )
789+ return _build_return_text (200 , bal_csv )
780790 else :
781791
782792 if event_path == api_routes ["ApiChartOfAccounts" ]:
783793 # conditionally filter the output
784794 _coa_chart = limit_chart (coa_chart , limit_length )
785- return _build_return (200 , _coa_chart )
795+ return _build_return_json (200 , _coa_chart )
786796
787797 elif event_path == api_routes ["ApiValidTags" ]:
788798 # build a list of strings from the processed dictionary
789799 valid_tags = list_tags (coa_chart , limit_length )
790- return _build_return (200 , valid_tags )
800+ return _build_return_json (200 , valid_tags )
791801 else :
792- return _build_return (404 , {"error" : "Invalid request path" })
802+ return _build_return_json (404 , {"error" : "Invalid request path" })
793803
794804 else :
795- return _build_return (
805+ return _build_return_json (
796806 400 , {"error" : f"Invalid event: No path found: { event } " }
797807 )
798808
799809 except Exception as exc :
800810 LOG .exception (exc )
801- return _build_return (500 , {"error" : str (exc )})
811+ return _build_return_json (500 , {"error" : str (exc )})
0 commit comments