carddb 0.3.0 → 0.3.5

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.
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'carddb'
5
+
6
+ client = CardDB::Client.new(secret_key: ENV.fetch('CARDDB_SECRET_KEY'))
7
+ publisher_id = ENV.fetch('CARDDB_PUBLISHER_ID')
8
+ game_key = ENV.fetch('CARDDB_GAME_KEY', 'example-tcg')
9
+
10
+ game = client.games.get_by_key(publisher_id: publisher_id, game_key: game_key, cache: false) ||
11
+ client.games.create(
12
+ input: {
13
+ publisherId: publisher_id,
14
+ key: game_key,
15
+ name: 'Example TCG',
16
+ visibility: 'PRIVATE'
17
+ }
18
+ )
19
+
20
+ client.import_formats.create(
21
+ input: {
22
+ gameId: game.id,
23
+ key: 'ci-json',
24
+ name: 'CI JSON',
25
+ config: { schemaVersion: 1 }
26
+ }
27
+ )
28
+
29
+ advanced_payload = {
30
+ datasets: [
31
+ {
32
+ name: 'cards',
33
+ schema: {
34
+ identifier: { type: 'STRING', isIdentifier: true },
35
+ name: { type: 'STRING', required: true }
36
+ },
37
+ records: [{ identifier: 'CARD-001', name: 'Example Card' }]
38
+ }
39
+ ]
40
+ }
41
+
42
+ preview = client.imports.preview_game(input: { gameId: game.id, data: advanced_payload })
43
+ raise "Import preview blocked: #{preview.warnings.map(&:message).join(', ')}" unless preview.can_proceed?
44
+
45
+ import_job = client.imports.run_game(
46
+ input: {
47
+ gameId: game.id,
48
+ data: advanced_payload,
49
+ options: { mode: 'CREATE', onConflict: 'UPDATE' }
50
+ }
51
+ )
52
+ completed_import = client.imports.wait_for_game_job(import_job.id)
53
+ raise completed_import.error_message unless completed_import.completed?
54
+
55
+ dataset = client.datasets.get_by_key(game_id: game.id, dataset_key: 'cards')
56
+ raise 'cards dataset was not created' unless dataset
57
+
58
+ upsert = client.records.upsert_batch(
59
+ input: {
60
+ datasetId: dataset.id,
61
+ records: [{ identifier: 'CARD-002', name: 'Second Example Card' }],
62
+ options: { mode: 'STRICT', onConflict: 'UPDATE', dryRun: true }
63
+ }
64
+ )
65
+
66
+ raise "Upsert validation failed: #{upsert.dry_run_result.errors.map(&:index).join(', ')}" if upsert.dry_run_result&.errors&.any?
67
+
68
+ delete_preview = client.records.delete_batch(
69
+ input: {
70
+ datasetId: dataset.id,
71
+ reconcileIdentifiers: %w[CARD-001 CARD-002],
72
+ dryRun: true
73
+ }
74
+ )
75
+ puts "Would delete #{delete_preview.matched_count} records"
76
+
77
+ export_job = client.exports.run(input: { datasetId: dataset.id, format: 'JSON' })
78
+ export_result = client.exports.wait_for_job(export_job.id)
79
+ export_with_fresh_url = client.exports.refresh_url(id: export_result.id)
80
+ puts export_with_fresh_url.download_url
data/lib/carddb/client.rb CHANGED
@@ -98,6 +98,34 @@ module CardDB
98
98
  @records ||= Resources::Records.new(self, connection, config)
99
99
  end
100
100
 
101
+ # Access the Import Formats resource
102
+ #
103
+ # @return [Resources::ImportFormats]
104
+ def import_formats
105
+ @import_formats ||= Resources::ImportFormats.new(self, connection, config)
106
+ end
107
+
108
+ # Access the Imports resource
109
+ #
110
+ # @return [Resources::Imports]
111
+ def imports
112
+ @imports ||= Resources::Imports.new(self, connection, config)
113
+ end
114
+
115
+ # Access the Exports resource
116
+ #
117
+ # @return [Resources::Exports]
118
+ def exports
119
+ @exports ||= Resources::Exports.new(self, connection, config)
120
+ end
121
+
122
+ # Access the Files resource
123
+ #
124
+ # @return [Resources::Files]
125
+ def files
126
+ @files ||= Resources::Files.new(self, connection, config)
127
+ end
128
+
101
129
  # Access the Decks resource
102
130
  #
103
131
  # @return [Resources::Decks]
@@ -344,6 +344,14 @@ module CardDB
344
344
  data['description']
345
345
  end
346
346
 
347
+ def metafy_game_uuid
348
+ data['metafyGameUuid']
349
+ end
350
+
351
+ def metafy_game_slug
352
+ data['metafyGameSlug']
353
+ end
354
+
347
355
  def website
348
356
  data['website']
349
357
  end
@@ -372,6 +380,10 @@ module CardDB
372
380
  data.dig('coverFile', 'url')
373
381
  end
374
382
 
383
+ def archived_at
384
+ parse_time(data['archivedAt'])
385
+ end
386
+
375
387
  def created_at
376
388
  parse_time(data['createdAt'])
377
389
  end
@@ -446,6 +458,10 @@ module CardDB
446
458
  data['purpose']
447
459
  end
448
460
 
461
+ def active_version_id
462
+ data['activeVersionId']
463
+ end
464
+
449
465
  def tcgplayer_product_id_field_key
450
466
  data['tcgplayerProductIdFieldKey']
451
467
  end
@@ -458,6 +474,22 @@ module CardDB
458
474
  data['isArchived']
459
475
  end
460
476
 
477
+ def archived_at
478
+ parse_time(data['archivedAt'])
479
+ end
480
+
481
+ def pos_x
482
+ data['posX']
483
+ end
484
+
485
+ def pos_y
486
+ data['posY']
487
+ end
488
+
489
+ def sort_order
490
+ data['sortOrder']
491
+ end
492
+
461
493
  def publisher_id
462
494
  data['publisherId']
463
495
  end
@@ -592,6 +624,10 @@ module CardDB
592
624
  @fields ||= (data['fields'] || []).map { |f| FieldInfo.new(f) }
593
625
  end
594
626
 
627
+ def field_keys
628
+ fields.map(&:key)
629
+ end
630
+
595
631
  def filterable_fields
596
632
  data['filterableFields'] || []
597
633
  end
@@ -650,10 +686,38 @@ module CardDB
650
686
  data['isIdentifier']
651
687
  end
652
688
 
689
+ def sort_order
690
+ data['sortOrder']
691
+ end
692
+
653
693
  def item_type
654
694
  data['itemType']
655
695
  end
656
696
 
697
+ def link_dataset_id
698
+ data['linkDatasetId']
699
+ end
700
+
701
+ def link_dataset_key
702
+ data['linkDatasetKey']
703
+ end
704
+
705
+ def link_dataset_name
706
+ data['linkDatasetName']
707
+ end
708
+
709
+ def link_field_key
710
+ data['linkFieldKey']
711
+ end
712
+
713
+ def link_alias
714
+ data['linkAlias']
715
+ end
716
+
717
+ def link_direction
718
+ data['linkDirection']
719
+ end
720
+
657
721
  def display_format
658
722
  data['displayFormat']
659
723
  end
@@ -662,10 +726,54 @@ module CardDB
662
726
  data['semanticType']
663
727
  end
664
728
 
729
+ def placeholder
730
+ data['placeholder']
731
+ end
732
+
733
+ def hidden?
734
+ data['isHidden']
735
+ end
736
+
737
+ def computed?
738
+ data['isComputed']
739
+ end
740
+
741
+ def system_field?
742
+ data['isSystemField']
743
+ end
744
+
745
+ def default_value
746
+ data['defaultValue']
747
+ end
748
+
665
749
  def allowed_values
666
750
  data['allowedValues']
667
751
  end
668
752
 
753
+ def min_length
754
+ data['minLength']
755
+ end
756
+
757
+ def max_length
758
+ data['maxLength']
759
+ end
760
+
761
+ def min_value
762
+ data['minValue']
763
+ end
764
+
765
+ def max_value
766
+ data['maxValue']
767
+ end
768
+
769
+ def pattern
770
+ data['pattern']
771
+ end
772
+
773
+ def unique?
774
+ data['isUnique']
775
+ end
776
+
669
777
  def nested_fields
670
778
  @nested_fields ||= (data['nestedFields'] || []).map { |f| FieldInfo.new(f) }
671
779
  end
@@ -698,6 +806,22 @@ module CardDB
698
806
  def target_dataset_id
699
807
  data['targetDatasetId']
700
808
  end
809
+
810
+ def target_field_key
811
+ data['targetFieldKey']
812
+ end
813
+
814
+ def link_alias
815
+ data['linkAlias']
816
+ end
817
+
818
+ def link_direction
819
+ data['linkDirection']
820
+ end
821
+
822
+ def array?
823
+ data['isArray']
824
+ end
701
825
  end
702
826
 
703
827
  # Wrapper for DatasetRecord objects
@@ -784,6 +908,399 @@ module CardDB
784
908
  end
785
909
  end
786
910
 
911
+ class File < Resource
912
+ def id = data['id']
913
+ def key = data['key']
914
+ def filename = data['filename']
915
+ def content_type = data['contentType']
916
+ def size = data['size']
917
+ def status = data['status']
918
+ def public? = !!data['isPublic']
919
+ def entity_type = data['entityType']
920
+ def entity_id = data['entityId']
921
+ def url = data['url']
922
+ def created_at = parse_time(data['createdAt'])
923
+ def updated_at = parse_time(data['updatedAt'])
924
+ end
925
+
926
+ class PresignedUpload < Resource
927
+ def upload_url = data['uploadUrl']
928
+ def expires_at = parse_time(data['expiresAt'])
929
+
930
+ def file
931
+ @file ||= data['file'] ? File.new(data['file'], client: client) : nil
932
+ end
933
+ end
934
+
935
+ class ObjectField < Resource
936
+ def id = data['id']
937
+ def object_type_id = data['objectTypeId']
938
+ def key = data['key']
939
+ def label = data['label']
940
+ def data_type = data['dataType']
941
+ def required? = !!data['isRequired']
942
+ def filterable? = !!data['isFilterable']
943
+ def searchable? = !!data['isSearchable']
944
+ def identifier? = !!data['isIdentifier']
945
+ def sort_order = data['sortOrder']
946
+ def description = data['description']
947
+ def placeholder = data['placeholder']
948
+ def display_format = data['displayFormat']
949
+ def semantic_type = data['semanticType']
950
+ def hidden? = !!data['isHidden']
951
+ def computed? = !!data['isComputed']
952
+ def system_field? = !!data['isSystemField']
953
+ def default_value = data['defaultValue']
954
+ def allowed_values = data['allowedValues']
955
+ def min_length = data['minLength']
956
+ def max_length = data['maxLength']
957
+ def min_value = data['minValue']
958
+ def max_value = data['maxValue']
959
+ def pattern = data['pattern']
960
+ def unique? = !!data['isUnique']
961
+ def item_type = data['itemType']
962
+ def link_dataset_id = data['linkDatasetId']
963
+ def link_field_key = data['linkFieldKey']
964
+ def link_alias = data['linkAlias']
965
+ def link_direction = data['linkDirection']
966
+ def parent_field_id = data['parentFieldId']
967
+ def created_at = parse_time(data['createdAt'])
968
+ def updated_at = parse_time(data['updatedAt'])
969
+ end
970
+
971
+ class RecordValidationError < Resource
972
+ def field = data['field']
973
+ def message = data['message']
974
+ def details = data['details'] || {}
975
+ end
976
+
977
+ class BulkRecordError < Resource
978
+ def dataset_key = data['datasetKey']
979
+ def index = data['index']
980
+
981
+ def errors
982
+ @errors ||= (data['errors'] || []).map { |error| RecordValidationError.new(error, client: client) }
983
+ end
984
+ end
985
+
986
+ class ImportStats < Resource
987
+ def total = data['total']
988
+ def inserted = data['inserted']
989
+ def updated = data['updated']
990
+ def skipped = data['skipped']
991
+ def failed = data['failed']
992
+ end
993
+
994
+ class BulkImportResult < Resource
995
+ def inserted
996
+ @inserted ||= (data['inserted'] || []).map { |record| Record.new(record, client: client) }
997
+ end
998
+
999
+ def updated
1000
+ @updated ||= (data['updated'] || []).map { |record| Record.new(record, client: client) }
1001
+ end
1002
+
1003
+ def skipped = data['skipped']
1004
+
1005
+ def errors
1006
+ @errors ||= (data['errors'] || []).map { |error| BulkRecordError.new(error, client: client) }
1007
+ end
1008
+
1009
+ def created_fields
1010
+ @created_fields ||= (data['createdFields'] || []).map { |field| ObjectField.new(field, client: client) }
1011
+ end
1012
+
1013
+ def stats
1014
+ @stats ||= data['stats'] ? ImportStats.new(data['stats'], client: client) : nil
1015
+ end
1016
+ end
1017
+
1018
+ class DatasetRecordsUpsertPayload < Resource
1019
+ def job
1020
+ @job ||= data['job'] ? ImportJob.new(data['job'], client: client) : nil
1021
+ end
1022
+
1023
+ def dry_run_result
1024
+ @dry_run_result ||= data['dryRunResult'] ? BulkImportResult.new(data['dryRunResult'], client: client) : nil
1025
+ end
1026
+ end
1027
+
1028
+ class DatasetRecordDeleteJobResult < Resource
1029
+ def target = data['target']
1030
+ def record_id = data['recordId']
1031
+ def identifier = data['identifier']
1032
+ def status = data['status']
1033
+ def message = data['message']
1034
+ end
1035
+
1036
+ class DatasetRecordDeleteJob < Resource
1037
+ def id = data['id']
1038
+ def publisher_id = data['publisherId']
1039
+ def dataset_id = data['datasetId']
1040
+ def account_id = data['accountId']
1041
+ def status = data['status']
1042
+ def target_type = data['targetType']
1043
+ def dry_run? = !!data['dryRun']
1044
+ def targets = data['targets'] || []
1045
+ def progress = data['progress']
1046
+ def total_targets = data['totalTargets']
1047
+ def processed_targets = data['processedTargets']
1048
+ def matched_count = data['matchedCount']
1049
+ def deleted_count = data['deletedCount']
1050
+ def missing_count = data['missingCount']
1051
+ def blocked_count = data['blockedCount']
1052
+ def failed_count = data['failedCount']
1053
+ def error_message = data['errorMessage']
1054
+ def started_at = parse_time(data['startedAt'])
1055
+ def completed_at = parse_time(data['completedAt'])
1056
+ def created_at = parse_time(data['createdAt'])
1057
+ def updated_at = parse_time(data['updatedAt'])
1058
+ def completed? = status == 'COMPLETED'
1059
+ def failed? = status == 'FAILED'
1060
+
1061
+ def publisher
1062
+ @publisher ||= data['publisher'] ? Publisher.new(data['publisher'], client: client) : nil
1063
+ end
1064
+
1065
+ def dataset
1066
+ @dataset ||= data['dataset'] ? Dataset.new(data['dataset'], client: client) : nil
1067
+ end
1068
+
1069
+ def results
1070
+ @results ||= (data['results'] || []).map { |result| DatasetRecordDeleteJobResult.new(result, client: client) }
1071
+ end
1072
+ end
1073
+
1074
+ class ImportJobAsset < Resource
1075
+ def file_id = data['fileId']
1076
+ def filename = data['filename']
1077
+
1078
+ def file
1079
+ @file ||= data['file'] ? File.new(data['file'], client: client) : nil
1080
+ end
1081
+ end
1082
+
1083
+ class ImportJob < Resource
1084
+ def id = data['id']
1085
+ def publisher_id = data['publisherId']
1086
+ def dataset_id = data['datasetId']
1087
+ def account_id = data['accountId']
1088
+ def status = data['status']
1089
+ def mode = data['mode']
1090
+ def on_conflict = data['onConflict']
1091
+ def format = data['format']
1092
+ def file_id = data['fileId']
1093
+ def progress = data['progress']
1094
+ def total_records = data['totalRecords']
1095
+ def processed_records = data['processedRecords']
1096
+ def inserted_count = data['insertedCount']
1097
+ def updated_count = data['updatedCount']
1098
+ def skipped_count = data['skippedCount']
1099
+ def failed_count = data['failedCount']
1100
+ def error_message = data['errorMessage']
1101
+ def started_at = parse_time(data['startedAt'])
1102
+ def completed_at = parse_time(data['completedAt'])
1103
+ def created_at = parse_time(data['createdAt'])
1104
+ def updated_at = parse_time(data['updatedAt'])
1105
+ def last_checkpoint_index = data['lastCheckpointIndex']
1106
+ def resumed? = !!data['isResumed']
1107
+ def images_pending = data['imagesPending']
1108
+ def images_completed = data['imagesCompleted']
1109
+ def images_failed = data['imagesFailed']
1110
+ def completed? = status == 'COMPLETED'
1111
+ def failed? = status == 'FAILED'
1112
+
1113
+ def publisher
1114
+ @publisher ||= data['publisher'] ? Publisher.new(data['publisher'], client: client) : nil
1115
+ end
1116
+
1117
+ def dataset
1118
+ @dataset ||= data['dataset'] ? Dataset.new(data['dataset'], client: client) : nil
1119
+ end
1120
+
1121
+ def created_fields
1122
+ @created_fields ||= (data['createdFields'] || []).map { |field| ObjectField.new(field, client: client) }
1123
+ end
1124
+
1125
+ def errors
1126
+ @errors ||= (data['errors'] || []).map { |error| BulkRecordError.new(error, client: client) }
1127
+ end
1128
+
1129
+ def assets
1130
+ @assets ||= (data['assets'] || []).map { |asset| ImportJobAsset.new(asset, client: client) }
1131
+ end
1132
+ end
1133
+
1134
+ class ImportJobLog < Resource
1135
+ def id = data['id']
1136
+ def import_job_id = data['importJobId']
1137
+ def level = data['level']
1138
+ def message = data['message']
1139
+ def dataset_key = data['datasetKey']
1140
+ def record_index = data['recordIndex']
1141
+ def field_key = data['fieldKey']
1142
+ def details = data['details'] || {}
1143
+ def created_at = parse_time(data['createdAt'])
1144
+ end
1145
+
1146
+ class FieldMapping < Resource
1147
+ def source_field = data['sourceField']
1148
+ def target_field = data['targetField']
1149
+ def target_field_label = data['targetFieldLabel']
1150
+ def inferred_type = data['inferredType']
1151
+ def inferred_item_type = data['inferredItemType']
1152
+ def inferred_children = data['inferredChildren'] || []
1153
+ def inferred_link_dataset = data['inferredLinkDataset']
1154
+ def inferred_link_field_key = data['inferredLinkFieldKey']
1155
+ def inferred_link_direction = data['inferredLinkDirection']
1156
+ def target_field_type = data['targetFieldType']
1157
+ def sample_values = data['sampleValues'] || []
1158
+ def status = data['status']
1159
+ end
1160
+
1161
+ class ImportWarning < Resource
1162
+ def dataset_key = data['datasetKey']
1163
+ def field = data['field']
1164
+ def message = data['message']
1165
+ def severity = data['severity']
1166
+ end
1167
+
1168
+ class DatasetImportPreview < Resource
1169
+ def dataset_key = data['datasetKey']
1170
+ def dataset_name = data['datasetName']
1171
+ def dataset_id = data['datasetId']
1172
+ def exists? = !!data['exists']
1173
+ def record_count = data['recordCount']
1174
+ def sample_records = data['sampleRecords'] || []
1175
+
1176
+ def field_mappings
1177
+ @field_mappings ||= (data['fieldMappings'] || []).map { |mapping| FieldMapping.new(mapping, client: client) }
1178
+ end
1179
+
1180
+ def warnings
1181
+ @warnings ||= (data['warnings'] || []).map { |warning| ImportWarning.new(warning, client: client) }
1182
+ end
1183
+ end
1184
+
1185
+ class DatasetImportPreviewResult < DatasetImportPreview
1186
+ def can_proceed? = !!data['canProceed']
1187
+
1188
+ def available_datasets
1189
+ @available_datasets ||= (data['availableDatasets'] || []).map { |dataset| Dataset.new(dataset, client: client) }
1190
+ end
1191
+ end
1192
+
1193
+ class GameImportPreview < Resource
1194
+ def import_order = data['importOrder'] || []
1195
+ def can_proceed? = !!data['canProceed']
1196
+
1197
+ def datasets
1198
+ @datasets ||= (data['datasets'] || []).map { |dataset| DatasetImportPreview.new(dataset, client: client) }
1199
+ end
1200
+
1201
+ def warnings
1202
+ @warnings ||= (data['warnings'] || []).map { |warning| ImportWarning.new(warning, client: client) }
1203
+ end
1204
+ end
1205
+
1206
+ class GameImportDatasetStatus < Resource
1207
+ def dataset_key = data['datasetKey']
1208
+ def dataset_id = data['datasetId']
1209
+ def dataset_name = data['datasetName']
1210
+ def status = data['status']
1211
+ def record_count = data['recordCount']
1212
+ def inserted_count = data['insertedCount']
1213
+ def updated_count = data['updatedCount']
1214
+ def skipped_count = data['skippedCount']
1215
+ def failed_count = data['failedCount']
1216
+ def error_message = data['errorMessage']
1217
+ end
1218
+
1219
+ class GameImportJob < Resource
1220
+ def id = data['id']
1221
+ def publisher_id = data['publisherId']
1222
+ def game_id = data['gameId']
1223
+ def account_id = data['accountId']
1224
+ def status = data['status']
1225
+ def mode = data['mode']
1226
+ def on_conflict = data['onConflict']
1227
+ def file_id = data['fileId']
1228
+ def total_datasets = data['totalDatasets']
1229
+ def completed_datasets = data['completedDatasets']
1230
+ def progress = data['progress']
1231
+ def import_order = data['importOrder'] || []
1232
+ def error_message = data['errorMessage']
1233
+ def started_at = parse_time(data['startedAt'])
1234
+ def completed_at = parse_time(data['completedAt'])
1235
+ def created_at = parse_time(data['createdAt'])
1236
+ def updated_at = parse_time(data['updatedAt'])
1237
+ def completed_dataset_keys = data['completedDatasetKeys'] || []
1238
+ def current_dataset_key = data['currentDatasetKey']
1239
+ def last_checkpoint_index = data['lastCheckpointIndex']
1240
+ def resumed? = !!data['isResumed']
1241
+ def images_pending = data['imagesPending']
1242
+ def images_completed = data['imagesCompleted']
1243
+ def images_failed = data['imagesFailed']
1244
+ def phase = data['phase']
1245
+ def link_building_dataset = data['linkBuildingDataset']
1246
+ def link_building_total_datasets = data['linkBuildingTotalDatasets']
1247
+ def link_building_completed_datasets = data['linkBuildingCompletedDatasets']
1248
+ def link_building_processed_records = data['linkBuildingProcessedRecords']
1249
+ def link_building_total_records = data['linkBuildingTotalRecords']
1250
+ def completed? = status == 'COMPLETED'
1251
+ def failed? = status == 'FAILED'
1252
+
1253
+ def publisher
1254
+ @publisher ||= data['publisher'] ? Publisher.new(data['publisher'], client: client) : nil
1255
+ end
1256
+
1257
+ def game
1258
+ @game ||= data['game'] ? Game.new(data['game'], client: client) : nil
1259
+ end
1260
+
1261
+ def assets
1262
+ @assets ||= (data['assets'] || []).map { |asset| ImportJobAsset.new(asset, client: client) }
1263
+ end
1264
+
1265
+ def dataset_statuses
1266
+ @dataset_statuses ||= (data['datasetStatuses'] || []).map do |status|
1267
+ GameImportDatasetStatus.new(status, client: client)
1268
+ end
1269
+ end
1270
+ end
1271
+
1272
+ class ExportJob < Resource
1273
+ def id = data['id']
1274
+ def publisher_id = data['publisherId']
1275
+ def dataset_id = data['datasetId']
1276
+ def account_id = data['accountId']
1277
+ def status = data['status']
1278
+ def format = data['format']
1279
+ def filter = data['filter']
1280
+ def total_records = data['totalRecords']
1281
+ def processed_records = data['processedRecords']
1282
+ def progress = data['progress']
1283
+ def file_id = data['fileId']
1284
+ def download_url = data['downloadUrl']
1285
+ def download_expires_at = parse_time(data['downloadExpiresAt'])
1286
+ def error_message = data['errorMessage']
1287
+ def started_at = parse_time(data['startedAt'])
1288
+ def completed_at = parse_time(data['completedAt'])
1289
+ def created_at = parse_time(data['createdAt'])
1290
+ def updated_at = parse_time(data['updatedAt'])
1291
+ def last_checkpoint_index = data['lastCheckpointIndex']
1292
+ def completed? = status == 'COMPLETED'
1293
+ def failed? = status == 'FAILED'
1294
+
1295
+ def publisher
1296
+ @publisher ||= data['publisher'] ? Publisher.new(data['publisher'], client: client) : nil
1297
+ end
1298
+
1299
+ def dataset
1300
+ @dataset ||= data['dataset'] ? Dataset.new(data['dataset'], client: client) : nil
1301
+ end
1302
+ end
1303
+
787
1304
  # Wrapper for hosted Deck objects
788
1305
  class Deck < Resource
789
1306
  def id