carddb 0.3.15 → 0.4.1

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.
@@ -409,6 +409,16 @@ module CardDB
409
409
  end
410
410
  end
411
411
 
412
+ # Fetch publisher-defined scan regions for this game.
413
+ def scan_regions(include_inactive: nil)
414
+ raise Error, 'No client available to fetch scan regions' unless client
415
+
416
+ publisher_slug = publisher&.[]('slug')
417
+ raise Error, 'Publisher slug not available on this game' unless publisher_slug
418
+
419
+ client.games.get_regions(publisher_slug: publisher_slug, game_key: key, include_inactive: include_inactive)
420
+ end
421
+
412
422
  private
413
423
 
414
424
  def fetch_datasets(purpose: nil, search: nil, first: nil, after: nil)
@@ -932,6 +942,257 @@ module CardDB
932
942
  end
933
943
  end
934
944
 
945
+ class ScanJobPayload < Resource
946
+ def created? = !!data['created']
947
+
948
+ def job
949
+ @job ||= data['job'] ? ScanJob.new(data['job'], client: client) : nil
950
+ end
951
+ end
952
+
953
+ class ScanBestMatch < Resource
954
+ def record_id = data['recordId']
955
+ def score = data['score']
956
+ def confidence = data['confidence']
957
+ end
958
+
959
+ class ScanCandidate < Resource
960
+ def record_id = data['recordId']
961
+ def rank = data['rank']
962
+ def score = data['score']
963
+ def confidence = data['confidence']
964
+ def reasons = data['reasons'] || []
965
+ end
966
+
967
+ class ScanJobError < Resource
968
+ def code = data['code']
969
+ def message = data['message']
970
+ end
971
+
972
+ class ScanWebhookState < Resource
973
+ def configured? = !!data['configured']
974
+ def url = data['url']
975
+ def latest_delivery_id = data['latestDeliveryId']
976
+ def status = data['status']
977
+ def attempt_count = data['attemptCount']
978
+ def next_attempt_at = parse_time(data['nextAttemptAt'])
979
+ def last_attempt_at = parse_time(data['lastAttemptAt'])
980
+ def delivered_at = parse_time(data['deliveredAt'])
981
+ def last_status_code = data['lastStatusCode']
982
+ def last_error = data['lastError']
983
+ end
984
+
985
+ class ScanStageMetrics < Resource
986
+ def name = data['name']
987
+ def status = data['status']
988
+ def duration_ms = data['durationMs']
989
+ def completed_at = parse_time(data['completedAt'])
990
+ def details = data['details'] || {}
991
+ end
992
+
993
+ class ScanJobMetrics < Resource
994
+ def feature_version = data['featureVersion']
995
+ def candidate_count = data['candidateCount']
996
+ def confidence_score = data['confidenceScore']
997
+ def confidence_label = data['confidenceLabel']
998
+ def timings = data['timings'] || {}
999
+
1000
+ def stages
1001
+ @stages ||= (data['stages'] || []).map { |stage| ScanStageMetrics.new(stage, client: client) }
1002
+ end
1003
+ end
1004
+
1005
+ class ScanJob < Resource
1006
+ def job_id = data['jobId']
1007
+ def status = data['status']
1008
+ def publisher_slug = data['publisherSlug']
1009
+ def game_key = data['gameKey']
1010
+ def dataset_key = data['datasetKey']
1011
+ def file_id = data['fileId']
1012
+ def client_request_id = data['clientRequestId']
1013
+ def template_id = data['templateId']
1014
+ def template_version_id = data['templateVersionId']
1015
+ def extracted = data['extracted'] || {}
1016
+ def created_at = parse_time(data['createdAt'])
1017
+ def updated_at = parse_time(data['updatedAt'])
1018
+ def started_at = parse_time(data['startedAt'])
1019
+ def completed_at = parse_time(data['completedAt'])
1020
+ def completed? = status.to_s.downcase == 'completed'
1021
+ def failed? = status.to_s.downcase == 'failed'
1022
+ def cancelled? = status.to_s.downcase == 'cancelled'
1023
+
1024
+ def best_match
1025
+ @best_match ||= data['bestMatch'] ? ScanBestMatch.new(data['bestMatch'], client: client) : nil
1026
+ end
1027
+
1028
+ def candidates
1029
+ @candidates ||= (data['candidates'] || []).map { |candidate| ScanCandidate.new(candidate, client: client) }
1030
+ end
1031
+
1032
+ def metrics
1033
+ @metrics ||= data['metrics'] ? ScanJobMetrics.new(data['metrics'], client: client) : nil
1034
+ end
1035
+
1036
+ def webhook
1037
+ @webhook ||= data['webhook'] ? ScanWebhookState.new(data['webhook'], client: client) : nil
1038
+ end
1039
+
1040
+ def error
1041
+ @error ||= data['error'] ? ScanJobError.new(data['error'], client: client) : nil
1042
+ end
1043
+ end
1044
+
1045
+ class ScanFeedbackPayload < Resource
1046
+ def feedback_id = data['feedbackId']
1047
+ def job_id = data['jobId']
1048
+ def status = data['status']
1049
+ def persisted? = !!data['persisted']
1050
+ def correct = data['correct']
1051
+ def predicted_record_id = data['predictedRecordId']
1052
+ def selected_record_id = data['selectedRecordId']
1053
+ def feature_version = data['featureVersion']
1054
+ def message = data['message']
1055
+ end
1056
+
1057
+ class ScanFeedbackMetrics < Resource
1058
+ def total = data['total']
1059
+ def correct = data['correct']
1060
+ def corrected = data['corrected']
1061
+ def unknown = data['unknown']
1062
+ def accuracy = data['accuracy']
1063
+ end
1064
+
1065
+ class ScanFeatureVersionMetrics < Resource
1066
+ def feature_version = data['featureVersion']
1067
+ def job_count = data['jobCount']
1068
+ def completed_jobs = data['completedJobs']
1069
+ def failed_jobs = data['failedJobs']
1070
+ def average_confidence = data['averageConfidence']
1071
+ def average_shortlist_size = data['averageShortlistSize']
1072
+ def feedback_count = data['feedbackCount']
1073
+ def correct_feedback = data['correctFeedback']
1074
+ def corrected_feedback = data['correctedFeedback']
1075
+ def feedback_accuracy = data['feedbackAccuracy']
1076
+ end
1077
+
1078
+ class ScanMetrics < Resource
1079
+ def publisher_slug = data['publisherSlug']
1080
+ def game_key = data['gameKey']
1081
+ def dataset_key = data['datasetKey']
1082
+ def since = parse_time(data['since'])
1083
+ def until = parse_time(data['until'])
1084
+ def generated_at = parse_time(data['generatedAt'])
1085
+ def total_jobs = data['totalJobs']
1086
+ def completed_jobs = data['completedJobs']
1087
+ def failed_jobs = data['failedJobs']
1088
+ def failure_rate = data['failureRate']
1089
+ def average_shortlist_size = data['averageShortlistSize']
1090
+ def confidence_distribution = data['confidenceDistribution'] || {}
1091
+
1092
+ def feedback
1093
+ @feedback ||= data['feedback'] ? ScanFeedbackMetrics.new(data['feedback'], client: client) : nil
1094
+ end
1095
+
1096
+ def feature_versions
1097
+ @feature_versions ||= (data['featureVersions'] || []).map do |version|
1098
+ ScanFeatureVersionMetrics.new(version, client: client)
1099
+ end
1100
+ end
1101
+ end
1102
+
1103
+ class ScanTemplateWarning < Resource
1104
+ def code = data['code']
1105
+ def region = data['region']
1106
+ def message = data['message']
1107
+ end
1108
+
1109
+ class ScanTemplateRegion < Resource
1110
+ def id = data['id']
1111
+ def key = data['key']
1112
+ def label = data['label']
1113
+ def sort_order = data['sortOrder']
1114
+ def shape_type = data['shapeType']
1115
+ def geometry = data['geometry'] || {}
1116
+ def semantic_type = data['semanticType']
1117
+ def extraction_mode = data['extractionMode']
1118
+ def lookup_mode = data['lookupMode']
1119
+ def required? = !!data['isRequired']
1120
+ def weight = data['weight']
1121
+ def config = data['config'] || {}
1122
+ end
1123
+
1124
+ class ScanTemplate < Resource
1125
+ def id = data['id']
1126
+ def key = data['key']
1127
+ def name = data['name']
1128
+ def status = data['status']
1129
+ def version = data['version']
1130
+ def default? = !!data['isDefault']
1131
+ def publisher_slug = data['publisherSlug']
1132
+ def game_key = data['gameKey']
1133
+ def dataset_key = data['datasetKey']
1134
+ def example_file_id = data['exampleFileId']
1135
+ def coordinate_space = data['coordinateSpace']
1136
+ def card_aspect_ratio = data['cardAspectRatio']
1137
+ def notes = data['notes']
1138
+ def config = data['config'] || {}
1139
+ def created_at = parse_time(data['createdAt'])
1140
+ def updated_at = parse_time(data['updatedAt'])
1141
+
1142
+ def regions
1143
+ @regions ||= (data['regions'] || []).map { |region| ScanTemplateRegion.new(region, client: client) }
1144
+ end
1145
+ end
1146
+
1147
+ class ScanTemplateResolution < Resource
1148
+ def template
1149
+ @template ||= data['template'] ? ScanTemplate.new(data['template'], client: client) : nil
1150
+ end
1151
+
1152
+ def warnings
1153
+ @warnings ||= (data['warnings'] || []).map { |warning| ScanTemplateWarning.new(warning, client: client) }
1154
+ end
1155
+ end
1156
+
1157
+ class ScanTemplatePayload < ScanTemplateResolution; end
1158
+
1159
+ class ScanTemplateRegionPreview < Resource
1160
+ def key = data['key']
1161
+ def label = data['label']
1162
+ def semantic_type = data['semanticType']
1163
+ def extraction_mode = data['extractionMode']
1164
+ def lookup_mode = data['lookupMode']
1165
+ def raw_text = data['rawText']
1166
+ def normalized_value = data['normalizedValue']
1167
+ def source = data['source']
1168
+ def status = data['status']
1169
+ def confidence = data['confidence']
1170
+ end
1171
+
1172
+ class ScanTemplatePreview < Resource
1173
+ def message = data['message']
1174
+
1175
+ def warnings
1176
+ @warnings ||= (data['warnings'] || []).map { |warning| ScanTemplateWarning.new(warning, client: client) }
1177
+ end
1178
+
1179
+ def regions
1180
+ @regions ||= (data['regions'] || []).map { |region| ScanTemplateRegionPreview.new(region, client: client) }
1181
+ end
1182
+ end
1183
+
1184
+ class GameScanRegion < Resource
1185
+ def id = data['id']
1186
+ def key = data['key']
1187
+ def name = data['name']
1188
+ def description = data['description']
1189
+ def sort_order = data['sortOrder']
1190
+ def active? = !!data['isActive']
1191
+ def metadata = data['metadata'] || {}
1192
+ def created_at = parse_time(data['createdAt'])
1193
+ def updated_at = parse_time(data['updatedAt'])
1194
+ end
1195
+
935
1196
  class ObjectField < Resource
936
1197
  def id = data['id']
937
1198
  def object_type_id = data['objectTypeId']
@@ -680,6 +680,160 @@ module CardDB
680
680
  GRAPHQL
681
681
  end
682
682
 
683
+ def create_scan_upload_session
684
+ <<~GRAPHQL
685
+ mutation CreateScanUploadSession($input: CreateScanUploadSessionInput!) {
686
+ createScanUploadSession(input: $input) {
687
+ #{presigned_upload_fields}
688
+ }
689
+ }
690
+ GRAPHQL
691
+ end
692
+
693
+ def confirm_scan_upload
694
+ <<~GRAPHQL
695
+ mutation ConfirmScanUpload($input: ConfirmScanUploadInput!) {
696
+ confirmScanUpload(input: $input) {
697
+ #{file_fields}
698
+ }
699
+ }
700
+ GRAPHQL
701
+ end
702
+
703
+ def create_scan_job
704
+ <<~GRAPHQL
705
+ mutation CreateScanJob($input: CreateScanJobInput!) {
706
+ createScanJob(input: $input) {
707
+ job {
708
+ #{scan_job_fields}
709
+ }
710
+ created
711
+ }
712
+ }
713
+ GRAPHQL
714
+ end
715
+
716
+ def scan_job
717
+ <<~GRAPHQL
718
+ query ScanJob($id: UUID!) {
719
+ scanJob(id: $id) {
720
+ #{scan_job_fields}
721
+ }
722
+ }
723
+ GRAPHQL
724
+ end
725
+
726
+ def submit_scan_feedback
727
+ <<~GRAPHQL
728
+ mutation SubmitScanFeedback($input: SubmitScanFeedbackInput!) {
729
+ submitScanFeedback(input: $input) {
730
+ feedbackId
731
+ jobId
732
+ status
733
+ persisted
734
+ correct
735
+ predictedRecordId
736
+ selectedRecordId
737
+ featureVersion
738
+ message
739
+ }
740
+ }
741
+ GRAPHQL
742
+ end
743
+
744
+ def scan_metrics
745
+ <<~GRAPHQL
746
+ query ScanMetrics($input: ScanMetricsInput!) {
747
+ scanMetrics(input: $input) {
748
+ #{scan_metrics_fields}
749
+ }
750
+ }
751
+ GRAPHQL
752
+ end
753
+
754
+ def resolve_scan_template
755
+ <<~GRAPHQL
756
+ query ResolveScanTemplate($input: ResolveScanTemplateInput!) {
757
+ resolveScanTemplate(input: $input) {
758
+ template {
759
+ #{scan_template_fields}
760
+ }
761
+ warnings {
762
+ #{scan_template_warning_fields}
763
+ }
764
+ }
765
+ }
766
+ GRAPHQL
767
+ end
768
+
769
+ def scan_templates
770
+ <<~GRAPHQL
771
+ query ScanTemplates($input: ScanTemplatesInput!) {
772
+ scanTemplates(input: $input) {
773
+ templates {
774
+ #{scan_template_fields}
775
+ }
776
+ }
777
+ }
778
+ GRAPHQL
779
+ end
780
+
781
+ def create_scan_template
782
+ <<~GRAPHQL
783
+ mutation CreateScanTemplate($input: SaveScanTemplateInput!) {
784
+ createScanTemplate(input: $input) {
785
+ template {
786
+ #{scan_template_fields}
787
+ }
788
+ warnings {
789
+ #{scan_template_warning_fields}
790
+ }
791
+ }
792
+ }
793
+ GRAPHQL
794
+ end
795
+
796
+ def update_scan_template
797
+ <<~GRAPHQL
798
+ mutation UpdateScanTemplate($id: UUID!, $input: SaveScanTemplateInput!) {
799
+ updateScanTemplate(id: $id, input: $input) {
800
+ template {
801
+ #{scan_template_fields}
802
+ }
803
+ warnings {
804
+ #{scan_template_warning_fields}
805
+ }
806
+ }
807
+ }
808
+ GRAPHQL
809
+ end
810
+
811
+ def preview_scan_template
812
+ <<~GRAPHQL
813
+ mutation PreviewScanTemplate($input: PreviewScanTemplateInput!) {
814
+ previewScanTemplate(input: $input) {
815
+ warnings {
816
+ #{scan_template_warning_fields}
817
+ }
818
+ regions {
819
+ #{scan_template_preview_region_fields}
820
+ }
821
+ message
822
+ }
823
+ }
824
+ GRAPHQL
825
+ end
826
+
827
+ def game_scan_regions
828
+ <<~GRAPHQL
829
+ query GameScanRegions($publisherSlug: String!, $gameKey: String!, $includeInactive: Boolean) {
830
+ gameScanRegions(publisherSlug: $publisherSlug, gameKey: $gameKey, includeInactive: $includeInactive) {
831
+ #{game_scan_region_fields}
832
+ }
833
+ }
834
+ GRAPHQL
835
+ end
836
+
683
837
  def export_job
684
838
  <<~GRAPHQL
685
839
  query ExportJob($id: UUID!) {
@@ -2252,6 +2406,181 @@ module CardDB
2252
2406
  FIELDS
2253
2407
  end
2254
2408
 
2409
+ def scan_job_fields
2410
+ <<~FIELDS
2411
+ jobId
2412
+ status
2413
+ publisherSlug
2414
+ gameKey
2415
+ datasetKey
2416
+ fileId
2417
+ clientRequestId
2418
+ templateId
2419
+ templateVersionId
2420
+ bestMatch {
2421
+ recordId
2422
+ score
2423
+ confidence
2424
+ }
2425
+ candidates {
2426
+ recordId
2427
+ rank
2428
+ score
2429
+ confidence
2430
+ reasons
2431
+ }
2432
+ extracted
2433
+ metrics {
2434
+ featureVersion
2435
+ candidateCount
2436
+ confidenceScore
2437
+ confidenceLabel
2438
+ stages {
2439
+ name
2440
+ status
2441
+ durationMs
2442
+ completedAt
2443
+ details
2444
+ }
2445
+ timings
2446
+ }
2447
+ webhook {
2448
+ configured
2449
+ url
2450
+ latestDeliveryId
2451
+ status
2452
+ attemptCount
2453
+ nextAttemptAt
2454
+ lastAttemptAt
2455
+ deliveredAt
2456
+ lastStatusCode
2457
+ lastError
2458
+ }
2459
+ error {
2460
+ code
2461
+ message
2462
+ }
2463
+ createdAt
2464
+ updatedAt
2465
+ startedAt
2466
+ completedAt
2467
+ FIELDS
2468
+ end
2469
+
2470
+ def scan_metrics_fields
2471
+ <<~FIELDS
2472
+ publisherSlug
2473
+ gameKey
2474
+ datasetKey
2475
+ since
2476
+ until
2477
+ generatedAt
2478
+ totalJobs
2479
+ completedJobs
2480
+ failedJobs
2481
+ failureRate
2482
+ averageShortlistSize
2483
+ confidenceDistribution
2484
+ feedback {
2485
+ total
2486
+ correct
2487
+ corrected
2488
+ unknown
2489
+ accuracy
2490
+ }
2491
+ featureVersions {
2492
+ featureVersion
2493
+ jobCount
2494
+ completedJobs
2495
+ failedJobs
2496
+ averageConfidence
2497
+ averageShortlistSize
2498
+ feedbackCount
2499
+ correctFeedback
2500
+ correctedFeedback
2501
+ feedbackAccuracy
2502
+ }
2503
+ FIELDS
2504
+ end
2505
+
2506
+ def scan_template_warning_fields
2507
+ <<~FIELDS
2508
+ code
2509
+ region
2510
+ message
2511
+ FIELDS
2512
+ end
2513
+
2514
+ def scan_template_region_fields
2515
+ <<~FIELDS
2516
+ id
2517
+ key
2518
+ label
2519
+ sortOrder
2520
+ shapeType
2521
+ geometry
2522
+ semanticType
2523
+ extractionMode
2524
+ lookupMode
2525
+ isRequired
2526
+ weight
2527
+ config
2528
+ FIELDS
2529
+ end
2530
+
2531
+ def scan_template_fields
2532
+ <<~FIELDS
2533
+ id
2534
+ key
2535
+ name
2536
+ status
2537
+ version
2538
+ isDefault
2539
+ publisherSlug
2540
+ gameKey
2541
+ datasetKey
2542
+ exampleFileId
2543
+ coordinateSpace
2544
+ cardAspectRatio
2545
+ notes
2546
+ config
2547
+ regions {
2548
+ #{scan_template_region_fields}
2549
+ }
2550
+ createdAt
2551
+ updatedAt
2552
+ FIELDS
2553
+ end
2554
+
2555
+ def scan_template_preview_region_fields
2556
+ <<~FIELDS
2557
+ key
2558
+ label
2559
+ semanticType
2560
+ extractionMode
2561
+ lookupMode
2562
+ rawText
2563
+ normalizedValue
2564
+ source
2565
+ status
2566
+ confidence
2567
+ FIELDS
2568
+ end
2569
+
2570
+ def game_scan_region_fields
2571
+ <<~FIELDS
2572
+ id
2573
+ key
2574
+ name
2575
+ description
2576
+ sortOrder
2577
+ isActive
2578
+ metadata
2579
+ createdAt
2580
+ updatedAt
2581
+ FIELDS
2582
+ end
2583
+
2255
2584
  def import_job_connection_fields
2256
2585
  connection_fields(import_job_fields)
2257
2586
  end
@@ -29,6 +29,39 @@ module CardDB
29
29
  File.new(data['fileUploadConfirm'], client: client)
30
30
  end
31
31
 
32
+ # Request a presigned URL, upload bytes directly to storage, and confirm the file.
33
+ def upload(
34
+ filename:,
35
+ content_type:,
36
+ body:,
37
+ size: nil,
38
+ purpose: nil,
39
+ is_public: false,
40
+ entity_type: nil,
41
+ entity_id: nil,
42
+ publisher_id: nil,
43
+ dataset_id: nil
44
+ )
45
+ upload_size = size || infer_upload_size(body)
46
+ raise ArgumentError, 'files.upload requires a positive size or an inferable body size' unless upload_size&.positive?
47
+
48
+ upload = request_upload(
49
+ input: {
50
+ filename: filename,
51
+ contentType: content_type,
52
+ size: upload_size,
53
+ isPublic: is_public,
54
+ entityType: entity_type || entity_type_for_purpose(purpose),
55
+ entityId: entity_id,
56
+ publisherId: publisher_id,
57
+ datasetId: dataset_id
58
+ }.compact
59
+ )
60
+
61
+ put_upload(upload.upload_url, content_type, body)
62
+ confirm_upload(id: upload.file.id)
63
+ end
64
+
32
65
  # Delete a file. Requires a server-side secret credential.
33
66
  # rubocop:disable Naming/PredicateMethod
34
67
  def delete(id:)
@@ -38,6 +71,48 @@ module CardDB
38
71
  !!data['fileDelete']
39
72
  end
40
73
  # rubocop:enable Naming/PredicateMethod
74
+
75
+ private
76
+
77
+ def put_upload(upload_url, content_type, body)
78
+ response = Faraday.put(upload_url) do |request|
79
+ request.headers['Content-Type'] = content_type
80
+ request.body = body
81
+ end
82
+
83
+ return if response.success?
84
+
85
+ raise Error, "file upload failed with #{response.status}: #{response.body}"
86
+ end
87
+
88
+ def infer_upload_size(body)
89
+ return body.bytesize if body.is_a?(String)
90
+ return body.bytesize if body.respond_to?(:bytesize)
91
+ return body.size if body.respond_to?(:size) && body.size.is_a?(Integer)
92
+ return body.stat.size if body.respond_to?(:stat)
93
+
94
+ infer_io_size(body)
95
+ end
96
+
97
+ def infer_io_size(body)
98
+ return nil unless body.respond_to?(:tell) && body.respond_to?(:seek)
99
+
100
+ original_position = body.tell
101
+ body.seek(0, IO::SEEK_END)
102
+ size = body.tell
103
+ body.seek(original_position, IO::SEEK_SET)
104
+ size
105
+ rescue IOError, SystemCallError
106
+ nil
107
+ end
108
+
109
+ def entity_type_for_purpose(purpose)
110
+ return nil if purpose.nil? || purpose.to_s == 'scan_image'
111
+ return 'import' if purpose.to_s == 'import'
112
+ return 'scan_template_example' if purpose.to_s == 'scan_template_example'
113
+
114
+ purpose.to_s
115
+ end
41
116
  end
42
117
  end
43
118
  end