Class: LabelZoom::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/labelzoom/client.rb

Overview

Converts labels through the LabelZoom API.

Authentication is optional. Without a credential the API serves a free tier: watermarked output, the first label only, a 1 MB request cap, and no multi-page, JSON-target or image-to-image conversion. Constructing a client with no key is therefore a supported, tested path rather than an error.

Constant Summary collapse

DEFAULT_BASE_URL =

The production API host.

"https://api.labelzoom.com"
API_KEY_ENVIRONMENT_VARIABLE =

The environment variable consulted when no credential is configured.

"LABELZOOM_API_KEY"
REQUEST_ID_HEADER =

The support handle the gateway stamps on every response. Net::HTTP looks headers up case-insensitively, which matters here: the gateway sets X-LZ-Request-Id but CORS exposes it as X-LZ-Request-ID.

"x-lz-request-id"
MAX_MESSAGE_LENGTH =

E2 caps the derived message here. APIError#raw_body keeps the whole body.

512
RETRYABLE_STATUSES =

Statuses worth retrying. Rule F1: nothing else, ever.

->(status) { status == 429 || status >= 500 }
REASON_PHRASES =

E2's last resort, when a body is empty and the reason phrase is too. Net::HTTP does not always surface a phrase, so this is derived from a table rather than the wire.

{
  400 => "Bad Request", 401 => "Unauthorized", 403 => "Forbidden", 404 => "Not Found",
  406 => "Not Acceptable", 413 => "Payload Too Large", 429 => "Too Many Requests",
  500 => "Internal Server Error", 502 => "Bad Gateway", 503 => "Service Unavailable",
  504 => "Gateway Timeout"
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(api_key: UNSET, base_url: DEFAULT_BASE_URL, max_retries: 2, timeout: 60, user_agent_suffix: nil, sleeper: nil, jitter: true, env: ENV, http_builder: nil) ⇒ Client

Returns a new instance of Client.

Parameters:

  • api_key (String, nil, LabelZoom::UNSET) (defaults to: UNSET)

    an lz_live_/lz_test_ key or a JWT. Left at UNSET the client reads LABELZOOM_API_KEY from env. Passing nil or an empty String forces anonymous mode and suppresses that fallback.

  • base_url (String) (defaults to: DEFAULT_BASE_URL)

    a path prefix is preserved, so a reverse proxy at https://proxy.example.com/labelzoom works.

  • max_retries (Integer) (defaults to: 2)

    retries after the initial attempt. 0 disables retrying.

  • timeout (Numeric) (defaults to: 60)

    per-attempt read and open timeout, in seconds.

  • user_agent_suffix (String, nil) (defaults to: nil)

    appended to the SDK's own User-Agent.

  • sleeper (#call, nil) (defaults to: nil)

    replaces the delay between retries. Substitute a recording no-op in tests so the retry paths cost no wall-clock time.

  • jitter (Boolean) (defaults to: true)

    full jitter on the retry backoff. Turn it off for deterministic tests; leave it on in production, where it is what stops a fleet retrying in lockstep.

  • env (#[]) (defaults to: ENV)

    the environment lookup. Injecting it keeps a developer's real key out of a test's outcome.

  • http_builder (#call, nil) (defaults to: nil)

    builds the Net::HTTP instance. An escape hatch for proxies and custom TLS; the conformance suite uses WebMock instead.

Raises:

  • (ArgumentError)


60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/labelzoom/client.rb', line 60

def initialize(api_key: UNSET, base_url: DEFAULT_BASE_URL, max_retries: 2, timeout: 60,
               user_agent_suffix: nil, sleeper: nil, jitter: true, env: ENV,
               http_builder: nil)
  raise ArgumentError, "max_retries cannot be negative" if max_retries.negative?

  # All trailing slashes, not one: a base URL of "https://api.labelzoom.com///" must
  # still produce a single-slash path.
  @base_url = base_url.to_s.sub(%r{/+\z}, "")
  @credential = resolve_credential(api_key, env)
  @max_retries = max_retries
  @timeout = timeout
  @sleeper = sleeper || ->(seconds) { Kernel.sleep(seconds) }
  @jitter = jitter
  @http_builder = http_builder

  # The server parses a "LabelZoomStudio/" User-Agent prefix as a Studio version and
  # silently changes PDF handling for versions <= 1.8.2, so the SDK's own token must
  # come first.
  suffix = user_agent_suffix.to_s.strip
  @user_agent = "labelzoom-ruby-sdk/#{VERSION} (ruby/#{RUBY_VERSION})"
  @user_agent += " #{suffix}" unless suffix.empty?
end

Instance Method Details

#authenticated?Boolean

Whether a credential was resolved. False means requests go out on the anonymous free tier, which is a supported mode rather than an error.

Returns:

  • (Boolean)


85
# File 'lib/labelzoom/client.rb', line 85

def authenticated? = !@credential.nil?

#build_uri(source, target, params = nil) ⇒ Object

The URL a given conversion would be posted to. Exposed because it is the first thing anyone debugging a proxy or a base-URL override wants to see.



127
128
129
130
131
132
133
134
135
# File 'lib/labelzoom/client.rb', line 127

def build_uri(source, target, params = nil)
  # Concatenated, not resolved: a base URL carrying a path prefix
  # (https://proxy.example.com/labelzoom) must keep it, which URI.join would discard.
  url = "#{@base_url}/api/v2/convert/#{Formats.source_token(source)}/to/" \
        "#{Formats.target_token(target)}"
  # Rule C7: no options means a bare URL, not an empty query string.
  url += "?#{URI.encode_www_form("params" => params)}" unless params.nil?
  URI.parse(url)
end

#convert(source, target, body, base64_text: false, **options) ⇒ ConversionResult

Runs one conversion.

Parameters:

  • source (Symbol)

    the format of body.

  • target (Symbol)

    the format to produce.

  • body (String)

    the document. For :url it is the URL to fetch, as text.

  • base64_text (Boolean) (defaults to: false)

    send body as base64 text/plain rather than the source's own media type. Only the binary sources support it.

  • options (Hash)

    conversion parameters, as nested keyword arguments -- label: { width: 4, height: 6 } rather than Python's flattened label_width. Only what you pass is sent; the SDK never fills in a client-side default, so a change to a server default reaches you without a gem upgrade.

    label is in INCHES, not dots, and omitting it entirely asks the server to detect the size. pdf: { page_number: } is 0-BASED; omit it to convert every page.

Returns:

Raises:

  • (ValidationError)

    if the request is rejected locally, before any network call.

  • (APIError)

    on any non-2xx response.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/labelzoom/client.rb', line 105

def convert(source, target, body, base64_text: false, **options)
  source = Formats.source!(source)
  target = Formats.target!(target)
  body = body.to_s

  if body.empty?
    # The gateway rejects a zero-length body with 400; catching it here saves a round
    # trip and gives a clearer message.
    raise ValidationError.new("body",
                              "Source body cannot be empty; the API rejects zero-length " \
                              "requests.")
  end

  params = Options.serialize(**options)
  uri = build_uri(source, target, params)
  headers = build_headers(source, base64_text)

  execute(uri, headers, body)
end