seatable-api 2.8.1__tar.gz → 2.8.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {seatable_api-2.8.1 → seatable-api-2.8.2}/PKG-INFO +3 -5
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/convert_airtable.py +225 -59
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api.egg-info/PKG-INFO +3 -5
- {seatable_api-2.8.1 → seatable-api-2.8.2}/setup.py +1 -1
- {seatable_api-2.8.1 → seatable-api-2.8.2}/LICENSE +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/README.md +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/__init__.py +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/api_gateway.py +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/column.py +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/constants.py +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/context.py +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/date_utils.py +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/exception.py +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/main.py +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/message.py +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/query.py +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/socket_io.py +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/utils.py +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api.egg-info/SOURCES.txt +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api.egg-info/dependency_links.txt +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api.egg-info/requires.txt +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api.egg-info/top_level.txt +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/setup.cfg +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/tests/__init__.py +0 -0
- {seatable_api-2.8.1 → seatable-api-2.8.2}/tests/dateutils_test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: seatable-api
|
|
3
|
-
Version: 2.8.
|
|
3
|
+
Version: 2.8.2
|
|
4
4
|
Summary: Python client for SeaTable web api
|
|
5
5
|
Home-page: https://github.com/seatable/seatable-api-python
|
|
6
6
|
Author: seatable
|
|
@@ -10,10 +10,6 @@ Platform: any
|
|
|
10
10
|
Classifier: Programming Language :: Python
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Requires-Dist: requests
|
|
14
|
-
Requires-Dist: python-socketio<5
|
|
15
|
-
Requires-Dist: ply
|
|
16
|
-
Requires-Dist: python_dateutil
|
|
17
13
|
|
|
18
14
|
# seatable-api-python
|
|
19
15
|
|
|
@@ -26,3 +22,5 @@ Chinese document: <https://seatable.github.io/seatable-scripts-cn/>
|
|
|
26
22
|
pypi: <https://pypi.org/project/seatable-api/>
|
|
27
23
|
|
|
28
24
|
github: <https://github.com/seatable/seatable-api-python>
|
|
25
|
+
|
|
26
|
+
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
1
3
|
import re
|
|
4
|
+
import sys
|
|
2
5
|
import time
|
|
3
6
|
import random
|
|
4
7
|
import requests
|
|
8
|
+
import urllib
|
|
5
9
|
from datetime import datetime
|
|
6
10
|
|
|
7
11
|
from .constants import ColumnTypes
|
|
@@ -23,6 +27,8 @@ ColumnTypes.BARCODE = 'barcode'
|
|
|
23
27
|
FILE = 'file'
|
|
24
28
|
IMAGE = 'image'
|
|
25
29
|
|
|
30
|
+
logging.basicConfig(format='[%(asctime)s] [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S', stream=sys.stdout, level=logging.INFO)
|
|
31
|
+
logger = logging.getLogger()
|
|
26
32
|
|
|
27
33
|
class LinksConvertor(object):
|
|
28
34
|
|
|
@@ -42,7 +48,7 @@ class LinksConvertor(object):
|
|
|
42
48
|
row_id_list.append(row_id)
|
|
43
49
|
other_rows_ids_map[row_id] = link
|
|
44
50
|
except Exception as e:
|
|
45
|
-
|
|
51
|
+
logger.exception('Error during link generation')
|
|
46
52
|
links = {
|
|
47
53
|
'link_id': link_data['link_id'],
|
|
48
54
|
'table_id': link_data['table_id'],
|
|
@@ -75,7 +81,7 @@ class FilesConvertor(object):
|
|
|
75
81
|
name=name, content=content, file_type=file_type)
|
|
76
82
|
return file_info
|
|
77
83
|
except Exception as e:
|
|
78
|
-
|
|
84
|
+
logger.exception('Could not upload file')
|
|
79
85
|
return None
|
|
80
86
|
|
|
81
87
|
def batch_upload_files(self, value):
|
|
@@ -215,7 +221,7 @@ class RowsConvertor(object):
|
|
|
215
221
|
else: # DEFAULT
|
|
216
222
|
cell_data = str(value)
|
|
217
223
|
except Exception as e:
|
|
218
|
-
|
|
224
|
+
logger.exception('Could not generate cell data')
|
|
219
225
|
cell_data = str(value)
|
|
220
226
|
return cell_data
|
|
221
227
|
|
|
@@ -226,6 +232,7 @@ class RowsConvertor(object):
|
|
|
226
232
|
for column in columns:
|
|
227
233
|
column_name = column['name']
|
|
228
234
|
column_type = ColumnTypes(column['type'])
|
|
235
|
+
|
|
229
236
|
value = row.get(column_name)
|
|
230
237
|
if value is None:
|
|
231
238
|
continue
|
|
@@ -446,7 +453,8 @@ class AirtableAPI(object):
|
|
|
446
453
|
|
|
447
454
|
def list_rows(self, table_name, offset=''):
|
|
448
455
|
headers = {'Authorization': 'Bearer ' + self.airtable_api_key}
|
|
449
|
-
|
|
456
|
+
# Table names must be encoded since they may contain slashes or other special characters
|
|
457
|
+
url = AIRTABLE_API_URL + self.airtable_base_id + '/' + urllib.parse.quote(table_name, safe='')
|
|
450
458
|
if offset:
|
|
451
459
|
url = url + '?offset=' + offset
|
|
452
460
|
response = requests.get(url, headers=headers, timeout=60)
|
|
@@ -468,29 +476,43 @@ class AirtableAPI(object):
|
|
|
468
476
|
while True:
|
|
469
477
|
rows, offset = self.list_rows(table_name, offset)
|
|
470
478
|
all_rows.extend(rows)
|
|
471
|
-
|
|
472
|
-
'[Info] Got [ %s ] rows in Airtable <%s>' % (len(all_rows), table_name))
|
|
479
|
+
logger.info('Retrieved %d rows from table "%s"', len(all_rows), table_name)
|
|
473
480
|
if not offset:
|
|
474
481
|
break
|
|
475
|
-
time.sleep(0.5)
|
|
482
|
+
# time.sleep(0.5)
|
|
476
483
|
return all_rows
|
|
477
484
|
|
|
485
|
+
def get_schema(self):
|
|
486
|
+
url = f'{AIRTABLE_API_URL}meta/bases/{self.airtable_base_id}/tables'
|
|
487
|
+
headers = {'Authorization': 'Bearer ' + self.airtable_api_key}
|
|
488
|
+
|
|
489
|
+
response = requests.get(url, headers=headers, timeout=60)
|
|
490
|
+
if response.status_code >= 400:
|
|
491
|
+
raise ConnectionError(response.status_code, response.text)
|
|
492
|
+
|
|
493
|
+
return response.json().get('tables', [])
|
|
494
|
+
|
|
478
495
|
|
|
479
496
|
class AirtableConvertor(object):
|
|
480
497
|
|
|
481
|
-
def __init__(self, airtable_api_key, airtable_base_id, base, table_names, first_columns=[], links=[]):
|
|
498
|
+
def __init__(self, airtable_api_key, airtable_base_id, base, table_names, first_columns=[], links=[], excluded_column_types=[], excluded_columns=[]):
|
|
482
499
|
"""
|
|
483
500
|
airtable_api_key: str
|
|
484
501
|
airtable_base_id: str
|
|
485
502
|
base: SeaTable Base
|
|
486
503
|
table_names: list[str], eg: ['table_name1', 'table_name2']
|
|
487
504
|
links: list[tuple], eg: [('table_name', 'column_name', 'other_table_name')]
|
|
505
|
+
excluded_column_types: list[ColumnTypes], e.g. [ColumnTypes.FORMULA, ColumnTypes.LINK_FORMULA]
|
|
506
|
+
excluded_columns: list[tuple[str, str]], e.g. [('Table1', 'Column1'), ('Table2', 'Column5')]
|
|
488
507
|
"""
|
|
489
508
|
self.airtable_api = AirtableAPI(airtable_api_key, airtable_base_id)
|
|
490
509
|
self.base = base
|
|
491
510
|
self.table_names = table_names
|
|
492
511
|
self.first_columns = first_columns
|
|
493
512
|
self.links = links
|
|
513
|
+
self.excluded_column_types = excluded_column_types
|
|
514
|
+
self.excluded_columns = excluded_columns
|
|
515
|
+
self.manually_migrated_columns = []
|
|
494
516
|
self.columns_parser = ColumnsParser()
|
|
495
517
|
self.files_convertor = FilesConvertor(airtable_api_key, base)
|
|
496
518
|
self.rows_convertor = RowsConvertor(self.files_convertor)
|
|
@@ -500,8 +522,13 @@ class AirtableConvertor(object):
|
|
|
500
522
|
|
|
501
523
|
def convert_metadata(self):
|
|
502
524
|
self.get_airtable_row_map(is_demo=True)
|
|
503
|
-
|
|
525
|
+
|
|
526
|
+
schema = self.airtable_api.get_schema()
|
|
527
|
+
self.parse_airtable_schema(schema)
|
|
528
|
+
|
|
504
529
|
self.convert_tables()
|
|
530
|
+
self.add_helper_table()
|
|
531
|
+
|
|
505
532
|
self.convert_columns()
|
|
506
533
|
self.convert_rows(is_demo=True)
|
|
507
534
|
self.convert_links(is_demo=True)
|
|
@@ -509,12 +536,144 @@ class AirtableConvertor(object):
|
|
|
509
536
|
def convert_data(self):
|
|
510
537
|
self.delete_demo_rows()
|
|
511
538
|
self.get_airtable_row_map()
|
|
512
|
-
self.convert_select_columns()
|
|
513
539
|
self.convert_rows()
|
|
514
540
|
self.convert_links()
|
|
515
541
|
|
|
542
|
+
def parse_airtable_schema(self, schema):
|
|
543
|
+
self.airtable_column_map = {}
|
|
544
|
+
|
|
545
|
+
# AirTable -> SeaTable
|
|
546
|
+
COLUMN_MAPPING = {
|
|
547
|
+
# From https://airtable.com/developers/web/api/model/field-type
|
|
548
|
+
# Note: Commented out column types are not supported and must be manually created
|
|
549
|
+
"singleLineText": ColumnTypes.TEXT,
|
|
550
|
+
"email": ColumnTypes.EMAIL,
|
|
551
|
+
"url": ColumnTypes.URL,
|
|
552
|
+
"multilineText": ColumnTypes.LONG_TEXT,
|
|
553
|
+
"number": ColumnTypes.NUMBER,
|
|
554
|
+
"percent": ColumnTypes.NUMBER,
|
|
555
|
+
"currency": ColumnTypes.NUMBER,
|
|
556
|
+
"singleSelect": ColumnTypes.SINGLE_SELECT,
|
|
557
|
+
"multipleSelects": ColumnTypes.MULTIPLE_SELECT,
|
|
558
|
+
"singleCollaborator": ColumnTypes.TEXT,
|
|
559
|
+
"multipleCollaborators": ColumnTypes.TEXT,
|
|
560
|
+
"multipleRecordLinks": ColumnTypes.LINK,
|
|
561
|
+
"date": ColumnTypes.DATE,
|
|
562
|
+
"dateTime": ColumnTypes.DATE,
|
|
563
|
+
# SeaTable does not support phone number columns
|
|
564
|
+
"phoneNumber": ColumnTypes.TEXT,
|
|
565
|
+
"multipleAttachments": ColumnTypes.FILE,
|
|
566
|
+
"checkbox": ColumnTypes.CHECKBOX,
|
|
567
|
+
"formula": ColumnTypes.FORMULA,
|
|
568
|
+
"createdTime": ColumnTypes.DATE,
|
|
569
|
+
#"rollup": '',
|
|
570
|
+
#"count": '',
|
|
571
|
+
#"lookup": '',
|
|
572
|
+
#"multipleLookupValues": '',
|
|
573
|
+
"autoNumber": ColumnTypes.TEXT,
|
|
574
|
+
# SeaTable does not support barcode columns
|
|
575
|
+
"barcode": ColumnTypes.TEXT,
|
|
576
|
+
"rating": ColumnTypes.RATE,
|
|
577
|
+
"richText": ColumnTypes.LONG_TEXT,
|
|
578
|
+
"duration": ColumnTypes.DURATION,
|
|
579
|
+
"lastModifiedTime": ColumnTypes.DATE,
|
|
580
|
+
# "button": ColumnTypes.BUTTON,
|
|
581
|
+
"createdBy": ColumnTypes.TEXT,
|
|
582
|
+
"lastModifiedBy": ColumnTypes.TEXT,
|
|
583
|
+
#"externalSyncSource": '',
|
|
584
|
+
#"aiText": '',
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
for table in schema:
|
|
588
|
+
columns = []
|
|
589
|
+
|
|
590
|
+
for field in table['fields']:
|
|
591
|
+
column_name = field['name']
|
|
592
|
+
column_type = field['type']
|
|
593
|
+
|
|
594
|
+
# TODO: Check if this is necessary
|
|
595
|
+
if column_name == '_id':
|
|
596
|
+
continue
|
|
597
|
+
|
|
598
|
+
seatable_column_type = COLUMN_MAPPING.get(column_type)
|
|
599
|
+
|
|
600
|
+
if seatable_column_type is None:
|
|
601
|
+
logger.warning('Column "%s" (table "%s") is of type "%s"; column must be manually added', column_name, table['name'], column_type)
|
|
602
|
+
self.manually_migrated_columns.append({'Column': column_name, 'Table': table['name'], 'Type': column_type, 'Metadata': json.dumps(field.get('options', ''))})
|
|
603
|
+
# TODO: Remove continue statement
|
|
604
|
+
continue
|
|
605
|
+
|
|
606
|
+
# Handle special cases
|
|
607
|
+
if seatable_column_type == ColumnTypes.DATE:
|
|
608
|
+
column_data = {'format': 'YYYY-MM-DD HH:mm'}
|
|
609
|
+
elif seatable_column_type == ColumnTypes.NUMBER:
|
|
610
|
+
if column_type == 'number':
|
|
611
|
+
column_data = {'format': 'number', 'decimal': 'dot', 'thousands': 'no'}
|
|
612
|
+
elif column_type == 'percent':
|
|
613
|
+
column_data = {'format': 'percent', 'decimal': 'dot', 'thousands': 'no'}
|
|
614
|
+
elif column_type == 'currency':
|
|
615
|
+
column_data = {'format': 'dollar', 'decimal': 'dot', 'thousands': 'no'}
|
|
616
|
+
else:
|
|
617
|
+
column_data = {}
|
|
618
|
+
elif seatable_column_type == ColumnTypes.LINK:
|
|
619
|
+
other_table_name = self.link_map.get(table['name'], {}).get(column_name)
|
|
620
|
+
|
|
621
|
+
if other_table_name is None:
|
|
622
|
+
logger.warning('Column "%s" (table "%s") was not found in link map', column_name, table['name'])
|
|
623
|
+
continue
|
|
624
|
+
|
|
625
|
+
column_data = {'other_table': other_table_name}
|
|
626
|
+
elif seatable_column_type in [ColumnTypes.SINGLE_SELECT, ColumnTypes.MULTIPLE_SELECT]:
|
|
627
|
+
column_data = {
|
|
628
|
+
'options': self.get_select_options(field['options']['choices']),
|
|
629
|
+
}
|
|
630
|
+
elif seatable_column_type == ColumnTypes.DURATION:
|
|
631
|
+
column_data = {
|
|
632
|
+
'format': 'duration',
|
|
633
|
+
# TODO: Read actual format from AirTable schema
|
|
634
|
+
'duration_format': 'h:mm:ss',
|
|
635
|
+
}
|
|
636
|
+
elif seatable_column_type == ColumnTypes.FORMULA:
|
|
637
|
+
column_data = {'formula': '"Formula to be defined"'}
|
|
638
|
+
elif seatable_column_type == ColumnTypes.RATE:
|
|
639
|
+
column_data = {'rate_max_number': field['options']['max']}
|
|
640
|
+
else:
|
|
641
|
+
column_data = {}
|
|
642
|
+
|
|
643
|
+
column = {
|
|
644
|
+
'name': column_name,
|
|
645
|
+
'type': seatable_column_type,
|
|
646
|
+
'data': column_data,
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
columns.append(column)
|
|
650
|
+
|
|
651
|
+
self.airtable_column_map[table['name']] = columns
|
|
652
|
+
|
|
653
|
+
def get_select_options(self, options):
|
|
654
|
+
return [{
|
|
655
|
+
'name': value['name'],
|
|
656
|
+
'id': self.random_num_id(),
|
|
657
|
+
'color': self.random_color(),
|
|
658
|
+
'textColor': TEXT_COLOR,
|
|
659
|
+
} for value in options]
|
|
660
|
+
|
|
661
|
+
def random_num_id(self):
|
|
662
|
+
num_str = '0123456789'
|
|
663
|
+
num = ''
|
|
664
|
+
for i in range(6):
|
|
665
|
+
num += random.choice(num_str)
|
|
666
|
+
return num
|
|
667
|
+
|
|
668
|
+
def random_color(self):
|
|
669
|
+
color_str = '0123456789ABCDEF'
|
|
670
|
+
color = '#'
|
|
671
|
+
for i in range(6):
|
|
672
|
+
color += random.choice(color_str)
|
|
673
|
+
return color
|
|
674
|
+
|
|
516
675
|
def convert_tables(self):
|
|
517
|
-
|
|
676
|
+
logger.info('Start adding tables and columns in SeaTable base')
|
|
518
677
|
self.get_table_map()
|
|
519
678
|
for table_name in self.table_names:
|
|
520
679
|
table = self.table_map.get(table_name)
|
|
@@ -525,6 +684,8 @@ class AirtableConvertor(object):
|
|
|
525
684
|
columns = []
|
|
526
685
|
for column in airtable_columns:
|
|
527
686
|
if column['type'] == ColumnTypes.LINK:
|
|
687
|
+
# Skip link columns for now
|
|
688
|
+
# They will be inserted after all the other columns (in convert_columns())
|
|
528
689
|
continue
|
|
529
690
|
item = {
|
|
530
691
|
'column_name': column['name'],
|
|
@@ -539,13 +700,29 @@ class AirtableConvertor(object):
|
|
|
539
700
|
else:
|
|
540
701
|
columns.append(item)
|
|
541
702
|
self.add_table(table_name, columns)
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
print('[Info] Success\n')
|
|
703
|
+
logger.info('Added table "%s" with %d columns', table_name, len(columns))
|
|
704
|
+
logger.info('Tables and columns added in SeaTable base')
|
|
545
705
|
time.sleep(1)
|
|
546
706
|
|
|
707
|
+
def add_helper_table(self):
|
|
708
|
+
table_name = 'Columns to be migrated manually'
|
|
709
|
+
|
|
710
|
+
# Add column which contains information which columns need to be manually migrated
|
|
711
|
+
columns = [
|
|
712
|
+
{'column_name': 'Column', 'column_type': ColumnTypes.TEXT.value},
|
|
713
|
+
{'column_name': 'Table', 'column_type': ColumnTypes.TEXT.value},
|
|
714
|
+
{'column_name': 'Type', 'column_type': ColumnTypes.TEXT.value},
|
|
715
|
+
{'column_name': 'Metadata', 'column_type': ColumnTypes.LONG_TEXT.value},
|
|
716
|
+
{'column_name': 'Completed', 'column_type': ColumnTypes.CHECKBOX.value},
|
|
717
|
+
]
|
|
718
|
+
|
|
719
|
+
self.add_table(table_name, columns)
|
|
720
|
+
logger.info('Table "%s" added', table_name)
|
|
721
|
+
|
|
722
|
+
self.batch_append_rows(table_name, self.manually_migrated_columns)
|
|
723
|
+
|
|
547
724
|
def convert_columns(self):
|
|
548
|
-
|
|
725
|
+
logger.info('Start adding link columns in SeaTable base')
|
|
549
726
|
self.get_table_map()
|
|
550
727
|
for table_name in self.table_names:
|
|
551
728
|
airtable_columns = self.airtable_column_map[table_name]
|
|
@@ -556,28 +733,37 @@ class AirtableConvertor(object):
|
|
|
556
733
|
if not exists_column:
|
|
557
734
|
self.add_column(
|
|
558
735
|
table_name, column_name, column['type'], column['data'])
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
print('[Info] Success\n')
|
|
736
|
+
logger.info('Added column "%s" to table "%s"', column['name'], table_name)
|
|
737
|
+
logger.info('Link columns added in SeaTable base')
|
|
562
738
|
time.sleep(1)
|
|
563
739
|
|
|
564
740
|
def convert_rows(self, is_demo=False):
|
|
565
|
-
|
|
741
|
+
logger.info('Start appending rows in SeaTable base')
|
|
566
742
|
self.get_table_map()
|
|
567
743
|
for table_name in self.table_names:
|
|
568
744
|
airtable_rows = self.airtable_row_map[table_name]
|
|
569
745
|
if is_demo:
|
|
570
746
|
airtable_rows = airtable_rows[:10]
|
|
571
747
|
columns = self.column_map[table_name]
|
|
748
|
+
|
|
749
|
+
# Remove excluded column types
|
|
750
|
+
columns = [c for c in columns if ColumnTypes(c['type']) not in self.excluded_column_types]
|
|
751
|
+
|
|
752
|
+
# Remove excluded columns
|
|
753
|
+
columns = [
|
|
754
|
+
column for column in columns
|
|
755
|
+
if (table_name, column['name']) not in self.excluded_columns
|
|
756
|
+
]
|
|
757
|
+
|
|
572
758
|
rows = self.rows_convertor.convert(columns, airtable_rows)
|
|
573
759
|
self.batch_append_rows(table_name, rows)
|
|
574
|
-
|
|
760
|
+
logger.info('Rows appended in SeaTable base')
|
|
575
761
|
time.sleep(1)
|
|
576
762
|
|
|
577
763
|
def convert_links(self, is_demo=False):
|
|
578
764
|
if not self.link_map:
|
|
579
765
|
return
|
|
580
|
-
|
|
766
|
+
logger.info('Start adding links between records in SeaTable base')
|
|
581
767
|
self.get_table_map()
|
|
582
768
|
for table_name, column_names in self.link_map.items():
|
|
583
769
|
table = self.table_map[table_name]
|
|
@@ -589,34 +775,17 @@ class AirtableConvertor(object):
|
|
|
589
775
|
links = self.links_convertor.convert(
|
|
590
776
|
column_name, link_data, airtable_rows)
|
|
591
777
|
self.batch_append_links(table_name, links)
|
|
592
|
-
|
|
778
|
+
logger.info('Links added between records in SeaTable base')
|
|
593
779
|
time.sleep(1)
|
|
594
780
|
|
|
595
781
|
def delete_demo_rows(self):
|
|
596
|
-
|
|
782
|
+
logger.info('Start deleting demo rows')
|
|
597
783
|
for table_name in self.table_names:
|
|
598
784
|
rows = self.list_rows(table_name)
|
|
599
785
|
if rows:
|
|
600
786
|
row_ids = [row['_id'] for row in rows]
|
|
601
787
|
self.batch_delete_rows(table_name, row_ids)
|
|
602
|
-
|
|
603
|
-
time.sleep(1)
|
|
604
|
-
|
|
605
|
-
def convert_select_columns(self):
|
|
606
|
-
print('[Info] Convert select columns')
|
|
607
|
-
self.get_table_map()
|
|
608
|
-
for table_name in self.table_names:
|
|
609
|
-
columns = self.table_map[table_name]
|
|
610
|
-
airtable_rows = self.airtable_row_map[table_name]
|
|
611
|
-
select_columns = self.columns_parser.parse_select(
|
|
612
|
-
columns, airtable_rows)
|
|
613
|
-
for column in select_columns:
|
|
614
|
-
column_name = column['name']
|
|
615
|
-
options = column['options']
|
|
616
|
-
self.add_column_options(table_name, column_name, options)
|
|
617
|
-
print(
|
|
618
|
-
'[Info] Added options to column [ %s ] in table <%s>' % (column_name, table_name))
|
|
619
|
-
print('[Info] Success\n')
|
|
788
|
+
logger.info('Demo rows deleted from SeaTable base')
|
|
620
789
|
time.sleep(1)
|
|
621
790
|
|
|
622
791
|
def get_first_column_map(self):
|
|
@@ -639,7 +808,11 @@ class AirtableConvertor(object):
|
|
|
639
808
|
return self.link_map
|
|
640
809
|
|
|
641
810
|
def get_airtable_row_map(self, is_demo=False):
|
|
642
|
-
|
|
811
|
+
if is_demo:
|
|
812
|
+
logger.info('Start retrieving demo data from Airtable')
|
|
813
|
+
else:
|
|
814
|
+
logger.info('Start retrieving data from Airtable')
|
|
815
|
+
|
|
643
816
|
self.airtable_row_map = {}
|
|
644
817
|
for table_name in self.table_names:
|
|
645
818
|
if is_demo:
|
|
@@ -647,17 +820,13 @@ class AirtableConvertor(object):
|
|
|
647
820
|
else:
|
|
648
821
|
rows = self.airtable_api.list_all_rows(table_name)
|
|
649
822
|
self.airtable_row_map[table_name] = rows
|
|
650
|
-
print('[Info] Success\n')
|
|
651
|
-
return self.airtable_row_map
|
|
652
823
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
self.airtable_column_map[table_name] = columns
|
|
660
|
-
return self.airtable_column_map
|
|
824
|
+
if is_demo:
|
|
825
|
+
logger.info('Demo data retrieved from Airtable')
|
|
826
|
+
else:
|
|
827
|
+
logger.info('Data retrieved from Airtable')
|
|
828
|
+
|
|
829
|
+
return self.airtable_row_map
|
|
661
830
|
|
|
662
831
|
def get_table_map(self):
|
|
663
832
|
self.table_map = {}
|
|
@@ -715,8 +884,7 @@ class AirtableConvertor(object):
|
|
|
715
884
|
if not row_split:
|
|
716
885
|
break
|
|
717
886
|
self.base.batch_append_rows(table_name, row_split)
|
|
718
|
-
|
|
719
|
-
'[Info] Appended [ %s ] rows to table <%s>' % (len(row_split), table_name))
|
|
887
|
+
logger.info('Appended %d rows to table "%s"', len(row_split), table_name)
|
|
720
888
|
time.sleep(0.5)
|
|
721
889
|
|
|
722
890
|
def batch_delete_rows(self, table_name, row_ids):
|
|
@@ -727,8 +895,7 @@ class AirtableConvertor(object):
|
|
|
727
895
|
if not row_id_split:
|
|
728
896
|
break
|
|
729
897
|
self.base.batch_delete_rows(table_name, row_id_split)
|
|
730
|
-
|
|
731
|
-
'[Info] Deleted [ %s ] rows in table <%s>' % (len(row_id_split), table_name))
|
|
898
|
+
logger.info('Deleted %d rows from table "%s"', len(row_id_split), table_name)
|
|
732
899
|
time.sleep(0.5)
|
|
733
900
|
|
|
734
901
|
def batch_append_links(self, table_name, links):
|
|
@@ -747,6 +914,5 @@ class AirtableConvertor(object):
|
|
|
747
914
|
row_id: other_rows_ids_map[row_id] for row_id in row_id_split}
|
|
748
915
|
self.base.batch_update_links(
|
|
749
916
|
link_id, table_id, other_table_id, row_id_split, other_rows_ids_map_split)
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
time.sleep(0.5)
|
|
917
|
+
logger.info('Added %d links to table "%s"', len(row_id_split), table_name)
|
|
918
|
+
# time.sleep(0.5)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: seatable-api
|
|
3
|
-
Version: 2.8.
|
|
3
|
+
Version: 2.8.2
|
|
4
4
|
Summary: Python client for SeaTable web api
|
|
5
5
|
Home-page: https://github.com/seatable/seatable-api-python
|
|
6
6
|
Author: seatable
|
|
@@ -10,10 +10,6 @@ Platform: any
|
|
|
10
10
|
Classifier: Programming Language :: Python
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Requires-Dist: requests
|
|
14
|
-
Requires-Dist: python-socketio<5
|
|
15
|
-
Requires-Dist: ply
|
|
16
|
-
Requires-Dist: python_dateutil
|
|
17
13
|
|
|
18
14
|
# seatable-api-python
|
|
19
15
|
|
|
@@ -26,3 +22,5 @@ Chinese document: <https://seatable.github.io/seatable-scripts-cn/>
|
|
|
26
22
|
pypi: <https://pypi.org/project/seatable-api/>
|
|
27
23
|
|
|
28
24
|
github: <https://github.com/seatable/seatable-api-python>
|
|
25
|
+
|
|
26
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|