calmlens 0.23.0 → 0.24.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/lib/calmlens/assets.rb +17 -10
- data/lib/calmlens/cdn.rb +2 -2
- data/lib/calmlens/internal.rb +1 -1
- data/lib/calmlens/testing/fake_api.rb +27 -15
- data/lib/calmlens/testing.rb +4 -4
- data/lib/calmlens/version.rb +1 -1
- data/sig/types.rbs +5 -3
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 977f88cea877f294fefbc6259eef7e99c7327ba3e90333540da37c86ad74b599
|
|
4
|
+
data.tar.gz: 1ac90ad7c9fa1a7c11bf1abce6595efffb58b0cbdbd2a93035f1f1bc8f0c5ab9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6654f2b874569fd13005016adc0a7efbec542db6bf378fa04d5ebfe4f3bb43c7cd85b04c504ac6110966d826f9a92c1e1d9a82aa796e19ed01b3f00920cc1179
|
|
7
|
+
data.tar.gz: 26e933bc44b4577d13be2e01907c9578cd84ebe3ec3dd2e51d969da5b6f990254466a3728dccb0ecd6e1a2c74699769ed5d5bfd0f8ab90a36ce8224bcf171e9b
|
data/lib/calmlens/assets.rb
CHANGED
|
@@ -359,24 +359,31 @@ module CalmLens
|
|
|
359
359
|
def poll_for_decision(asset_id, request_options, timeout)
|
|
360
360
|
deadline = @session.monotonic + timeout
|
|
361
361
|
delay = DECISION_POLL_FIRST_DELAY
|
|
362
|
+
processing = moderation = nil
|
|
362
363
|
loop do
|
|
363
|
-
|
|
364
|
+
remaining = deadline - @session.monotonic
|
|
365
|
+
break if timeout.positive? && !remaining.positive?
|
|
366
|
+
|
|
367
|
+
# The outer deadline covers transport retries and the entire response read.
|
|
368
|
+
asset = ::Timeout.timeout(timeout.zero? ? nil : remaining) do
|
|
369
|
+
retrieve('assetId' => asset_id, 'request_options' => request_options)
|
|
370
|
+
end
|
|
371
|
+
remaining = deadline - @session.monotonic
|
|
372
|
+
break if timeout.positive? && !remaining.positive?
|
|
373
|
+
|
|
364
374
|
processing = asset.dig('classification', 'status')
|
|
365
375
|
moderation = asset.dig('classification', 'moderation', 'status')
|
|
366
376
|
return asset if SETTLED_STATUSES.include?(processing)
|
|
367
|
-
|
|
368
|
-
remaining = deadline - @session.monotonic
|
|
369
|
-
unless remaining.positive?
|
|
370
|
-
raise TimeoutError.new(SdkErrorCodes::TIMEOUT,
|
|
371
|
-
"Asset #{asset['id']} was not settled after #{timeout}s " \
|
|
372
|
-
"(processing #{processing}, moderation #{moderation})",
|
|
373
|
-
details: { 'assetId' => asset['id'], 'classificationStatus' => processing,
|
|
374
|
-
'moderationStatus' => moderation })
|
|
375
|
-
end
|
|
377
|
+
break unless remaining.positive?
|
|
376
378
|
|
|
377
379
|
sleep([delay, remaining].min)
|
|
378
380
|
delay = [delay * DECISION_POLL_FACTOR, DECISION_POLL_MAX_DELAY].min
|
|
379
381
|
end
|
|
382
|
+
raise ::Timeout::Error
|
|
383
|
+
rescue ::Timeout::Error
|
|
384
|
+
raise TimeoutError.new(SdkErrorCodes::TIMEOUT, "Asset #{asset_id} was not settled after #{timeout}s",
|
|
385
|
+
details: { 'assetId' => asset_id, 'classificationStatus' => processing,
|
|
386
|
+
'moderationStatus' => moderation })
|
|
380
387
|
end
|
|
381
388
|
|
|
382
389
|
def submit(options, request_options, wait)
|
data/lib/calmlens/cdn.rb
CHANGED
|
@@ -54,8 +54,8 @@ module CalmLens
|
|
|
54
54
|
# client accepts, a PDF, or browser-safe video or audio), `original` (the exact upload), `preview` (a
|
|
55
55
|
# still image for the asset), `hls` (the HLS master playlist of audio or video, at `hls/index.m3u8`,
|
|
56
56
|
# once `content.hls.status` is `ready`), `embed` (an HTML page that shows the asset, a viewer for its type, for an
|
|
57
|
-
# iframe), `placeholder` (a small blurred PNG
|
|
58
|
-
#
|
|
57
|
+
# iframe), `placeholder` (a small blurred PNG, without age confirmation; private or blocked assets
|
|
58
|
+
# require a signed URL or download permission) or `info` (the asset's display info, as
|
|
59
59
|
# {#get_asset_info} reads it). Without it, the URL is the asset URL itself. Lenses and transforms
|
|
60
60
|
# apply to `preview`, and to `rendered` when it is an image; the CDN refuses them on rendered PDFs,
|
|
61
61
|
# video and audio with `transform/invalid`, and the SDK refuses them on the other resources and
|
data/lib/calmlens/internal.rb
CHANGED
|
@@ -14,7 +14,7 @@ module CalmLens
|
|
|
14
14
|
IDEMPOTENCY_KEY = /\A[^\r\n\0]+\z/
|
|
15
15
|
HEADER_VALUE = /\A[^\r\n\0]*\z/
|
|
16
16
|
# The CDN resources that take no lens or transform: the exact upload, the adaptive stream, the embed page,
|
|
17
|
-
# the
|
|
17
|
+
# the placeholder and the display info.
|
|
18
18
|
UNSELECTABLE_CDN_RESOURCES = %w[original hls embed placeholder info].freeze
|
|
19
19
|
|
|
20
20
|
module_function
|
|
@@ -730,7 +730,8 @@ module CalmLens
|
|
|
730
730
|
end
|
|
731
731
|
|
|
732
732
|
def show_project_settings
|
|
733
|
-
settings = { '
|
|
733
|
+
settings = { 'retainContent' => true, 'optOutJurisdictions' => [], 'blockUnmoderatedAssets' => true,
|
|
734
|
+
'storeSensitiveMetadata' => false,
|
|
734
735
|
'streamingEnabled' => false, 'aiMediaSearchEnabled' => false, 'remoteUrlRule' => nil,
|
|
735
736
|
'websiteSubmissionsPerMinute' => nil }.merge(@project_settings)
|
|
736
737
|
settings['humanReview'] = { 'allowedSamplePercent' => 0, 'restrictedSamplePercent' => 0, 'uncertaintyMarginPercent' => 0,
|
|
@@ -842,8 +843,9 @@ module CalmLens
|
|
|
842
843
|
end
|
|
843
844
|
now = @now.call
|
|
844
845
|
mime = options['contentType'].to_s
|
|
845
|
-
processing = options['processingOptions'] || {
|
|
846
|
-
|
|
846
|
+
processing = (options['processingOptions'] || {}).dup
|
|
847
|
+
processing['retainContent'] = show_project_settings['settings']['retainContent'] if processing['retainContent'].nil?
|
|
848
|
+
keep = processing.is_a?(Hash) && processing['retainContent'] == true
|
|
847
849
|
context = options['context'].is_a?(Hash) ? ModerationContext.select_items(options['context'].fetch('items', [])) : []
|
|
848
850
|
# Fields the API returns as null when they are not set.
|
|
849
851
|
asset = { 'id' => @create_id.call('asset'), 'projectId' => @project_id, 'parentId' => nil, 'parentPages' => [],
|
|
@@ -946,8 +948,12 @@ module CalmLens
|
|
|
946
948
|
fail!(ErrorCodes::UPLOAD_PRESET_DISABLED) unless preset['enabled']
|
|
947
949
|
check_preset_fields(preset, fields)
|
|
948
950
|
check_preset_limits(preset, fields, data)
|
|
951
|
+
retain = fields.dig('processingOptions', 'retainContent')
|
|
952
|
+
retain = preset.dig('processingOptions', 'retainContent') if retain.nil?
|
|
949
953
|
fields.merge('folderId' => preset['folderId'] || fields['folderId'], 'visibility' => preset['visibility'],
|
|
950
|
-
'processingOptions' => fields['processingOptions'] || deep_copy(preset['processingOptions']))
|
|
954
|
+
'processingOptions' => (fields['processingOptions'] || deep_copy(preset['processingOptions']) || {}).merge(
|
|
955
|
+
'retainContent' => retain
|
|
956
|
+
))
|
|
951
957
|
end
|
|
952
958
|
|
|
953
959
|
def check_preset_fields(preset, fields)
|
|
@@ -1782,8 +1788,8 @@ module CalmLens
|
|
|
1782
1788
|
# Answers like the CDN (serveAssetResource.ts): `original` is the upload; `rendered` the image,
|
|
1783
1789
|
# PDF, video or audio file `content.rendered` describes; `preview` the still `content.preview`
|
|
1784
1790
|
# describes; `hls` the adaptive stream's files, which the mock never prepares; `embed` a small
|
|
1785
|
-
# page standing in for the embed page of any asset; and `placeholder` the preview's ThumbHash as a PNG
|
|
1786
|
-
#
|
|
1791
|
+
# page standing in for the embed page of any asset; and `placeholder` the preview's ThumbHash as a PNG.
|
|
1792
|
+
# A safety hold refuses every resource. Other resources pass delivery checks first,
|
|
1787
1793
|
# which refuse everyone (content still uploading, blocked or purged, failed processing, removed
|
|
1788
1794
|
# content), then an age-restricted asset needs `viewerClaims.ageConfirmed=true` whatever the key,
|
|
1789
1795
|
# and a private asset needs an API key with `assets:download` for its folder. The mock converts
|
|
@@ -1809,21 +1815,21 @@ module CalmLens
|
|
|
1809
1815
|
authorised = !grant.nil? || can_download?(item['folderId'], headers)
|
|
1810
1816
|
return info(method, item, authorised, params) if resource == 'info'
|
|
1811
1817
|
return not_available if item['content']['safetyHold']
|
|
1812
|
-
return placeholder(method, item) if resource == 'placeholder'
|
|
1818
|
+
return placeholder(method, item, authorised) if resource == 'placeholder'
|
|
1813
1819
|
|
|
1814
1820
|
deliver(method, item, resource, params, selected ? options || {} : nil, authorised)
|
|
1815
1821
|
end
|
|
1816
1822
|
|
|
1817
1823
|
# What a grant doesn't cover, or, like the CDN, a private asset for anyone without a grant or the
|
|
1818
1824
|
# project's API key: it answers as an unknown asset does, whatever its state, except for its
|
|
1819
|
-
#
|
|
1825
|
+
# info, which has its own rule. An error response, or nil.
|
|
1820
1826
|
def refuse_viewer(item, resource, grant, headers)
|
|
1821
1827
|
if resource == 'original' && grant && !grant[:original]
|
|
1822
1828
|
return error(ErrorCodes::SIGNED_URL_ORIGINAL_NOT_GRANTED, "This signed asset URL doesn't grant the original file", status: 403)
|
|
1823
1829
|
end
|
|
1824
1830
|
return if grant || item['visibility'] == 'public' || header(headers, 'X-Api-Key') == @api_key
|
|
1825
1831
|
|
|
1826
|
-
asset_not_found unless
|
|
1832
|
+
asset_not_found unless resource == 'info'
|
|
1827
1833
|
end
|
|
1828
1834
|
|
|
1829
1835
|
# Whether the path after the resource selects a lens or transform, and its options; nil when the
|
|
@@ -1866,12 +1872,15 @@ module CalmLens
|
|
|
1866
1872
|
restricted = item['visibility'] != 'public'
|
|
1867
1873
|
return asset_not_found if restricted && !authorised
|
|
1868
1874
|
|
|
1875
|
+
restricted ||= %w[blocked purged].include?(item['classification']['moderation']['status'])
|
|
1876
|
+
return not_available if restricted && !authorised
|
|
1877
|
+
|
|
1869
1878
|
claims = JSON.parse(params.fetch('viewerClaims', '{}'))
|
|
1870
1879
|
body = { 'id' => item['id'], 'projectId' => item['projectId'], 'type' => item['type'], 'externalUrl' => item['externalUrl'],
|
|
1871
1880
|
'content' => item['content'].slice(*%w[format width height durationSeconds pageCount rendered preview hls]),
|
|
1872
1881
|
'availability' => availability(item, claims.is_a?(Hash) && claims['ageConfirmed'] == true) }
|
|
1873
1882
|
cdn_response(method, JSON.generate(body), { 'content-type' => 'application/json',
|
|
1874
|
-
'cache-control' =>
|
|
1883
|
+
'cache-control' => restricted ? 'private, no-store' : 'public, no-cache',
|
|
1875
1884
|
'cache-tag' => "asset-#{item['id']}" })
|
|
1876
1885
|
rescue JSON::ParserError
|
|
1877
1886
|
error(ErrorCodes::INVALID_VIEWER_CLAIMS, 'Invalid viewer claims', status: 400)
|
|
@@ -1980,15 +1989,18 @@ module CalmLens
|
|
|
1980
1989
|
|
|
1981
1990
|
def not_available = error('asset/not-available', 'Asset is not available', status: 403)
|
|
1982
1991
|
|
|
1983
|
-
#
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1992
|
+
# A preview requires access for private or blocked assets and disappears with its content.
|
|
1993
|
+
def placeholder(method, item, authorised)
|
|
1994
|
+
private_asset = item['visibility'] != 'public'
|
|
1995
|
+
blocked = item['classification']['moderation']['status'] == 'blocked'
|
|
1996
|
+
return asset_not_found if private_asset && !authorised
|
|
1997
|
+
return not_available if blocked && !authorised
|
|
1998
|
+
|
|
1987
1999
|
thumbhash = item.dig('content', 'preview', 'thumbhash')
|
|
1988
2000
|
return not_available if thumbhash.nil? || item['classification']['moderation']['status'] == 'purged'
|
|
1989
2001
|
|
|
1990
2002
|
cdn_response(method, Placeholder.png(thumbhash),
|
|
1991
|
-
'content-type' => 'image/png', 'cache-control' => 'public,
|
|
2003
|
+
'content-type' => 'image/png', 'cache-control' => private_asset || blocked ? 'private, no-store' : 'public, no-cache',
|
|
1992
2004
|
'cache-tag' => "asset-#{item['id']}", 'etag' => "\"#{thumbhash}\"")
|
|
1993
2005
|
end
|
|
1994
2006
|
|
data/lib/calmlens/testing.rb
CHANGED
|
@@ -17,7 +17,7 @@ module CalmLens
|
|
|
17
17
|
# callback decides each upload, retry and rerun with a status or a {MockDecision}, and the events
|
|
18
18
|
# feed gets the pipeline's `asset.uploaded`, `asset.classified`, `asset.<status>` and `asset.failed`
|
|
19
19
|
# events. Without it, queued uploads stay `pending` until {#complete_processing} and the rest are
|
|
20
|
-
# `allowed`. Content
|
|
20
|
+
# `allowed`. Content retention follows upload, preset and project settings; when disabled, content is removed
|
|
21
21
|
# once classification is `completed`, as in the API.
|
|
22
22
|
# A `purged` outcome removes the content with reason `policy`. The CDN answers like the real one:
|
|
23
23
|
# `asset/not-available` (403) for blocked, purged, failed or removed content,
|
|
@@ -28,8 +28,8 @@ module CalmLens
|
|
|
28
28
|
# `{cdn_base_url}/assets/{project_id}/sig_…`, a readable grant that only the mock's CDN accepts,
|
|
29
29
|
# which serves `original` only when signed with `original: true`. {#fetch_cdn} loads such a URL
|
|
30
30
|
# without the API key, as a browser would; without the key or a grant, a private asset answers
|
|
31
|
-
# as an unknown one does, `asset/not-found` (404) whatever its state,
|
|
32
|
-
#
|
|
31
|
+
# as an unknown one does, `asset/not-found` (404) whatever its state, including its placeholder
|
|
32
|
+
# and display info. {#decide} applies a reviewer's decision, and {Assets#wait_for_decision} takes no real
|
|
33
33
|
# time: a pending asset times out at once.
|
|
34
34
|
# Filters join comparisons with `;` (and), which binds tighter than `,` (or), and group them with
|
|
35
35
|
# `( … )`; list cursors resume after the last item returned. {Assets#search} is the API's keyword
|
|
@@ -275,7 +275,7 @@ module CalmLens
|
|
|
275
275
|
# asset URL from {CDN#sign_asset_url} (with a resource appended), or a public one from
|
|
276
276
|
# {CDN#get_asset_url}. The request is recorded like any other. Error statuses are returned, not
|
|
277
277
|
# raised. A private asset without a grant answers `asset/not-found` (404) whatever its state,
|
|
278
|
-
#
|
|
278
|
+
# including its placeholder and display info.
|
|
279
279
|
# @param url [String]
|
|
280
280
|
# @return [Response] with its body read
|
|
281
281
|
def fetch_cdn(url)
|
data/lib/calmlens/version.rb
CHANGED
data/sig/types.rbs
CHANGED
|
@@ -70,12 +70,12 @@ module CalmLens
|
|
|
70
70
|
?face_detection: { enabled: bool, ?age_estimation: bool }?,
|
|
71
71
|
?link_detection: { enabled: bool }?,
|
|
72
72
|
# Preview. Prepares an adaptive stream (HLS) of audio or video, served at the `hls` resource and
|
|
73
|
-
# played by the `embed` resource; needs `
|
|
73
|
+
# played by the `embed` resource; needs `retain_content`. Left out, the project's
|
|
74
74
|
# streaming setting decides.
|
|
75
75
|
?streaming: { enabled: bool }?
|
|
76
76
|
}
|
|
77
77
|
type processing_options_input = {
|
|
78
|
-
?
|
|
78
|
+
?retain_content: bool?,
|
|
79
79
|
# Overrides the project's `storeSensitiveMetadata` setting: false removes embedded EXIF, XMP and
|
|
80
80
|
# IPTC metadata before the image is stored; true stores it as uploaded.
|
|
81
81
|
?store_sensitive_metadata: bool?,
|
|
@@ -658,7 +658,7 @@ module CalmLens
|
|
|
658
658
|
type upload_preset_access = "token" | "anyone"
|
|
659
659
|
# An upload preset's processing options, as the API returns them.
|
|
660
660
|
type upload_preset_processing_options = {
|
|
661
|
-
?"
|
|
661
|
+
?"retainContent" => bool?,
|
|
662
662
|
?"storeSensitiveMetadata" => bool?,
|
|
663
663
|
?"features" => {
|
|
664
664
|
?"ocr" => { "enabled" => bool }?,
|
|
@@ -703,6 +703,7 @@ module CalmLens
|
|
|
703
703
|
}
|
|
704
704
|
type public_project_settings = {
|
|
705
705
|
"optOutJurisdictions" => Array[String],
|
|
706
|
+
"retainContent" => bool,
|
|
706
707
|
"blockUnmoderatedAssets" => bool,
|
|
707
708
|
"storeSensitiveMetadata" => bool,
|
|
708
709
|
"streamingEnabled" => bool,
|
|
@@ -1204,6 +1205,7 @@ module CalmLens
|
|
|
1204
1205
|
# The complete settings: those left out return to their defaults.
|
|
1205
1206
|
type project_settings_input = {
|
|
1206
1207
|
?opt_out_jurisdictions: Array[String],
|
|
1208
|
+
?retain_content: bool,
|
|
1207
1209
|
?block_unmoderated_assets: bool,
|
|
1208
1210
|
?store_sensitive_metadata: bool,
|
|
1209
1211
|
?streaming_enabled: bool,
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: calmlens
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.24.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- CalmLens
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: 'Server-side client for the CalmLens API: moderation, uploads, events,
|
|
14
14
|
webhooks and CDN delivery.'
|
|
@@ -69,7 +69,7 @@ metadata:
|
|
|
69
69
|
homepage_uri: https://calmlens.com
|
|
70
70
|
documentation_uri: https://calmlens.com/docs/sdk/ruby
|
|
71
71
|
source_code_uri: https://github.com/CalmLens/sdks/tree/main/ruby
|
|
72
|
-
changelog_uri: https://
|
|
72
|
+
changelog_uri: https://github.com/CalmLens/sdks/blob/main/ruby/CHANGELOG.md
|
|
73
73
|
rubygems_mfa_required: 'true'
|
|
74
74
|
post_install_message:
|
|
75
75
|
rdoc_options: []
|