Skip to content

Commit ff454c4

Browse files
authoredFeb 11, 2021
Move the implementation to __init__.py directly. (pcorpet#46)
1 parent e999bc0 commit ff454c4

File tree

4 files changed

+241
-250
lines changed

4 files changed

+241
-250
lines changed
 

‎README.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Airtable Python.
2626

2727
.. code:: python
2828
29-
from airtable import airtable
29+
import airtable
3030
at = airtable.Airtable('BASE_ID', 'API_KEY')
3131
at.get('TABLE_NAME')
3232

‎airtable/__init__.py

+240-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,240 @@
1-
from __future__ import absolute_import
2-
from .airtable import (
3-
Airtable as Airtable,
4-
AirtableError as AirtableError,
5-
IsNotInteger as IsNotInteger,
6-
IsNotString as IsNotString,
7-
Record as Record,
8-
Table as Table,
9-
)
1+
import json
2+
import posixpath
3+
import requests
4+
import six
5+
from typing import Any, Generic, Mapping, TypeVar
6+
from collections import OrderedDict
7+
8+
API_URL = 'https://api.airtable.com/v%s/'
9+
API_VERSION = '0'
10+
11+
12+
class IsNotInteger(Exception):
13+
pass
14+
15+
16+
class IsNotString(Exception):
17+
pass
18+
19+
20+
def check_integer(n):
21+
if not n:
22+
return False
23+
elif not isinstance(n, six.integer_types):
24+
raise IsNotInteger('Expected an integer', n)
25+
else:
26+
return True
27+
28+
29+
def check_string(s):
30+
if not s:
31+
return False
32+
elif not isinstance(s, six.string_types):
33+
raise IsNotString('Expected a string', s)
34+
else:
35+
return True
36+
37+
38+
def create_payload(data):
39+
return {'fields': data}
40+
41+
42+
_T = TypeVar('_T', bound=Mapping[str, Any])
43+
44+
45+
class Record(Generic[_T]):
46+
47+
def __init__(self):
48+
raise NotImplementedError(
49+
'This class is only used as a type of records returned by this module, however '
50+
'it should only be used as a type (see typing stubs) and not as an actual class.'
51+
)
52+
53+
def __getitem__(self, key):
54+
pass
55+
56+
def get(self, key, default=None):
57+
pass
58+
59+
60+
class AirtableError(Exception):
61+
62+
def __init__(self, error_type, message):
63+
super(AirtableError, self).__init__()
64+
# Examples of types are:
65+
# TABLE_NOT_FOUND
66+
# VIEW_NAME_NOT_FOUND
67+
self.type = error_type
68+
self.message = message
69+
70+
def __repr__(self):
71+
return '%s(%r, %r)' % (self.__class__.__name__, self.type, self.message)
72+
73+
def __str__(self):
74+
return self.message or self.__class__.__name__
75+
76+
77+
class Airtable(object):
78+
def __init__(self, base_id, api_key, dict_class=OrderedDict):
79+
"""Create a client to connect to an Airtable Base.
80+
81+
Args:
82+
- base_id: The ID of the base, e.g. "appA0CDAE34F"
83+
- api_key: The API secret key, e.g. "keyBAAE123C"
84+
- dict_class: the class to use to build dictionaries for returning
85+
fields. By default the fields are kept in the order they were
86+
returned by the API using an OrderedDict, but you can switch
87+
to a simple dict if you prefer.
88+
"""
89+
self.airtable_url = API_URL % API_VERSION
90+
self.base_url = posixpath.join(self.airtable_url, base_id)
91+
self.headers = {'Authorization': 'Bearer %s' % api_key}
92+
self._dict_class = dict_class
93+
94+
def __request(self, method, url, params=None, payload=None):
95+
if method in ['POST', 'PUT', 'PATCH']:
96+
self.headers.update({'Content-type': 'application/json'})
97+
r = requests.request(method,
98+
posixpath.join(self.base_url, url),
99+
params=params,
100+
data=payload,
101+
headers=self.headers)
102+
if r.status_code == requests.codes.ok:
103+
return r.json(object_pairs_hook=self._dict_class)
104+
else:
105+
error_json = r.json().get('error', {})
106+
raise AirtableError(
107+
error_type=error_json.get('type', str(r.status_code)),
108+
message=error_json.get('message', json.dumps(r.json())))
109+
110+
def get(
111+
self, table_name, record_id=None, limit=0, offset=None,
112+
filter_by_formula=None, view=None, max_records=0, fields=[]):
113+
params = {}
114+
if check_string(record_id):
115+
url = posixpath.join(table_name, record_id)
116+
else:
117+
url = table_name
118+
if limit and check_integer(limit):
119+
params.update({'pageSize': limit})
120+
if offset and check_string(offset):
121+
params.update({'offset': offset})
122+
if filter_by_formula is not None:
123+
params.update({'filterByFormula': filter_by_formula})
124+
if view is not None:
125+
params.update({'view': view})
126+
if max_records and check_integer(max_records):
127+
params.update({'maxRecords': max_records})
128+
if fields and type(fields) is list:
129+
for field in fields:
130+
check_string(field)
131+
params.update({'fields': fields})
132+
133+
return self.__request('GET', url, params)
134+
135+
def iterate(
136+
self, table_name, batch_size=0, filter_by_formula=None,
137+
view=None, max_records=0, fields=[]):
138+
"""Iterate over all records of a table.
139+
140+
Args:
141+
table_name: the name of the table to list.
142+
batch_size: the number of records to fetch per request. The default
143+
(0) is using the default of the API which is (as of 2016-09)
144+
100. Note that the API does not allow more than that (but
145+
allow for less).
146+
filter_by_formula: a formula used to filter records. The formula
147+
will be evaluated for each record, and if the result is not 0,
148+
false, "", NaN, [], or #Error! the record will be included in
149+
the response. If combined with view, only records in that view
150+
which satisfy the formula will be returned.
151+
view: the name or ID of a view in the table. If set, only the
152+
records in that view will be returned. The records will be
153+
sorted according to the order of the view.
154+
Yields:
155+
A dict for each record containing at least three fields: "id",
156+
"createdTime" and "fields".
157+
"""
158+
offset = None
159+
while True:
160+
response = self.get(
161+
table_name, limit=batch_size, offset=offset, max_records=max_records,
162+
fields=fields, filter_by_formula=filter_by_formula, view=view)
163+
for record in response.pop('records'):
164+
yield record
165+
if 'offset' in response:
166+
offset = response['offset']
167+
else:
168+
break
169+
170+
def create(self, table_name, data):
171+
if check_string(table_name):
172+
payload = create_payload(data)
173+
return self.__request('POST', table_name,
174+
payload=json.dumps(payload))
175+
176+
def update(self, table_name, record_id, data):
177+
if check_string(table_name) and check_string(record_id):
178+
url = posixpath.join(table_name, record_id)
179+
payload = create_payload(data)
180+
return self.__request('PATCH', url,
181+
payload=json.dumps(payload))
182+
183+
def update_all(self, table_name, record_id, data):
184+
if check_string(table_name) and check_string(record_id):
185+
url = posixpath.join(table_name, record_id)
186+
payload = create_payload(data)
187+
return self.__request('PUT', url,
188+
payload=json.dumps(payload))
189+
190+
def delete(self, table_name, record_id):
191+
if check_string(table_name) and check_string(record_id):
192+
url = posixpath.join(table_name, record_id)
193+
return self.__request('DELETE', url)
194+
195+
def table(self, table_name):
196+
return Table(self, table_name)
197+
198+
199+
class Table(Generic[_T]):
200+
def __init__(self, base_id, table_name, api_key=None, dict_class=OrderedDict):
201+
"""Create a client to connect to an Airtable Table.
202+
203+
Args:
204+
- base_id: The ID of the base, e.g. "appA0CDAE34F"
205+
- table_name: The name or ID of the table, e.g. "tbl123adfe4"
206+
- api_key: The API secret key, e.g. "keyBAAE123C"
207+
- dict_class: the class to use to build dictionaries for returning
208+
fields. By default the fields are kept in the order they were
209+
returned by the API using an OrderedDict, but you can switch
210+
to a simple dict if you prefer.
211+
"""
212+
self.table_name = table_name
213+
if isinstance(base_id, Airtable):
214+
self._client = base_id
215+
return
216+
self._client = Airtable(base_id, api_key, dict_class=dict_class)
217+
218+
def get(
219+
self, record_id=None, limit=0, offset=None,
220+
filter_by_formula=None, view=None, max_records=0, fields=[]):
221+
return self._client.get(
222+
self.table_name, record_id, limit, offset, filter_by_formula, view, max_records, fields)
223+
224+
def iterate(
225+
self, batch_size=0, filter_by_formula=None,
226+
view=None, max_records=0, fields=[]):
227+
return self._client.iterate(
228+
self.table_name, batch_size, filter_by_formula, view, max_records, fields)
229+
230+
def create(self, data):
231+
return self._client.create(self.table_name, data)
232+
233+
def update(self, record_id, data):
234+
return self._client.update(self.table_name, record_id, data)
235+
236+
def update_all(self, record_id, data):
237+
return self._client.update_all(self.table_name, record_id, data)
238+
239+
def delete(self, record_id):
240+
return self._client.delete(self.table_name, record_id)
File renamed without changes.

‎airtable/airtable.py

-240
This file was deleted.

0 commit comments

Comments
 (0)
Please sign in to comment.