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.
Files changed (25) hide show
  1. {seatable_api-2.8.1 → seatable-api-2.8.2}/PKG-INFO +3 -5
  2. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/convert_airtable.py +225 -59
  3. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api.egg-info/PKG-INFO +3 -5
  4. {seatable_api-2.8.1 → seatable-api-2.8.2}/setup.py +1 -1
  5. {seatable_api-2.8.1 → seatable-api-2.8.2}/LICENSE +0 -0
  6. {seatable_api-2.8.1 → seatable-api-2.8.2}/README.md +0 -0
  7. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/__init__.py +0 -0
  8. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/api_gateway.py +0 -0
  9. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/column.py +0 -0
  10. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/constants.py +0 -0
  11. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/context.py +0 -0
  12. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/date_utils.py +0 -0
  13. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/exception.py +0 -0
  14. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/main.py +0 -0
  15. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/message.py +0 -0
  16. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/query.py +0 -0
  17. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/socket_io.py +0 -0
  18. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api/utils.py +0 -0
  19. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api.egg-info/SOURCES.txt +0 -0
  20. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api.egg-info/dependency_links.txt +0 -0
  21. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api.egg-info/requires.txt +0 -0
  22. {seatable_api-2.8.1 → seatable-api-2.8.2}/seatable_api.egg-info/top_level.txt +0 -0
  23. {seatable_api-2.8.1 → seatable-api-2.8.2}/setup.cfg +0 -0
  24. {seatable_api-2.8.1 → seatable-api-2.8.2}/tests/__init__.py +0 -0
  25. {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.1
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
- print('[Warning] gen links error:', e)
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
- print('[Warning] upload file error:', e)
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
- print('[Warning] gen cell data error:', e)
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
- url = AIRTABLE_API_URL + self.airtable_base_id + '/' + table_name
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
- print(
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
- self.get_airtable_column_map()
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
- print('[Info] Convert tables')
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
- print(
543
- '[Info] Added table [ %s ] with %s columns' % (table_name, len(columns)))
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
- print('[Info] Convert columns')
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
- print(
560
- '[Info] Added column [ %s ] to table <%s>' % (column['name'], table_name))
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
- print('[Info] Convert %s rows' % ('demo' if is_demo else ''))
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
- print('[Info] Success\n')
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
- print('[Info] Convert %s links' % ('demo' if is_demo else ''))
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
- print('[Info] Success\n')
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
- print('[Info] Delete demo rows')
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
- print('[Info] Success\n')
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
- print('[Info] List Airtable %s rows' % ('demo' if is_demo else ''))
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
- def get_airtable_column_map(self):
654
- self.airtable_column_map = {}
655
- for table_name in self.table_names:
656
- airtable_rows = self.airtable_row_map[table_name]
657
- columns = self.columns_parser.parse(
658
- self.link_map, table_name, airtable_rows)
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
- print(
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
- print(
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
- print(
751
- '[Info] Added [ %s ] Links to table <%s>' % (len(row_id_split), table_name))
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.1
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,6 +1,6 @@
1
1
  from setuptools import setup, find_packages
2
2
 
3
- __version__ = '2.8.1'
3
+ __version__ = '2.8.2'
4
4
 
5
5
  with open('README.md', 'r', encoding='utf-8') as fh:
6
6
  long_description = fh.read()
File without changes
File without changes
File without changes