sifiapi 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +8 -0
  3. data/LICENSE +22 -0
  4. data/README.md +83 -0
  5. data/Rakefile +7 -0
  6. data/examples/complete_workflow.rb +93 -0
  7. data/lib/faraday/request/convert_file_to_upload_io.rb +37 -0
  8. data/lib/faraday/request/json_encode.rb +11 -0
  9. data/lib/faraday/response/parse_json.rb +32 -0
  10. data/lib/faraday/response/raise_sifi_error.rb +37 -0
  11. data/lib/sifi_api/ad.rb +2 -0
  12. data/lib/sifi_api/ad_file_type.rb +2 -0
  13. data/lib/sifi_api/ad_size.rb +2 -0
  14. data/lib/sifi_api/bid_type.rb +2 -0
  15. data/lib/sifi_api/branded_datum.rb +2 -0
  16. data/lib/sifi_api/browser.rb +2 -0
  17. data/lib/sifi_api/campaign.rb +2 -0
  18. data/lib/sifi_api/campaign_stat.rb +2 -0
  19. data/lib/sifi_api/campaign_type.rb +2 -0
  20. data/lib/sifi_api/change.rb +2 -0
  21. data/lib/sifi_api/client.rb +2 -0
  22. data/lib/sifi_api/company.rb +2 -0
  23. data/lib/sifi_api/connection.rb +59 -0
  24. data/lib/sifi_api/context.rb +2 -0
  25. data/lib/sifi_api/dayparting.rb +2 -0
  26. data/lib/sifi_api/device.rb +2 -0
  27. data/lib/sifi_api/dma.rb +2 -0
  28. data/lib/sifi_api/domain.rb +2 -0
  29. data/lib/sifi_api/error.rb +15 -0
  30. data/lib/sifi_api/geo_target.rb +2 -0
  31. data/lib/sifi_api/ip_range.rb +2 -0
  32. data/lib/sifi_api/keyword.rb +2 -0
  33. data/lib/sifi_api/keyword_category.rb +2 -0
  34. data/lib/sifi_api/operating_system.rb +2 -0
  35. data/lib/sifi_api/recency.rb +2 -0
  36. data/lib/sifi_api/recurring_report.rb +2 -0
  37. data/lib/sifi_api/report.rb +2 -0
  38. data/lib/sifi_api/report_asset.rb +2 -0
  39. data/lib/sifi_api/report_type.rb +2 -0
  40. data/lib/sifi_api/resource.rb +150 -0
  41. data/lib/sifi_api/user.rb +2 -0
  42. data/lib/sifi_api/version.rb +9 -0
  43. data/lib/sifi_api.rb +41 -0
  44. data/lib/sifiapi.rb +1 -0
  45. data/sifiapi.gemspec +22 -0
  46. data/spec/sifi_api/ad_file_type_spec.rb +7 -0
  47. data/spec/sifi_api/ad_size_spec.rb +7 -0
  48. data/spec/sifi_api/ad_spec.rb +7 -0
  49. data/spec/sifi_api/bid_type_spec.rb +7 -0
  50. data/spec/sifi_api/branded_datum_spec.rb +7 -0
  51. data/spec/sifi_api/browser_spec.rb +7 -0
  52. data/spec/sifi_api/campaign_spec.rb +7 -0
  53. data/spec/sifi_api/campaign_stat_spec.rb +7 -0
  54. data/spec/sifi_api/campaign_type_spec.rb +7 -0
  55. data/spec/sifi_api/change_spec.rb +7 -0
  56. data/spec/sifi_api/client_spec.rb +7 -0
  57. data/spec/sifi_api/company_spec.rb +7 -0
  58. data/spec/sifi_api/context_spec.rb +7 -0
  59. data/spec/sifi_api/dayparting_spec.rb +7 -0
  60. data/spec/sifi_api/device_spec.rb +7 -0
  61. data/spec/sifi_api/dma_spec.rb +7 -0
  62. data/spec/sifi_api/domain_spec.rb +7 -0
  63. data/spec/sifi_api/geo_target_spec.rb +7 -0
  64. data/spec/sifi_api/ip_range_spec.rb +8 -0
  65. data/spec/sifi_api/keyword_category_spec.rb +7 -0
  66. data/spec/sifi_api/keyword_spec.rb +8 -0
  67. data/spec/sifi_api/operating_system_spec.rb +8 -0
  68. data/spec/sifi_api/recency_spec.rb +7 -0
  69. data/spec/sifi_api/recurring_report_spec.rb +8 -0
  70. data/spec/sifi_api/report_asset_spec.rb +8 -0
  71. data/spec/sifi_api/report_spec.rb +8 -0
  72. data/spec/sifi_api/report_type_spec.rb +8 -0
  73. data/spec/sifi_api/user_spec.rb +7 -0
  74. data/spec/spec_helper.rb +27 -0
  75. data/spec/support/resource_shared_example.rb +97 -0
  76. metadata +200 -0
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .DS_Store
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem "rake"
7
+ gem "rspec", "~> 2.7.0"
8
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Brandon Aaron
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # SifiApi
2
+
3
+ Offers a simple way to interact with the Simpli.fi API via Ruby.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'sifiapi'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install sifiapi
18
+
19
+ ## Basic Usage
20
+
21
+ APPKEY = "your_app_key"
22
+ connection = SifiApi::Connection.new(APPKEY)
23
+
24
+ user = connection.user(user_key, :include => "companies,clients")
25
+
26
+ # see available resources
27
+ puts user.resources
28
+
29
+ # get the company and it's client
30
+ company = user.companies.first
31
+ client = company.clients.first
32
+
33
+ ## Create, Update, and Destroy Resource
34
+
35
+ campaign = client.create(:campaign)
36
+ campaign.update({ :name => "Testing via API!" })
37
+ campaign.delete
38
+
39
+ ## Capturing Errors
40
+
41
+ # Capture for all resources
42
+ class SifiApi::Resource
43
+ rescue_from SifiApi::NotFound, :with => proc{|e| puts "Not found!" }
44
+ rescue_from SifiApi::UnprocessableEntity, :with => proc{|e| puts "Failed to update!" }
45
+ end
46
+
47
+ # Capture per an individual resource
48
+ class SifiApi::User
49
+ rescue_from SifiApi::NotFound, :with => proc{|e| puts "User: Not found!" }
50
+ end
51
+
52
+ # Locally capture an error
53
+ begin
54
+ campaign.update({ :bid => "invalid" })
55
+ rescue SifiApi::UnprocessableEntity => exception
56
+ puts exception.message
57
+ end
58
+
59
+ ## Attaching Files to updates
60
+
61
+ keywords = campaign.keywords
62
+ csv = Tempfile.new(['keywords', 'csv'])
63
+ csv.print("keyword one,2.50\nkeyword two,2.25\nkeyword three,5.00")
64
+ csv.rewind
65
+ keywords.update(:csv => csv)
66
+
67
+ ## Downloading Files
68
+
69
+ keywords = campaign.keywords
70
+ csv = keywords.download
71
+ csv.is_a?(Tempfile) # true
72
+
73
+ ## Ruby Versions
74
+
75
+ This has been tested against Ruby 1.9.2.
76
+
77
+ ## Contributing
78
+
79
+ 1. Fork it
80
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
81
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
82
+ 4. Push to the branch (`git push origin my-new-feature`)
83
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+ RSpec::Core::RakeTask.new("spec") do |t|
5
+ t.rspec_opts = ["-c", "-f progress", "-r ./spec/spec_helper.rb"]
6
+ t.pattern = 'spec/**/*_spec.rb'
7
+ end
@@ -0,0 +1,93 @@
1
+ $:.unshift File.expand_path('..', __FILE__)
2
+ $:.unshift File.expand_path('../../lib', __FILE__)
3
+
4
+ require 'sifiapi'
5
+ require 'date'
6
+ require 'tempfile'
7
+
8
+ # add application and user api keys
9
+ APPKEY = 'APPLICATION_KEY_GOES_HERE'
10
+ USERKEY = 'USER_KEY_GOES_HERE'
11
+
12
+ api = SifiApi::Connection.new(APPKEY)
13
+ # can optionally pass a uri as the second param for the api end point (if you are testing against app-playground for instance)
14
+ #api = SifiApi::Connection.new(APPKEY, "https://app-playground.simpli.fi/api/")
15
+
16
+ # get the user and include the companies and clients in the result
17
+ user = api.user(USERKEY, :include => "companies,clients")
18
+
19
+ # grab the first company and the first companies client
20
+ company = user.companies.first
21
+ client = company.clients.first
22
+
23
+
24
+ # create a campaign from the client
25
+ search_campaign = client.create(:campaign)
26
+ search_campaign_settings = {
27
+ :name => "Example Search Campaign via API",
28
+ :campaign_type_id => 1,
29
+ :recency_id => 1,
30
+ :advertiser => "Example Advertiser",
31
+ :start_date => Date.today.strftime("%Y-%m-%d"),
32
+ :end_date => (Date.today + 30).strftime("%Y-%m-%d"),
33
+ :bid_type_id => 1,
34
+ :bid => 5.00,
35
+ :daily_budget => 500.00,
36
+ :dayparting_ids => [1,2],
37
+ :browser_ids => [601,604],
38
+ :device_ids => [701],
39
+ :operating_system_ids => [801],
40
+ :context_ids => [201,207],
41
+ :geo_target_ids => [1,8180],
42
+ :branded_data_ids => ['ECohortsDigital_26','ECohortsDigital_27'],
43
+ :frequency_capping => { :how_many_times => 1, :hours => 1 },
44
+ :campaign_goal => { :goal_type => "ctr", :goal_value => 0.05 },
45
+ :impression_cap => 10000,
46
+ :pacing => 75
47
+ }
48
+ # update the campaign with the desired settings
49
+ search_campaign.update(search_campaign_settings)
50
+
51
+ # reload the campaign and include the nested resources
52
+ puts search_campaign.reload(:include => "daypartings,browsers,devices,operating_systems,context_ids,branded_data").inspect
53
+
54
+
55
+ # setup an ad
56
+ html_ad_settings = {
57
+ :name => "Example HTML Ad",
58
+ :ad_file_type_id => 4,
59
+ :ad_size_id => 3,
60
+ :target_url => "http://www.simpli.fi/",
61
+ :html => "<a href=\"{{clickTag}}\" target=\"_blank\">Simpli.fi</a>"
62
+ }
63
+ search_campaign.create(:ad, html_ad_settings)
64
+
65
+ puts search_campaign.ads.inspect
66
+
67
+ # upload some keywords for the campaign
68
+ keywords = search_campaign.keywords.first
69
+ keyword_csv = Tempfile.new(['keywords', 'csv'])
70
+ keyword_csv.print("keyword one,2.50\nkeyword two,2.25\nkeyword three,5.00")
71
+ keyword_csv.rewind
72
+ keywords.update(:csv => keyword_csv)
73
+
74
+ puts keywords.reload.inspect
75
+
76
+ # upload some domains
77
+ domains = search_campaign.domains.first
78
+ domain_csv = Tempfile.new(['domains', 'csv'])
79
+ domain_csv.print("http://simpli.fi/")
80
+ domain_csv.rewind
81
+ domains.update(:csv => domain_csv, :list_type => "whitelist")
82
+
83
+ puts domains.reload.inspect
84
+
85
+ # upload some ip ranges
86
+ ip_ranges = search_campaign.ip_ranges.first
87
+ ip_ranges_csv = Tempfile.new(['ip_ranges', 'csv'])
88
+ ip_ranges_csv.print("192.168.1.1,192.168.1.9\n192.168.2.1,192.168.2.9")
89
+ ip_ranges_csv.rewind
90
+ ip_ranges.update(:csv => ip_ranges_csv)
91
+
92
+ puts ip_ranges.reload.inspect
93
+
@@ -0,0 +1,37 @@
1
+ module Faraday
2
+ class Request::ConvertFileToUploadIO < Faraday::Middleware
3
+ def call(env)
4
+ if env[:body].is_a?(Hash)
5
+ resource = env[:body].keys.first
6
+ env[:body][resource].each do |key, value|
7
+ if value.is_a?(File) || value.is_a?(Tempfile)
8
+ env[:body][resource][key] = Faraday::UploadIO.new(value, mime_type(value.path), value.path)
9
+ elsif value.is_a?(Hash) && (value['io'].is_a?(IO) || value['io'].is_a?(StringIO))
10
+ env[:body][resource][key] = Faraday::UploadIO.new(value['io'], mime_type('.'+value['type']), '')
11
+ end
12
+ end
13
+ end
14
+ @app.call(env)
15
+ end
16
+
17
+ private
18
+
19
+ def mime_type(path)
20
+ case path
21
+ when /\.jpe?g/i
22
+ 'image/jpeg'
23
+ when /\.gif$/i
24
+ 'image/gif'
25
+ when /\.png$/i
26
+ 'image/png'
27
+ when /\.swf$/i
28
+ 'application/swf'
29
+ when /\.csv$/i
30
+ 'text/csv'
31
+ else
32
+ 'application/octet-stream'
33
+ end
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,11 @@
1
+ module Faraday
2
+ class Request::JsonEncode < Faraday::Middleware
3
+ def call(env)
4
+ if env[:request_headers]["Content-Type"] == nil
5
+ env[:body] = MultiJson.encode(env[:body]) if env[:body] != nil
6
+ env[:request_headers]["Content-Type"] = "application/json"
7
+ end
8
+ @app.call(env)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ module Faraday
2
+ class Response::ParseJson < Faraday::Middleware
3
+ dependency 'multi_json'
4
+
5
+ def call(env)
6
+ @app.call(env).on_complete do
7
+ if env[:response_headers]["content-type"].include?("application/json")
8
+ env[:body] = convert_to_json(env[:body])
9
+ end
10
+ end
11
+ end
12
+
13
+ protected
14
+
15
+ def convert_to_json(body)
16
+ case body.strip
17
+ when ''
18
+ nil
19
+ when 'true'
20
+ true
21
+ when 'false'
22
+ false
23
+ else
24
+ begin
25
+ ::MultiJson.decode(body)
26
+ rescue Exception => exception
27
+ raise SifiApi::InvalidJson
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ module Faraday
2
+ class Response::RaiseSifiError < Response::Middleware
3
+ def on_complete(response)
4
+ case response[:status].to_i
5
+ when 400
6
+ raise SifiApi::BadRequest, error_message(response)
7
+ when 401
8
+ raise SifiApi::Unauthorized, error_message(response)
9
+ when 403
10
+ raise SifiApi::Forbidden, error_message(response)
11
+ when 404
12
+ raise SifiApi::NotFound, error_message(response)
13
+ when 406
14
+ raise SifiApi::NotAcceptable, error_message(response)
15
+ when 422
16
+ raise SifiApi::UnprocessableEntity, error_message(response)
17
+ when 500
18
+ raise SifiApi::InternalServerError, error_message(response)
19
+ when 501
20
+ raise SifiApi::NotImplemented, error_message(response)
21
+ when 502
22
+ raise SifiApi::BadGateway, error_message(response)
23
+ when 503
24
+ raise SifiApi::ServiceUnavailable, error_message(response)
25
+ end
26
+ end
27
+
28
+ def error_message(response)
29
+ msg = "#{response[:method].to_s.upcase} #{response[:url].to_s}: #{response[:status]}"
30
+ if errors = response[:body] && response[:body]["errors"]
31
+ msg << "\n"
32
+ msg << errors.join("\n")
33
+ end
34
+ msg
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::Ad < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::AdFileType < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::AdSize < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::BidType < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::BrandedDatum < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::Browser < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::Campaign < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::CampaignStat < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::CampaignType < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::Change < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::Client < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::Company < SifiApi::Resource
2
+ end
@@ -0,0 +1,59 @@
1
+ require 'faraday/request/convert_file_to_upload_io'
2
+ require 'faraday/request/json_encode'
3
+ require 'faraday/response/raise_sifi_error'
4
+ require 'faraday/response/parse_json'
5
+
6
+ class SifiApi::Connection
7
+ def initialize(app_key, site="https://app.simpli.fi/api/")
8
+ @site = site
9
+ @app_key = app_key
10
+ @connection = Faraday::Connection.new(:url => site, :ssl => { :verify => false }) do |builder|
11
+ builder.use Faraday::Request::ConvertFileToUploadIO
12
+ builder.request :multipart
13
+ builder.use Faraday::Request::JsonEncode
14
+
15
+ builder.use Faraday::Response::RaiseSifiError
16
+ builder.use Faraday::Response::ParseJson
17
+
18
+ builder.adapter Faraday.default_adapter
19
+ end
20
+ end
21
+
22
+ def user(user_key, params={})
23
+ SifiApi::User.get_via_uri(self, user_key, '', params).first
24
+ end
25
+
26
+ def get(user_key, route='', params={}, headers={})
27
+ begin
28
+ @connection.run_request(:get, @connection.build_url(route, params), nil, { "X-App-Key" => @app_key, "X-User-Key" => user_key }.merge(headers))
29
+ rescue Faraday::Error::TimeoutError => e
30
+ raise SifiApi::Timeout, "Timeout"
31
+ end
32
+ end
33
+
34
+ def post(user_key, route='', body=nil, headers={})
35
+ begin
36
+ @connection.run_request(:post, route, body, { "X-App-Key" => @app_key, "X-User-Key" => user_key }.merge(headers))
37
+ rescue Faraday::Error::TimeoutError => e
38
+ raise SifiApi::Timeout, "Timeout"
39
+ end
40
+ end
41
+
42
+ def put(user_key, route='', body=nil, headers={})
43
+ begin
44
+ @connection.run_request(:put, route, body, { "X-App-Key" => @app_key, "X-User-Key" => user_key }.merge(headers))
45
+ rescue Faraday::Error::TimeoutError => e
46
+ raise SifiApi::Timeout, "Timeout"
47
+ end
48
+ end
49
+
50
+ def delete(user_key, route='', body=nil, headers={})
51
+ begin
52
+ response = @connection.run_request(:delete, route, body, { "X-App-Key" => @app_key, "X-User-Key" => user_key }.merge(headers))
53
+ response[:status] == 204
54
+ rescue Faraday::Error::TimeoutError => e
55
+ raise SifiApi::Timeout, "Timeout"
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::Context < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::Dayparting < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::Device < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::Dma < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::Domain < SifiApi::Resource
2
+ end
@@ -0,0 +1,15 @@
1
+ module SifiApi
2
+ class Error < StandardError; end
3
+ class BadRequest < Error; end
4
+ class Unauthorized < Error; end
5
+ class Forbidden < Error; end
6
+ class NotFound < Error; end
7
+ class NotAcceptable < Error; end
8
+ class UnprocessableEntity < Error; end
9
+ class InternalServerError < Error; end
10
+ class NotImplemented < Error; end
11
+ class BadGateway < Error; end
12
+ class ServiceUnavailable < Error; end
13
+ class InvalidJson < Error; end
14
+ class Timeout < Error; end
15
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::GeoTarget < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::IpRange < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::Keyword < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::KeywordCategory < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::OperatingSystem < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::Recency < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::RecurringReport < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::Report < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::ReportAsset < SifiApi::Resource
2
+ end
@@ -0,0 +1,2 @@
1
+ class SifiApi::ReportType < SifiApi::Resource
2
+ end