Skip to content

Commit

Permalink
Merge c4df07e into 9d9a9a1
Browse files Browse the repository at this point in the history
  • Loading branch information
kescobo committed Oct 20, 2020
2 parents 9d9a9a1 + c4df07e commit 8b3fe17
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 27 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:
${{ runner.os }}-
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
env:
AIRTABLE_KEY: $${{ secrets.AIRTABLE_KEY }}
- uses: julia-actions/julia-uploadcoveralls@v1
env:
COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
Expand Down
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "Airtable"
uuid = "96f7d883-6668-4fbe-bb01-b60427b16035"
authors = ["Kevin Bonham", "PhD <kevbonham@gmail.com> and contributors"]
authors = ["Kevin Bonham PhD <kevbonham@gmail.com>", "contributors"]
version = "0.1.0"

[deps]
Expand All @@ -9,3 +9,5 @@ JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"

[compat]
julia = "1.5"
HTTP = "0.8"
JSON3 = "1"
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,10 @@
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://kescobo.github.io/Airtable.jl/dev)
[![Build Status](https://github.com/kescobo/Airtable.jl/workflows/CI/badge.svg)](https://github.com/kescobo/Airtable.jl/actions)
[![Coverage](https://coveralls.io/repos/github/kescobo/Airtable.jl/badge.svg?branch=master)](https://coveralls.io/github/kescobo/Airtable.jl?branch=master)

An (unofficial) API for [Airtable](http://airtable.com).

If you haven't yet signed up for an airtable account,
use [this referal link](https://airtable.com/invite/r/MVGCWzpJ)
and you + the dummy account I use for testing will get $10 in credits.
Don't know how I'd use them yet, but might as well!
3 changes: 2 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ makedocs(;

deploydocs(;
repo="github.com/kescobo/Airtable.jl",
devbranch="main"
devbranch="main",
push_preview=true,
)
155 changes: 153 additions & 2 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,160 @@ CurrentModule = Airtable

# Airtable

An (unofficial) API for interacting with the [Airtable](http://www.airtable.com) API.

```@index
Order = [:type, :function]
```

## Using this package

This package is a very thin wrapper around the Airtable REST API,
using [`HTTP.jl`](https://juliaweb.github.io/HTTP.jl/stable/) to do the hard stuff.
No types or methods are exported,
mostly because I didn't want to think too hard about naming them.

This documentation should be used in close conjuntion with the airtable API
documentation, which is generated automatically for you using your actual tables
(see below).

Most functions require 4 parts:

1. a [`Credential`](@ref), which stores your [API key](@ref apikey)
2. a [Base ID](@ref baseid)
3. [a `tablename`](@ref Tablename) - which refers to the specific table from your base
4. an [API query](@ref apiquery), in the form of keyword arguments

### [API key](@id apikey)

To obtain your API key, go to your [account settings page](https://airtable.com/account)
and click on the "generate API key" button.
If you previously made a key, you can regenerate it, or just copy the one that's there.

![Get airtable API key]()

You can then create an [`Airtable.Credential`](@ref) using that key as a string,
or set it as an environmental variable (`AIRTABLE_KEY` by default).

```@docs
Credential
```

### [Base ID](@id baseid)

Open your airtable base, click the help button in the upper right,
and then click "API documentation".
Airtable generates documentation for your sepecific base -
near the top you should see a sentence like the follwing,
with a different alphanumeric sequence for your base:

> The ID of this base is appphImnhJO8AXmmo
It will also appear in the url of the base documentation.
For example, the `Test` base for this repo has the url `https://airtable.com/appphImnhJO8AXmmo/api/docs`.

### Tablename

Within each base, you may have multiple tables.
The `tablename` argument in the following functions is just a string
with the table name, eg `"Table 1"`.

### [API Query](@id apiquery)

Use keyword arguments to add commponents to the API request body.
For example, if you want a `GET` request to only contain the `Name` field,
you could include `; fields=["Name"]` keyword argument to the [`Airtable.get`](@ref)
function.

## Interface

The primary function is [`Airtable.request`](@ref),
which contains all of the components for building an API query
and parses the returned data with [`JSON3.jl`](https://github.com/quinnj/JSON3.jl).

The following examples use [this airtable base](https://airtable.com/shrx4BWLV1HurniFD),
which has the ID "appphImnhJO8AXmmo", and the API key described above.
To run this code, you will need to substitute the API key and ID
from your own base.
These examples only scratch the surface -
much more information is available in the API documentation for your own base.

```@docs
request
```

### Retrieve records

```jldoctest api; setup = :(using Airtable)
julia> key=Airtable.Credential();
julia> req1 = Airtable.request("GET", key, "appphImnhJO8AXmmo", "Table 1"; maxRecords=2)
JSON3.Object{Base.CodeUnits{UInt8, String}, Vector{UInt64}} with 1 entry:
:records => JSON3.Object[{…
julia> req1.records
2-element JSON3.Array{JSON3.Object, Base.CodeUnits{UInt8, String}, SubArray{UInt64, 1, Vector{UInt64}, Tuple{UnitRange{Int64}}, true}}:
{
"id": "recMc1HOSIxQPJyyc",
"fields": {
"Name": "Record 1",
"Notes": "Some notes",
"Status": "Todo"
},
"createdTime": "2020-10-16T21:04:11.000Z"
}
{
"id": "recMwT4P4tKlSLJoH",
"fields": {
"Name": "Record 2",
"Notes": "Other notes",
"Status": "In progress"
},
"createdTime": "2020-10-16T21:04:11.000Z"
}
julia> req2 = Airtable.request("GET", key, "appphImnhJO8AXmmo", "Table 1"; filterByFormula="Status = 'Done'")
JSON3.Object{Base.CodeUnits{UInt8, String}, Vector{UInt64}} with 1 entry:
:records => JSON3.Object[{…
julia> req2.records
1-element JSON3.Array{JSON3.Object, Base.CodeUnits{UInt8, String}, SubArray{UInt64, 1, Vector{UInt64}, Tuple{UnitRange{Int64}}, true}}:
{
"id": "recSStgr3yJnQc2Wg",
"fields": {
"Name": "Record 3 ",
"Status": "Done"
},
"createdTime": "2020-10-16T21:04:11.000Z"
}
```

```@autodocs
Modules = [Airtable]
### Retrieving lots of records

The airtable API will only return 100 records per request[^1],
and only allows 5 requests/sec.
To facilitate retrieving lots of records,
You can use the [`Airtable.query`](@ref) function.

```@docs
query
```

[^1]: This is the default, you can change this with the `pageSize` parameter,
but 100 is the maximum.

### Add/Update Records

I haven't actually figured this out yet 🤔.
If you want to help, let me know!

### Other functions

Here are some shorthands for `GET`, `POST`, `PATCH`, and `PUT`.

```@docs
get
post
patch
put
```
2 changes: 2 additions & 0 deletions src/Airtable.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ module Airtable
using HTTP
using JSON3

const API_VERSION = "v0"

include("auth.jl")
include("interface.jl")

Expand Down
10 changes: 8 additions & 2 deletions src/auth.jl
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
"""
Credential(; api_key)
A credentials and identity object for Trello.
A credential object for Airtable.
If the api_key or api_token are not provided,
they will be read from the `AIRTABLE_KEY` environment variable.
Go to [Airtable account settings](https://airtable.com/account)
to aquire your credentials.
```jldoctest; setup = :(using Airtable)
# after running `export AIRTABLE_KEY=<api key>` in the shell
julia> key = Airtable.Credential()
Airtable.Credential(<secrets>)
```
"""
struct Credential
api_key::String
Expand Down
43 changes: 23 additions & 20 deletions src/interface.jl
Original file line number Diff line number Diff line change
@@ -1,61 +1,63 @@
"""
Airtable.request(method::AbstractString, cred::Credential, path::AbstractString; query_kwargs...)
Airtable.request(method::AbstractString, cred::Credential, baseid::AbstractString; query_kwargs...)
Make a request to the Airtable API.
Required arguments:
- `method`: one of "GET", "PUT", "POST", or "PATCH",
- `cred`: an [`Airtable.Credential`](@ref) containing your API key
- `path`: the endpoint of your Airtable base. See https://airtable.com/api for details
- `baseid`: the endpoint of your Airtable base. See https://airtable.com/api for details
- `tablename`: The name of the table (view) for the query
Query parameters are in the form of keyword arguments,
eg `filterByFormla = "NOT({Name} = '')", maxRecords=2`.
See Airtable API reference for more information.
"""
function request(method::AbstractString, cred::Credential, path::AbstractString; query_kwargs...)
function request(method::AbstractString, cred::Credential, baseid::AbstractString, tablename::AbstractString; query_kwargs...)
method in ("GET", "PUT", "POST", "PATCH") || error("Invalid API method: $method")

query = ["api_key"=> cred.api_key]
for (key, value) in query_kwargs
isempty(value) && continue
push!(query, string(key) => string(value))
end
path = joinpath("/", API_VERSION, baseid, HTTP.escapeuri(tablename))
uri = HTTP.URI(host="api.airtable.com", scheme="https", path=path, query=query)
resp = HTTP.request(method, uri)
return JSON3.read(String(resp.body))
end

"""
Airtable.get(cred::Credential, path::AbstractString; query_kwargs...)
Airtable.get(cred::Credential, baseid::AbstractString, tablename; query_kwargs...)
Shorthand for [`Airtable.request("GET", cred, path; query_kwargs)`](@ref `Airtable.request`)
Shorthand for [`Airtable.request("GET", cred, baseid, tablename; query_kwargs)`](@ref Airtable.request)
"""
get(cred::Credential, path::AbstractString; query_kwargs...) = request("GET", cred, path; query_kwargs...)
get(cred::Credential, baseid::AbstractString, tablename; query_kwargs...) = request("GET", cred, baseid, tablename; query_kwargs...)

"""
Airtable.put(cred::Credential, path::AbstractString; query_kwargs...)
Airtable.put(cred::Credential, baseid::AbstractString, tablename; query_kwargs...)
Shorthand for [`Airtable.request("PUT", cred, path; query_kwargs)`](@ref `Airtable.request`)
Shorthand for [`Airtable.request("PUT", cred, baseid, tablename; query_kwargs)`](@ref Airtable.request)
"""
put(cred::Credential, path::AbstractString; query_kwargs...) = request("PUT", cred, path; query_kwargs...)
put(cred::Credential, baseid::AbstractString, tablename; query_kwargs...) = request("PUT", cred, baseid, tablename; query_kwargs...)

"""
Airtable.post(cred::Credential, path::AbstractString; query_kwargs...)
Airtable.post(cred::Credential, baseid::AbstractString, tablename; query_kwargs...)
Shorthand for [`Airtable.request("POST", cred, path; query_kwargs)`](@ref `Airtable.request`)
Shorthand for [`Airtable.request("POST", cred, baseid, tablename; query_kwargs)`](@ref Airtable.request)
"""
post(cred::Credential, path::AbstractString; query_kwargs...) = request("PUT", cred, path; query_kwargs...)
post(cred::Credential, baseid::AbstractString, tablename; query_kwargs...) = request("PUT", cred, baseid, tablename; query_kwargs...)

"""
Airtable.patch(cred::Credential, path::AbstractString; query_kwargs...)
Airtable.patch(cred::Credential, baseid::AbstractString, tablename; query_kwargs...)
Shorthand for [`Airtable.request("PATCH", cred, path; query_kwargs)`](@ref `Airtable.request`)
Shorthand for [`Airtable.request("PATCH", cred, baseid, tablename; query_kwargs)`](@ref Airtable.request)
"""
patch(cred::Credential, path::AbstractString; query_kwargs...) = request("PUT", cred, path; query_kwargs...)
patch(cred::Credential, baseid::AbstractString, tablename; query_kwargs...) = request("PUT", cred, baseid, tablename; query_kwargs...)

"""
Airtable.query(cred::Credential, path::AbstractString; query_kwargs...)
Airtable.query(cred::Credential, baseid::AbstractString, tablename; query_kwargs...)
Shorthand for a "GET" request that handles continuation and rate-limiting.
Expand All @@ -68,19 +70,20 @@ after pausing 0.21 seconds in between.
Required arguments:
- `cred`: an [`Airtable.Credential`](@ref) containing your API key
- `path`: the endpoint of your Airtable base. See https://airtable.com/api for details
- `baseid`: the endpoint of your Airtable base. See https://airtable.com/api for details
- `tablename`: the name of the table in your base (eg `"Table 1"`)
Query parameters are in the form of keyword arguments,
eg `filterByFormla = "NOT({Name} = '')", maxRecords=2`.
See Airtable API reference for more information.
"""
function query(cred::Credential, path::AbstractString; query_kwargs...)
req = get(cred, path; query_kwargs...)
function query(cred::Credential, baseid::AbstractString, tablename::AbstractString; query_kwargs...)
req = get(cred, baseid, tablename; query_kwargs...)
records = req.records
append!(records, req.records)
while haskey(req, :offset)
@info "Making another request with offset $(req.offset)"
req = get(cred, path; offset=req.offset, query_kwargs...)
req = get(cred, baseid, tablename; offset=req.offset, query_kwargs...)
append!(records, req.records)
sleep(0.210)
end
Expand Down
4 changes: 3 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ using Airtable
using Test

@testset "Airtable.jl" begin
# Write your tests here.
key = Airtable.Credential()


end

0 comments on commit 8b3fe17

Please sign in to comment.