carddb 0.3.10 → 0.4.0

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,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module CardDB
6
+ module Resources
7
+ # Scan job resource for GraphQL-first card scanning workflows.
8
+ class Scans < Base
9
+ TERMINAL_STATUSES = %w[completed failed cancelled].freeze
10
+
11
+ CREATE_JOB_KEY_MAP = {
12
+ publisher_slug: 'publisherSlug',
13
+ game_key: 'gameKey',
14
+ dataset_key: 'datasetKey',
15
+ file_id: 'fileId',
16
+ client_request_id: 'clientRequestId',
17
+ template_id: 'templateId',
18
+ template_version_id: 'templateVersionId',
19
+ client_ocr_hints: 'clientOcrHints',
20
+ client_normalization: 'clientNormalization',
21
+ webhook_url: 'webhookUrl',
22
+ webhook_secret: 'webhookSecret'
23
+ }.freeze
24
+
25
+ FEEDBACK_KEY_MAP = {
26
+ job_id: 'jobId',
27
+ selected_record_id: 'selectedRecordId'
28
+ }.freeze
29
+
30
+ METRICS_KEY_MAP = {
31
+ publisher_slug: 'publisherSlug',
32
+ game_key: 'gameKey',
33
+ dataset_key: 'datasetKey'
34
+ }.freeze
35
+
36
+ # Create a scan job for an already uploaded image file.
37
+ def create_job(input: nil, **params)
38
+ request = scoped_input(graphql_input(input || params, CREATE_JOB_KEY_MAP))
39
+ data = connection.execute(QueryBuilder.create_scan_job, { input: request })
40
+
41
+ ScanJobPayload.new(data['createScanJob'], client: client)
42
+ end
43
+
44
+ # Get a scan job and its latest processing result.
45
+ def get_job(id, cache: nil)
46
+ key = cache_key('scans', 'get_job', id: id)
47
+ with_cache(key, resource: :scans, cache: cache) do
48
+ data = connection.execute(QueryBuilder.scan_job, { id: id })
49
+ data['scanJob'] ? ScanJob.new(data['scanJob'], client: client) : nil
50
+ end
51
+ end
52
+
53
+ # Poll a scan job until it reaches a terminal status.
54
+ def poll_job(id, timeout: 300, interval: 1, backoff: 1.5, max_interval: 10, cancel: nil)
55
+ started_at = monotonic_time
56
+ current_interval = [interval.to_f, 0.001].max
57
+ max_interval = [max_interval.to_f, current_interval].max
58
+
59
+ loop do
60
+ raise Error, 'scan job polling cancelled' if cancel&.call
61
+
62
+ job = get_job(id, cache: false)
63
+ raise NotFoundError, "scan job '#{id}' was not found" unless job
64
+ return job if TERMINAL_STATUSES.include?(job.status.to_s.downcase)
65
+
66
+ remaining = timeout.to_f - (monotonic_time - started_at)
67
+ raise TimeoutError, "timed out waiting for scan job '#{id}'" if remaining <= 0
68
+
69
+ sleep([current_interval, remaining].min)
70
+ current_interval = [current_interval * [backoff.to_f, 1.0].max, max_interval].min
71
+ end
72
+ end
73
+
74
+ # Submit correctness feedback for a completed scan job.
75
+ def submit_feedback(input: nil, **params)
76
+ request = graphql_input(input || params, FEEDBACK_KEY_MAP)
77
+ data = connection.execute(QueryBuilder.submit_scan_feedback, { input: request })
78
+
79
+ ScanFeedbackPayload.new(data['submitScanFeedback'], client: client)
80
+ end
81
+
82
+ # Aggregate scan metrics for a publisher/game/dataset scope.
83
+ def metrics(input: nil, **params)
84
+ request = scoped_input(graphql_input(input || params, METRICS_KEY_MAP))
85
+ data = connection.execute(QueryBuilder.scan_metrics, { input: request })
86
+
87
+ ScanMetrics.new(data['scanMetrics'], client: client)
88
+ end
89
+
90
+ # Verify an X-CardDB-Signature header value for a webhook body.
91
+ # rubocop:disable Naming/PredicateMethod
92
+ def verify_webhook_signature(body:, signature:, secret:)
93
+ expected = "sha256=#{OpenSSL::HMAC.hexdigest('SHA256', secret.to_s, body.to_s)}"
94
+
95
+ secure_compare?(signature.to_s.strip.downcase, expected)
96
+ end
97
+ # rubocop:enable Naming/PredicateMethod
98
+
99
+ private
100
+
101
+ def scoped_input(input)
102
+ publisher_slug = input['publisherSlug'] || resolve_publisher(nil)
103
+ game_key = input['gameKey'] || resolve_game(nil)
104
+ validate_access!(publisher_slug, game_key)
105
+
106
+ input.merge('publisherSlug' => publisher_slug, 'gameKey' => game_key)
107
+ end
108
+
109
+ def graphql_input(input, key_map)
110
+ input.each_with_object({}) do |(key, value), output|
111
+ next if value.nil?
112
+
113
+ output[key_map[key.to_sym] || key.to_s] = value
114
+ end
115
+ end
116
+
117
+ def secure_compare?(left, right)
118
+ max_length = [left.bytesize, right.bytesize].max
119
+ diff = left.bytesize ^ right.bytesize
120
+
121
+ max_length.times do |index|
122
+ diff |= (left.getbyte(index) || 0) ^ (right.getbyte(index) || 0)
123
+ end
124
+
125
+ diff.zero?
126
+ end
127
+
128
+ def monotonic_time
129
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
130
+ end
131
+ end
132
+ end
133
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CardDB
4
- VERSION = '0.3.10'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/carddb.rb CHANGED
@@ -8,6 +8,7 @@ require_relative 'carddb/query_builder'
8
8
  require_relative 'carddb/filter_builder'
9
9
  require_relative 'carddb/collection'
10
10
  require_relative 'carddb/cache'
11
+ require_relative 'carddb/deck_tokens'
11
12
  require_relative 'carddb/resources/base'
12
13
  require_relative 'carddb/resources/publishers'
13
14
  require_relative 'carddb/resources/games'
@@ -17,6 +18,8 @@ require_relative 'carddb/resources/import_formats'
17
18
  require_relative 'carddb/resources/imports'
18
19
  require_relative 'carddb/resources/exports'
19
20
  require_relative 'carddb/resources/files'
21
+ require_relative 'carddb/resources/scans'
22
+ require_relative 'carddb/resources/scan_templates'
20
23
  require_relative 'carddb/resources/decks'
21
24
  require_relative 'carddb/resources/rules'
22
25
  require_relative 'carddb/batch'
@@ -142,6 +145,20 @@ module CardDB
142
145
  default_client.files
143
146
  end
144
147
 
148
+ # Access the Scans resource via the default client.
149
+ #
150
+ # @return [Resources::Scans]
151
+ def scans
152
+ default_client.scans
153
+ end
154
+
155
+ # Access the Scan Templates resource via the default client.
156
+ #
157
+ # @return [Resources::ScanTemplates]
158
+ def scan_templates
159
+ default_client.scan_templates
160
+ end
161
+
145
162
  # Access the Decks resource via the default client.
146
163
  #
147
164
  # @return [Resources::Decks]
@@ -185,8 +202,8 @@ module CardDB
185
202
  # results[0] # => Game
186
203
  # results[1] # => Game
187
204
  # results[2] # => Publisher
188
- def batch(&block)
189
- default_client.batch(&block)
205
+ def batch(&)
206
+ default_client.batch(&)
190
207
  end
191
208
  end
192
209
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carddb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.10
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - CardDB Team
@@ -9,6 +9,20 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ed25519
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.3'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: faraday
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -40,7 +54,7 @@ dependencies:
40
54
  description: A Ruby client library for interacting with the CardDB GraphQL API. Search
41
55
  and fetch card game data with an expressive filter DSL.
42
56
  email:
43
- - support@carddb.xtda.org
57
+ - support@carddb.dev
44
58
  executables: []
45
59
  extensions: []
46
60
  extra_rdoc_files: []
@@ -58,6 +72,7 @@ files:
58
72
  - examples/filtering.rb
59
73
  - examples/pagination.rb
60
74
  - examples/publisher_content_pipeline.rb
75
+ - examples/scan_workflow.rb
61
76
  - lib/carddb.rb
62
77
  - lib/carddb/batch.rb
63
78
  - lib/carddb/cache.rb
@@ -65,6 +80,7 @@ files:
65
80
  - lib/carddb/collection.rb
66
81
  - lib/carddb/configuration.rb
67
82
  - lib/carddb/connection.rb
83
+ - lib/carddb/deck_tokens.rb
68
84
  - lib/carddb/errors.rb
69
85
  - lib/carddb/filter_builder.rb
70
86
  - lib/carddb/query_builder.rb
@@ -79,15 +95,17 @@ files:
79
95
  - lib/carddb/resources/publishers.rb
80
96
  - lib/carddb/resources/records.rb
81
97
  - lib/carddb/resources/rules.rb
98
+ - lib/carddb/resources/scan_templates.rb
99
+ - lib/carddb/resources/scans.rb
82
100
  - lib/carddb/version.rb
83
101
  - scripts/publish.sh
84
- homepage: https://github.com/xtda/carddb-ruby
102
+ homepage: https://carddb.dev
85
103
  licenses:
86
104
  - MIT
87
105
  metadata:
88
- homepage_uri: https://github.com/xtda/carddb-ruby
89
- source_code_uri: https://github.com/xtda/carddb-ruby
90
- changelog_uri: https://github.com/xtda/carddb-ruby/blob/main/CHANGELOG.md
106
+ homepage_uri: https://carddb.dev
107
+ source_code_uri: https://github.com/carddb/carddb-ruby
108
+ changelog_uri: https://github.com/carddb/carddb-ruby/blob/main/CHANGELOG.md
91
109
  rubygems_mfa_required: 'true'
92
110
  rdoc_options: []
93
111
  require_paths:
@@ -96,7 +114,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
96
114
  requirements:
97
115
  - - ">="
98
116
  - !ruby/object:Gem::Version
99
- version: 3.0.0
117
+ version: 3.2.0
100
118
  required_rubygems_version: !ruby/object:Gem::Requirement
101
119
  requirements:
102
120
  - - ">="