calmlens 0.20.0 → 0.21.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.
@@ -49,6 +49,8 @@ module CalmLens
49
49
  attr_reader :events
50
50
  # @return [CDN]
51
51
  attr_reader :cdn
52
+ # @return [Lenses]
53
+ attr_reader :lenses
52
54
 
53
55
  # @param api_key [String] a project API key. Keep it on your server.
54
56
  # @param project_id [String]
@@ -64,7 +66,7 @@ module CalmLens
64
66
  # @param max_response_bytes [Integer] the largest response body read; a larger one fails with
65
67
  # `sdk/response-too-large`, before it is read when its `Content-Length` says so.
66
68
  # @param max_retries [Integer] retries of requests that are safe to repeat: GET/HEAD, and uploads,
67
- # which carry an `Idempotency-Key`. Connection failures, timeouts, 429, 502, 503, 504 and
69
+ # which carry an `Idempotency-Key`. Connection failures, timeouts, 429, 500, 502, 503, 504 and
68
70
  # `idempotency/request-in-progress` are retried with jittered exponential backoff, honouring
69
71
  # `Retry-After`. Other writes are never retried. 0 disables retries.
70
72
  # @param retry_base_delay [Numeric] seconds before the first retry when the API gives no
@@ -119,11 +121,11 @@ module CalmLens
119
121
  nil
120
122
  end
121
123
 
122
- # Creates or updates named lenses (saved transforms) and returns them. Definitions
123
- # this client already saved are not sent again. Supplying the same definition and version is
124
- # idempotent; changing a lens needs a higher `version`. More than 100 new definitions are sent in
125
- # consecutive requests, in order.
126
- # @option options [Array<Hash>] :lenses `[{identifier:, name:, type:, version:, transform:, ...}]`
124
+ # Creates named lenses (saved transforms), or changes them to these definitions, and returns
125
+ # them. Definitions this client already saved are not sent again, and sending the same
126
+ # definition again returns the lens unchanged. More than 100 new definitions are sent in
127
+ # consecutive requests, in order. {#lenses} manages lenses one by one.
128
+ # @option options [Array<Hash>] :lenses `[{identifier:, name:, type:, active:, transform:, ...}]`
127
129
  # @return [Array<Hash>] the saved lenses, in the given order.
128
130
  def ensure_lenses(options) = @lenses.ensure_lenses(options)
129
131
 
@@ -10,7 +10,7 @@ module CalmLens
10
10
  # The resources share one session per client. It never prints the API key.
11
11
  # @api private
12
12
  class Session
13
- RETRYABLE_STATUSES = [429, 502, 503, 504].freeze
13
+ RETRYABLE_STATUSES = [429, 500, 502, 503, 504].freeze
14
14
  USER_AGENT = "calmlens-ruby/#{VERSION} (ruby/#{RUBY_VERSION})".freeze
15
15
 
16
16
  # Raised through the transport so an exception from the caller's streaming block is
@@ -99,10 +99,11 @@ module CalmLens
99
99
 
100
100
  # Sends a CDN request, retrying like an API read, and returns the last response rather than
101
101
  # raising an error status. With a block, yields the streaming {Response} and returns the block's
102
- # value; otherwise returns the response with its body read.
103
- def cdn(method, url, headers, request_options, &block)
102
+ # value; otherwise returns the response with its body read. `authenticate: false` sends no API
103
+ # key, as a browser would.
104
+ def cdn(method, url, headers, request_options, authenticate: true, &block)
104
105
  limit = request_options.fetch(:max_response_bytes, nil)
105
- execute(method, url, headers, nil, request_options, allow_http_errors: true) do |response|
106
+ execute(method, url, headers, nil, request_options, allow_http_errors: true, authenticate: authenticate) do |response|
106
107
  next response.tap { response.body(limit: limit) } unless block
107
108
 
108
109
  streaming = Response.new(status: response.status, headers: response.headers) do |&sink|
@@ -122,6 +123,14 @@ module CalmLens
122
123
  end
123
124
  end
124
125
 
126
+ # Fetches JSON from the CDN without the API key, as a browser would, and returns it parsed. An
127
+ # error status raises the error the CDN's body names, such as `signed-url/expired`.
128
+ def cdn_json(url)
129
+ execute('GET', url, { 'Accept' => 'application/json' }, nil, {}, authenticate: false) do |response|
130
+ parse('GET', response, @max_response_bytes)
131
+ end
132
+ end
133
+
125
134
  # Resolves lens definitions, sending only definitions this client has not saved before, at most
126
135
  # ServerClient::ENSURE_LENSES_MAX_PER_REQUEST per request, in order. Calls are serialized, so concurrent calls
127
136
  # for the same definitions send them once.
@@ -142,7 +151,7 @@ module CalmLens
142
151
  raise InvalidResponseError.new(SdkErrorCodes::INVALID_RESPONSE, 'Unexpected lens response')
143
152
  end
144
153
 
145
- # A new version of a lens replaces the cached definition with the same identifier.
154
+ # A changed lens replaces the cached definition with the same identifier.
146
155
  chunk.keys.zip(returned).each do |key, lens|
147
156
  @lenses.delete_if { |_, saved| saved['identifier'] == lens['identifier'] }
148
157
  @lenses[key] = lens
@@ -156,9 +165,16 @@ module CalmLens
156
165
  end
157
166
  end
158
167
 
159
- def headers_for(extra)
160
- extra.reject { |name, _| %w[x-api-key user-agent authorization].include?(name.downcase) }
161
- .merge('X-Api-Key' => @api_key, 'User-Agent' => USER_AGENT)
168
+ # Drops the cached definitions of the lenses the block selects, which lenses.create, update
169
+ # and delete changed, so ensure_lenses sends their definitions again.
170
+ def forget_lenses(&selected)
171
+ @lens_lock.synchronize { @lenses.delete_if { |_, lens| selected.call(lens) } }
172
+ end
173
+
174
+ def headers_for(extra, authenticate: true)
175
+ headers = extra.reject { |name, _| %w[x-api-key user-agent authorization].include?(name.downcase) }
176
+ .merge('User-Agent' => USER_AGENT)
177
+ authenticate ? headers.merge('X-Api-Key' => @api_key) : headers
162
178
  end
163
179
 
164
180
  private
@@ -168,7 +184,7 @@ module CalmLens
168
184
  def lens_key(lens)
169
185
  fields = { 'identifier' => lens['identifier'], 'name' => lens['name'], 'description' => lens['description'],
170
186
  'type' => lens['type'], 'transform' => lens['transform'] }
171
- %w[active version].each { |name| fields[name] = lens[name] if lens.key?(name) }
187
+ fields['active'] = lens['active'] if lens.key?('active')
172
188
  JSON.generate(canonical(fields))
173
189
  end
174
190
 
@@ -186,14 +202,15 @@ module CalmLens
186
202
  returned.zip(sent).all? { |saved, definition| saved.is_a?(Hash) && saved['identifier'] == definition['identifier'] }
187
203
  end
188
204
 
189
- # Sends a request, retrying transport failures, timeouts, 429, 502–504 and
205
+ # Sends a request, retrying transport failures, timeouts, 429, 500, 502–504 and
190
206
  # `idempotency/request-in-progress` when it is safe to repeat: reads, and requests with an
191
207
  # `Idempotency-Key` whose body can be sent again. Redirects are never followed.
192
- def execute(method, url, headers, body, request_options, allow_http_errors: false, deadline: nil, &handler)
208
+ def execute(method, url, headers, body, request_options, allow_http_errors: false, deadline: nil, authenticate: true, &handler)
193
209
  retries = request_options.fetch(:max_retries, @max_retries)
194
210
  retryable = retries.positive? && (%w[GET HEAD].include?(method) || headers.key?('Idempotency-Key')) &&
195
211
  (body.nil? || body.replayable?)
196
- request = Transport::Request.new(method: method, url: url, headers: headers_for(headers), body: body&.content,
212
+ request = Transport::Request.new(method: method, url: url, headers: headers_for(headers, authenticate: authenticate),
213
+ body: body&.content,
197
214
  open_timeout: request_options.fetch(:open_timeout, @open_timeout),
198
215
  timeout: request_options.fetch(:timeout, @timeout))
199
216
  limit = request_options.fetch(:max_response_bytes, @max_response_bytes)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generated by packages/sdk-contract/mockAssetFilters.ts from ASSET_FILTER_FIELDS (packages/shared-backend/src/AssetFilters.ts) and listFilterOperators (PaginationUtils.ts); do not edit.
4
+ # A filter may name only these fields, each with only its operators; anything else is
5
+ # filter/invalid. A number field's value must be a finite number, and `null` is a
6
+ # missing value only on a nullable field.
7
+ module CalmLens
8
+ module MockAssetFilters
9
+ # Each field a filter may name, with its kind, whether it is nullable and its operators.
10
+ FIELDS = {
11
+ 'id' => { kind: 'text', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
12
+ 'parentId' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
13
+ 'folderId' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
14
+ 'uploadPresetId' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
15
+ 'createdAt' => { kind: 'number', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
16
+ 'updatedAt' => { kind: 'number', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
17
+ 'type' => { kind: 'text', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
18
+ 'name' => { kind: 'text', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
19
+ 'description' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
20
+ 'externalId' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
21
+ 'externalUrl' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
22
+ 'authorId' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
23
+ 'tags' => { kind: 'list', nullable: false, operators: ['=in=', '=out='].freeze }.freeze,
24
+ 'visibility' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
25
+ 'upload.status' => { kind: 'text', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
26
+ 'upload.updatedAt' => { kind: 'number', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
27
+ 'classification.status' => { kind: 'text', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
28
+ 'classification.updatedAt' => { kind: 'number', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
29
+ 'classification.completedAt' => { kind: 'number', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
30
+ 'classification.moderation.status' => { kind: 'text', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
31
+ 'content.status' => { kind: 'text', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
32
+ 'content.removalReason' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
33
+ 'content.updatedAt' => { kind: 'number', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
34
+ 'content.format' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
35
+ 'content.extension' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
36
+ 'content.sizeInBytes' => { kind: 'number', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
37
+ }.freeze
38
+ end
39
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Generated by packages/sdk-contract/mockClassification.ts from packages/shared/src/ModerationCategories.ts and ModerationPolicyDimensions.ts; do not edit.
3
+ # Generated by packages/sdk-contract/mockClassification.ts from packages/shared/src/ModerationCategories.ts, ModerationPolicyDimensions.ts and TestTriggers.ts; do not edit.
4
4
  module CalmLens
5
5
  module MockClassification
6
6
  # The parent categories, in the API's order.
@@ -8,7 +8,7 @@ module CalmLens
8
8
 
9
9
  # Each category's qualifiers.
10
10
  QUALIFIERS = {
11
- "sexual" => ["sexual-pornographic-media", "sexual-nudity", "sexual-activity", "sexual-explicit-language", "sexual-assault-media", "sexual-non-consensual", "sexual-intimate-image-abuse", "sexual-extreme", "sexual-minors", "sexual-solicitation", "sexual-paid", "sexual-adult-promotion", "sexual-revealing-clothing", "sexual-suggestive", "sexual-non-erotic-nudity", "sexual-text", "sexual-historical", "sexual-documentary", "sexual-educational", "sexual-discussion", "sexual-fictional", "sexual-support-prevention"].freeze,
11
+ "sexual" => ["sexual-pornographic-media", "sexual-nudity", "sexual-activity", "sexual-explicit-language", "sexual-assault-media", "sexual-non-consensual", "sexual-intimate-image-abuse", "sexual-extreme", "sexual-strangulation", "sexual-incest", "sexual-minors", "sexual-solicitation", "sexual-paid", "sexual-adult-promotion", "sexual-revealing-clothing", "sexual-suggestive", "sexual-non-erotic-nudity", "sexual-text", "sexual-historical", "sexual-documentary", "sexual-educational", "sexual-discussion", "sexual-fictional", "sexual-support-prevention"].freeze,
12
12
  "violence" => ["violence-high-impact", "violence-attack", "violence-threat", "violence-preparation", "violence-encouragement", "violence-wish", "violence-coded-incitement", "violence-extremism", "violence-injury", "violence-violent-death", "violence-natural-death", "violence-graphic", "violence-decapitation-act", "violence-gore", "violence-weapon", "violence-sport", "violence-text", "violence-historical", "violence-documentary", "violence-educational", "violence-discussion", "violence-fictional", "violence-fantasy", "violence-support-prevention"].freeze,
13
13
  "self-harm" => ["self-harm-age-restricted", "self-harm-thoughts", "self-harm-preparation", "self-harm-act", "self-harm-methods", "self-harm-encouragement", "self-harm-historical", "self-harm-documentary", "self-harm-educational", "self-harm-fictional", "self-harm-support-prevention"].freeze,
14
14
  "harassment" => ["harassment-abuse", "harassment-mild", "harassment-bullying", "harassment-public-target", "harassment-threat", "harassment-sexual", "harassment-encouragement", "harassment-historical", "harassment-documentary", "harassment-educational", "harassment-fictional", "harassment-support-prevention"].freeze,
@@ -20,5 +20,11 @@ module CalmLens
20
20
 
21
21
  # Qualifiers the input type sets, which a test input scenario cannot list.
22
22
  INPUT_QUALIFIERS = ["sexual-text", "violence-text"].freeze
23
+
24
+ # Test scenario detections: entries per list, the basis points of a full side, the oldest age and the age presets.
25
+ TEST_DETECTIONS_MAX = 32
26
+ TEST_DETECTION_BOX_MAX = 10000
27
+ TEST_FACE_AGE_MAX = 120
28
+ TEST_FACE_AGE_PRESETS = ["adult", "minor"].freeze
23
29
  end
24
30
  end