Class: Deckle::Client
- Inherits:
-
Object
- Object
- Deckle::Client
- Defined in:
- lib/deckle/client.rb
Constant Summary collapse
- RETRYABLE_STATUS_CODES =
Status codes that are safe to retry.
[429, 500, 502, 503, 504].freeze
Instance Attribute Summary collapse
-
#templates ⇒ Object
readonly
Returns the value of attribute templates.
Instance Method Summary collapse
-
#batch(items:, webhook: nil) ⇒ Hash
Submit a batch of PDF generation jobs for async processing.
-
#from_react(react:, data: nil, styles: nil, options: nil, output: "url", webhook: nil) ⇒ Hash
Generate a PDF from a React component string.
-
#from_template(template:, data:, options: nil, output: "url", webhook: nil) ⇒ Hash
Generate a PDF from a saved template with dynamic data.
-
#generate(html:, options: nil, output: "url", webhook: nil, watermark: nil) ⇒ Hash
Generate a PDF from raw HTML.
-
#generate_template_from_prompt(prompt:, variables: nil, template_type: "other", style: "professional") ⇒ Object
Generate a template from a natural-language prompt.
-
#get_generation(id) ⇒ Hash
Get a generation by ID.
- #get_template_version(template_id, version_id) ⇒ Object
-
#get_usage ⇒ Hash
Get usage statistics for the current billing period.
-
#initialize(api_key:, base_url: "https://api.getdeckle.dev", timeout: 30, max_retries: 3) ⇒ Client
constructor
Create a new Deckle client.
-
#list_generations(limit: 50, offset: 0) ⇒ Hash
List recent generations.
-
#list_template_versions(template_id) ⇒ Object
── Template versions ─────────────────────────────────────────.
- #marketplace_clone(id) ⇒ Object
- #marketplace_get(id) ⇒ Object
-
#marketplace_list ⇒ Object
── Marketplace ────────────────────────────────────────────────.
- #marketplace_publish(id) ⇒ Object
-
#marketplace_report(id, reason:, notes: nil) ⇒ Object
Report a public marketplace template for moderator review.
- #marketplace_unpublish(id) ⇒ Object
-
#pdf_add_form_fields(pdf:, fields:, output: "url") ⇒ Object
Add text / checkbox / dropdown form fields to a PDF.
-
#pdf_fill_form(pdf:, fields:, flatten: false, output: "url") ⇒ Object
Fill named form fields in an existing AcroForm PDF.
-
#pdf_info(pdf:) ⇒ Object
Get metadata about a PDF (page count, title, author, etc.).
-
#pdf_list_form_fields(pdf:) ⇒ Object
List the form fields on a PDF.
-
#pdf_merge(pdfs:, output: "url") ⇒ Object
Merge multiple base64-encoded PDFs into one.
-
#pdf_protect(pdf:, user_password: nil, owner_password: nil, permissions: nil, output: "url") ⇒ Object
AES-256 encrypt a PDF.
-
#pdf_sign_annotation(pdf:, name:, reason: nil, location: nil, contact: nil, page: nil, x: nil, y: nil, width: nil, height: nil, output: "url", signature: nil) ⇒ Object
Sign a PDF.
-
#pdf_split(pdf:, ranges: nil, output: "url") ⇒ Object
Split a PDF by page ranges.
-
#pdf_to_pdfa(pdf:, title: nil, author: nil, subject: nil, output: "url") ⇒ Object
Convert a PDF to PDF/A-1b archival format.
-
#request(method, path, body: nil) ⇒ Hash
Make an HTTP request and handle errors.
- #restore_template_version(template_id, version_id) ⇒ Object
- #starter_templates_clone(slug) ⇒ Object
- #starter_templates_get(slug) ⇒ Object
-
#starter_templates_list ⇒ Object
── Starter templates ──────────────────────────────────────────.
Constructor Details
#initialize(api_key:, base_url: "https://api.getdeckle.dev", timeout: 30, max_retries: 3) ⇒ Client
Create a new Deckle client.
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/deckle/client.rb', line 19 def initialize(api_key:, base_url: "https://api.getdeckle.dev", timeout: 30, max_retries: 3) raise ArgumentError, "Deckle API key is required" if api_key.nil? || api_key.empty? @api_key = api_key @base_url = base_url.chomp("/") @timeout = timeout @max_retries = max_retries @conn = Faraday.new(url: @base_url) do |f| f..timeout = @timeout f..open_timeout = @timeout f.headers["Authorization"] = "Bearer #{@api_key}" f.headers["Content-Type"] = "application/json" f.headers["User-Agent"] = "deckle-ruby/#{Deckle::VERSION}" f.adapter Faraday.default_adapter end @templates = Templates.new(self) end |
Instance Attribute Details
#templates ⇒ Object (readonly)
Returns the value of attribute templates.
8 9 10 |
# File 'lib/deckle/client.rb', line 8 def templates @templates end |
Instance Method Details
#batch(items:, webhook: nil) ⇒ Hash
Submit a batch of PDF generation jobs for async processing.
93 94 95 96 97 98 |
# File 'lib/deckle/client.rb', line 93 def batch(items:, webhook: nil) body = { "items" => items } body["webhook"] = webhook if webhook request(:post, "/v1/generate/batch", body: body) end |
#from_react(react:, data: nil, styles: nil, options: nil, output: "url", webhook: nil) ⇒ Hash
Generate a PDF from a React component string.
78 79 80 81 82 83 84 85 86 |
# File 'lib/deckle/client.rb', line 78 def from_react(react:, data: nil, styles: nil, options: nil, output: "url", webhook: nil) body = { "react" => react, "output" => output } body["data"] = data if data body["styles"] = styles if styles body["options"] = if body["webhook"] = webhook if webhook request(:post, "/v1/generate", body: body) end |
#from_template(template:, data:, options: nil, output: "url", webhook: nil) ⇒ Hash
Generate a PDF from a saved template with dynamic data.
62 63 64 65 66 67 68 |
# File 'lib/deckle/client.rb', line 62 def from_template(template:, data:, options: nil, output: "url", webhook: nil) body = { "template" => template, "data" => data, "output" => output } body["options"] = if body["webhook"] = webhook if webhook request(:post, "/v1/generate", body: body) end |
#generate(html:, options: nil, output: "url", webhook: nil, watermark: nil) ⇒ Hash
Generate a PDF from raw HTML.
46 47 48 49 50 51 52 53 |
# File 'lib/deckle/client.rb', line 46 def generate(html:, options: nil, output: "url", webhook: nil, watermark: nil) body = { "html" => html, "output" => output } body["options"] = if body["webhook"] = webhook if webhook body["watermark"] = watermark if watermark request(:post, "/v1/generate", body: body) end |
#generate_template_from_prompt(prompt:, variables: nil, template_type: "other", style: "professional") ⇒ Object
Generate a template from a natural-language prompt. The HTML returned has been server-sanitized so it’s safe to render. Requires the server to be configured with ANTHROPIC_API_KEY.
296 297 298 299 300 301 |
# File 'lib/deckle/client.rb', line 296 def generate_template_from_prompt(prompt:, variables: nil, template_type: "other", style: "professional") body = { "prompt" => prompt, "type" => template_type, "style" => style } body["variables"] = variables if variables request(:post, "/v1/ai/generate-template", body: body) end |
#get_generation(id) ⇒ Hash
Get a generation by ID.
104 105 106 |
# File 'lib/deckle/client.rb', line 104 def get_generation(id) request(:get, "/v1/generations/#{id}") end |
#get_template_version(template_id, version_id) ⇒ Object
282 283 284 |
# File 'lib/deckle/client.rb', line 282 def get_template_version(template_id, version_id) request(:get, "/v1/templates/#{template_id}/versions/#{version_id}") end |
#get_usage ⇒ Hash
Get usage statistics for the current billing period.
120 121 122 |
# File 'lib/deckle/client.rb', line 120 def get_usage request(:get, "/v1/usage") end |
#list_generations(limit: 50, offset: 0) ⇒ Hash
List recent generations.
113 114 115 |
# File 'lib/deckle/client.rb', line 113 def list_generations(limit: 50, offset: 0) request(:get, "/v1/generations?limit=#{limit}&offset=#{offset}") end |
#list_template_versions(template_id) ⇒ Object
── Template versions ─────────────────────────────────────────
278 279 280 |
# File 'lib/deckle/client.rb', line 278 def list_template_versions(template_id) request(:get, "/v1/templates/#{template_id}/versions") end |
#marketplace_clone(id) ⇒ Object
235 236 237 |
# File 'lib/deckle/client.rb', line 235 def marketplace_clone(id) request(:post, "/v1/marketplace/#{id}/clone") end |
#marketplace_get(id) ⇒ Object
231 232 233 |
# File 'lib/deckle/client.rb', line 231 def marketplace_get(id) request(:get, "/v1/marketplace/#{id}") end |
#marketplace_list ⇒ Object
── Marketplace ────────────────────────────────────────────────
227 228 229 |
# File 'lib/deckle/client.rb', line 227 def marketplace_list request(:get, "/v1/marketplace") end |
#marketplace_publish(id) ⇒ Object
239 240 241 |
# File 'lib/deckle/client.rb', line 239 def marketplace_publish(id) request(:post, "/v1/marketplace/#{id}/publish") end |
#marketplace_report(id, reason:, notes: nil) ⇒ Object
Report a public marketplace template for moderator review.
reason: one of “spam”, “malicious”, “copyright”, “inappropriate”, “other”. notes: optional, max 1000 chars.
Returns { “report_id” => …, “auto_actioned” => bool }. auto_actioned is true when this report tripped the auto-hide threshold (3 independent reports). Re-reporting the same template from the same user yields a 409 (Deckle::Error).
256 257 258 259 260 |
# File 'lib/deckle/client.rb', line 256 def marketplace_report(id, reason:, notes: nil) body = { "reason" => reason } body["notes"] = notes unless notes.nil? request(:post, "/v1/marketplace/#{id}/report", body: body) end |
#marketplace_unpublish(id) ⇒ Object
243 244 245 |
# File 'lib/deckle/client.rb', line 243 def marketplace_unpublish(id) request(:post, "/v1/marketplace/#{id}/unpublish") end |
#pdf_add_form_fields(pdf:, fields:, output: "url") ⇒ Object
Add text / checkbox / dropdown form fields to a PDF.
159 160 161 162 163 164 165 |
# File 'lib/deckle/client.rb', line 159 def pdf_add_form_fields(pdf:, fields:, output: "url") request(:post, "/v1/pdf/forms/add-fields", body: { "pdf" => pdf, "fields" => fields, "output" => output }) end |
#pdf_fill_form(pdf:, fields:, flatten: false, output: "url") ⇒ Object
Fill named form fields in an existing AcroForm PDF.
149 150 151 152 153 154 155 156 |
# File 'lib/deckle/client.rb', line 149 def pdf_fill_form(pdf:, fields:, flatten: false, output: "url") request(:post, "/v1/pdf/forms/fill", body: { "pdf" => pdf, "fields" => fields, "flatten" => flatten, "output" => output }) end |
#pdf_info(pdf:) ⇒ Object
Get metadata about a PDF (page count, title, author, etc.).
144 145 146 |
# File 'lib/deckle/client.rb', line 144 def pdf_info(pdf:) request(:post, "/v1/pdf/info", body: { "pdf" => pdf }) end |
#pdf_list_form_fields(pdf:) ⇒ Object
List the form fields on a PDF.
168 169 170 |
# File 'lib/deckle/client.rb', line 168 def pdf_list_form_fields(pdf:) request(:post, "/v1/pdf/forms/list-fields", body: { "pdf" => pdf }) end |
#pdf_merge(pdfs:, output: "url") ⇒ Object
Merge multiple base64-encoded PDFs into one. Requires >= 2 inputs.
132 133 134 |
# File 'lib/deckle/client.rb', line 132 def pdf_merge(pdfs:, output: "url") request(:post, "/v1/pdf/merge", body: { "pdfs" => pdfs, "output" => output }) end |
#pdf_protect(pdf:, user_password: nil, owner_password: nil, permissions: nil, output: "url") ⇒ Object
AES-256 encrypt a PDF. At least one of user_password / owner_password must be set. If only one is supplied the other is mirrored on the server so an empty owner password cannot be used to strip restrictions.
permissions: hash with optional keys :print (“none” | “low” | “full”), :modify (bool), :copy (bool), :annotate (bool).
213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/deckle/client.rb', line 213 def pdf_protect(pdf:, user_password: nil, owner_password: nil, permissions: nil, output: "url") if user_password.nil? && owner_password.nil? raise ArgumentError, "user_password or owner_password is required" end body = { "pdf" => pdf, "output" => output } body["user_password"] = user_password unless user_password.nil? body["owner_password"] = owner_password unless owner_password.nil? body["permissions"] = unless .nil? request(:post, "/v1/pdf/protect", body: body) end |
#pdf_sign_annotation(pdf:, name:, reason: nil, location: nil, contact: nil, page: nil, x: nil, y: nil, width: nil, height: nil, output: "url", signature: nil) ⇒ Object
Sign a PDF.
Without :signature → adds a VISUAL annotation only. Response has signature_annotation_added=true and cryptographically_signed=false.
With :signature → also embeds a real PAdES-B-B cryptographic signature. :signature must be a hash with:
:p12 base64-encoded PKCS#12 (P12/PFX) blob, max 100 KB decoded
:password P12 passphrase ("" for unprotected P12s)
The P12 is sent over TLS and used ephemerally — never persisted.
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/deckle/client.rb', line 191 def pdf_sign_annotation(pdf:, name:, reason: nil, location: nil, contact: nil, page: nil, x: nil, y: nil, width: nil, height: nil, output: "url", signature: nil) body = { "pdf" => pdf, "name" => name, "output" => output } body["reason"] = reason unless reason.nil? body["location"] = location unless location.nil? body["contact"] = contact unless contact.nil? body["page"] = page unless page.nil? body["x"] = x unless x.nil? body["y"] = y unless y.nil? body["width"] = width unless width.nil? body["height"] = height unless height.nil? body["signature"] = signature unless signature.nil? request(:post, "/v1/pdf/sign", body: body) end |
#pdf_split(pdf:, ranges: nil, output: "url") ⇒ Object
Split a PDF by page ranges. Pass nil ranges to split every page.
137 138 139 140 141 |
# File 'lib/deckle/client.rb', line 137 def pdf_split(pdf:, ranges: nil, output: "url") body = { "pdf" => pdf, "output" => output } body["ranges"] = ranges if ranges request(:post, "/v1/pdf/split", body: body) end |
#pdf_to_pdfa(pdf:, title: nil, author: nil, subject: nil, output: "url") ⇒ Object
Convert a PDF to PDF/A-1b archival format.
173 174 175 176 177 178 179 |
# File 'lib/deckle/client.rb', line 173 def pdf_to_pdfa(pdf:, title: nil, author: nil, subject: nil, output: "url") body = { "pdf" => pdf, "output" => output } body["title"] = title if title body["author"] = if body["subject"] = subject if subject request(:post, "/v1/pdf/pdfa", body: body) end |
#request(method, path, body: nil) ⇒ Hash
Make an HTTP request and handle errors. Retries on 429/5xx with exponential backoff.
NOTE: this method is intentionally public so that Templates (a separate class) can call ‘@client.request(…)`. Ruby’s ‘protected` keyword forbids cross-class invocations, which previously caused every `templates.*` call to raise NoMethodError.
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 |
# File 'lib/deckle/client.rb', line 315 def request(method, path, body: nil) last_exception = nil (@max_retries + 1).times do |attempt| begin response = @conn.run_request(method, path, body ? JSON.generate(body) : nil, nil) rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e last_exception = e if attempt < @max_retries sleep(2**attempt) next end raise end begin data = JSON.parse(response.body) rescue JSON::ParserError raise Error.new( "Non-JSON response from API (status #{response.status})", status_code: response.status, code: "INVALID_RESPONSE" ) end # Retry on retryable status codes unless exhausted if RETRYABLE_STATUS_CODES.include?(response.status) && attempt < @max_retries delay = if response.status == 429 (response.headers["Retry-After"] || (2**attempt).to_s).to_f else 2**attempt end sleep(delay) next end if response.status == 401 raise AuthenticationError, data.dig("error", "message") || "Unauthorized" end if response.status == 403 raise UsageLimitError, data.dig("error", "message") || "Forbidden" end if response.status == 404 raise NotFoundError, data.dig("error", "message") || "Not found" end if response.status == 429 retry_after = (response.headers["Retry-After"] || "1").to_i raise RateLimitError.new( data.dig("error", "message") || "Rate limit exceeded", retry_after: retry_after ) end unless response.success? error = data["error"] || {} raise Error.new( error["message"] || "Request failed", status_code: response.status, code: error["code"] || "UNKNOWN" ) end return data end raise last_exception end |
#restore_template_version(template_id, version_id) ⇒ Object
286 287 288 289 |
# File 'lib/deckle/client.rb', line 286 def restore_template_version(template_id, version_id) request(:post, "/v1/templates/#{template_id}/restore", body: { "version_id" => version_id }) end |
#starter_templates_clone(slug) ⇒ Object
272 273 274 |
# File 'lib/deckle/client.rb', line 272 def starter_templates_clone(slug) request(:post, "/v1/starter-templates/#{slug}/clone") end |
#starter_templates_get(slug) ⇒ Object
268 269 270 |
# File 'lib/deckle/client.rb', line 268 def starter_templates_get(slug) request(:get, "/v1/starter-templates/#{slug}") end |
#starter_templates_list ⇒ Object
── Starter templates ──────────────────────────────────────────
264 265 266 |
# File 'lib/deckle/client.rb', line 264 def starter_templates_list request(:get, "/v1/starter-templates") end |