keboola.component 1.6.0__tar.gz → 1.6.3__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 (18) hide show
  1. {keboola.component-1.6.0/src/keboola.component.egg-info → keboola.component-1.6.3}/PKG-INFO +1 -1
  2. {keboola.component-1.6.0 → keboola.component-1.6.3}/setup.py +1 -1
  3. {keboola.component-1.6.0 → keboola.component-1.6.3}/src/keboola/component/dao.py +212 -100
  4. {keboola.component-1.6.0 → keboola.component-1.6.3}/src/keboola/component/interface.py +33 -10
  5. {keboola.component-1.6.0 → keboola.component-1.6.3/src/keboola.component.egg-info}/PKG-INFO +1 -1
  6. {keboola.component-1.6.0 → keboola.component-1.6.3}/LICENSE +0 -0
  7. {keboola.component-1.6.0 → keboola.component-1.6.3}/README.md +0 -0
  8. {keboola.component-1.6.0 → keboola.component-1.6.3}/setup.cfg +0 -0
  9. {keboola.component-1.6.0 → keboola.component-1.6.3}/src/keboola/component/__init__.py +0 -0
  10. {keboola.component-1.6.0 → keboola.component-1.6.3}/src/keboola/component/base.py +0 -0
  11. {keboola.component-1.6.0 → keboola.component-1.6.3}/src/keboola/component/exceptions.py +0 -0
  12. {keboola.component-1.6.0 → keboola.component-1.6.3}/src/keboola/component/sync_actions.py +0 -0
  13. {keboola.component-1.6.0 → keboola.component-1.6.3}/src/keboola/component/table_schema.py +0 -0
  14. {keboola.component-1.6.0 → keboola.component-1.6.3}/src/keboola.component.egg-info/SOURCES.txt +0 -0
  15. {keboola.component-1.6.0 → keboola.component-1.6.3}/src/keboola.component.egg-info/dependency_links.txt +0 -0
  16. {keboola.component-1.6.0 → keboola.component-1.6.3}/src/keboola.component.egg-info/not-zip-safe +0 -0
  17. {keboola.component-1.6.0 → keboola.component-1.6.3}/src/keboola.component.egg-info/requires.txt +0 -0
  18. {keboola.component-1.6.0 → keboola.component-1.6.3}/src/keboola.component.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: keboola.component
3
- Version: 1.6.0
3
+ Version: 1.6.3
4
4
  Summary: General library for Python applications running in Keboola Connection environment
5
5
  Home-page: https://github.com/keboola/python-component
6
6
  Author: Keboola KDS Team
@@ -13,7 +13,7 @@ project_urls = {
13
13
 
14
14
  setuptools.setup(
15
15
  name="keboola.component",
16
- version="1.6.0",
16
+ version="1.6.3",
17
17
  author="Keboola KDS Team",
18
18
  project_urls=project_urls,
19
19
  setup_requires=['pytest-runner', 'flake8'],
@@ -1,14 +1,20 @@
1
+ # Python 3.7 support
2
+ from __future__ import annotations
1
3
  import dataclasses
2
4
  import json
3
5
  import logging
6
+ import warnings
4
7
  from abc import ABC, abstractmethod
8
+ from collections import OrderedDict
5
9
  from dataclasses import dataclass, field
6
10
  from datetime import datetime
7
11
  from enum import Enum
8
12
  from pathlib import Path
9
13
  from typing import List, Union, Dict, Optional, OrderedDict as TypeOrderedDict
10
- from collections import OrderedDict
14
+
15
+
11
16
  from deprecated import deprecated
17
+
12
18
  from .exceptions import UserException
13
19
 
14
20
  try:
@@ -218,6 +224,22 @@ class TableMetadata:
218
224
 
219
225
  Returns: dict
220
226
 
227
+ """
228
+
229
+ return self._get_legacy_column_metadata_for_manifest()
230
+
231
+ def _get_legacy_column_metadata_for_manifest(self) -> dict:
232
+ """
233
+ Returns column metadata dict as required by the
234
+ [manifest format](https://developers.keboola.com/extend/common-interface/manifest-files/#dataintables
235
+ -manifests)
236
+
237
+ e.g.
238
+ tm = TableMetadata()
239
+ manifest['column_metadata'] = tm.column_metadata
240
+
241
+ Returns: dict
242
+
221
243
  """
222
244
  final_column_metadata = dict()
223
245
 
@@ -420,9 +442,9 @@ class TableMetadata:
420
442
 
421
443
 
422
444
  @dataclass
423
- class DataType(dict):
445
+ class DataType:
424
446
  dtype: str
425
- length: Optional[int] = None
447
+ length: Optional[str] = None
426
448
  default: Optional[str] = None
427
449
 
428
450
  def __post_init__(self):
@@ -430,10 +452,41 @@ class DataType(dict):
430
452
  self.dtype = self.dtype.value
431
453
 
432
454
 
433
- class BaseType(DataType):
434
- def __init__(self, dtype: SupportedDataTypes = SupportedDataTypes.STRING, length: Optional[int] = None,
455
+ class BaseType(dict):
456
+ def __init__(self, dtype: SupportedDataTypes = SupportedDataTypes.STRING, length: Optional[str] = None,
435
457
  default: Optional[str] = None):
436
- super().__init__(dtype=dtype, length=length, default=default)
458
+ super().__init__(base=DataType(dtype=dtype, length=length, default=default))
459
+
460
+ @classmethod
461
+ def string(cls, length: Optional[str] = None, default: Optional[str] = None) -> 'BaseType':
462
+ return BaseType(dtype=SupportedDataTypes.STRING, length=length, default=default)
463
+
464
+ @classmethod
465
+ def integer(cls, length: Optional[str] = None,
466
+ default: Optional[str] = None) -> 'BaseType':
467
+ return BaseType(dtype=SupportedDataTypes.INTEGER, length=length, default=default)
468
+
469
+ @classmethod
470
+ def numeric(cls, length: Optional[str] = None,
471
+ default: Optional[str] = None) -> 'BaseType':
472
+ return BaseType(dtype=SupportedDataTypes.NUMERIC, length=length, default=default)
473
+
474
+ @classmethod
475
+ def float(cls, length: Optional[str] = None,
476
+ default: Optional[str] = None) -> 'BaseType':
477
+ return BaseType(dtype=SupportedDataTypes.FLOAT, length=length, default=default)
478
+
479
+ @classmethod
480
+ def boolean(cls, default: Optional[str] = None) -> 'BaseType':
481
+ return BaseType(dtype=SupportedDataTypes.BOOLEAN, default=default)
482
+
483
+ @classmethod
484
+ def date(cls, default: Optional[str] = None) -> 'BaseType':
485
+ return BaseType(dtype=SupportedDataTypes.DATE, default=default)
486
+
487
+ @classmethod
488
+ def timestamp(cls, default: Optional[str] = None) -> 'BaseType':
489
+ return BaseType(dtype=SupportedDataTypes.TIMESTAMP, default=default)
437
490
 
438
491
 
439
492
  @dataclass
@@ -457,15 +510,6 @@ class ColumnDefinition:
457
510
  description: Optional[str] = None
458
511
  metadata: Optional[Dict[str, str]] = None
459
512
 
460
- def __post_init__(self):
461
- if isinstance(self.data_types, DataType):
462
- self.data_types = {"base": self.data_types}
463
-
464
- def normalize_data_type(self, data_type: Union[Dict[str, DataType], BaseType]) -> Dict[str, DataType]:
465
- if isinstance(data_type, DataType):
466
- return {"base": data_type}
467
- return data_type
468
-
469
513
  def update_properties(self, **kwargs):
470
514
  for key, value in kwargs.items():
471
515
  if hasattr(self, key):
@@ -553,9 +597,6 @@ class IODefinition(ABC):
553
597
  def __init__(self, full_path):
554
598
  self.full_path = full_path
555
599
 
556
- # infer stage by default
557
- self.__stage = self.__get_stage_inferred()
558
-
559
600
  @classmethod
560
601
  def build_from_manifest(cls,
561
602
  manifest_file_path: str
@@ -575,13 +616,13 @@ class IODefinition(ABC):
575
616
  """
576
617
  Helper property marking the stage of the file. (str)
577
618
  """
578
- return self.__stage
619
+ return self._stage
579
620
 
580
621
  @stage.setter
581
622
  def stage(self, stage: str):
582
623
  if stage not in ['in', 'out']:
583
624
  raise ValueError(f'Invalid stage "{stage}", supported values are: "in", "out"')
584
- self.__stage = stage
625
+ self._stage = stage
585
626
 
586
627
  @property
587
628
  @abstractmethod
@@ -599,17 +640,6 @@ class IODefinition(ABC):
599
640
  """
600
641
  raise NotImplementedError
601
642
 
602
- def __get_stage_inferred(self):
603
- stage = 'in'
604
- if not self.full_path or not Path(self.full_path).exists():
605
- return stage
606
-
607
- if Path(self.full_path).parent.parent.name == 'in':
608
- stage = 'in'
609
- elif Path(self.full_path).parent.parent.name == 'out':
610
- stage = 'out'
611
- return stage
612
-
613
643
  # ############ Staging parameters
614
644
 
615
645
  @dataclass
@@ -740,29 +770,19 @@ class TableDefinition(IODefinition):
740
770
  def __init__(self, name: str,
741
771
  full_path: Optional[Union[str, None]] = None,
742
772
  is_sliced: Optional[bool] = False,
743
-
744
773
  destination: Optional[str] = '',
745
774
  primary_key: Optional[List[str]] = None,
746
- columns: Optional[List[str]] = None,
775
+ schema: Optional[Union[TypeOrderedDict[str, ColumnDefinition], list[str]]] = None,
747
776
  incremental: Optional[bool] = None,
748
777
  table_metadata: Optional[TableMetadata] = None,
749
778
  enclosure: Optional[str] = '"',
750
779
  delimiter: Optional[str] = ',',
751
780
  delete_where: Optional[dict] = None,
752
- stage: Optional[str] = 'in',
781
+ stage: Optional[str] = 'out',
753
782
  write_always: Optional[bool] = False,
754
- schema: Optional[TypeOrderedDict[str, ColumnDefinition]] = None,
755
- rows_count: Optional[int] = None,
756
- data_size_bytes: Optional[int] = None,
757
- is_alias: Optional[bool] = False,
758
783
  has_header: Optional[bool] = None,
759
-
760
784
  # input
761
- uri: Optional[str] = None,
762
- id: Optional[str] = '',
763
- created: Optional[str] = None,
764
- last_change_date: Optional[str] = None,
765
- last_import_date: Optional[str] = None
785
+ **kwargs
766
786
  ):
767
787
  """
768
788
 
@@ -776,9 +796,9 @@ class TableDefinition(IODefinition):
776
796
  The full_path is None when dealing with [workspaces](
777
797
  https://developers.keboola.com/extend/common-interface/folders/#exchanging-data-via-workspace)
778
798
  is_sliced: True if the full_path points to a folder with sliced tables
799
+ has_header: True if the file has a header, if emtpy inferred.
779
800
  destination: String name of the table in Storage.
780
801
  primary_key: List with names of columns used for primary key.
781
- columns: List of columns for headless CSV files
782
802
  incremental: Set to true to enable incremental loading
783
803
  table_metadata: <.dao.TableMetadata> object containing column and table metadata (deprecated)
784
804
  enclosure: str: CSV enclosure, by default "
@@ -787,19 +807,28 @@ class TableDefinition(IODefinition):
787
807
  stage: str: Storage Stage 'in' or 'out'
788
808
  write_always: Bool: If true, the table will be saved to Storage even when the job execution
789
809
  fails.
790
- schema: (dict) Mapping of column names andColumnDefinition objects
810
+ schema: (dict|lis[str]) Mapping of column names andColumnDefinition objects, or a list of names
791
811
 
792
812
  """
793
813
  super().__init__(full_path)
794
814
  self._name = name
795
815
  self.is_sliced = is_sliced
796
816
 
797
- self.schema = schema
798
-
799
817
  # initialize manifest properties
800
818
  self._destination = None
801
819
  self.destination = destination
802
- self.columns = columns
820
+ self._schema = dict()
821
+
822
+ if schema:
823
+ self.schema = schema
824
+ # deprecated argument for backward compatibility
825
+ self._legacy_mode = False
826
+ if kwargs.get('force_legacy_mode'):
827
+ self._legacy_mode = True
828
+ if kwargs.get('columns'):
829
+ self.columns = kwargs['columns']
830
+
831
+ self._legacy_primary_key = list()
803
832
  self.primary_key = primary_key
804
833
  self._incremental = incremental
805
834
 
@@ -815,20 +844,25 @@ class TableDefinition(IODefinition):
815
844
  self.delete_where_operator = None
816
845
 
817
846
  self.set_delete_where_from_dict(delete_where)
818
- self.stage = stage
819
847
  self.write_always = write_always
820
- self.legacy_columns = columns
821
- self.has_header = has_header or self._has_header_in_file()
822
848
 
823
849
  # input manifest properties
824
- self._id = id
825
- self._uri = uri
826
- self._created = created
827
- self._last_change_date = last_change_date
828
- self._last_import_date = last_import_date
829
- self._rows_count = rows_count
830
- self._data_size_bytes = data_size_bytes
831
- self._is_alias = is_alias
850
+ self._id = kwargs.get('id')
851
+ self._uri = kwargs.get('uri')
852
+ self._created = kwargs.get('created')
853
+ self._last_change_date = kwargs.get('last_change_date')
854
+ self._last_import_date = kwargs.get('last_import_date')
855
+ self._rows_count = kwargs.get('rows_count')
856
+ self._data_size_bytes = kwargs.get('data_size_bytes')
857
+ self._is_alias = kwargs.get('is_alias')
858
+
859
+ self.stage = stage
860
+ self.has_header = has_header or self._has_header_in_file()
861
+
862
+ def __get_stage_inferred(self):
863
+ if self._uri:
864
+ return 'in'
865
+ return 'out'
832
866
 
833
867
  @classmethod
834
868
  def build_output_definition(cls, name: str,
@@ -979,6 +1013,7 @@ class TableDefinition(IODefinition):
979
1013
  schema[col.get("name")] = ColumnDefinition().from_dict(col)
980
1014
 
981
1015
  else:
1016
+ # legacy support
982
1017
  columns_metadata = json_data.get('column_metadata', {})
983
1018
  primary_key = json_data.get('primary_key', [])
984
1019
  columns = json_data.get('columns', [])
@@ -1045,11 +1080,25 @@ class TableDefinition(IODefinition):
1045
1080
  else:
1046
1081
  name = Path(manifest_file_path).stem
1047
1082
 
1083
+ # test if the manifest is output and incompatible
1084
+ force_legacy_mode = False
1085
+ if not manifest.get('columns') and manifest.get('primary_key'):
1086
+ warnings.warn('Primary key is set but columns are not. Forcing legacy mode for CSV file.',
1087
+ DeprecationWarning)
1088
+ force_legacy_mode = True
1089
+
1090
+ if manifest.get('id'):
1091
+ stage = 'in'
1092
+ else:
1093
+ stage = 'out'
1094
+
1048
1095
  table_def = cls(name=name,
1096
+ stage=stage,
1049
1097
  full_path=full_path,
1050
1098
  is_sliced=is_sliced,
1051
1099
  id=manifest.get('id'),
1052
1100
  table_metadata=TableMetadata(manifest),
1101
+ primary_key=manifest.get('primary_key'),
1053
1102
  schema=cls.return_schema_from_manifest(manifest),
1054
1103
  uri=manifest.get('uri'),
1055
1104
  created=manifest.get('created'),
@@ -1057,12 +1106,13 @@ class TableDefinition(IODefinition):
1057
1106
  last_import_date=manifest.get('last_import_date'),
1058
1107
  rows_count=manifest.get('rows_count'),
1059
1108
  data_size_bytes=manifest.get('data_size_bytes'),
1060
- is_alias=manifest.get('is_alias')
1109
+ is_alias=manifest.get('is_alias'),
1110
+ force_legacy_mode=force_legacy_mode
1061
1111
  )
1062
1112
 
1063
1113
  return table_def
1064
1114
 
1065
- def get_manifest_dictionary(self, manifest_type: Optional[str] = None, legacy_queue: bool = False,
1115
+ def get_manifest_dictionary(self, manifest_type: Optional[str] = 'out', legacy_queue: bool = False,
1066
1116
  legacy_manifest: Optional[bool] = None) -> dict:
1067
1117
  """
1068
1118
  Returns manifest dictionary in appropriate manifest_type: either 'in' or 'out'.
@@ -1089,6 +1139,8 @@ class TableDefinition(IODefinition):
1089
1139
  if not manifest_type:
1090
1140
  manifest_type = self.stage
1091
1141
 
1142
+ if self._legacy_mode:
1143
+ legacy_manifest = True
1092
1144
  dictionary = self._filter_attributes_by_manifest_type(manifest_type, legacy_queue, legacy_manifest)
1093
1145
 
1094
1146
  filtered_dictionary = self._filter_dictionary(dictionary)
@@ -1134,14 +1186,13 @@ class TableDefinition(IODefinition):
1134
1186
  'is_alias': self._is_alias,
1135
1187
 
1136
1188
  'destination': self.destination,
1137
- 'columns': self.columns if not legacy_manifest else self.legacy_columns,
1138
1189
  'incremental': self.incremental,
1139
1190
  'primary_key': self.primary_key,
1140
1191
  'write_always': self.write_always,
1141
1192
  'delimiter': self.delimiter,
1142
1193
  'enclosure': self.enclosure,
1143
1194
  'metadata': self.table_metadata.get_table_metadata_for_manifest(legacy_manifest=True),
1144
- 'column_metadata': self.table_metadata.get_column_metadata_for_manifest(),
1195
+ 'column_metadata': self.table_metadata._get_legacy_column_metadata_for_manifest(),
1145
1196
  'manifest_type': manifest_type,
1146
1197
  'has_header': self.has_header,
1147
1198
  'description': None,
@@ -1150,8 +1201,10 @@ class TableDefinition(IODefinition):
1150
1201
  'delete_where_values': self.delete_where_values,
1151
1202
  'delete_where_operator': self.delete_where_operator,
1152
1203
  'schema': [col.to_dict(name)
1153
- for name, col in self.schema.items()] if isinstance(self.schema, OrderedDict) else []
1204
+ for name, col in self.schema.items()] if isinstance(self.schema, (OrderedDict, dict)) else []
1154
1205
  }
1206
+ if legacy_manifest:
1207
+ fields['columns'] = self.column_names
1155
1208
 
1156
1209
  new_dict = fields.copy()
1157
1210
 
@@ -1164,7 +1217,7 @@ class TableDefinition(IODefinition):
1164
1217
  def _has_header_in_file(self):
1165
1218
  if self.is_sliced:
1166
1219
  has_header = False
1167
- elif self.columns and not self.stage == 'in':
1220
+ elif self.column_names and not self.stage == 'in':
1168
1221
  has_header = False
1169
1222
  else:
1170
1223
  has_header = True
@@ -1175,10 +1228,17 @@ class TableDefinition(IODefinition):
1175
1228
  return self._schema
1176
1229
 
1177
1230
  @schema.setter
1178
- def schema(self, value):
1179
- self._schema = OrderedDict()
1231
+ def schema(self, value: Union[TypeOrderedDict[str, ColumnDefinition], list[str]]):
1180
1232
  if value:
1181
- self._schema = value
1233
+ if not isinstance(value, (list, dict, OrderedDict)):
1234
+ raise TypeError("Columns must be a list or a mapping of column names and ColumnDefinition objects")
1235
+
1236
+ if isinstance(value, list):
1237
+ self._schema = OrderedDict()
1238
+ for col in value:
1239
+ self._schema[col] = ColumnDefinition()
1240
+ else:
1241
+ self._schema = value
1182
1242
 
1183
1243
  @property
1184
1244
  def _manifest_attributes(self) -> SupportedManifestAttributes:
@@ -1232,21 +1292,27 @@ class TableDefinition(IODefinition):
1232
1292
  @property
1233
1293
  @deprecated(version='1.5.1', reason="Please use new column_names method instead of columns property")
1234
1294
  def columns(self) -> List[str]:
1235
- if isinstance(self.schema, OrderedDict):
1295
+ if isinstance(self.schema, (OrderedDict, dict)):
1236
1296
  return list(self.schema.keys())
1237
1297
  else:
1238
1298
  return []
1239
1299
 
1240
1300
  @columns.setter
1241
- @deprecated(version='1.5.1', reason="Columns can be set by add_columns method")
1301
+ @deprecated(version='1.5.1', reason="Please use new column_names method instead of schema property")
1242
1302
  def columns(self, val: List[str]):
1243
- if val:
1244
- if not isinstance(val, list):
1245
- raise TypeError("Columns must be a list")
1303
+ """
1304
+ Set columns for the table.
1305
+ If list of names provided, the columns will be created with default settings Basetype.String.
1306
+ Args:
1307
+ val:
1246
1308
 
1247
- self.schema = OrderedDict()
1248
- for col in val:
1249
- self.schema[col] = ColumnDefinition()
1309
+ Returns:
1310
+
1311
+ """
1312
+ if not isinstance(val, list):
1313
+ raise TypeError("Columns must be a list")
1314
+
1315
+ self.schema = val
1250
1316
 
1251
1317
  @property
1252
1318
  def column_names(self) -> List[str]:
@@ -1274,8 +1340,10 @@ class TableDefinition(IODefinition):
1274
1340
 
1275
1341
  @property
1276
1342
  def primary_key(self) -> List[str]:
1277
- if isinstance(self.schema, OrderedDict):
1343
+ if not self._legacy_mode:
1278
1344
  return [column_name for column_name, column_def in self.schema.items() if column_def.primary_key]
1345
+ else:
1346
+ return self._legacy_primary_key
1279
1347
 
1280
1348
  @primary_key.setter
1281
1349
  def primary_key(self, primary_key: List[str]):
@@ -1284,12 +1352,15 @@ class TableDefinition(IODefinition):
1284
1352
 
1285
1353
  if not isinstance(primary_key, list):
1286
1354
  raise TypeError("Primary key must be a list")
1287
-
1288
- for col in primary_key:
1289
- if col in self.schema:
1290
- self.schema[col].primary_key = True
1291
- else:
1292
- raise UserException(f"Primary key column {col} not found in columns")
1355
+ if not self._legacy_mode:
1356
+ for col in primary_key:
1357
+ if col in self.schema:
1358
+ self.schema[col].primary_key = True
1359
+ else:
1360
+ raise UserException(f"Primary key column {col} not found in schema. "
1361
+ f"Please specify all columns / schema")
1362
+ else:
1363
+ self._legacy_primary_key = primary_key
1293
1364
 
1294
1365
  @property
1295
1366
  def delimiter(self) -> str:
@@ -1314,8 +1385,10 @@ class TableDefinition(IODefinition):
1314
1385
  @table_metadata.setter
1315
1386
  def table_metadata(self, table_metadata: TableMetadata):
1316
1387
  self._table_metadata = table_metadata
1317
-
1318
- for col, val in table_metadata.get_column_metadata_for_manifest().items():
1388
+ # backward compatibility legacy support
1389
+ for col, val in table_metadata._get_legacy_column_metadata_for_manifest().items():
1390
+ if not self.schema.get(col):
1391
+ self.schema[col] = ColumnDefinition()
1319
1392
  self.schema[col].metadata = {item['key']: item['value'] for item in val}
1320
1393
 
1321
1394
  @property
@@ -1325,6 +1398,22 @@ class TableDefinition(IODefinition):
1325
1398
  else:
1326
1399
  return None
1327
1400
 
1401
+ @property
1402
+ def uri(self) -> str:
1403
+ return self._uri
1404
+
1405
+ @property
1406
+ def last_change_date(self) -> str:
1407
+ return self._last_change_date
1408
+
1409
+ @property
1410
+ def last_import_date(self) -> str:
1411
+ return self._last_import_date
1412
+
1413
+ @property
1414
+ def is_alias(self) -> bool:
1415
+ return self._is_alias
1416
+
1328
1417
  def add_column(self, name: str, definition: ColumnDefinition = ColumnDefinition()):
1329
1418
  """
1330
1419
  Add column definition, accepts either ColumnDefinition or a string
@@ -1441,6 +1530,7 @@ class FileDefinition(IODefinition):
1441
1530
  "notify"]
1442
1531
 
1443
1532
  def __init__(self, full_path: str,
1533
+ stage: Optional[str] = 'out',
1444
1534
  tags: Optional[List[str]] = None,
1445
1535
  is_public: Optional[bool] = False,
1446
1536
  is_permanent: Optional[bool] = False,
@@ -1457,6 +1547,7 @@ class FileDefinition(IODefinition):
1457
1547
 
1458
1548
  Args:
1459
1549
  full_path (str): Full path of the file.
1550
+ stage (str): Storage Stage 'in' or 'out' default out
1460
1551
  tags (list):
1461
1552
  List of tags that are assigned to this file
1462
1553
  is_public: When true, the file URL will be permanent and publicly accessible.
@@ -1467,6 +1558,8 @@ class FileDefinition(IODefinition):
1467
1558
  """
1468
1559
  super().__init__(full_path)
1469
1560
 
1561
+ self.stage = stage
1562
+
1470
1563
  self.tags = tags
1471
1564
  self.is_public = is_public
1472
1565
  self.is_permanent = is_permanent
@@ -1507,7 +1600,7 @@ class FileDefinition(IODefinition):
1507
1600
  Returns:
1508
1601
  An instance of FileDefinition configured for output files.
1509
1602
  """
1510
- return cls(full_path=full_path, tags=tags, is_public=is_public, is_permanent=is_permanent,
1603
+ return cls(full_path=full_path, stage="out", tags=tags, is_public=is_public, is_permanent=is_permanent,
1511
1604
  is_encrypted=is_encrypted, notify=notify)
1512
1605
 
1513
1606
  @classmethod
@@ -1537,7 +1630,7 @@ class FileDefinition(IODefinition):
1537
1630
  Returns:
1538
1631
  An instance of FileDefinition configured for input files.
1539
1632
  """
1540
- return cls(full_path=full_path, id=id, s3=s3, abs=abs, created=created, size_bytes=size_bytes,
1633
+ return cls(full_path=full_path, stage="in", id=id, s3=s3, abs=abs, created=created, size_bytes=size_bytes,
1541
1634
  max_age_days=max_age_days)
1542
1635
 
1543
1636
  @classmethod
@@ -1572,7 +1665,13 @@ class FileDefinition(IODefinition):
1572
1665
 
1573
1666
  full_path = str(file_path)
1574
1667
 
1668
+ if manifest.get('id'):
1669
+ stage = 'in'
1670
+ else:
1671
+ stage = 'out'
1672
+
1575
1673
  file_def = cls(full_path=full_path,
1674
+ stage=stage,
1576
1675
  tags=manifest.get('tags', []),
1577
1676
  is_public=manifest.get('is_public', False),
1578
1677
  is_permanent=manifest.get('is_permanent', False),
@@ -1637,18 +1736,31 @@ class FileDefinition(IODefinition):
1637
1736
 
1638
1737
  """
1639
1738
 
1640
- return {
1641
- 'id': self.id,
1642
- 'created': self.created.strftime('%Y-%m-%dT%H:%M:%S%z') if self.created else None,
1643
- 'is_public': self.is_public,
1644
- 'is_encrypted': self.is_encrypted,
1645
- 'name': self.name,
1646
- 'size_bytes': self.size_bytes,
1647
- 'tags': self.tags,
1648
- 'notify': self.notify,
1649
- 'max_age_days': self.max_age_days,
1650
- 'is_permanent': self.is_permanent,
1651
- }
1739
+ if manifest_type == 'in':
1740
+
1741
+ manifest_dictionary = {
1742
+ 'id': self.id,
1743
+ 'created': self.created.strftime('%Y-%m-%dT%H:%M:%S%z') if self.created else None,
1744
+ 'is_public': self.is_public,
1745
+ 'is_encrypted': self.is_encrypted,
1746
+ 'name': self.name,
1747
+ 'size_bytes': self.size_bytes,
1748
+ 'tags': self.tags,
1749
+ 'notify': self.notify,
1750
+ 'max_age_days': self.max_age_days,
1751
+ 'is_permanent': self.is_permanent,
1752
+ }
1753
+
1754
+ else:
1755
+ manifest_dictionary = {
1756
+ 'is_public': self.is_public,
1757
+ 'is_permanent': self.is_permanent,
1758
+ 'is_encrypted': self.is_encrypted,
1759
+ 'tags': self.tags,
1760
+ 'notify': self.notify,
1761
+ }
1762
+
1763
+ return manifest_dictionary
1652
1764
 
1653
1765
  @property
1654
1766
  def name(self) -> str:
@@ -1,3 +1,6 @@
1
+ # Python 3.7 support
2
+ from __future__ import annotations
3
+
1
4
  import argparse
2
5
  import csv
3
6
  import glob
@@ -5,17 +8,18 @@ import json
5
8
  import logging
6
9
  import os
7
10
  import sys
11
+ import warnings
8
12
  from datetime import datetime
9
13
  from pathlib import Path
10
- from typing import List, Dict, Optional, Union
14
+ from typing import List, Dict, Optional, Union, OrderedDict
11
15
 
12
16
  from deprecated import deprecated
13
17
  from pygelf import GelfUdpHandler, GelfTcpHandler
14
18
  from pytz import utc
15
19
 
16
20
  from . import dao
17
- from .exceptions import UserException
18
21
  from .dao import ColumnDefinition
22
+ from .exceptions import UserException
19
23
 
20
24
 
21
25
  def register_csv_dialect():
@@ -331,7 +335,9 @@ class CommonInterface:
331
335
  delimiter: str = ',',
332
336
  delete_where: dict = None,
333
337
  write_always: bool = False,
334
- schema: List[ColumnDefinition] = None) -> dao.TableDefinition:
338
+ schema: Union[
339
+ OrderedDict[str, ColumnDefinition], list[str]] = None,
340
+ has_header: Optional[bool] = None) -> dao.TableDefinition:
335
341
  """
336
342
  Helper method for dao.TableDefinition creation along with the "manifest".
337
343
  It initializes path according to the storage_stage type.
@@ -361,6 +367,12 @@ class CommonInterface:
361
367
  else:
362
368
  raise ValueError(f'Invalid storage_stage value "{storage_stage}". Supported values are: "in" or "out"!')
363
369
 
370
+ # for transition period we need to force legacy mode for csv files w headers.
371
+ force_legacy_mode = False
372
+ if not schema and not columns and primary_key:
373
+ warnings.warn('Primary key is set but columns are not. Forcing legacy mode for CSV file.',
374
+ DeprecationWarning)
375
+ force_legacy_mode = True
364
376
  return dao.TableDefinition(name=name,
365
377
  full_path=full_path,
366
378
  is_sliced=is_sliced,
@@ -374,7 +386,9 @@ class CommonInterface:
374
386
  delete_where=delete_where,
375
387
  stage=storage_stage,
376
388
  write_always=write_always,
377
- schema=schema)
389
+ schema=schema,
390
+ has_header=has_header,
391
+ force_legacy_mode=force_legacy_mode)
378
392
 
379
393
  def create_in_table_definition(self, name: str,
380
394
  is_sliced: bool = False,
@@ -412,18 +426,22 @@ class CommonInterface:
412
426
  delete_where=delete_where,
413
427
  schema=schema)
414
428
 
429
+ SCHEMA_TYPE = Union[Dict[str, ColumnDefinition], OrderedDict[str, ColumnDefinition], List[str]]
430
+
415
431
  def create_out_table_definition(self, name: str,
416
432
  is_sliced: bool = False,
417
433
  destination: str = '',
418
434
  primary_key: List[str] = None,
419
- columns: List[str] = None,
435
+ schema: SCHEMA_TYPE = None,
420
436
  incremental: bool = None,
421
437
  table_metadata: dao.TableMetadata = None,
422
438
  enclosure: str = '"',
423
439
  delimiter: str = ',',
424
440
  delete_where: dict = None,
425
441
  write_always: bool = False,
426
- schema: List[ColumnDefinition] = False) -> dao.TableDefinition:
442
+ has_header: Optional[bool] = None,
443
+ **kwargs
444
+ ) -> dao.TableDefinition:
427
445
  """
428
446
  Helper method for output dao.TableDefinition creation along with the "manifest".
429
447
  It initializes path in data/tables/out/ folder.
@@ -433,7 +451,9 @@ class CommonInterface:
433
451
  is_sliced: True if the full_path points to a folder with sliced tables
434
452
  destination: String name of the table in Storage.
435
453
  primary_key: List with names of columns used for primary key.
436
- columns: List of columns for headless CSV files
454
+ schema: List of columns or mapping of column names and ColumnDefinition objects.
455
+ if list of strings is provided, the columns will be created with default settings
456
+ (BaseType.string)
437
457
  incremental: Set to true to enable incremental loading
438
458
  table_metadata: <.dao.TableMetadata> object containing column and table metadata
439
459
  enclosure: str: CSV enclosure, by default "
@@ -441,6 +461,8 @@ class CommonInterface:
441
461
  delete_where: Dict with settings for deleting rows
442
462
  write_always: Bool: If true, the table will be saved to Storage even when the job execution
443
463
  fails.
464
+ has_header:Optional[bool] = flag whether the header is present in the file,
465
+ if None legacy method is used
444
466
  """
445
467
 
446
468
  return self._create_table_definition(name=name,
@@ -448,14 +470,15 @@ class CommonInterface:
448
470
  is_sliced=is_sliced,
449
471
  destination=destination,
450
472
  primary_key=primary_key,
451
- columns=columns,
473
+ columns=kwargs.get('columns'),
452
474
  incremental=incremental,
453
475
  table_metadata=table_metadata,
454
476
  enclosure=enclosure,
455
477
  delimiter=delimiter,
456
478
  delete_where=delete_where,
457
479
  write_always=write_always,
458
- schema=schema)
480
+ schema=schema,
481
+ has_header=has_header)
459
482
 
460
483
  # # File processing
461
484
 
@@ -927,7 +950,7 @@ class CommonInterface:
927
950
  """
928
951
 
929
952
  if not legacy_manifest:
930
- legacy_manifest = self.environment_variables.data_type_support not in ('authoritative', 'hint')
953
+ legacy_manifest = self.environment_variables.data_type_support not in ('authoritative', 'hints')
931
954
 
932
955
  manifest = io_definition.get_manifest_dictionary(legacy_queue=self.is_legacy_queue,
933
956
  legacy_manifest=legacy_manifest)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: keboola.component
3
- Version: 1.6.0
3
+ Version: 1.6.3
4
4
  Summary: General library for Python applications running in Keboola Connection environment
5
5
  Home-page: https://github.com/keboola/python-component
6
6
  Author: Keboola KDS Team