Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
PATH
remote: .
specs:
printavo-ruby (0.14.0)
printavo-ruby (0.15.0)
faraday (~> 2.0)
faraday-retry (~> 2.0)
thor (~> 1.0)

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -235,7 +236,7 @@ CHECKSUMS
notiffany (0.1.3) sha256=d37669605b7f8dcb04e004e6373e2a780b98c776f8eb503ac9578557d7808738
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
printavo-ruby (0.14.0)
printavo-ruby (0.15.0)
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
pry (0.16.0) sha256=d76c69065698ed1f85e717bd33d7942c38a50868f6b0673c636192b3d1b6054e
public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
Expand Down
8 changes: 8 additions & 0 deletions bin/printavo
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

$LOAD_PATH.unshift(File.join(__dir__, '..', 'lib'))

require 'printavo/cli'

Printavo::CLI.start(ARGV)
17 changes: 17 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.15.0] - 2026-04-01

### Added
- `Printavo::Client` now accepts `max_retries:` (default: 2) and `retry_on_rate_limit:` (default: true)
- `Printavo::Connection` — exponential backoff with ±50% jitter on retries (`backoff_factor: 2`,
`interval_randomness: 0.5`); retry statuses are `[500, 502, 503]` plus `429` when
`retry_on_rate_limit: true`
- `Printavo::CLI` — Thor-based command-line interface:
- `printavo customers [--first N]` — list customers (tab-aligned output)
- `printavo orders [--first N]` — list orders (default subcommand)
- `printavo orders find ID` — find and display a single order
- Credentials read from `PRINTAVO_EMAIL` / `PRINTAVO_TOKEN` environment variables
- `bin/printavo` executable (installed to PATH as `printavo`)
- `thor ~> 1.0` added as a runtime dependency
- `.graphql` files and `bin/` now included in `spec.files` (fixes missing graphql files in
published gem)

## [0.14.0] - 2026-04-01

### Added
Expand Down
10 changes: 5 additions & 5 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,12 +374,12 @@ return values — they are exposed via model field accessors, not separate resou
- [x] `Orders#delete` — `quoteDelete`
- [x] `Orders#duplicate` — `quoteDuplicate`

### v0.15.0 — Retry / Backoff & CLI
### v0.15.0 — Retry / Backoff & CLI

- [ ] Configurable `max_retries` on `Printavo::Client`
- [ ] Exponential backoff with jitter on 429 responses
- [ ] `retry_on_rate_limit: true/false` client option
- [ ] Thor-based `printavo` CLI (`customers`, `orders`, `orders find <id>`)
- [x] Configurable `max_retries` on `Printavo::Client`
- [x] Exponential backoff with jitter on 429 responses
- [x] `retry_on_rate_limit: true/false` client option
- [x] Thor-based `printavo` CLI (`customers`, `orders`, `orders find <id>`)

### v0.99.0 — API Freeze

Expand Down
82 changes: 82 additions & 0 deletions lib/printavo/cli.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# lib/printavo/cli.rb
# frozen_string_literal: true

require 'thor'
require 'printavo'

module Printavo
# Shared credential helper included by CLI and its subcommands.
module CLIHelpers
private

# Builds a Printavo::Client from PRINTAVO_EMAIL / PRINTAVO_TOKEN env vars.
# Raises Thor::Error with a human-readable message if either is missing.
def build_client
email = ENV.fetch('PRINTAVO_EMAIL', nil)
token = ENV.fetch('PRINTAVO_TOKEN', nil)
raise Thor::Error, 'PRINTAVO_EMAIL environment variable is required' unless email && !email.empty?
raise Thor::Error, 'PRINTAVO_TOKEN environment variable is required' unless token && !token.empty?

Printavo::Client.new(email: email, token: token)
end
end

class CLI < Thor
include CLIHelpers

package_name 'printavo'

def self.exit_on_failure? = true

# ---------------------------------------------------------------------------
# version
# ---------------------------------------------------------------------------

desc 'version', 'Print the printavo-ruby gem version'
def version
say Printavo::VERSION
end

# ---------------------------------------------------------------------------
# customers
# ---------------------------------------------------------------------------

desc 'customers', 'List customers'
method_option :first, type: :numeric, default: 25, desc: 'Number of records to return'
def customers
build_client.customers.all(first: options[:first]).each do |c|
say "#{c.id.to_s.ljust(10)} #{c.full_name.to_s.ljust(30)} #{c.email}"
end
end

# ---------------------------------------------------------------------------
# orders subcommand
# ---------------------------------------------------------------------------

class Orders < Thor
include CLIHelpers

default_task :list

desc 'list', 'List orders (default)'
method_option :first, type: :numeric, default: 25, desc: 'Number of records to return'
def list
build_client.orders.all(first: options[:first]).each do |o|
say "#{o.id.to_s.ljust(10)} #{o.nickname.to_s.ljust(30)} #{o.total_price}"
end
end

desc 'find ID', 'Find an order by ID'
def find(id)
o = build_client.orders.find(id)
say "id: #{o.id}"
say "nickname: #{o.nickname}"
say "total_price: #{o.total_price}"
say "status: #{o.status}"
end
end

desc 'orders SUBCOMMAND ...ARGS', 'List orders or find a specific order'
subcommand 'orders', Orders
end
end
20 changes: 14 additions & 6 deletions lib/printavo/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ class Client
# Creates a new Printavo API client. Each instance is independent —
# multiple clients with different credentials can coexist in one process.
#
# @param email [String] the email address associated with your Printavo account
# @param token [String] the API token from your Printavo My Account page
# @param timeout [Integer] HTTP timeout in seconds (default: 30)
# @param email [String] the email address associated with your Printavo account
# @param token [String] the API token from your Printavo My Account page
# @param timeout [Integer] HTTP timeout in seconds (default: 30)
# @param max_retries [Integer] max retry attempts on 5xx/429 responses (default: 2)
# @param retry_on_rate_limit [Boolean] retry automatically on 429 Too Many Requests (default: true)
#
# @example
# client = Printavo::Client.new(
Expand All @@ -20,9 +22,15 @@ class Client
# client.customers.all
# client.orders.find("12345")
# client.graphql.query("{ customers { nodes { id } } }")
def initialize(email:, token:, timeout: 30)
connection = Connection.new(email: email, token: token, timeout: timeout).build
@graphql = GraphqlClient.new(connection)
def initialize(email:, token:, timeout: 30, max_retries: 2, retry_on_rate_limit: true)
connection = Connection.new(
email: email,
token: token,
timeout: timeout,
max_retries: max_retries,
retry_on_rate_limit: retry_on_rate_limit
).build
@graphql = GraphqlClient.new(connection)
end

def account
Expand Down
46 changes: 37 additions & 9 deletions lib/printavo/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,20 @@

module Printavo
class Connection
def initialize(email:, token:, base_url: Config::BASE_URL, timeout: 30)
@email = email
@token = token
@base_url = base_url
@timeout = timeout
# @param email [String] Printavo account email
# @param token [String] Printavo API token
# @param base_url [String] API base URL (default: Config::BASE_URL)
# @param timeout [Integer] HTTP timeout in seconds (default: 30)
# @param max_retries [Integer] Max retry attempts on 5xx/429 (default: 2)
# @param retry_on_rate_limit [Boolean] Retry on 429 Too Many Requests (default: true)
def initialize(email:, token:, base_url: Config::BASE_URL, timeout: 30, # rubocop:disable Metrics/ParameterLists
max_retries: 2, retry_on_rate_limit: true)
@email = email
@token = token
@base_url = base_url
@timeout = timeout
@max_retries = max_retries
@retry_on_rate_limit = retry_on_rate_limit
end

def build
Expand All @@ -19,15 +28,34 @@ def build
f.headers['Accept'] = 'application/json'
f.headers['email'] = @email
f.headers['token'] = @token

f.request :retry, max: 2, interval: 0.5, retry_statuses: [429, 500, 502, 503]

f.request :retry, **retry_options
f.response :json
f.options.timeout = @timeout
f.options.open_timeout = @timeout

f.adapter Faraday.default_adapter
end
end

private

# Exponential backoff: base interval × 2^attempt, ±50% jitter.
# interval_randomness adds up to 50% random variance per retry.
def retry_options
{
max: @max_retries,
interval: 0.5,
backoff_factor: 2,
interval_randomness: 0.5,
retry_statuses: retry_statuses
}
end

# Returns the set of HTTP status codes that trigger a retry.
# 429 is only included when +retry_on_rate_limit+ is true (the default).
def retry_statuses
statuses = [500, 502, 503]
statuses << 429 if @retry_on_rate_limit
statuses
end
end
end
2 changes: 1 addition & 1 deletion lib/printavo/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
# frozen_string_literal: true

module Printavo
VERSION = '0.14.0'
VERSION = '0.15.0'
end
4 changes: 4 additions & 0 deletions printavo-ruby.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,20 @@ Gem::Specification.new do |spec|
}

spec.files = Dir[
'bin/*',
'lib/**/*.graphql',
'lib/**/*.rb',
'docs/**/*.md',
'LICENSE',
'README.md'
]

spec.executables = ['printavo']
spec.require_paths = ['lib']

spec.add_dependency 'faraday', '~> 2.0'
spec.add_dependency 'faraday-retry', '~> 2.0'
spec.add_dependency 'thor', '~> 1.0'

spec.add_development_dependency 'faker', '~> 3.0'
spec.add_development_dependency 'guard', '~> 2.0'
Expand Down
Loading
Loading