Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
FreeAgent file download
#I @"packages"
#r @"Http.Fs\lib\net40\HttpClient.dll"
#r @"FSharp.Data\lib\net40\FSharp.Data.dll"
open HttpClient
open FSharp.Data
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 "?"))
[<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=1234",
"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=1234",
"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=1234",
"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)
let rootFolder = @"YOUR DOWNLOAD FOLDER"
open System
open System.IO
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")
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(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(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(whenExists(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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment