Skip to content

Instantly share code, notes, and snippets.

@isaacabraham
Last active July 20, 2016 09:55
Show Gist options
  • Select an option

  • Save isaacabraham/aa3e9a73f503e7855aa7 to your computer and use it in GitHub Desktop.

Select an option

Save isaacabraham/aa3e9a73f503e7855aa7 to your computer and use it in GitHub Desktop.

Revisions

  1. isaacabraham revised this gist Nov 26, 2015. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions 2. FreeAgentApi.fs
    Original file line number Diff line number Diff line change
    @@ -90,7 +90,7 @@ module FreeAgent =
    "attachment":
    {
    "url":"https://api.freeagent.com/v2/attachments/3",
    "content_src":"https://s3.amazonaws.com/freeagent-dev/attachments/1/original.png?AWSAccessKeyId=1K3MW21E6T8KWBY84B02&Expires=1314281186&Signature=GFAKDo%2Bi%2FsUMTYEgg6ZWGysB4k4%3D",
    "content_src":"https://s3.amazonaws.com/freeagent-dev/attachments/1/original.png?AWSAccessKeyId=1234",
    "content_type":"image/png",
    "file_name":"barcode.png",
    "file_size":7673
    @@ -132,7 +132,7 @@ module FreeAgent =
    "attachment":
    {
    "url":"https://api.freeagent.com/v2/attachments/3",
    "content_src":"https://s3.amazonaws.com/freeagent-dev/attachments/1/original.png?AWSAccessKeyId=1K3MW21E6T8KWBY84B02&Expires=1314281186&Signature=GFAKDo%2Bi%2FsUMTYEgg6ZWGysB4k4%3D",
    "content_src":"https://s3.amazonaws.com/freeagent-dev/attachments/1/original.png?AWSAccessKeyId=1234",
    "content_type":"image/png",
    "file_name":"barcode.png",
    "file_size":7673
    @@ -182,7 +182,7 @@ module FreeAgent =
    "attachment":
    {
    "url":"https://api.freeagent.com/v2/attachments/3",
    "content_src":"https://s3.amazonaws.com/freeagent-dev/attachments/2/original.pdf?AWSAccessKeyId=1K3MW21E6T8KWBY84B02&Expires=1316186571&Signature=tA4V5%2BJEE%2Fc3JTg5AiIO494m0cA%3D",
    "content_src":"https://s3.amazonaws.com/freeagent-dev/attachments/2/original.pdf?AWSAccessKeyId=1234",
    "content_type":"application/pdf",
    "file_name":"About Stacks.pdf",
    "file_size":466028
  2. isaacabraham revised this gist Jun 12, 2015. 1 changed file with 0 additions and 2 deletions.
    2 changes: 0 additions & 2 deletions 1. FreeAgentCore.fs
    Original file line number Diff line number Diff line change
    @@ -1,11 +1,9 @@
    #I @"packages"
    #r @"Http.Fs\lib\net40\HttpClient.dll"
    #r @"FSharp.Data\lib\net40\FSharp.Data.dll"
    #load @"Deedle\Deedle.fsx"

    open HttpClient
    open FSharp.Data
    open Deedle

    module Security =
    type RefreshTokenResponse = JsonProvider< """{
  3. isaacabraham revised this gist Jun 12, 2015. 1 changed file with 4 additions and 4 deletions.
    8 changes: 4 additions & 4 deletions 3. DownloadAttachments.fs
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@ let rootFolder = @"YOUR DOWNLOAD FOLDER"
    open System
    open System.IO

    let getItem getProp item = match getProp item with Some prop -> Some(item, prop) | None -> None
    let whenExists getProp item = match getProp item with Some prop -> Some(item, prop) | None -> None
    let buildPath rootFolder subFolder (date:System.DateTime) filename =
    let year = date.Year.ToString("0000")
    let month = date.Month.ToString("00")
    @@ -25,23 +25,23 @@ let downloadFile (path, uri) =
    let bankTxns =
    getBankAccounts()
    |> Seq.collect(fun b -> getBankTxnExplanations b.Url)
    |> Seq.choose(getItem(fun e -> e.Attachment))
    |> Seq.choose(whenExists(fun e -> e.Attachment))
    |> Seq.map(fun (e, attachment) -> buildPath rootFolder "BankTxns" e.DatedOn attachment.FileName, attachment.ContentSrc)
    |> Seq.map downloadFile
    |> Async.Parallel
    |> Async.RunSynchronously

    let bills =
    getBills()
    |> Seq.choose(getItem(fun e -> e.Attachment))
    |> Seq.choose(whenExists(fun e -> e.Attachment))
    |> Seq.map(fun (e, attachment) -> buildPath rootFolder "bills" e.DatedOn attachment.FileName, attachment.ContentSrc)
    |> Seq.map downloadFile
    |> Async.Parallel
    |> Async.RunSynchronously

    let expenses =
    getExpenses()
    |> Seq.choose(getItem(fun e -> e.Attachment))
    |> Seq.choose(whenExists(fun e -> e.Attachment))
    |> Seq.map(fun (e, attachment) -> buildPath rootFolder "expenses" e.DatedOn attachment.FileName, attachment.ContentSrc)
    |> Seq.map downloadFile
    |> Async.Parallel
  4. isaacabraham revised this gist Jun 12, 2015. 3 changed files with 9 additions and 0 deletions.
    9 changes: 9 additions & 0 deletions FreeAgentCore.fs → 1. FreeAgentCore.fs
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,12 @@
    #I @"packages"
    #r @"Http.Fs\lib\net40\HttpClient.dll"
    #r @"FSharp.Data\lib\net40\FSharp.Data.dll"
    #load @"Deedle\Deedle.fsx"

    open HttpClient
    open FSharp.Data
    open Deedle

    module Security =
    type RefreshTokenResponse = JsonProvider< """{
    "access_token":"ACCESS TOKEN",
    File renamed without changes.
    File renamed without changes.
  5. isaacabraham revised this gist Jun 12, 2015. 2 changed files with 309 additions and 0 deletions.
    48 changes: 48 additions & 0 deletions DownloadAttachments.fs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,48 @@
    let rootFolder = @"YOUR DOWNLOAD FOLDER"

    open System
    open System.IO

    let getItem getProp item = match getProp item with Some prop -> Some(item, prop) | None -> None
    let buildPath rootFolder subFolder (date:System.DateTime) filename =
    let year = date.Year.ToString("0000")
    let month = date.Month.ToString("00")
    let dom = date.Day.ToString("00")
    Path.Combine(rootFolder, subFolder, year, month, dom, filename)
    let downloadFile (path, uri) =
    async {
    let! contents =
    uri
    |> createRequest Get
    |> getResponseBytesAsync

    Directory.CreateDirectory(Path.GetDirectoryName(path)) |> ignore
    use s = File.Create(path)
    do! s.WriteAsync(contents, 0, contents.Length) |> Async.AwaitTask
    return path
    }

    let bankTxns =
    getBankAccounts()
    |> Seq.collect(fun b -> getBankTxnExplanations b.Url)
    |> Seq.choose(getItem(fun e -> e.Attachment))
    |> Seq.map(fun (e, attachment) -> buildPath rootFolder "BankTxns" e.DatedOn attachment.FileName, attachment.ContentSrc)
    |> Seq.map downloadFile
    |> Async.Parallel
    |> Async.RunSynchronously

    let bills =
    getBills()
    |> Seq.choose(getItem(fun e -> e.Attachment))
    |> Seq.map(fun (e, attachment) -> buildPath rootFolder "bills" e.DatedOn attachment.FileName, attachment.ContentSrc)
    |> Seq.map downloadFile
    |> Async.Parallel
    |> Async.RunSynchronously

    let expenses =
    getExpenses()
    |> Seq.choose(getItem(fun e -> e.Attachment))
    |> Seq.map(fun (e, attachment) -> buildPath rootFolder "expenses" e.DatedOn attachment.FileName, attachment.ContentSrc)
    |> Seq.map downloadFile
    |> Async.Parallel
    |> Async.RunSynchronously
    261 changes: 261 additions & 0 deletions FreeAgentApi.fs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,261 @@
    [<AutoOpen>]
    module FreeAgent =
    type GetProjectsResponse = JsonProvider< """{ "projects":[
    {
    "url":"https://api.freeagent.com/v2/projects/1",
    "name":"Test Project",
    "contact":"https://api.freeagent.com/v2/contacts/1",
    "budget":0,
    "is_ir35":false,
    "status":"Active",
    "budget_units":"Hours",
    "normal_billing_rate":"0.0",
    "hours_per_day":"8.0",
    "uses_project_invoice_sequence":false,
    "currency":"GBP",
    "billing_period":"hour",
    "created_at":"2011-09-14T16:05:57Z",
    "updated_at":"2011-09-14T16:05:57Z"
    }
    ]}""">

    type GetCustomersResponse = JsonProvider< """{ "contacts":[
    {
    "organisation_name":"foo",
    "url":"https://api.freeagent.com/v2/contacts/2",
    "first_name":"test",
    "last_name":"me",
    "contact_name_on_invoices":true,
    "country":"United Kingdom",
    "locale":"en",
    "account_balance":"-100.0",
    "uses_contact_invoice_sequence":false,
    "created_at":"2011-09-14T16:00:41Z",
    "updated_at":"2011-09-16T09:34:41Z"
    }
    ]}""">

    type GetTimeslipsResponse = JsonProvider< """{ "timeslips":[
    {
    "url":"https://api.freeagent.com/v2/timeslips/25",
    "user":"https://api.freeagent.com/v2/users/1",
    "project":"https://api.freeagent.com/v2/projects/1",
    "task":"https://api.freeagent.com/v2/tasks/1",
    "dated_on":"2011-08-15",
    "hours":"12.0",
    "updated_at":"2011-08-16T13:32:00Z",
    "created_at":"2011-08-16T13:32:00Z"
    }
    ]}""">

    type GetInvoicesResponse = JsonProvider< """{ "invoices": [ {
    "url":"https://api.freeagent.com/v2/invoices/1",
    "contact":"https://api.freeagent.com/v2/contacts/2",
    "dated_on":"2011-08-29T00:00:00+00:00",
    "due_on":"2011-09-28T00:00:00+00:00",
    "reference":"001",
    "currency":"GBP",
    "exchange_rate":"1.0",
    "net_value":"0.0",
    "sales_tax_value":"0.0",
    "total_value": "200.0",
    "paid_value": "50.0",
    "due_value": "150.0",
    "status":"Draft",
    "comments":"An example invoice comment.",
    "omit_header":false,
    "payment_terms_in_days":30,
    "ec_status":"EC Goods",
    "created_at":"2011-08-29T00:00:00Z",
    "updated_at":"2011-08-29T00:00:00Z"
    }
    ]}""">

    type GetExpensesResponse = JsonProvider< """{ "expenses":[
    {
    "url":"https://api.freeagent.com/v2/expenses/1",
    "user":"https://api.freeagent.com/v2/users/1",
    "category":"https://api.freeagent.com/v2/categories/285",
    "dated_on":"2011-08-24",
    "currency":"USD",
    "gross_value":"-20.0",
    "native_gross_value":"-12.0",
    "sales_tax_rate":"1.0",
    "sales_tax_value": "-0.2",
    "native_sales_tax_value": "-0.12",
    "description":"Some description",
    "manual_sales_tax_amount":"0.12",
    "updated_at":"2011-08-24T08:10:40Z",
    "created_at":"2011-08-24T08:10:40Z",
    "attachment":
    {
    "url":"https://api.freeagent.com/v2/attachments/3",
    "content_src":"https://s3.amazonaws.com/freeagent-dev/attachments/1/original.png?AWSAccessKeyId=1K3MW21E6T8KWBY84B02&Expires=1314281186&Signature=GFAKDo%2Bi%2FsUMTYEgg6ZWGysB4k4%3D",
    "content_type":"image/png",
    "file_name":"barcode.png",
    "file_size":7673
    }
    },
    {
    "url":"https://api.freeagent.com/v2/expenses/1",
    "user":"https://api.freeagent.com/v2/users/1",
    "category":"https://api.freeagent.com/v2/categories/285",
    "dated_on":"2011-08-24",
    "currency":"USD",
    "gross_value":"-20.0",
    "native_gross_value":"-12.0",
    "sales_tax_rate":"1.0",
    "sales_tax_value": "-0.2",
    "native_sales_tax_value": "-0.12",
    "description":"Some description",
    "manual_sales_tax_amount":"0.12",
    "updated_at":"2011-08-24T08:10:40Z",
    "created_at":"2011-08-24T08:10:40Z"
    }
    ]}""">

    type GetBillsResponse = JsonProvider< """{ "bills":[{
    "url":"https://api.freeagent.com/v2/bills/1",
    "contact":"https://api.freeagent.com/v2/contacts/1",
    "category":"https://api.freeagent.com/v2/categories/285",
    "reference":"acsad",
    "dated_on":"2011-07-28",
    "due_on":"2011-08-27",
    "total_value":"213.0",
    "paid_value":"200.0",
    "due_value":"13.0",
    "sales_tax_value":"-35.5",
    "sales_tax_rate":"20.0",
    "status":"Open",
    "updated_at":"2011-07-28T12:43:36Z",
    "created_at":"2011-07-28T12:43:36Z",
    "attachment":
    {
    "url":"https://api.freeagent.com/v2/attachments/3",
    "content_src":"https://s3.amazonaws.com/freeagent-dev/attachments/1/original.png?AWSAccessKeyId=1K3MW21E6T8KWBY84B02&Expires=1314281186&Signature=GFAKDo%2Bi%2FsUMTYEgg6ZWGysB4k4%3D",
    "content_type":"image/png",
    "file_name":"barcode.png",
    "file_size":7673
    }
    },
    {
    "url":"https://api.freeagent.com/v2/bills/1",
    "contact":"https://api.freeagent.com/v2/contacts/1",
    "category":"https://api.freeagent.com/v2/categories/285",
    "reference":"acsad",
    "dated_on":"2011-07-28",
    "due_on":"2011-08-27",
    "total_value":"213.0",
    "paid_value":"200.0",
    "due_value":"13.0",
    "sales_tax_value":"-35.5",
    "sales_tax_rate":"20.0",
    "status":"Open",
    "updated_at":"2011-07-28T12:43:36Z",
    "created_at":"2011-07-28T12:43:36Z"
    }
    ]}""">

    type GetBankAccountsResponse = JsonProvider< """{ "bank_accounts":[
    {
    "url":"https://api.freeagent.com/v2/bank_accounts/1",
    "opening_balance":"0.0",
    "type":"StandardBankAccount",
    "name":"Default bank account",
    "is_personal":false,
    "currency": "GBP",
    "current_balance": "0.0",
    "updated_at":"2011-07-28T11:25:20Z",
    "created_at":"2011-07-28T11:25:11Z"
    }
    ]}""">

    type GetBankTxnExplanationResponse = JsonProvider< """{ "bank_transaction_explanations": [
    {
    "url": "https://api.freeagent.com/v2/bank_transaction_explanations/20",
    "bank_transaction": "https://api.freeagent.com/v2/bank_transactions/20",
    "bank_account": "https://api.freeagent.com/v2/bank_accounts/1",
    "category": "https://api.freeagent.com/v2/categories/366",
    "dated_on": "2010-12-01",
    "description": "transform plug-and-play convergence",
    "gross_value": "-90.0",
    "attachment":
    {
    "url":"https://api.freeagent.com/v2/attachments/3",
    "content_src":"https://s3.amazonaws.com/freeagent-dev/attachments/2/original.pdf?AWSAccessKeyId=1K3MW21E6T8KWBY84B02&Expires=1316186571&Signature=tA4V5%2BJEE%2Fc3JTg5AiIO494m0cA%3D",
    "content_type":"application/pdf",
    "file_name":"About Stacks.pdf",
    "file_size":466028
    }
    },
    {
    "url": "https://api.freeagent.com/v2/bank_transaction_explanations/20",
    "bank_transaction": "https://api.freeagent.com/v2/bank_transactions/20",
    "bank_account": "https://api.freeagent.com/v2/bank_accounts/1",
    "category": "https://api.freeagent.com/v2/categories/366",
    "dated_on": "2010-12-01",
    "description": "transform plug-and-play convergence",
    "gross_value": "-90.0"
    }]
    }""">

    let buildUri = sprintf "https://api.freeagent.com/v2/%s"

    let Projects =
    "projects"
    |> buildUri
    |> Communication.createPagedRequest
    GetProjectsResponse.Parse
    (fun p -> p.Projects)
    |> Seq.cache

    let Customers =
    "contacts"
    |> buildUri
    |> Communication.createPagedRequest
    GetCustomersResponse.Parse
    (fun t -> t.Contacts)
    |> Seq.map(fun x -> x.Url, x)
    |> Map.ofSeq

    let getTimeslips =
    sprintf "timeslips?project=%s"
    >> buildUri
    >> Communication.createPagedRequest
    GetTimeslipsResponse.Parse
    (fun t -> t.Timeslips)

    let getInvoices =
    sprintf "invoices?project=%s"
    >> buildUri
    >> Communication.createPagedRequest
    GetInvoicesResponse.Parse
    (fun t -> t.Invoices)

    let getExpenses() =
    "expenses"
    |> buildUri
    |> Communication.createPagedRequest
    GetExpensesResponse.Parse
    (fun t -> t.Expenses)

    let getBills() =
    "bills"
    |> buildUri
    |> Communication.createPagedRequest
    GetBillsResponse.Parse
    (fun t -> t.Bills)

    let getBankAccounts() =
    "bank_accounts"
    |> buildUri
    |> Communication.createPagedRequest
    GetBankAccountsResponse.Parse
    (fun t -> t.BankAccounts)

    let getBankTxnExplanations =
    sprintf "bank_transaction_explanations?bank_account=%s"
    >> buildUri
    >> Communication.createPagedRequest
    GetBankTxnExplanationResponse.Parse
    (fun t -> t.BankTransactionExplanations)
  6. isaacabraham created this gist Jun 12, 2015.
    75 changes: 75 additions & 0 deletions FreeAgentCore.fs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,75 @@
    module Security =
    type RefreshTokenResponse = JsonProvider< """{
    "access_token":"ACCESS TOKEN",
    "token_type":"bearer",
    "expires_in":604800
    }""">

    let private contactTokenServer body =
    "https://api.freeagent.com/v2/token_endpoint"
    |> createRequest Post
    |> withBasicAuthentication clientId secret
    |> withHeader (ContentType "application/x-www-form-urlencoded")
    |> withHeader (RequestHeader.AcceptCharset "UTF-8")
    |> withBody body
    |> getResponseBody

    let private refreshToken = "REFRESH TOKEN GOES HERE"

    let private getFreshToken =
    sprintf "grant_type=refresh_token&refresh_token=%s"
    >> contactTokenServer
    >> RefreshTokenResponse.Parse

    let accessToken = getFreshToken refreshToken

    module Communication =
    let private withFreeAgentAuth =
    sprintf "Bearer %s"
    >> Authorization
    >> withHeader
    let private withAuth = withFreeAgentAuth Security.accessToken.AccessToken

    let withStandardHeaders =
    withAuth
    >> withHeader (Accept "application/json")
    >> withHeader (ContentType "application/json")
    >> withHeader (UserAgent "fsharp interactive")

    let makeStandardRequest parser =
    createRequest Get
    >> withStandardHeaders
    >> getResponseBody
    >> parser

    let private getNextPageUri =
    let splitOn (text:string) (item:string) = item.Split([|text|], System.StringSplitOptions.RemoveEmptyEntries)
    fun (reponse:Response) ->
    reponse.Headers.[ResponseHeader.Link] |> splitOn ", "
    |> Seq.map (fun item ->
    let [|uri;direction|] = item |> splitOn "; "
    uri, (direction |> splitOn "=").[1].Trim '\'')
    |> Seq.tryFind (snd >> (=) "next")
    |> Option.map fst
    |> Option.map(fun s -> s.Trim('<', '>'))

    let createPagedRequest parser getCollection uri =
    let rec getNextPage uri =
    seq {
    let response =
    uri
    |> createRequest Get
    |> withStandardHeaders
    |> getResponse
    match response.EntityBody with
    | Some body ->
    let collection = body |> parser |> getCollection
    yield! collection
    let nextUri = getNextPageUri response
    match nextUri with
    | Some nextUri -> yield! getNextPage nextUri
    | None -> ()
    | None -> ()
    }

    getNextPage (sprintf "%s%spage=1&per_page=100" uri (if uri.Contains "?" then "&" else "?"))