pylon-api 1.1.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d98b4389f4182e2f3d62261c2a60d9f4a325c18a5376c09668cc17e1594219c
4
- data.tar.gz: ea1c690a74e4e8d2890098211d58909ae551af8deecaf757d3463e5de37f3cdc
3
+ metadata.gz: ae2b3657a71f50dfe1fbe13af8222fd19fb8026356543cae2b9372bb12bee3fa
4
+ data.tar.gz: 5232f1d4a792a6d06bf847ce5170bd4272166b7c47700f50929f676319dd0b88
5
5
  SHA512:
6
- metadata.gz: b3a1dcd703454b3efc5818292f01a00366a50ddfa5f06d6dbbdbfc865907b907b05ea35d0ca591e0eb93d5ffbec26e4ad90b85e47379aa029f61346b766e81f1
7
- data.tar.gz: aa703a9d4f20da2243ce49b31627548afd719835a3a591fab2ffafd5480ab84cbce02b3c88a54b25979a8138641b584c9e32054ae673f548ca4936daa2531e53
6
+ metadata.gz: 1d87c185ffa7b4657a9daaa49e87aa1e03b17913c2c65601270f076165902b1689b7c1e02a0142bfb26a10c751507987547fc49658848104e965861d454ba5fc
7
+ data.tar.gz: 0fc12093a40563fbb376d6cd84dcf90dfdcd3e359702ab1f99cf750c1b458436a8ccd806d4d9089d42afaadeffeec1a36b25a3faaae290f4f32ee92145fc567e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.0] - 2026-09-20
4
+
5
+ ### Added
6
+ - `list_email_suppressions` for `GET /email-suppressions` with optional `email`, `cursor`, and `limit` query parameters. Pass `email` to check a single address; an empty collection means it is not suppressed.
7
+ - `delete_email_suppression` for `DELETE /email-suppressions/{id}`
8
+ - `Pylon::Models::EmailSuppression` model (`id`, `email`, `reason`, `created_at`, `bounce_details`)
9
+
10
+ ### Changed
11
+ - Documentation only: README examples now use `body_html` when creating issues and note the 365-day window for `list_issues`. No existing method signatures, paths, or return types changed.
12
+
13
+ ## [1.1.1] - 2025-04-18
14
+
15
+ ### Fixed
16
+ - Fixed URL-based file attachments by ensuring they use proper multipart form encoding
17
+
18
+ ## [1.1.0] - 2025-04-17
19
+
20
+ ### Added
21
+ - Support for URL-based file attachments
22
+ - Enhanced attachment handling for various file types
23
+ - Added MIME type detection for file uploads
24
+
3
25
  ## [1.0.0] - 2024-03-21
4
26
 
5
27
  ### Changed
data/README.md CHANGED
@@ -55,25 +55,35 @@ account, response = client.get_account('account_id')
55
55
  #### Issues
56
56
 
57
57
  ```ruby
58
- # List issues (requires time range, max 30 days)
58
+ # List issues (requires time range, max 365 days)
59
59
  start_time = Time.now.utc - 86400 # 24 hours ago
60
60
  end_time = Time.now.utc
61
61
 
62
- issues, response = client.list_issues(
62
+ issues = client.list_issues(
63
63
  start_time: start_time.strftime("%Y-%m-%dT%H:%M:%SZ"),
64
64
  end_time: end_time.strftime("%Y-%m-%dT%H:%M:%SZ"),
65
65
  page: 1,
66
- per_page: 20,
67
- status: 'open' # optional filter
66
+ per_page: 20
68
67
  )
69
68
 
70
- # Create an issue
71
- issue, response = client.create_issue(
69
+ # Create an issue (title and body_html are required by the API)
70
+ issue = client.create_issue(
72
71
  title: 'New Issue',
73
- description: 'Issue description'
72
+ body_html: '<p>Issue description</p>',
73
+ requester_email: 'customer@example.com'
74
74
  )
75
75
  ```
76
76
 
77
+ #### Email Suppressions
78
+
79
+ ```ruby
80
+ # Check whether a single address is suppressed (empty collection means not suppressed)
81
+ suppressions = client.list_email_suppressions(email: 'user@example.com')
82
+
83
+ # Remove a suppression
84
+ client.delete_email_suppression(suppressions.first.id) unless suppressions.size.zero?
85
+ ```
86
+
77
87
  #### Teams
78
88
 
79
89
  ```ruby
data/lib/pylon/client.rb CHANGED
@@ -7,7 +7,22 @@ module Pylon
7
7
  # client = Pylon::Client.new(api_key: 'your_api_key')
8
8
  # issues = client.list_issues(start_time: '2024-03-01T00:00:00Z', end_time: '2024-03-31T23:59:59Z')
9
9
  # issues.each { |issue| puts issue.title }
10
+ # rubocop:disable Metrics/ClassLength
10
11
  class Client
12
+ # Common MIME types for file uploads
13
+ MIME_TYPES = {
14
+ ".jpg" => "image/jpeg",
15
+ ".jpeg" => "image/jpeg",
16
+ ".png" => "image/png",
17
+ ".gif" => "image/gif",
18
+ ".pdf" => "application/pdf",
19
+ ".doc" => "application/msword",
20
+ ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
21
+ ".xls" => "application/vnd.ms-excel",
22
+ ".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
23
+ ".csv" => "text/csv",
24
+ ".txt" => "text/plain"
25
+ }
11
26
  # Base URL for the Pylon API
12
27
  BASE_URL = "https://api.usepylon.com"
13
28
 
@@ -45,10 +60,36 @@ module Pylon
45
60
 
46
61
  # Create a new attachment
47
62
  #
48
- # @param file [String] The file content to upload
63
+ # @param file [File, String, nil] The file to upload (File object or file content)
64
+ # @param description [String, nil] A text description of the file
65
+ # @param file_url [String, nil] A URL to fetch the file from if file is not provided
49
66
  # @return [Models::Attachment] Created attachment object
50
- def create_attachment(file)
51
- post("/attachments", body: { file: file }, model_class: Models::Attachment)
67
+ # @raise [ArgumentError] If neither file nor file_url is provided
68
+ def create_attachment(file = nil, description: nil, file_url: nil)
69
+ if file.nil? && file_url.nil?
70
+ raise ArgumentError, "Either file or file_url must be provided"
71
+ end
72
+
73
+ params = {}
74
+ params[:description] = description if description
75
+ params[:file_url] = file_url if file_url
76
+
77
+ if file
78
+ if file.is_a?(::File) || (file.respond_to?(:path) && file.respond_to?(:read))
79
+ # For File objects or IO-like objects
80
+ params[:file] = Faraday::UploadIO.new(file, mime_type_for_file(file), File.basename(file.path))
81
+ else
82
+ # For raw content as string, create a temp file
83
+ temp_file = Tempfile.new(["upload", ".bin"])
84
+ temp_file.binmode
85
+ temp_file.write(file)
86
+ temp_file.rewind
87
+ params[:file] = Faraday::UploadIO.new(temp_file, "application/octet-stream")
88
+ end
89
+ end
90
+
91
+ # Always use multipart for attachments, whether file or file_url
92
+ post_multipart("/attachments", params, model_class: Models::Attachment)
52
93
  end
53
94
 
54
95
  # Get details for a specific attachment
@@ -111,7 +152,30 @@ module Pylon
111
152
  post("/custom_fields", body: params)
112
153
  end
113
154
 
114
- # Lists issues within a specified time range (max 30 days)
155
+ # List email suppressions, most recently suppressed first
156
+ #
157
+ # Pass +email+ to check a single address. An empty collection means the
158
+ # address is not suppressed. Uses the API's cursor-based pagination.
159
+ #
160
+ # @param email [String, nil] Only return the suppression for this address, if one exists
161
+ # @param cursor [String, nil] Cursor for the next page of results
162
+ # @param limit [Integer, nil] Number of suppressions to fetch (API default 100, max 1000)
163
+ # @return [Models::Collection<Models::EmailSuppression>] Collection of email suppression objects
164
+ def list_email_suppressions(email: nil, cursor: nil, limit: nil)
165
+ query = { email: email, cursor: cursor, limit: limit }.compact
166
+ get("/email-suppressions", query: query,
167
+ model_class: Models::EmailSuppression, collection: true)
168
+ end
169
+
170
+ # Remove an email suppression so Pylon resumes sending to the address
171
+ #
172
+ # @param suppression_id [String] The ID of the email suppression to remove
173
+ # @return [Array(Hash, Faraday::Response)] Response data and raw response
174
+ def delete_email_suppression(suppression_id)
175
+ delete("/email-suppressions/#{suppression_id}")
176
+ end
177
+
178
+ # Lists issues within a specified time range (max 365 days)
115
179
  #
116
180
  # @param start_time [String] Start time in RFC3339 format
117
181
  # @param end_time [String] End time in RFC3339 format
@@ -324,6 +388,17 @@ module Pylon
324
388
  end
325
389
  end
326
390
 
391
+ # Determine MIME type for file
392
+ #
393
+ # @param file [File] The file to determine MIME type for
394
+ # @return [String] The MIME type
395
+ def mime_type_for_file(file)
396
+ return "application/octet-stream" unless file.respond_to?(:path)
397
+
398
+ ext = File.extname(file.path).downcase
399
+ MIME_TYPES[ext] || "application/octet-stream"
400
+ end
401
+
327
402
  # Handle API response and raise appropriate errors
328
403
  #
329
404
  # @param response [Faraday::Response] The API response
@@ -427,6 +502,34 @@ module Pylon
427
502
  handle_response(connection.post(path, body.to_json), model_class, collection)
428
503
  end
429
504
 
505
+ # Make a multipart POST request for file uploads
506
+ #
507
+ # @param path [String] The API endpoint path
508
+ # @param params [Hash] Request parameters including file uploads
509
+ # @param model_class [Class] The model class to use for wrapping the response
510
+ # @param collection [Boolean] Whether the response is a collection of items
511
+ # @return [Models::Base, Models::Collection, Array] Model, Collection, or response data array
512
+ def post_multipart(path, params = {}, model_class: nil, collection: false)
513
+ # Create a connection without the default JSON content type for multipart uploads
514
+ multipart_conn = Faraday.new(@base_url) do |f|
515
+ f.request :multipart
516
+ # Only force multipart for file_url uploads, omit url_encoded middleware
517
+ f.response :json, content_type: /\bjson$/
518
+ f.response :logger if @debug
519
+ f.adapter Faraday.default_adapter
520
+ f.headers["Authorization"] = "Bearer #{api_key}"
521
+ f.headers["Accept"] = "application/json"
522
+ end
523
+
524
+ # If we have a file_url but no actual file, create a dummy file part to force multipart encoding
525
+ if params[:file_url] && !params[:file]
526
+ # Create an empty file part to force multipart encoding
527
+ params[:_dummy] = Faraday::FilePart.new(StringIO.new(""), "application/octet-stream", "dummy")
528
+ end
529
+
530
+ handle_response(multipart_conn.post(path, params), model_class, collection)
531
+ end
532
+
430
533
  # Make a PATCH request
431
534
  #
432
535
  # @param path [String] The API endpoint path
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pylon
4
+ module Models
5
+ # An email address Pylon will not send to, either because a message
6
+ # hard bounced or because someone suppressed it manually.
7
+ #
8
+ # Attributes: id, email, reason ("hard_bounce" or "manual"),
9
+ # created_at (RFC3339), bounce_details
10
+ class EmailSuppression < Base
11
+ end
12
+ end
13
+ end
data/lib/pylon/version.rb CHANGED
@@ -4,7 +4,7 @@ module Pylon
4
4
  # Major version for breaking changes
5
5
  MAJOR = 1
6
6
  # Minor version for new features
7
- MINOR = 1
7
+ MINOR = 2
8
8
  # Patch version for bug fixes
9
9
  PATCH = 0
10
10
  # Pre-release version (optional)
data/lib/pylon.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "faraday"
4
4
  require "faraday/multipart"
5
5
  require "json"
6
+ require "tempfile"
6
7
 
7
8
  require_relative "pylon/version"
8
9
  require_relative "pylon/models/base"
@@ -16,6 +17,7 @@ require_relative "pylon/models/attachment"
16
17
  require_relative "pylon/models/contact"
17
18
  require_relative "pylon/models/ticket_form"
18
19
  require_relative "pylon/models/article"
20
+ require_relative "pylon/models/email_suppression"
19
21
  require_relative "pylon/client"
20
22
 
21
23
  module Pylon
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pylon-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Odom
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-17 00:00:00.000000000 Z
11
+ date: 2026-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -140,6 +140,7 @@ files:
140
140
  - lib/pylon/models/base.rb
141
141
  - lib/pylon/models/collection.rb
142
142
  - lib/pylon/models/contact.rb
143
+ - lib/pylon/models/email_suppression.rb
143
144
  - lib/pylon/models/issue.rb
144
145
  - lib/pylon/models/tag.rb
145
146
  - lib/pylon/models/team.rb