{"id":450,"date":"2021-04-13T12:18:00","date_gmt":"2021-04-13T12:18:00","guid":{"rendered":"https:\/\/eipsoftware.com\/musings\/?p=450"},"modified":"2024-04-13T20:49:10","modified_gmt":"2024-04-13T20:49:10","slug":"address-verification-api","status":"publish","type":"post","link":"https:\/\/eipsoftware.com\/musings\/address-verification-api\/","title":{"rendered":"Address Verification API"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Where do you live? May be a simple question, but when companies are asking where someone lives, the answer isn&#8217;t always so easy.  The physical address and the mailing address of a person can be two completely separate things.  And when companies want to send direct mail, they need to ensure the message is appropriate.  <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Today I am going to show a Python program that I wrote that can process millions of addresses and verify if the address is a valid mailing address that matches the physical address. And how the majority of online mapping services get it wrong.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">  <\/p>\n\n\n\n<!--more-->\n\n\n\n<p class=\"wp-block-paragraph\">One the main challenges about physical addresses is that you can&#8217;t rely on online mapping services for 100% verification.  Many of the top mapping services, i.e. Google Maps, will approximate an address if it isn&#8217;t in its database.  An example of this is 1500 West Pine Street, Anywhere, AK.  If Google Maps knows the street and the city, it will mark the location as the closest street number to the requested street number.  <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The result may be close enough for driving directions or to help give you an estimate of where the location is, but for the postal system, this doesn&#8217;t work.  If the building does exist, it is ok; however if it doesn&#8217;t exist, then your mail will be returned. And a marketing opportunity will have been lost.  <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The United States Postal Service does have an address verification tool. However it doesn&#8217;t allow for large datasets to be verified. Many of the other online API tools, paid or unpaid are the same. I resolved the issue by splitting the source file into manageable block sizes for requests.  Next when the response is received I combine the responses and re-arrange the responses to match the order the requests were made. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The program uses concurrent futures package to allow for multi-threaded operations and allow for numerous requests to be sent and received at the same time.  <\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Program Overview<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The program uses the tkinter UI package.  I won&#8217;t go in-depth on how the tkinter package works, that would be a separate posting all on its own.  This post will be long enough as it is. <\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"488\" src=\"https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/Process_1-1024x488.png\" alt=\"\" class=\"wp-image-451\" srcset=\"https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/Process_1-1024x488.png 1024w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/Process_1-300x143.png 300w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/Process_1-768x366.png 768w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/Process_1-900x428.png 900w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/Process_1.png 1172w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Program Files<\/h2>\n\n\n\n<figure class=\"wp-block-table is-style-regular\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">File Name<\/th><th class=\"has-text-align-left\" data-align=\"left\">Description<\/th><\/tr><\/thead><tbody><tr><td class=\"has-text-align-left\" data-align=\"left\">LocusApiSearch.py<\/td><td class=\"has-text-align-left\" data-align=\"left\">The main file imports the packages and contains the main program loop <\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">cls\/apiHandler.py<\/td><td class=\"has-text-align-left\" data-align=\"left\">class to handle connecting to the API<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">cls\/csvFileHandler.py<\/td><td class=\"has-text-align-left\" data-align=\"left\">creates interim .csv files and <\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">cls\/outputFile.py<\/td><td class=\"has-text-align-left\" data-align=\"left\">defines the definition, i.e. colums of the output file<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">cls\/pyUtilities.py<\/td><td class=\"has-text-align-left\" data-align=\"left\">helper function to merge interim .csv files into the final file<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">cls\/runtimeParameters.py<\/td><td class=\"has-text-align-left\" data-align=\"left\">class to store user parameters from the UI<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">cls\/ui.py<\/td><td class=\"has-text-align-left\" data-align=\"left\">main class for handling the UI components<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">cls\/uiConfig.py<\/td><td class=\"has-text-align-left\" data-align=\"left\">reading configuration options from the UI and saving to local file<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">cls\/userAddresses.py<\/td><td class=\"has-text-align-left\" data-align=\"left\">definition of the input data file and methods to clean data<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\"><\/td><td class=\"has-text-align-left\" data-align=\"left\"><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Packages<\/h2>\n\n\n\n<ul class=\"wp-block-list\"><li>datetime<\/li><li>time<\/li><li>os<\/li><li>pathlib<\/li><li>numpy<\/li><li>pandas<\/li><li>json<\/li><li>requests<\/li><li>tkinter<\/li><li>concurrent.futures<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Program Flow<\/h2>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:css decode:true \">LocusApiSearch\n   Initiate the UI\n      Read the configuration settings, populate the UI\n      Configuration settings file, doesn't exist use default\n\n   Start the Main Loop\n      Load the runtime parameters from the UI\n      Initiate the log file\n      Read the input file, store into a data frame\n      Start new threads, each handling a block of data\n      Process the csv file\n          prep the data, based on parser and UI options\n          save json file\n      Make API call\n          Process returned json file\n          If file not processed, sleep, then retry\n          Save file as .csv file\n      Momentary pause, don't overload remote api\n      Merge all .csv files<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Main<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">LocusApiSearch.py<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" ># main entry point to start the gui\nif __name__ == '__main__':\n    # load the gui\n    root = tk.Tk()\n    app = Application(app_root=root)\n    # start the gui\n    app.mainloop()\n    print(\"complete\")<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The first code block starts the UI, I assigned the UI to root. and assigned the application to app.  Next I tell the UI to start the main loop.  <\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">UI<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"488\" src=\"https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/options_edited-1024x488.png\" alt=\"\" class=\"wp-image-464\" srcset=\"https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/options_edited-1024x488.png 1024w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/options_edited-300x143.png 300w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/options_edited-768x366.png 768w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/options_edited-900x428.png 900w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/options_edited.png 1172w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">I am going to skip over the UI startup and drawing.  There are multiple tutorials on how to use the tkinter UI package for Python.  In summary cls\/ui.py and cls\/uiConfig.py handle drawing the screen windows and controls in the UI.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Application Start<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">LocusApiSearch.py<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">   start_job()<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" >def start_job(user_args: dict):\n    \"\"\"f(x) to initiate &amp; control the API request threads\"\"\"\n    try:\n        global logfile\n\n        # execution\n        app.update_progress(\"Starting Job\")\n\n        # --------------------------------------------------------------------\n        # set the run time parameters\n        # --------------------------------------------------------------------\n        args = runtimeParameters(user_args, batch=True)\n\n        # --------------------------------------------------------------------\n        # check and set logfile locale, write log file to results directory\n        # --------------------------------------------------------------------\n        log_path = Path(args.usr_args['output_dir'] + '\/logs\/')\n        if not log_path.is_dir():\n            os.mkdir(log_path)\n        nm = logfile.name\n        logfile = Path(log_path \/ nm)\n\n        with open(logfile, 'a+') as file_handle:\n            file_handle.write(\"starting job: \" + datetime.now().strftime(\"%Y-%m-%d.%H%M%S\") + \"\\n\")\n            file_handle.close()<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">start_job(user_args: dict)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pass in the dictionary that was generated from reading contents of the UI. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Next, use the runtimeParameters class to organize the parameters.  Because we are using multi-threaded processing need to have a local copy of the parameters for the current batch of data that is being processed. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The next thing is store into the log file that starting the job, and release the lock on the file.<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" >\n        # initiate file handler to read the input file\n        cfh = csvFileHandler(args.usr_args)\n\n        # load the .csv file into a panda data frame\n        # rm # cfh.get_file(args.csv_file_name, args.test_mode)\n        cfh.get_file(csv_file_name=args.usr_args['input_file'])\n        app.update_progress(f\"File Length: {len(cfh.csv_df)}\")\n\n        # set the run time argument for file size\n        # and starting rows, ending rows, and rows to process in batch\n        args.set_file_length(len(cfh.csv_df))<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Will use the csvFileHandler class to read the data file.  Write a message to the UI to the let user know where we are in the processing. Update the args.set_file_length property with the file length.  <\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Initiate the Thread Pool<\/h3>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" >        # multi thread the process_csv_file f(x)\n        # to initiate multiple threads\n        # loop through input source document in groups specified by step size\n        with cf.ThreadPoolExecutor(max_workers=8) as executor:\n            call_locus_api = {executor.submit(process_csv_file, args, cfh, curr_idx):\n                             curr_idx for curr_idx in np.arange(args.start_row, args.end_row, args.step_size)}\n            for worker in cf.as_completed(call_locus_api):\n                # stop new threads from being processed,\n                # won't prevent existing results from being processed\n                if app.continue_processing == False:\n                    executor.shutdown(wait=False)\n                    worker.cancel()\n                    break\n                \n                fx_result = call_locus_api[worker]\n                try:\n                    worker_result = worker.result()\n                except Exception as e:\n                    print(f\"Exception {e}\")\n                    app.update_progress(f\"{fx_result} raised an exception {e}\")\n                else:\n                    app.update_result(f\"{fx_result} result: {worker_result}\")<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Here we will start processing the blocks of data.  Each block of data will be assigned its own thread.  I capped the maximum number of threads at 8. The number of threads could be set in the UI, but I opted against it.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">with cf.ThreadPoolExecutor(max_workers=8) as executor:<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">cf is from the concurrent.futures package and the method ThreadPoolExecutor is what is used to manage the pool of threads.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">call_locus_api = {executor.submit(process_csv_file, args, cfh, curr_idx):\n                             curr_idx for curr_idx in np.arange(args.start_row, args.end_row, args.step_size)}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">call_locus_api is definition of the function I want to execute with all of the applicable parameters. <\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">for worker in cf.as_completed(call_locus_api):\n                # stop new threads from being processed,\n                # won't prevent existing results from being processed\n                if app.continue_processing == False:\n                    executor.shutdown(wait=False)\n                    worker.cancel()\n                    break<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Use a for loop to process each data block.  I do a quick check to see if the cancel button was pushed to prevent the data block from processing.  Threads that are currently processing will finish.  <\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">                \n                fx_result = call_locus_api[worker]\n                try:\n                    worker_result = worker.result()\n                except Exception as e:\n                    print(f\"Exception {e}\")\n                    app.update_progress(f\"{fx_result} raised an exception {e}\")\n                else:\n                    app.update_result(f\"{fx_result} result: {worker_result}\")<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">fx_result = call_locus_api[worker]<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now send the request out to the remote API, using the function that was defined for call_locus_api.  Explanation of the <a href=\"#process_csv_file\">process_csv_file<\/a> function below.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use a try \/ except block to handle any errors incurred from the calling the API.  If the API results are successful notify the user in the result messages window.  If an error was raised, notify the user in the progress window.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"488\" src=\"https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/Messages_2-1024x488.png\" alt=\"\" class=\"wp-image-467\" srcset=\"https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/Messages_2-1024x488.png 1024w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/Messages_2-300x143.png 300w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/Messages_2-768x366.png 768w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/Messages_2-900x428.png 900w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2021\/10\/Messages_2.png 1172w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">LocusApiSearch.py<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"process_csv_file\">   process_csv_file()<\/h4>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" >def process_csv_file(args: runtimeParameters\n                     , cfh: csvFileHandler\n                     , cidx: int):\n    \"\"\"function to do the locus API calls\"\"\"\n    # counter to track requests sent to api\n    max_tries = 4\n    current_index = cidx\n\n    # start retry loop\n    for retry_attempts in range(max_tries):\n        if app.continue_processing == False:\n            return\n        # write status to gui window\n        app.update_progress(f\"Current index == {current_index} retry attempt: {retry_attempts}\")\n        app.update_row(f\"{str(current_index)}\")\n<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Pass the parameters needed to call the API.  Hard coded the maximum retry attempts to 4.  My remote API&#8217;s are highly reliable, if the block fails four time most likely there is an issue with the data vs the API not working. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Do a quick check to ensure the user didn&#8217;t click the cancel button, and if so continue on.  Print messages to the progress messages window. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" >        # don't reset \/ re-run if there was an error\n        if retry_attempts == 0:\n            # prep the user addresses\n            # df_curr is the current data frame range to process\n            df_curr = cfh.csv_df.iloc[current_index:(current_index + args.step_size)]\n\n            usr_addr = userAddresses(raw_df=df_curr, args=args)\n            usr_addr.selectDataColumns()\n            usr_addr.prepAddressess()\n            usr_addr.populateAddresses()\n            # --- DEBUG --- view the json formatted addresses\n            # print(usr_addr.json_addresses)\n            # --- LOG -- json request\n            # append to file, + if file not there create\n            with open(logfile, 'a+') as file_handle:\n                file_handle.write(usr_addr.json_addresses + \"\\n\")\n                file_handle.close()<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">If it is the first attempt with the data block, will prep the data using the userAddresses class.  <\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">df_curr = cfh.csv_df.iloc[current_index:(current_index + args.step_size)]<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Take a subset of the data from the data frame.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">usr_addr = userAddresses(raw_df=df_curr, args=args)\n            usr_addr.selectDataColumns()\n            usr_addr.prepAddressess()\n            usr_addr.populateAddresses()<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The userAddresses class to select the appropriate columns from the input data set, cleanse the address and create a json file to send to the API.<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" >            # make API calls\n            api = apiHandler(api_url=args.usr_args['api_url']\n                             , api_key=args.usr_args['api_key'])\n\n        api.sendRequest(usr_addr.json_addresses)\n\n        # initiate the output file handler\n        outf = outputFile(file_len=args.file_length\n                          , output_path=args.csv_output_path)\n        outf.parse_file(current_index, api.json_response\n                        , df_curr, df_curr['uid'].tolist())\n<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Use the apiHandler class for each different api that will reference. <\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">api.sendRequest(usr_addr.json_addresses)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Send the request to the api.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># initiate the output file handler\n        outf = outputFile(file_len=args.file_length\n                          , output_path=args.csv_output_path)\n        outf.parse_file(current_index, api.json_response\n                        , df_curr, df_curr['uid'].tolist())<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Get the response from the API, parse the json file.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" >        if outf.success == True:\n            # exit the loop\n            break\n        else:\n            # sleep between retries to see if retry will work\n            time.sleep(1.5)\n\n    # exit the for loop\n    args.current_index = args.current_index + args.step_size\n\n    # compose result message\n    result_msg = f\"complete = {str(outf.success)} using {str(retry_attempts)} retry attempts\"\n\n    # implicity destroy local var to help with gc\n    outf = None\n    usr_addr = None\n    api = None\n    df_j = None\n    r_val = None\n    df_v = None\n    df_output = None\n\n    # momentary pause don't over load address API\n    time.sleep(0.5)\n    return result_msg<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">If the response is successful break out of the retry loop. And update the result message window. If the try failed, go to sleep for 1,5 seconds to not overload the remote API.  <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Even if successful will rate limit requests to the remote API by going to sleep for 0,5 seconds.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Where do you live? May be a simple question, but when companies are asking where someone lives, the answer isn&#8217;t always so easy. The physical address and the mailing address of a person can be two completely separate things. And when companies want to send direct mail, they need to ensure the message is appropriate. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_crdt_document":"","footnotes":""},"categories":[3,8,4,9],"tags":[28,30,50,61,62],"series":[],"class_list":["post-450","post","type-post","status-publish","format-standard","hentry","category-python","category-pandas","category-code","category-numpy","tag-python","tag-code","tag-pandas","tag-api","tag-numpy"],"_links":{"self":[{"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/posts\/450","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/comments?post=450"}],"version-history":[{"count":17,"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/posts\/450\/revisions"}],"predecessor-version":[{"id":470,"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/posts\/450\/revisions\/470"}],"wp:attachment":[{"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/media?parent=450"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/categories?post=450"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/tags?post=450"},{"taxonomy":"series","embeddable":true,"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/series?post=450"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}