shelfwatch 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c88743581650ab50a89f0fb2b35a3fba38f1b674b7649ff672666d28f66f5a59
4
+ data.tar.gz: d5ac216d35acf1949856d1546dbf5733482e39d69fc67204cb6a18d5660dd8ba
5
+ SHA512:
6
+ metadata.gz: 9d514a01d9b052a35d41544b6e79793341c7f23115110cc4d9d1ceca0a6ad36f7d709d4ed02cf2fa0a5f966075a1663d1cb170eca615acdb09d35e773459073d
7
+ data.tar.gz: 1c6e882c03f0a381e127b633ed38dd8f30681e3ef0660b2293bccb0b05083d8ac3b6e67c78e783e8647b18ec860d506f94864e880b1dd413f02fb645b4f8ff2d
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShelfWatch / ParallelDots
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # ShelfWatch Ruby SDK
2
+
3
+ Official Ruby client for [ShelfWatch APIs v2](https://api.shelfwatch.io).
4
+
5
+ ```bash
6
+ gem install shelfwatch
7
+ ```
8
+
9
+ Requires Ruby 2.6 or newer.
10
+
11
+ ## Quick start
12
+
13
+ Create credentials in ShelfWatch Console → **Integrations**, then:
14
+
15
+ ```ruby
16
+ require "shelfwatch"
17
+
18
+ client = ShelfWatch.new(
19
+ api_key: "swpk_…",
20
+ project_id: "PROJECT_UUID",
21
+ )
22
+
23
+ visits = client.visits.list(
24
+ start_date: "2026-07-01",
25
+ end_date: "2026-07-31",
26
+ )
27
+ puts visits["data"]
28
+
29
+ detail = client.visits.get(
30
+ visits["data"][0]["visit_uuid"],
31
+ include_kpis: true,
32
+ )
33
+ ```
34
+
35
+ ### OAuth client credentials
36
+
37
+ ```ruby
38
+ client = ShelfWatch.new(
39
+ client_id: "swoc_…",
40
+ client_secret: "swocs_…",
41
+ project_id: "PROJECT_UUID",
42
+ )
43
+ # Access tokens are fetched and refreshed automatically.
44
+ ```
45
+
46
+ ## API coverage
47
+
48
+ | Resource | Methods |
49
+ |----------|---------|
50
+ | `client.visits` | `list`, `get` |
51
+ | `client.mdm` | `stores`, `users`, `categories`, `brands`, `skus`, `schedules` |
52
+ | `client.reports` | `list`, `generate` |
53
+
54
+ Filters that accept multiple values can be passed as a comma-separated string or an array:
55
+
56
+ ```ruby
57
+ client.visits.list(
58
+ start_date: "2026-07-01",
59
+ end_date: "2026-07-31",
60
+ visit_status: ["completed"],
61
+ store_code: ["S001", "S002"],
62
+ )
63
+
64
+ stores = client.mdm.stores(q: "delhi")
65
+ reports = client.reports.list
66
+ rows = client.reports.generate(
67
+ "visit-level",
68
+ start_date: "2026-07-01",
69
+ end_date: "2026-07-07",
70
+ )
71
+ ```
72
+
73
+ Full HTTP reference: ShelfWatch Console → **Help and Support**, or the [apis-v2 docs](https://github.com/ParallelDots/apis-v2).
74
+
75
+ ## Errors
76
+
77
+ Typed exceptions map to HTTP status codes:
78
+
79
+ | Exception | Status |
80
+ |-----------|--------|
81
+ | `ShelfWatch::ValidationError` | 400 |
82
+ | `ShelfWatch::AuthenticationError` | 401 |
83
+ | `ShelfWatch::ForbiddenError` | 403 |
84
+ | `ShelfWatch::NotFoundError` | 404 |
85
+ | `ShelfWatch::RateLimitError` | 429 |
86
+ | `ShelfWatch::ServerError` | 5xx |
87
+ | `ShelfWatch::Error` | other |
88
+
89
+ ```ruby
90
+ require "shelfwatch"
91
+
92
+ begin
93
+ client.visits.get("missing-uuid")
94
+ rescue ShelfWatch::NotFoundError => e
95
+ puts "#{e.status_code} #{e.message}"
96
+ end
97
+ ```
98
+
99
+ ## Development
100
+
101
+ ```bash
102
+ bundle install
103
+ bundle exec rake test
104
+ ```
105
+
106
+ ## Publish to RubyGems
107
+
108
+ Create an API key at [rubygems.org/profile/api_keys](https://rubygems.org/profile/api_keys) with the **Push rubygem** scope (see [API key scopes](https://guides.rubygems.org/api-key-scopes/)). Then either:
109
+
110
+ ```bash
111
+ # credentials file (~/.gem/credentials or ~/.local/share/gem/credentials)
112
+ mkdir -p ~/.gem
113
+ chmod 0700 ~/.gem
114
+ printf -- "---\n:rubygems_api_key: rubygems_YOUR_KEY\n" > ~/.gem/credentials
115
+ chmod 0600 ~/.gem/credentials
116
+ ```
117
+
118
+ or set `GEM_HOST_API_KEY` for a one-off push:
119
+
120
+ ```bash
121
+ export GEM_HOST_API_KEY=rubygems_YOUR_KEY
122
+ ```
123
+
124
+ Publish:
125
+
126
+ ```bash
127
+ ./scripts/publish.sh
128
+ ```
129
+
130
+ Or manually:
131
+
132
+ ```bash
133
+ gem build shelfwatch.gemspec
134
+ gem push shelfwatch-*.gem
135
+ ```
136
+
137
+ Bump `VERSION` in `lib/shelfwatch/version.rb` (User-Agent follows automatically) before each release.
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module ShelfWatch
8
+ # Synchronous client for ShelfWatch APIs v2.
9
+ #
10
+ # Authenticate with either an API key or OAuth client credentials.
11
+ # Credentials are project-scoped (create them in Console → Integrations).
12
+ #
13
+ # Example (API key):
14
+ #
15
+ # client = ShelfWatch::Client.new(
16
+ # api_key: "swpk_…",
17
+ # project_id: "PROJECT_UUID",
18
+ # )
19
+ # visits = client.visits.list(
20
+ # start_date: "2026-07-01",
21
+ # end_date: "2026-07-31",
22
+ # )
23
+ #
24
+ # Example (OAuth):
25
+ #
26
+ # client = ShelfWatch::Client.new(
27
+ # client_id: "swoc_…",
28
+ # client_secret: "swocs_…",
29
+ # project_id: "PROJECT_UUID",
30
+ # )
31
+ class Client
32
+ DEFAULT_BASE_URL = "https://api.shelfwatch.io"
33
+ DEFAULT_TIMEOUT = 60.0
34
+ # Refresh OAuth tokens this many seconds before expires_in.
35
+ TOKEN_SKEW_SECONDS = 60
36
+
37
+ attr_reader :project_id, :base_url, :visits, :mdm, :reports
38
+
39
+ def initialize(
40
+ api_key: nil,
41
+ client_id: nil,
42
+ client_secret: nil,
43
+ project_id: nil,
44
+ base_url: DEFAULT_BASE_URL,
45
+ timeout: DEFAULT_TIMEOUT,
46
+ connection: nil
47
+ )
48
+ has_key = !api_key.nil? && api_key != ""
49
+ has_oauth = (!client_id.nil? && client_id != "") ||
50
+ (!client_secret.nil? && client_secret != "")
51
+
52
+ if has_key && has_oauth
53
+ raise ValidationError, "Provide either api_key or client_id/client_secret, not both"
54
+ end
55
+ if has_oauth && (client_id.nil? || client_id == "" || client_secret.nil? || client_secret == "")
56
+ raise ValidationError, "Both client_id and client_secret are required"
57
+ end
58
+ unless has_key || has_oauth
59
+ raise ValidationError, "Provide api_key or client_id and client_secret"
60
+ end
61
+
62
+ @project_id = project_id
63
+ @base_url = base_url.to_s.sub(%r{/+\z}, "")
64
+ @api_key = has_key ? api_key : nil
65
+ @client_id = has_oauth ? client_id : nil
66
+ @client_secret = has_oauth ? client_secret : nil
67
+ @access_token = nil
68
+ @token_expires_at = 0.0
69
+
70
+ @connection = connection || Faraday.new(url: @base_url) do |f|
71
+ f.options.timeout = timeout
72
+ f.options.open_timeout = timeout
73
+ f.headers["Accept"] = "application/json"
74
+ f.headers["User-Agent"] = "shelfwatch-ruby/#{VERSION}"
75
+ f.adapter Faraday.default_adapter
76
+ end
77
+
78
+ @visits = Resources::Visits.new(self)
79
+ @mdm = Resources::Mdm.new(self)
80
+ @reports = Resources::Reports.new(self)
81
+
82
+ return unless block_given?
83
+
84
+ begin
85
+ yield self
86
+ ensure
87
+ close
88
+ end
89
+ end
90
+
91
+ # Resolve project_id from the call or the client default.
92
+ def project(project_id = nil)
93
+ value = project_id || @project_id
94
+ if value.nil? || value == ""
95
+ raise ValidationError,
96
+ "project_id is required (pass it to ShelfWatch::Client.new(...) or to this method)"
97
+ end
98
+ value
99
+ end
100
+
101
+ # Send an authenticated request and return the JSON body.
102
+ def request(method, path, params: nil, data: nil, json: nil)
103
+ token = ensure_token
104
+ response = @connection.run_request(method.downcase.to_sym, path, nil, {}) do |req|
105
+ req.headers["Authorization"] = "Bearer #{token}"
106
+ req.params.update(stringify_keys(params)) if params
107
+ if data
108
+ req.headers["Content-Type"] = "application/x-www-form-urlencoded"
109
+ req.body = URI.encode_www_form(stringify_keys(data))
110
+ elsif !json.nil?
111
+ req.headers["Content-Type"] = "application/json"
112
+ req.body = JSON.generate(json)
113
+ end
114
+ end
115
+
116
+ Http.raise_for_status(response.status, response.body, response.reason_phrase)
117
+
118
+ return {} if response.status == 204 || response.body.nil? || response.body.empty?
119
+
120
+ begin
121
+ payload = JSON.parse(response.body)
122
+ rescue JSON::ParserError
123
+ return { "data" => response.body }
124
+ end
125
+ return { "data" => payload } unless payload.is_a?(Hash)
126
+
127
+ payload
128
+ end
129
+
130
+ # Call GET /health (no auth).
131
+ def health
132
+ response = @connection.get("/health")
133
+ Http.raise_for_status(response.status, response.body, response.reason_phrase)
134
+ payload = JSON.parse(response.body)
135
+ return { "data" => payload } unless payload.is_a?(Hash)
136
+
137
+ payload
138
+ end
139
+
140
+ # Expose the underlying Faraday connection (useful for tests).
141
+ def connection
142
+ @connection
143
+ end
144
+
145
+ # Close the underlying HTTP connection.
146
+ def close
147
+ @connection.close if @connection.respond_to?(:close)
148
+ end
149
+
150
+ private
151
+
152
+ def stringify_keys(hash)
153
+ hash.each_with_object({}) do |(key, value), out|
154
+ out[key.to_s] = value
155
+ end
156
+ end
157
+
158
+ def ensure_token
159
+ return @api_key if @api_key
160
+
161
+ now = Time.now.to_f
162
+ if @access_token && now < (@token_expires_at - TOKEN_SKEW_SECONDS)
163
+ return @access_token
164
+ end
165
+
166
+ response = @connection.post("/v2/oauth/token") do |req|
167
+ req.headers["Content-Type"] = "application/x-www-form-urlencoded"
168
+ req.body = URI.encode_www_form(
169
+ "grant_type" => "client_credentials",
170
+ "client_id" => @client_id,
171
+ "client_secret" => @client_secret
172
+ )
173
+ end
174
+ Http.raise_for_status(response.status, response.body, response.reason_phrase)
175
+
176
+ payload = begin
177
+ JSON.parse(response.body)
178
+ rescue JSON::ParserError
179
+ {}
180
+ end
181
+
182
+ token = payload.is_a?(Hash) ? payload["access_token"] : nil
183
+ if token.nil? || token == ""
184
+ raise AuthenticationError.new(
185
+ "OAuth token response missing access_token",
186
+ status_code: response.status,
187
+ body: payload
188
+ )
189
+ end
190
+
191
+ expires_in = (payload.is_a?(Hash) ? (payload["expires_in"] || 3600) : 3600).to_i
192
+ @access_token = token
193
+ @token_expires_at = now + expires_in
194
+ token
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShelfWatch
4
+ # Base exception for all ShelfWatch SDK errors.
5
+ class Error < StandardError
6
+ attr_reader :message, :status_code, :body
7
+
8
+ def initialize(message, status_code: nil, body: nil)
9
+ @message = message
10
+ @status_code = status_code
11
+ @body = body
12
+ super(to_s)
13
+ end
14
+
15
+ def to_s
16
+ if status_code
17
+ "[#{status_code}] #{message}"
18
+ else
19
+ message.to_s
20
+ end
21
+ end
22
+ end
23
+
24
+ # Raised on 401 responses (missing/invalid credential or token).
25
+ class AuthenticationError < Error; end
26
+
27
+ # Raised on 403 responses (wrong project or missing scope).
28
+ class ForbiddenError < Error; end
29
+
30
+ # Raised on 404 responses.
31
+ class NotFoundError < Error; end
32
+
33
+ # Raised on 400 responses (invalid parameters) and client-side validation.
34
+ class ValidationError < Error; end
35
+
36
+ # Raised on 429 responses.
37
+ class RateLimitError < Error; end
38
+
39
+ # Raised on 5xx responses.
40
+ class ServerError < Error; end
41
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "json"
5
+
6
+ module ShelfWatch
7
+ # HTTP helpers and response handling.
8
+ module Http
9
+ module_function
10
+
11
+ # Map HTTP errors to typed SDK exceptions.
12
+ def raise_for_status(status, body, reason_phrase = nil)
13
+ return if status >= 200 && status < 300
14
+
15
+ parsed = decode_body(body)
16
+ fallback = (reason_phrase && !reason_phrase.empty?) ? reason_phrase : "Request failed"
17
+ message = detail_message(parsed, fallback)
18
+
19
+ case status
20
+ when 400 then raise ValidationError.new(message, status_code: status, body: parsed)
21
+ when 401 then raise AuthenticationError.new(message, status_code: status, body: parsed)
22
+ when 403 then raise ForbiddenError.new(message, status_code: status, body: parsed)
23
+ when 404 then raise NotFoundError.new(message, status_code: status, body: parsed)
24
+ when 429 then raise RateLimitError.new(message, status_code: status, body: parsed)
25
+ else
26
+ if status >= 500
27
+ raise ServerError.new(message, status_code: status, body: parsed)
28
+ end
29
+ raise Error.new(message, status_code: status, body: parsed)
30
+ end
31
+ end
32
+
33
+ # Omit nil query params so the API receives only set values.
34
+ # Booleans are serialized as "true" / "false" strings.
35
+ def drop_none(params)
36
+ out = {}
37
+ params.each do |key, value|
38
+ next if value.nil?
39
+
40
+ out[key] = if value == true || value == false
41
+ value ? "true" : "false"
42
+ else
43
+ value
44
+ end
45
+ end
46
+ out
47
+ end
48
+
49
+ # Accept a string or sequence and serialize as comma-separated.
50
+ def join_csv(value)
51
+ return nil if value.nil?
52
+ return value if value.is_a?(String)
53
+ return value.map(&:to_s).join(",") if value.is_a?(Array)
54
+
55
+ value.to_s
56
+ end
57
+
58
+ # Format a date-like value for visit/schedule endpoints.
59
+ # Strings pass through. Time/Date/DateTime → ISO-8601.
60
+ def format_date(value)
61
+ case value
62
+ when String
63
+ value
64
+ when Time
65
+ value.iso8601
66
+ when DateTime
67
+ value.iso8601
68
+ when Date
69
+ value.iso8601
70
+ else
71
+ value.to_s
72
+ end
73
+ end
74
+
75
+ # Format a calendar day as YYYY-MM-DD for report endpoints.
76
+ def format_day(value)
77
+ case value
78
+ when String
79
+ value
80
+ when Time
81
+ value.to_date.iso8601
82
+ when DateTime
83
+ value.to_date.iso8601
84
+ when Date
85
+ value.iso8601
86
+ else
87
+ value.to_s
88
+ end
89
+ end
90
+
91
+ def decode_body(raw)
92
+ return nil if raw.nil? || raw == ""
93
+
94
+ JSON.parse(raw)
95
+ rescue JSON::ParserError
96
+ raw
97
+ end
98
+
99
+ def detail_message(body, fallback)
100
+ if body.is_a?(Hash)
101
+ detail = body["detail"]
102
+ if detail.is_a?(String) && !detail.empty?
103
+ return detail
104
+ end
105
+ if detail.is_a?(Array) && !detail.empty?
106
+ parts = detail.map do |item|
107
+ if item.is_a?(Hash)
108
+ msg = item["msg"] || item["message"]
109
+ msg ? msg.to_s : nil
110
+ else
111
+ item.to_s
112
+ end
113
+ end.compact
114
+ return parts.join("; ") unless parts.empty?
115
+ end
116
+ message = body["message"]
117
+ return message if message.is_a?(String) && !message.empty?
118
+ end
119
+ return body if body.is_a?(String) && !body.empty?
120
+
121
+ fallback
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShelfWatch
4
+ module Resources
5
+ # Master data management endpoints.
6
+ class Mdm
7
+ def initialize(client)
8
+ @client = client
9
+ end
10
+
11
+ # List stores for the project.
12
+ def stores(
13
+ project_id: nil,
14
+ limit: 100,
15
+ offset: 0,
16
+ store_code: nil,
17
+ q: nil,
18
+ include_total: true
19
+ )
20
+ @client.request(
21
+ "GET",
22
+ "/v2/mdm/stores",
23
+ params: Http.drop_none(
24
+ project_id: @client.project(project_id),
25
+ limit: limit,
26
+ offset: offset,
27
+ store_code: Http.join_csv(store_code),
28
+ q: q,
29
+ include_total: include_total
30
+ )
31
+ )
32
+ end
33
+
34
+ # List users for the project.
35
+ def users(
36
+ project_id: nil,
37
+ limit: 100,
38
+ offset: 0,
39
+ user_uuid: nil,
40
+ q: nil,
41
+ include_total: true
42
+ )
43
+ @client.request(
44
+ "GET",
45
+ "/v2/mdm/users",
46
+ params: Http.drop_none(
47
+ project_id: @client.project(project_id),
48
+ limit: limit,
49
+ offset: offset,
50
+ user_uuid: Http.join_csv(user_uuid),
51
+ q: q,
52
+ include_total: include_total
53
+ )
54
+ )
55
+ end
56
+
57
+ # List MDM categories for the project.
58
+ def categories(
59
+ project_id: nil,
60
+ limit: 100,
61
+ offset: 0,
62
+ q: nil,
63
+ include_inactive: false,
64
+ include_total: true
65
+ )
66
+ @client.request(
67
+ "GET",
68
+ "/v2/mdm/categories",
69
+ params: Http.drop_none(
70
+ project_id: @client.project(project_id),
71
+ limit: limit,
72
+ offset: offset,
73
+ q: q,
74
+ include_inactive: include_inactive,
75
+ include_total: include_total
76
+ )
77
+ )
78
+ end
79
+
80
+ # List MDM brands for the project.
81
+ def brands(
82
+ project_id: nil,
83
+ limit: 100,
84
+ offset: 0,
85
+ mdm_category_id: nil,
86
+ brand_id: nil,
87
+ q: nil,
88
+ include_inactive: false,
89
+ include_total: true
90
+ )
91
+ @client.request(
92
+ "GET",
93
+ "/v2/mdm/brands",
94
+ params: Http.drop_none(
95
+ project_id: @client.project(project_id),
96
+ limit: limit,
97
+ offset: offset,
98
+ mdm_category_id: Http.join_csv(mdm_category_id),
99
+ brand_id: Http.join_csv(brand_id),
100
+ q: q,
101
+ include_inactive: include_inactive,
102
+ include_total: include_total
103
+ )
104
+ )
105
+ end
106
+
107
+ # List MDM SKUs for the project.
108
+ def skus(
109
+ project_id: nil,
110
+ limit: 100,
111
+ offset: 0,
112
+ mdm_category_id: nil,
113
+ brand_id: nil,
114
+ sku_id: nil,
115
+ q: nil,
116
+ include_inactive: false,
117
+ include_total: true
118
+ )
119
+ @client.request(
120
+ "GET",
121
+ "/v2/mdm/skus",
122
+ params: Http.drop_none(
123
+ project_id: @client.project(project_id),
124
+ limit: limit,
125
+ offset: offset,
126
+ mdm_category_id: Http.join_csv(mdm_category_id),
127
+ brand_id: Http.join_csv(brand_id),
128
+ sku_id: Http.join_csv(sku_id),
129
+ q: q,
130
+ include_inactive: include_inactive,
131
+ include_total: include_total
132
+ )
133
+ )
134
+ end
135
+
136
+ # List schedules (max 31-day range on start_datetime).
137
+ def schedules(
138
+ start_date:,
139
+ end_date:,
140
+ project_id: nil,
141
+ limit: 100,
142
+ offset: 0,
143
+ store_code: nil,
144
+ user_uuid: nil,
145
+ frequency: nil,
146
+ include_total: true
147
+ )
148
+ @client.request(
149
+ "GET",
150
+ "/v2/mdm/schedules",
151
+ params: Http.drop_none(
152
+ project_id: @client.project(project_id),
153
+ start_date: Http.format_date(start_date),
154
+ end_date: Http.format_date(end_date),
155
+ limit: limit,
156
+ offset: offset,
157
+ store_code: Http.join_csv(store_code),
158
+ user_uuid: Http.join_csv(user_uuid),
159
+ frequency: frequency,
160
+ include_total: include_total
161
+ )
162
+ )
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShelfWatch
4
+ module Resources
5
+ # Report list and generate endpoints.
6
+ class Reports
7
+ def initialize(client)
8
+ @client = client
9
+ end
10
+
11
+ # List available reports for the project.
12
+ def list(project_id: nil)
13
+ @client.request(
14
+ "GET",
15
+ "/v2/reports",
16
+ params: Http.drop_none(project_id: @client.project(project_id))
17
+ )
18
+ end
19
+
20
+ # Generate a report by report_id (see #list).
21
+ def generate(report_id, start_date:, end_date:, project_id: nil)
22
+ @client.request(
23
+ "GET",
24
+ "/v2/reports/generate",
25
+ params: Http.drop_none(
26
+ project_id: @client.project(project_id),
27
+ report_id: report_id,
28
+ start_date: Http.format_day(start_date),
29
+ end_date: Http.format_day(end_date)
30
+ )
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShelfWatch
4
+ module Resources
5
+ # Visit list and detail endpoints.
6
+ class Visits
7
+ def initialize(client)
8
+ @client = client
9
+ end
10
+
11
+ # List visits for a project (max 31-day range).
12
+ def list(
13
+ start_date:,
14
+ end_date:,
15
+ project_id: nil,
16
+ limit: 100,
17
+ offset: 0,
18
+ user_uuid: nil,
19
+ store_code: nil,
20
+ visit_status: nil,
21
+ has_images: nil,
22
+ has_survey: nil,
23
+ include_total: true
24
+ )
25
+ @client.request(
26
+ "GET",
27
+ "/v2/visits",
28
+ params: Http.drop_none(
29
+ project_id: @client.project(project_id),
30
+ start_date: Http.format_date(start_date),
31
+ end_date: Http.format_date(end_date),
32
+ limit: limit,
33
+ offset: offset,
34
+ user_uuid: Http.join_csv(user_uuid),
35
+ store_code: Http.join_csv(store_code),
36
+ visit_status: Http.join_csv(visit_status),
37
+ has_images: has_images,
38
+ has_survey: has_survey,
39
+ include_total: include_total
40
+ )
41
+ )
42
+ end
43
+
44
+ # Get visit details (images, survey; optionally KPIs or a template).
45
+ def get(
46
+ visit_uuid,
47
+ project_id: nil,
48
+ include_kpis: false,
49
+ kpi_level: "topline",
50
+ template_id: nil,
51
+ category: nil
52
+ )
53
+ @client.request(
54
+ "GET",
55
+ "/v2/visits/#{visit_uuid}",
56
+ params: Http.drop_none(
57
+ project_id: @client.project(project_id),
58
+ include_kpis: include_kpis,
59
+ kpi_level: kpi_level,
60
+ template_id: template_id,
61
+ category: category
62
+ )
63
+ )
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShelfWatch
4
+ VERSION = "0.1.0"
5
+ end
data/lib/shelfwatch.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "shelfwatch/version"
5
+ require "shelfwatch/errors"
6
+ require "shelfwatch/http"
7
+ require "shelfwatch/resources/visits"
8
+ require "shelfwatch/resources/mdm"
9
+ require "shelfwatch/resources/reports"
10
+ require "shelfwatch/client"
11
+
12
+ # Official Ruby SDK for ShelfWatch APIs v2.
13
+ #
14
+ # client = ShelfWatch.new(api_key: "swpk_…", project_id: "PROJECT_UUID")
15
+ # visits = client.visits.list(start_date: "2026-07-01", end_date: "2026-07-31")
16
+ #
17
+ module ShelfWatch
18
+ # Construct a {Client} (mirrors the Python ``ShelfWatch(...)`` entrypoint).
19
+ def self.new(**kwargs)
20
+ Client.new(**kwargs)
21
+ end
22
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shelfwatch
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - ShelfWatch / ParallelDots
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '1.10'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3'
33
+ - !ruby/object:Gem::Dependency
34
+ name: minitest
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '5.0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '5.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '13.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '13.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: webmock
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.18'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.18'
75
+ description: Synchronous Ruby SDK for ShelfWatch visits, MDM, and reports APIs.
76
+ email:
77
+ - support@shelfwatch.io
78
+ executables: []
79
+ extensions: []
80
+ extra_rdoc_files: []
81
+ files:
82
+ - LICENSE
83
+ - README.md
84
+ - lib/shelfwatch.rb
85
+ - lib/shelfwatch/client.rb
86
+ - lib/shelfwatch/errors.rb
87
+ - lib/shelfwatch/http.rb
88
+ - lib/shelfwatch/resources/mdm.rb
89
+ - lib/shelfwatch/resources/reports.rb
90
+ - lib/shelfwatch/resources/visits.rb
91
+ - lib/shelfwatch/version.rb
92
+ homepage: https://github.com/ParallelDots/apis-ruby-sdk
93
+ licenses:
94
+ - MIT
95
+ metadata:
96
+ homepage_uri: https://shelfwatch.io
97
+ source_code_uri: https://github.com/ParallelDots/apis-ruby-sdk
98
+ documentation_uri: https://api.shelfwatch.io
99
+ rubygems_mfa_required: 'true'
100
+ post_install_message:
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: 2.6.0
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubygems_version: 3.0.3.1
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: Official Ruby client for ShelfWatch APIs v2
119
+ test_files: []