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.
- checksums.yaml +4 -4
- data/README.md +3 -1
- data/lib/calmlens/assets.rb +107 -98
- data/lib/calmlens/cdn.rb +197 -41
- data/lib/calmlens/error_codes.rb +80 -84
- data/lib/calmlens/errors.rb +2 -2
- data/lib/calmlens/events.rb +50 -1
- data/lib/calmlens/folders.rb +14 -9
- data/lib/calmlens/hash_lists.rb +29 -9
- data/lib/calmlens/internal.rb +9 -19
- data/lib/calmlens/lenses.rb +79 -6
- data/lib/calmlens/option_keys.rb +46 -25
- data/lib/calmlens/policies.rb +50 -18
- data/lib/calmlens/resource.rb +24 -6
- data/lib/calmlens/server_client.rb +8 -6
- data/lib/calmlens/session.rb +29 -12
- data/lib/calmlens/testing/asset_filters.rb +39 -0
- data/lib/calmlens/testing/classification.rb +8 -2
- data/lib/calmlens/testing/fake_api.rb +682 -368
- data/lib/calmlens/testing/mock_decision.rb +5 -4
- data/lib/calmlens/testing/rsql.rb +245 -0
- data/lib/calmlens/testing/test_input_detections.rb +61 -0
- data/lib/calmlens/testing/test_input_scenario.rb +6 -2
- data/lib/calmlens/testing/test_inputs.rb +31 -10
- data/lib/calmlens/testing.rb +36 -12
- data/lib/calmlens/transform_rules.rb +1 -1
- data/lib/calmlens/upload_mime_types.rb +9 -1
- data/lib/calmlens/upload_tokens.rb +3 -3
- data/lib/calmlens/version.rb +1 -1
- data/lib/calmlens/webhooks.rb +58 -13
- data/sig/calmlens.rbs +87 -13
- data/sig/error_codes.rbs +18 -20
- data/sig/qualifiers.rbs +2 -0
- data/sig/types.rbs +326 -187
- metadata +5 -2
|
@@ -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
|
|
123
|
-
# this client already saved are not sent again
|
|
124
|
-
#
|
|
125
|
-
# consecutive requests, in order.
|
|
126
|
-
# @option options [Array<Hash>] :lenses `[{identifier:, name:, type:,
|
|
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
|
|
data/lib/calmlens/session.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|