atlas_rb 0.0.89 → 0.0.92

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a987c6f2b9d67ba84bfb29752682c630a47d69e943af83d44f5957136b2ae680
4
- data.tar.gz: 5b2eea6ac511508959d56be1d7dce4306a2187eba6eeb5e1e4556ed7efe3fc9c
3
+ metadata.gz: 9ecdf5a4da61e030d7220fc9094a43bd91a850eaa1c599179d75c3d5381b300a
4
+ data.tar.gz: eea705d4f0601b112b2182848241c5439ef4beb10b87e87939e73ea70eb30907
5
5
  SHA512:
6
- metadata.gz: b0279a6e5f20724c89a4e5bb31232e33c9860f4a28ee416a34539e279715e43c8229e436f41a696cc56d762049e4b69abde7a21ba72adaf687bebc5330044e8c
7
- data.tar.gz: a480b47e2927b337e222d4890d5903b366acebfc27283d366d3967fb98c53a96a84ff039c590ba1a2d67751982c8dd379123a2ad86391e84cf4f937c30225956
6
+ metadata.gz: 4aa3b1a2367bde1309f589eebf0bf9bf8ff9f744dc1cdf5648c820cd4d38b4e2afa496a9b50308947f00cb9ceab4d10df4e92018d7715ad7928aa415fb556e0c
7
+ data.tar.gz: b006492ee9c5adde683c863d06cf7cbd233848709e6aa6176e0e6b2fbd663f81f70166b0e7da4889a9005e47c7ecd3cc39e7aa58bde32461b15b4448b18d786d
data/.version CHANGED
@@ -1 +1 @@
1
- 0.0.89
1
+ 0.0.92
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- atlas_rb (0.0.89)
4
+ atlas_rb (0.0.92)
5
5
  faraday (~> 2.7)
6
6
  faraday-follow_redirects (~> 0.3.0)
7
7
  faraday-multipart (~> 1)
data/README.md CHANGED
@@ -73,6 +73,34 @@ AtlasRb::FileSet.create("w-789", "primary") # file_set under work w-789, classif
73
73
  AtlasRb::Blob.create("w-789", path, name) # blob under work w-789 with original filename preserved
74
74
  ```
75
75
 
76
+ `Work.create`, `FileSet.create`, and `Blob.create` each accept an optional
77
+ `idempotency_key:` kwarg for retry-safe bulk-deposit jobs. The caller
78
+ generates the UUID; the Atlas server enforces uniqueness scoped to the
79
+ acting user. A repeat call with the same key returns the originally-created
80
+ resource (or `410` if it has since been tombstoned). The gem does **not**
81
+ generate keys, cache responses, or retry — those concerns belong to the
82
+ calling job runner (e.g. Cerberus's Solid Queue).
83
+
84
+ ```ruby
85
+ key = SecureRandom.uuid
86
+ AtlasRb::Work.create("col-456", idempotency_key: key)
87
+ AtlasRb::FileSet.create("w-789", "primary", idempotency_key: key)
88
+ AtlasRb::Blob.create("w-789", path, name, idempotency_key: key)
89
+ ```
90
+
91
+ ### Listing and monitoring Works
92
+
93
+ `Work.list` exposes the paginated `GET /works` index, with an optional
94
+ `in_progress:` filter for finding deposits that haven't yet been marked
95
+ complete. `Work.complete` flips a Work's `in_progress` flag to `false`
96
+ once a bulk-deposit job confirms all expected children have been deposited.
97
+
98
+ ```ruby
99
+ AtlasRb::Work.list(in_progress: true) # stuck deposits
100
+ AtlasRb::Work.list(in_progress: false, page: 2) # completed deposits, page 2
101
+ AtlasRb::Work.complete("w-789") # mark w-789 done
102
+ ```
103
+
76
104
  ## End-to-end example
77
105
 
78
106
  JSON responses come back as `AtlasRb::Mash` (a `Hashie::Mash` subclass), so
data/lib/atlas_rb/blob.rb CHANGED
@@ -65,19 +65,29 @@ module AtlasRb
65
65
  # @param blob_path [String] path to the binary file on disk to upload.
66
66
  # @param original_filename [String] the user-facing filename Atlas
67
67
  # should record (e.g. `"final_thesis.pdf"`).
68
+ # @param idempotency_key [String, nil] optional UUID. A repeat call with
69
+ # the same key returns the originally-created Blob instead of creating
70
+ # a new one. See {AtlasRb::Work.create} for full semantics.
68
71
  # @return [Hash] the created `"blob"` payload, including its `"id"`.
69
72
  #
70
73
  # @example
71
74
  # AtlasRb::Blob.create("w-789", "/tmp/upload.tmp", "final_thesis.pdf")
72
75
  # # => { "id" => "b-321", "original_filename" => "final_thesis.pdf", ... }
73
- def self.create(id, blob_path, original_filename)
76
+ #
77
+ # @example Retry-safe bulk-deposit create
78
+ # key = SecureRandom.uuid
79
+ # AtlasRb::Blob.create("w-789", "/tmp/upload.tmp", "thesis.pdf",
80
+ # idempotency_key: key)
81
+ def self.create(id, blob_path, original_filename, idempotency_key: nil)
74
82
  payload = { work_id: id,
75
83
  original_filename: original_filename,
76
84
  binary: Faraday::Multipart::FilePart.new(File.open(blob_path),
77
85
  "application/octet-stream",
78
86
  File.basename(blob_path)) }
79
87
 
80
- AtlasRb::Mash.new(JSON.parse(multipart({}).post(ROUTE, payload)&.body))['blob']
88
+ AtlasRb::Mash.new(JSON.parse(
89
+ multipart(nil, idempotency_key: idempotency_key).post(ROUTE, payload)&.body
90
+ ))['blob']
81
91
  end
82
92
 
83
93
  # Delete a Blob (the bytes *and* the metadata record).
@@ -60,6 +60,41 @@ module AtlasRb
60
60
  connection({}).delete(ROUTE + id)
61
61
  end
62
62
 
63
+ # Tombstone (withdraw) a Collection.
64
+ #
65
+ # The Collection remains in Atlas storage but is marked as withdrawn:
66
+ # search and show pages return a withdrawn stub for every user. Atlas
67
+ # rejects the request with `422 has_live_children` if the Collection
68
+ # still has live (non-tombstoned) Works.
69
+ #
70
+ # @param id [String] the Collection ID.
71
+ # @param nuid [String] the acting user's NUID, stamped on the resource
72
+ # as `tombstoned_by` for audit purposes.
73
+ # @return [Faraday::Response] the raw response. `200`/`204` on success;
74
+ # `422` with `{"code":"has_live_children"}` if the Collection is not empty.
75
+ #
76
+ # @example
77
+ # AtlasRb::Collection.tombstone("col-456", nuid: "000000002")
78
+ def self.tombstone(id, nuid:)
79
+ connection({}, nuid).post(ROUTE + id + '/tombstone')
80
+ end
81
+
82
+ # Restore a previously tombstoned Collection.
83
+ #
84
+ # **Operator-only.** Restoration is intentionally not exposed in any
85
+ # end-user UI; call this from a Rails console session (or a future
86
+ # admin panel) when the library has decided an object should come back.
87
+ #
88
+ # @param id [String] the Collection ID.
89
+ # @param nuid [String] the acting user's NUID.
90
+ # @return [Faraday::Response] the raw response.
91
+ #
92
+ # @example Operator restoring from `bundle exec rails console`
93
+ # AtlasRb::Collection.restore("col-456", nuid: "000000002")
94
+ def self.restore(id, nuid:)
95
+ connection({}, nuid).post(ROUTE + id + '/restore')
96
+ end
97
+
63
98
  # List the Works in a Collection.
64
99
  #
65
100
  # The endpoint returns just the noids; resolve each through
@@ -64,6 +64,41 @@ module AtlasRb
64
64
  connection({}).delete(ROUTE + id)
65
65
  end
66
66
 
67
+ # Tombstone (withdraw) a Community.
68
+ #
69
+ # The Community remains in Atlas storage but is marked as withdrawn:
70
+ # search and show pages return a withdrawn stub for every user. Atlas
71
+ # rejects the request with `422 has_live_children` if the Community
72
+ # still has live (non-tombstoned) members.
73
+ #
74
+ # @param id [String] the Community ID.
75
+ # @param nuid [String] the acting user's NUID, stamped on the resource
76
+ # as `tombstoned_by` for audit purposes.
77
+ # @return [Faraday::Response] the raw response. `200`/`204` on success;
78
+ # `422` with `{"code":"has_live_children"}` if the Community is not empty.
79
+ #
80
+ # @example
81
+ # AtlasRb::Community.tombstone("c-123", nuid: "000000002")
82
+ def self.tombstone(id, nuid:)
83
+ connection({}, nuid).post(ROUTE + id + '/tombstone')
84
+ end
85
+
86
+ # Restore a previously tombstoned Community.
87
+ #
88
+ # **Operator-only.** Restoration is intentionally not exposed in any
89
+ # end-user UI; call this from a Rails console session (or a future
90
+ # admin panel) when the library has decided an object should come back.
91
+ #
92
+ # @param id [String] the Community ID.
93
+ # @param nuid [String] the acting user's NUID.
94
+ # @return [Faraday::Response] the raw response.
95
+ #
96
+ # @example Operator restoring from `bundle exec rails console`
97
+ # AtlasRb::Community.restore("c-123", nuid: "000000002")
98
+ def self.restore(id, nuid:)
99
+ connection({}, nuid).post(ROUTE + id + '/restore')
100
+ end
101
+
67
102
  # List the immediate children (sub-Communities and Collections) of a Community.
68
103
  #
69
104
  # The endpoint returns just the noids; resolve each through
@@ -23,17 +23,23 @@ module AtlasRb
23
23
  # or `metadata:` without manually serializing.
24
24
  # @param nuid [String, nil] optional Northeastern University ID to send in
25
25
  # the `User` header. Defaults to `nil` (no NUID context).
26
+ # @param idempotency_key [String, nil] optional UUID to send in the
27
+ # `Idempotency-Key` header. Used by retry-safe create flows (currently
28
+ # `POST /works`, `POST /file_sets`, `POST /files`) to deduplicate replays
29
+ # against the originally-created resource. Generated by the caller —
30
+ # this gem does not mint keys.
26
31
  # @return [Faraday::Connection] a connection that follows redirects and
27
32
  # uses Faraday's default adapter.
28
33
  #
29
34
  # @example Fetching a community
30
35
  # AtlasRb::Community.connection({}).get('/communities/abc123')
31
- def connection(params, nuid=nil)
36
+ def connection(params, nuid=nil, idempotency_key: nil)
32
37
  headers = {
33
38
  "Content-Type" => "application/json",
34
39
  "Authorization" => "Bearer #{ENV.fetch("ATLAS_TOKEN", nil)}"
35
40
  }
36
41
  headers["User"] = "NUID #{nuid}" if nuid
42
+ headers["Idempotency-Key"] = idempotency_key if idempotency_key
37
43
 
38
44
  Faraday.new(
39
45
  url: ENV.fetch("ATLAS_URL", nil),
@@ -53,6 +59,9 @@ module AtlasRb
53
59
  # `Faraday::Multipart::FilePart` instances.
54
60
  #
55
61
  # @param nuid [String, nil] optional NUID for the `User` header.
62
+ # @param idempotency_key [String, nil] optional UUID to send in the
63
+ # `Idempotency-Key` header. See {#connection} for semantics; the
64
+ # `POST /files` (Blob) create flow uses this transport.
56
65
  # @return [Faraday::Connection] a multipart-aware connection.
57
66
  #
58
67
  # @example Posting a binary blob
@@ -63,11 +72,12 @@ module AtlasRb
63
72
  # "scan.pdf")
64
73
  # }
65
74
  # AtlasRb::Blob.multipart({}).post('/files/', payload)
66
- def multipart(nuid=nil)
75
+ def multipart(nuid=nil, idempotency_key: nil)
67
76
  headers = {
68
77
  "Authorization" => "Bearer #{ENV.fetch("ATLAS_TOKEN", nil)}"
69
78
  }
70
79
  headers["User"] = "NUID #{nuid}" if nuid
80
+ headers["Idempotency-Key"] = idempotency_key if idempotency_key
71
81
 
72
82
  Faraday.new(
73
83
  url: ENV.fetch("ATLAS_URL", nil),
@@ -31,14 +31,24 @@ module AtlasRb
31
31
  # @param classification [String] role tag for the FileSet — e.g.
32
32
  # `"primary"`, `"supplemental"`, `"thumbnail"`. The exact set is
33
33
  # defined by the Atlas server.
34
+ # @param idempotency_key [String, nil] optional UUID. A repeat call with
35
+ # the same key returns the originally-created FileSet instead of
36
+ # creating a new one. See {AtlasRb::Work.create} for full semantics.
34
37
  # @return [Hash] the created `"file_set"` payload, including its `"id"`
35
38
  # which can then be passed to {.update} to attach a binary.
36
39
  #
37
40
  # @example
38
41
  # fs = AtlasRb::FileSet.create("w-789", "primary")
39
42
  # AtlasRb::FileSet.update(fs["id"], "/tmp/article.pdf")
40
- def self.create(id, classification)
41
- AtlasRb::Mash.new(JSON.parse(connection({ work_id: id, classification: classification }).post(ROUTE)&.body))["file_set"]
43
+ #
44
+ # @example Retry-safe bulk-deposit create
45
+ # key = SecureRandom.uuid
46
+ # AtlasRb::FileSet.create("w-789", "primary", idempotency_key: key)
47
+ def self.create(id, classification, idempotency_key: nil)
48
+ AtlasRb::Mash.new(JSON.parse(
49
+ connection({ work_id: id, classification: classification }, nil,
50
+ idempotency_key: idempotency_key).post(ROUTE)&.body
51
+ ))["file_set"]
42
52
  end
43
53
 
44
54
  # Delete a FileSet.
data/lib/atlas_rb/work.rb CHANGED
@@ -26,6 +26,33 @@ module AtlasRb
26
26
  AtlasRb::Mash.new(JSON.parse(connection({}).get(ROUTE + id)&.body))["work"]
27
27
  end
28
28
 
29
+ # List Works, paginated.
30
+ #
31
+ # Wraps `GET /works`. Returns the full pagination envelope rather than a
32
+ # bare array so callers can page through results — the shape matches
33
+ # {AtlasRb::Community.children} and {AtlasRb::Collection.children}.
34
+ #
35
+ # @param in_progress [Boolean, nil] when set, filter to Works whose
36
+ # `in_progress` flag matches. Omit (or pass `nil`) for "all works".
37
+ # @param page [Integer, nil] 1-indexed page number.
38
+ # @param per_page [Integer, nil] page size override.
39
+ # @return [AtlasRb::Mash] `{ "works" => [...], "pagination" => {...} }`.
40
+ # Each entry in `"works"` is a Work summary (`id`, `title`,
41
+ # `description`, `in_progress`).
42
+ #
43
+ # @example Find stuck deposits
44
+ # AtlasRb::Work.list(in_progress: true)
45
+ #
46
+ # @example Page through all works
47
+ # AtlasRb::Work.list(page: 2, per_page: 50)
48
+ def self.list(in_progress: nil, page: nil, per_page: nil)
49
+ params = {}
50
+ params[:in_progress] = in_progress unless in_progress.nil?
51
+ params[:page] = page if page
52
+ params[:per_page] = per_page if per_page
53
+ AtlasRb::Mash.new(JSON.parse(connection(params).get(ROUTE)&.body))
54
+ end
55
+
29
56
  # Create a new Work in an existing Collection.
30
57
  #
31
58
  # **Note**: unlike {Community.create} and {Collection.create}, the `id`
@@ -36,6 +63,14 @@ module AtlasRb
36
63
  # @param xml_path [String, nil] optional path to a MODS XML file. When
37
64
  # given, the Work is created and immediately patched with the metadata
38
65
  # in the file.
66
+ # @param idempotency_key [String, nil] optional UUID. A repeat call with
67
+ # the same key returns the originally-created Work instead of creating
68
+ # a new one (or `410` if it has since been tombstoned, or `410` with
69
+ # no body if it has been hard-deleted). Keys are scoped to the acting
70
+ # user and only apply to the initial `POST /works` — the optional
71
+ # follow-up PATCH/GET when `xml_path` is given do not carry the key.
72
+ # The caller (e.g. Cerberus's Solid Queue job) generates and persists
73
+ # the UUID; this gem does not mint keys.
39
74
  # @return [Hash] the created Work payload (post-update if `xml_path` was
40
75
  # supplied).
41
76
  #
@@ -44,8 +79,14 @@ module AtlasRb
44
79
  #
45
80
  # @example Work seeded from MODS
46
81
  # AtlasRb::Work.create("col-456", "/tmp/work-mods.xml")
47
- def self.create(id, xml_path = nil)
48
- result = AtlasRb::Mash.new(JSON.parse(connection({ collection_id: id }).post(ROUTE)&.body))["work"]
82
+ #
83
+ # @example Retry-safe bulk-deposit create
84
+ # key = SecureRandom.uuid
85
+ # AtlasRb::Work.create("col-456", idempotency_key: key)
86
+ def self.create(id, xml_path = nil, idempotency_key: nil)
87
+ result = AtlasRb::Mash.new(JSON.parse(
88
+ connection({ collection_id: id }, nil, idempotency_key: idempotency_key).post(ROUTE)&.body
89
+ ))["work"]
49
90
  return result unless xml_path.present?
50
91
 
51
92
  update(result["id"], xml_path)
@@ -63,6 +104,64 @@ module AtlasRb
63
104
  connection({}).delete(ROUTE + id)
64
105
  end
65
106
 
107
+ # Tombstone (withdraw) a Work.
108
+ #
109
+ # The Work remains in Atlas storage along with its FileSets and Blobs,
110
+ # but is marked as withdrawn: search and show pages return a withdrawn
111
+ # stub for every user. Unlike Communities and Collections, Works are
112
+ # always tombstoneable regardless of how many files they hold — the
113
+ # FileSets and Blobs ride along.
114
+ #
115
+ # @param id [String] the Work ID.
116
+ # @param nuid [String] the acting user's NUID, stamped on the resource
117
+ # as `tombstoned_by` for audit purposes.
118
+ # @return [Faraday::Response] the raw response.
119
+ #
120
+ # @example
121
+ # AtlasRb::Work.tombstone("w-789", nuid: "000000002")
122
+ def self.tombstone(id, nuid:)
123
+ connection({}, nuid).post(ROUTE + id + '/tombstone')
124
+ end
125
+
126
+ # Mark a Work complete.
127
+ #
128
+ # Cerberus's bulk-deposit job calls this once it has confirmed all
129
+ # expected children (FileSets / Blobs) are deposited. Atlas's monitoring
130
+ # query `GET /works?in_progress=true` then drops this Work from the
131
+ # "stuck" list.
132
+ #
133
+ # Idempotent on the server: calling `complete` on an already-complete
134
+ # Work is a no-op — Atlas simply re-saves with `in_progress: false`.
135
+ # Atlas does not currently stamp a `completed_by` audit field; the
136
+ # `nuid:` parameter is plumbed through for parity with the other
137
+ # lifecycle bindings and in case Atlas adds completion audit later.
138
+ #
139
+ # @param id [String] the Work ID.
140
+ # @param nuid [String, nil] optional NUID of the acting user.
141
+ # @return [Faraday::Response] the raw response. Status `200` on success.
142
+ #
143
+ # @example
144
+ # AtlasRb::Work.complete("w-789")
145
+ def self.complete(id, nuid: nil)
146
+ connection({}, nuid).post(ROUTE + id + '/complete')
147
+ end
148
+
149
+ # Restore a previously tombstoned Work.
150
+ #
151
+ # **Operator-only.** Restoration is intentionally not exposed in any
152
+ # end-user UI; call this from a Rails console session (or a future
153
+ # admin panel) when the library has decided an object should come back.
154
+ #
155
+ # @param id [String] the Work ID.
156
+ # @param nuid [String] the acting user's NUID.
157
+ # @return [Faraday::Response] the raw response.
158
+ #
159
+ # @example Operator restoring from `bundle exec rails console`
160
+ # AtlasRb::Work.restore("w-789", nuid: "000000002")
161
+ def self.restore(id, nuid:)
162
+ connection({}, nuid).post(ROUTE + id + '/restore')
163
+ end
164
+
66
165
  # Replace a Work's metadata by uploading a MODS XML document.
67
166
  #
68
167
  # @param id [String] the Work ID.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atlas_rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.89
4
+ version: 0.0.92
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cliff
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-07 00:00:00.000000000 Z
11
+ date: 2026-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday