doohly 0.1.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.
@@ -0,0 +1,4 @@
1
+ {
2
+ "id": "b0324f10-14d8-41ca-8ae5-d03e6d6c37bf",
3
+ "uploadUrl": "https://doohly-production-file-uploads.s3.ap-southeast-2.amazonaws.com/creative/winboard-adverto/add04745-d007-4dfb-a856-0e53eca0a28b/b0324f10-14d8-41ca-8ae5-d03e6d6c37bf.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAV6CZOKOV5MUX2UG4%2F20250820%2Fap-southeast-2%2Fs3%2Faws4_request&X-Amz-Date=20250820T044932Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEIX%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLXNvdXRoZWFzdC0yIkgwRgIhAItpbnNxukjLdf9gSFrQvfa3ZAzzWNO8I7APJlR2K0t4AiEAjVMz36tsWPr6ptaW3sJuqIQhncJXMUinSWQ89fFz%2FFQq0gUIzv%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARACGgw0MDgyMDk0Nzg1NzEiDNy0%2BI2Vi9xkv44dWCqmBTH%2BJ6av3wsnOPe9xPNLyPMqaSWAdIl4q3Mz4t%2BE9brbakVTGo6pHPyuNAgdybx%2FEzqVnJ0DDwiXOlaiXpe4GhFRXtknBwz4GK6paDl2IqZEYWFDkjZuhzGpnu2z2sAeaZu4%2F6NjoIVFypWtUbtecAHzGUpXZWbLBAPLTGV4MpkR5owho8zsbBcQBXUSok4n5WOSTI39%2FvyKAinzsay%2BbQeBgBZBPHaDUOQ%2FopOwfm6qe50DSFXgxeV%2F2qPwWjeUXs1TXxHradB6L9wk3ut%2FKJocnVC%2F7WSHTedLrbynn4q%2F48EW2EzHVdXUruU0WsoesrGSa%2FWcgwLCtqg1hhOrb4V2ByDagRQBmaQWwaGD%2FTsQ%2FnshMgwqmbm7X4ZO%2FY22yiaCFlMzrqwl1xKpoBA5YYRu1CN68DcDNPrD8mPxgn6GBIqgm6lnkwId5lsP7d086POzTi2zv4UIBhjOMazhSfD1SyxcT22m5ZtHywYtL27kGn3%2FhxnTkvQnpOay64LmhQXXkIifFvzC6OY2iMFn3u9cWtk2oaBU3sU3DPmFTp0bDbhUb5AWztHbtuaB2jQlJRNE2EaLopH%2FRrhqQlazLQ132yPXzhXijTd7y1pIegZ9hqHq%2FJFHceP8osWkAMOxssM%2BzFVLy7WdeAFYwi2X30hJXn%2Fh1o4jM7G6QRWTclGBa%2BVUkynL4oATjXAmht%2B4ukr2ZsQQG9vdpphSF53ZzW1kejeOt7JUTB8eNSWYvpG4PRdLioZREaldo8DtUx85fMiKYDo0lKVhYBVsOabjkLr0KtML9X0h2u7uwTv0P8OtIT8AgyM5May4F01UbnGMpqj8B8tyCpoBZ%2BHIhGWnAlfR9oEWcVbtG8n8%2FZdv5y5gxWLOmKpw4UbGEH%2B0QWHbI4YDn5Tr3DDJppXFBjqwAR2K8QDwQuGg10sfj6%2FUE0eDnA3lrWNYFn6%2BW%2FDmTc6N3bUJAJJQ%2FH3OWPMgEN6mkBN8qgJ7OzQ6%2B5ueo%2FFTm%2FbauiO1EEEjBLD8P6KuLNI4SNL0V1QhlbCoBq8yvDrbtGXA9VyE%2FGi2sV197my6UM0gYXdWVnwvuyFTHyXOo%2Fzjr7F5WJJE6Ou7RzbIIOKtdfMmZmLXS6Fw8xcd9tdvPsjqlcIu2KGCbd5vre1ZJ3C9&X-Amz-Signature=a92086f6b872d1ec77fa2d8a0e12750d3749fca1d06dedfdfa3df39f22fe98a7&X-Amz-SignedHeaders=content-length%3Bhost&x-id=PutObject"
4
+ }
data/doohly.gemspec ADDED
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/doohly/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "doohly"
7
+ spec.version = Doohly::VERSION
8
+ spec.authors = ["Sentia"]
9
+ spec.email = ["developer@sentia.com.au"]
10
+
11
+ spec.summary = "Ruby client for the Doohly DOOH advertising platform API"
12
+ spec.description = "A Ruby client library for interacting with the Doohly (Digital Out-of-Home) " \
13
+ "advertising platform API. Supports managing bookings, devices, creatives, and more."
14
+ spec.homepage = "https://github.com/Sentia/doohly"
15
+ spec.license = "MIT"
16
+ spec.required_ruby_version = ">= 3.0.0"
17
+
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = "https://github.com/Sentia/doohly"
20
+ spec.metadata["changelog_uri"] = "https://github.com/Sentia/doohly/blob/main/CHANGELOG.md"
21
+ spec.metadata["bug_tracker_uri"] = "https://github.com/Sentia/doohly/issues"
22
+ spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/doohly"
23
+ spec.metadata["rubygems_mfa_required"] = "true"
24
+
25
+ # Specify which files should be added to the gem when it is released.
26
+ spec.files = Dir.chdir(__dir__) do
27
+ `git ls-files -z`.split("\x0").reject do |f|
28
+ (File.expand_path(f) == __FILE__) ||
29
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
30
+ end
31
+ end
32
+ spec.bindir = "exe"
33
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
34
+ spec.require_paths = ["lib"]
35
+
36
+ # Runtime dependencies
37
+ spec.add_dependency "faraday", "~> 2.0"
38
+ spec.add_dependency "faraday-multipart", "~> 1.0"
39
+
40
+ # Development dependencies
41
+ spec.add_development_dependency "rake", "~> 13.0"
42
+ spec.add_development_dependency "rspec", "~> 3.12"
43
+ spec.add_development_dependency "rubocop", "~> 1.50"
44
+ spec.add_development_dependency "rubocop-rspec", "~> 2.20"
45
+ spec.add_development_dependency "simplecov", "~> 0.22"
46
+ spec.add_development_dependency "vcr", "~> 6.1"
47
+ spec.add_development_dependency "webmock", "~> 3.18"
48
+ end
data/examples/usage.rb ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "doohly"
6
+
7
+ # Configure with your API token
8
+ api_token = ENV.fetch("DOOHLY_API_TOKEN", "your_api_token_here")
9
+
10
+ # Option 1: Global configuration
11
+ Doohly.configure do |config|
12
+ config.api_token = api_token
13
+ config.timeout = 30
14
+ end
15
+
16
+ client = Doohly::Client.new
17
+
18
+ # Option 2: Direct client initialization
19
+ # client = Doohly::Client.new(api_token: api_token)
20
+
21
+ puts "=" * 80
22
+ puts "Doohly Ruby Client - Usage Examples"
23
+ puts "=" * 80
24
+
25
+ # List devices
26
+ puts "\nšŸ“± Fetching devices..."
27
+ begin
28
+ devices = client.devices
29
+ device_count = devices.is_a?(Array) ? devices.count : 0
30
+ puts "Found #{device_count} devices"
31
+ if devices.is_a?(Array) && devices.any?
32
+ first_device = devices.first
33
+ puts " First device: #{first_device["name"]} (#{first_device["id"]})"
34
+ end
35
+ rescue Doohly::APIError => e
36
+ puts "Error fetching devices: #{e.message}"
37
+ end
38
+
39
+ # List bookings
40
+ puts "\nšŸ“… Fetching bookings..."
41
+ begin
42
+ bookings = client.bookings
43
+ booking_list = bookings.is_a?(Hash) ? bookings["bookings"] : bookings
44
+ booking_count = booking_list.is_a?(Array) ? booking_list.count : 0
45
+ puts "Found #{booking_count} bookings"
46
+ if booking_list.is_a?(Array) && booking_list.any?
47
+ first_booking = booking_list.first
48
+ puts " First booking: #{first_booking["name"]} (#{first_booking["status"]})"
49
+ end
50
+ rescue Doohly::APIError => e
51
+ puts "Error fetching bookings: #{e.message}"
52
+ end
53
+
54
+ # Get signed upload URL
55
+ puts "\nšŸŽØ Getting signed upload URL for creative..."
56
+ begin
57
+ upload_info = client.get_signed_upload_url(
58
+ name: "example-creative.png",
59
+ mime_type: "image/png",
60
+ file_size: 100_000,
61
+ playback_scaling: "contain"
62
+ )
63
+ puts "Upload ID: #{upload_info["id"]}"
64
+ puts "Upload URL: #{upload_info["uploadUrl"][0..50]}..."
65
+ rescue Doohly::APIError => e
66
+ puts "Error getting upload URL: #{e.message}"
67
+ end
68
+
69
+ puts "\nāœ… Example completed!"
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/multipart"
5
+ require "json"
6
+
7
+ module Doohly
8
+ # Main API client for Doohly
9
+ class Client
10
+ attr_reader :api_token, :api_base_url
11
+
12
+ def initialize(api_token: nil, api_base_url: nil)
13
+ @api_token = api_token || Doohly.configuration.api_token
14
+ @api_base_url = api_base_url || Doohly.configuration.api_base_url
15
+
16
+ raise ConfigurationError, "API token is required" if @api_token.nil? || @api_token.empty?
17
+
18
+ @connection = build_connection
19
+ end
20
+
21
+ # Bookings API
22
+
23
+ # GET /v2/bookings
24
+ # @param status [String, nil] Filter by status (e.g., 'booked', 'paused', 'completed')
25
+ # @return [Hash] List of bookings
26
+ def bookings(status: nil)
27
+ params = {}
28
+ params[:status] = status if status
29
+ get("v2/bookings", params)
30
+ end
31
+
32
+ # GET /v2/bookings/:id
33
+ # @param id [String] Booking ID
34
+ # @return [Hash] Booking details
35
+ def booking(id)
36
+ get("v2/bookings/#{id}")
37
+ end
38
+
39
+ # POST /v2/bookings - Create a new booking
40
+ # @param name [String] Booking name
41
+ # @param external_id [String, nil] External reference ID
42
+ # @param plays_per_loop [Integer, nil] Number of plays per loop
43
+ # @param loops_per_play [Integer, nil] Number of loops per play
44
+ # @param play_consecutively [Boolean, nil] Whether to play consecutively
45
+ # @param purchase_type [String, nil] Purchase type (e.g., 'Sold', 'Bonus')
46
+ # @param campaign [Hash, nil] Campaign details
47
+ # @param schedule [Hash, nil] Schedule configuration
48
+ # @param assigned_creatives [Array<Hash>, nil] Array of creative assignments
49
+ # @param assigned_frames [Array<Hash>, nil] Array of frame assignments
50
+ # @param tags [Array<String>, nil] Tags for the booking
51
+ # @param seedooh [Hash, nil] SeeDooh configuration
52
+ # @param status [String, nil] Booking status
53
+ # @return [Hash] Created booking details
54
+ def create_booking(name:, external_id: nil, plays_per_loop: nil, loops_per_play: nil,
55
+ play_consecutively: nil, purchase_type: nil, campaign: nil,
56
+ schedule: nil, assigned_creatives: nil, assigned_frames: nil,
57
+ tags: nil, seedooh: nil, status: nil)
58
+ body = { name: name }
59
+ body[:externalId] = external_id if external_id
60
+ body[:playsPerLoop] = plays_per_loop if plays_per_loop
61
+ body[:loopsPerPlay] = loops_per_play if loops_per_play
62
+ body[:playConsecutively] = play_consecutively unless play_consecutively.nil?
63
+ body[:purchaseType] = purchase_type if purchase_type
64
+ body[:campaign] = campaign if campaign
65
+ body[:schedule] = schedule if schedule
66
+ body[:assignedCreatives] = assigned_creatives if assigned_creatives
67
+ body[:assignedFrames] = assigned_frames if assigned_frames
68
+ body[:tags] = tags if tags
69
+ body[:seedooh] = seedooh if seedooh
70
+ body[:status] = status if status
71
+
72
+ post("v2/bookings", body)
73
+ end
74
+
75
+ # PATCH /v2/bookings/:id - Update an existing booking
76
+ # @param id [String] Booking ID
77
+ # @param name [String, nil] Booking name
78
+ # @param external_id [String, nil] External reference ID
79
+ # @param plays_per_loop [Integer, nil] Number of plays per loop
80
+ # @param loops_per_play [Integer, nil] Number of loops per play
81
+ # @param play_consecutively [Boolean, nil] Whether to play consecutively
82
+ # @param purchase_type [String, nil] Purchase type
83
+ # @param campaign [Hash, nil] Campaign details
84
+ # @param schedule [Hash, nil] Schedule configuration
85
+ # @param assigned_creatives [Array<Hash>, nil] Array of creative assignments
86
+ # @param assigned_frames [Array<Hash>, nil] Array of frame assignments
87
+ # @param tags [Array<String>, nil] Tags for the booking
88
+ # @param seedooh [Hash, nil] SeeDooh configuration
89
+ # @param status [String, nil] Booking status
90
+ # @return [Hash] Updated booking details
91
+ def update_booking(id, name: nil, external_id: nil, plays_per_loop: nil, loops_per_play: nil,
92
+ play_consecutively: nil, purchase_type: nil, campaign: nil,
93
+ schedule: nil, assigned_creatives: nil, assigned_frames: nil,
94
+ tags: nil, seedooh: nil, status: nil)
95
+ body = {}
96
+ body[:name] = name if name
97
+ body[:externalId] = external_id unless external_id.nil?
98
+ body[:playsPerLoop] = plays_per_loop if plays_per_loop
99
+ body[:loopsPerPlay] = loops_per_play if loops_per_play
100
+ body[:playConsecutively] = play_consecutively unless play_consecutively.nil?
101
+ body[:purchaseType] = purchase_type if purchase_type
102
+ body[:campaign] = campaign unless campaign.nil?
103
+ body[:schedule] = schedule if schedule
104
+ body[:assignedCreatives] = assigned_creatives if assigned_creatives
105
+ body[:assignedFrames] = assigned_frames if assigned_frames
106
+ body[:tags] = tags if tags
107
+ body[:seedooh] = seedooh if seedooh
108
+ body[:status] = status if status
109
+
110
+ patch("v2/bookings/#{id}", body)
111
+ end
112
+
113
+ # DELETE /v2/bookings/:id - Delete an existing booking
114
+ # @param id [String] Booking ID
115
+ # @return [Hash] Deletion result
116
+ def delete_booking(id)
117
+ delete("v2/bookings/#{id}")
118
+ end
119
+
120
+ # Devices API
121
+
122
+ # GET /v1/devices
123
+ # @return [Hash] List of devices
124
+ def devices
125
+ get("v1/devices")
126
+ end
127
+
128
+ # GET /v2/devices/:id
129
+ # @param id [String] Device ID
130
+ # @return [Hash] Device details
131
+ def device(id)
132
+ get("v2/devices/#{id}")
133
+ end
134
+
135
+ # Creatives API
136
+
137
+ # GET /v1/library/creatives/upload/:id - Get creative upload status
138
+ # @param id [String] Upload ID
139
+ # @return [Hash] Upload status
140
+ def creative_upload_status(id)
141
+ get("v1/library/creatives/upload/#{id}")
142
+ end
143
+
144
+ # POST /v1/library/creatives/upload - Get signed upload URL
145
+ # @param name [String] Creative name
146
+ # @param mime_type [String] MIME type (e.g., 'image/png', 'video/mp4')
147
+ # @param file_size [Integer] File size in bytes
148
+ # @param playback_scaling [String, nil] Playback scaling mode (e.g., 'contain', 'cover')
149
+ # @param path [Array<String>, nil] Folder path for organization
150
+ # @return [Hash] Upload information including signed URL
151
+ def get_signed_upload_url(name:, mime_type:, file_size:, playback_scaling: nil, path: nil)
152
+ body = {
153
+ name: name,
154
+ mimeType: mime_type,
155
+ fileSize: file_size
156
+ }
157
+ body[:playbackScaling] = playback_scaling if playback_scaling
158
+ body[:path] = path if path
159
+
160
+ post("v1/library/creatives/upload", body)
161
+ end
162
+
163
+ private
164
+
165
+ def build_connection
166
+ Faraday.new(url: @api_base_url) do |conn|
167
+ conn.request :authorization, "Bearer", @api_token
168
+ conn.request :json
169
+ conn.response :json, content_type: /json$/
170
+ conn.response :logger, Doohly.configuration.logger if Doohly.configuration.logger
171
+ conn.options.timeout = Doohly.configuration.timeout
172
+ conn.options.open_timeout = Doohly.configuration.open_timeout
173
+ conn.adapter Faraday.default_adapter
174
+ end
175
+ end
176
+
177
+ def get(path, params = {})
178
+ response = @connection.get(path, params)
179
+ handle_response(response)
180
+ end
181
+
182
+ def post(path, body)
183
+ response = @connection.post(path) do |req|
184
+ req.headers["Content-Type"] = "application/json"
185
+ req.body = body.to_json
186
+ end
187
+ handle_response(response)
188
+ end
189
+
190
+ def patch(path, body)
191
+ response = @connection.patch(path) do |req|
192
+ req.headers["Content-Type"] = "application/json"
193
+ req.body = body.to_json
194
+ end
195
+ handle_response(response)
196
+ end
197
+
198
+ def delete(path)
199
+ response = @connection.delete(path)
200
+ handle_response(response)
201
+ end
202
+
203
+ def handle_response(response)
204
+ case response.status
205
+ when 200..299
206
+ response.body
207
+ when 400
208
+ raise BadRequestError.new(
209
+ "Bad Request: #{response.body}",
210
+ status: response.status,
211
+ body: response.body,
212
+ response: response
213
+ )
214
+ when 401
215
+ raise AuthenticationError.new(
216
+ "Authentication failed: #{response.body}",
217
+ status: response.status,
218
+ body: response.body,
219
+ response: response
220
+ )
221
+ when 404
222
+ raise NotFoundError.new(
223
+ "Resource not found: #{response.body}",
224
+ status: response.status,
225
+ body: response.body,
226
+ response: response
227
+ )
228
+ when 429
229
+ raise RateLimitError.new(
230
+ "Rate limit exceeded: #{response.body}",
231
+ status: response.status,
232
+ body: response.body,
233
+ response: response
234
+ )
235
+ when 500..599
236
+ raise ServerError.new(
237
+ "Server error: #{response.body}",
238
+ status: response.status,
239
+ body: response.body,
240
+ response: response
241
+ )
242
+ else
243
+ raise APIError.new(
244
+ "API Error: #{response.status} - #{response.body}",
245
+ status: response.status,
246
+ body: response.body,
247
+ response: response
248
+ )
249
+ end
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doohly
4
+ # Configuration management for Doohly client
5
+ class Configuration
6
+ attr_accessor :api_token, :api_base_url, :timeout, :open_timeout, :logger
7
+
8
+ DEFAULT_API_BASE_URL = "https://api.dooh.ly/api/public"
9
+ DEFAULT_TIMEOUT = 30
10
+ DEFAULT_OPEN_TIMEOUT = 10
11
+
12
+ def initialize
13
+ @api_token = nil
14
+ @api_base_url = DEFAULT_API_BASE_URL
15
+ @timeout = DEFAULT_TIMEOUT
16
+ @open_timeout = DEFAULT_OPEN_TIMEOUT
17
+ @logger = nil
18
+ end
19
+
20
+ def validate!
21
+ raise ConfigurationError, "API token is required" if api_token.nil? || api_token.empty?
22
+
23
+ true
24
+ end
25
+ end
26
+
27
+ class << self
28
+ attr_writer :configuration
29
+
30
+ def configuration
31
+ @configuration ||= Configuration.new
32
+ end
33
+
34
+ def configure
35
+ yield(configuration)
36
+ configuration.validate!
37
+ end
38
+
39
+ def reset_configuration!
40
+ @configuration = Configuration.new
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doohly
4
+ # Base error class for all Doohly errors
5
+ class Error < StandardError; end
6
+
7
+ # Raised when API request fails
8
+ class APIError < Error
9
+ attr_reader :status, :body, :response
10
+
11
+ def initialize(message, status: nil, body: nil, response: nil)
12
+ @status = status
13
+ @body = body
14
+ @response = response
15
+ super(message)
16
+ end
17
+ end
18
+
19
+ # Raised when configuration is invalid
20
+ class ConfigurationError < Error; end
21
+
22
+ # Raised when authentication fails
23
+ class AuthenticationError < APIError; end
24
+
25
+ # Raised when resource is not found
26
+ class NotFoundError < APIError; end
27
+
28
+ # Raised when request is invalid
29
+ class BadRequestError < APIError; end
30
+
31
+ # Raised when rate limit is exceeded
32
+ class RateLimitError < APIError; end
33
+
34
+ # Raised when server error occurs
35
+ class ServerError < APIError; end
36
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doohly
4
+ VERSION = "0.1.0"
5
+ end
data/lib/doohly.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "doohly/version"
4
+ require_relative "doohly/error"
5
+ require_relative "doohly/configuration"
6
+ require_relative "doohly/client"
7
+
8
+ # Main module for Doohly Ruby client
9
+ module Doohly
10
+ class << self
11
+ # Quick client initialization
12
+ # @param api_token [String] Doohly API token
13
+ # @return [Doohly::Client] Configured client instance
14
+ def client(api_token: nil)
15
+ Client.new(api_token: api_token)
16
+ end
17
+ end
18
+ end
data/sig/doohly.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Doohly
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
data/test_api.rb ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "doohly"
6
+
7
+ api_token = ENV.fetch("DOOHLY_API_TOKEN")
8
+
9
+ client = Doohly::Client.new(api_token: api_token)
10
+
11
+ puts "Testing API connection..."
12
+ puts
13
+
14
+ begin
15
+ devices = client.devices
16
+ puts "āœ… API connection successful!"
17
+ puts "Response type: #{devices.class}"
18
+ puts "Response: #{devices.inspect}"
19
+ rescue Doohly::AuthenticationError => e
20
+ puts "āŒ Authentication failed: #{e.message}"
21
+ rescue Doohly::APIError => e
22
+ puts "āŒ API error: #{e.message} (Status: #{e.status})"
23
+ rescue StandardError => e
24
+ puts "āŒ Error: #{e.class} - #{e.message}"
25
+ puts e.backtrace.first(5)
26
+ end