tap-rep 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 97ad289534c117812255b709866eaedb3c1a632e
4
+ data.tar.gz: 1bb29d8b7e8296728ed5233eb518b638ed0b8768
5
+ SHA512:
6
+ metadata.gz: e28a7a36cd99c18faa3a99ffd007bd52758ea23c050f81b26e9881dabc515f04b67b1b90bac514819bf7c1c3c1fffd68701e3cc8bf025f31d2468d1fa29b872d
7
+ data.tar.gz: 86d94fe2f88ceded233916c5db61a42dd6fa56858a979a6c2dbacfe06aa3965c8dbdb4dd9e11a03768cf75fad0469e23448a0a2ba0bcb03905e145c9ccfc4add
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 follain
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.
@@ -0,0 +1,23 @@
1
+ tap-rep
2
+ ========
3
+
4
+ ## A [singer.io](http://singer.io) tap to extract data from [Rep](http://rep.ai) and load into any Singer target, like Stitch or CSV
5
+
6
+ # Configuration
7
+
8
+ {
9
+ "token":"bearer-token-from-rep"
10
+ }
11
+
12
+ # Usage (with [Stitch target](https://github.com/singer-io/target-stitch))
13
+
14
+ > bundle exec tap-rep
15
+ Usage: tap-rep [options]
16
+ -c, --config config_file Set config file (json)
17
+ -s, --state state_file Set state file (json)
18
+ -h, --help Displays help
19
+ -v, --verbose Enables verbose logging to STDERR
20
+
21
+ > pip install target-stitch
22
+ > gem install tap-rep
23
+ > bundle exec tap-rep -c config.rep.json -s state.json | target-stitch --config config.stitch.json | tail -1 > state.new.json
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler'
4
+
5
+ Bundler.require
6
+
7
+ require 'concurrent'
8
+ require 'optparse'
9
+
10
+ require_relative '../lib/client'
11
+ require_relative '../lib/schema'
12
+ require_relative '../lib/models/base'
13
+ require_relative '../lib/models/session'
14
+
15
+ config_file = nil
16
+ state_file = nil
17
+ verbose = false
18
+
19
+ parser = OptionParser.new do |opts|
20
+ opts.banner = "Usage: #{$0} [options]"
21
+ opts.on('-c', '--config config_file', 'Set config file (json)') do |config|
22
+ config_file = config
23
+ end
24
+
25
+ opts.on('-s', '--state state_file', 'Set state file (json)') do |state|
26
+ state_file = state
27
+ end
28
+
29
+ opts.on('-h', '--help', 'Displays help') do
30
+ puts opts
31
+ exit
32
+ end
33
+
34
+ opts.on('-v', '--verbose', 'Enables verbose logging to STDERR') do
35
+ verbose = true
36
+ end
37
+ end
38
+
39
+ parser.parse!
40
+
41
+ if config_file.nil?
42
+ puts parser
43
+ exit
44
+ end
45
+
46
+ config = JSON.parse(File.read(config_file))
47
+
48
+ state = {}
49
+ if state_file
50
+ state = JSON.parse(File.read(state_file))
51
+ end
52
+
53
+
54
+ client = TapRep::Client.new(
55
+ config['token'],
56
+ verbose,
57
+ Concurrent::Hash.new.merge!(state),
58
+ $stdout
59
+ )
60
+
61
+ TapRep::Models::Base.subclasses.each do |model|
62
+ client.output model.schema
63
+ end
64
+
65
+ TapRep::Models::Base.subclasses.each do |model|
66
+ client.process model
67
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday_middleware'
5
+ require 'json'
6
+ require 'logger'
7
+
8
+ module TapRep
9
+ DEFAULT_START_TIME = '0001-01-01T00:00:00+00:00'
10
+ LIMIT = 50
11
+ BASE_URL = 'https://app.rep.ai'
12
+
13
+ # rubocop:disable Metrics/BlockLength
14
+ Client = Struct.new(:token, :verbose, :state, :stream) do
15
+ def initialize(**kwargs)
16
+ super(*members.map { |k| kwargs[k] })
17
+ end
18
+
19
+ def process(model)
20
+ records = get(model)
21
+ return unless records.any?
22
+
23
+ output_records model, records
24
+ output_state model, records.last['end_time']
25
+ process model
26
+ end
27
+
28
+ def output(hash)
29
+ stream.puts JSON.generate(hash)
30
+ end
31
+
32
+ private
33
+
34
+ def output_records(model, records)
35
+ records.each do |record|
36
+ model.new(record, self).records.flatten.each do |model_record|
37
+ output model_record
38
+ end
39
+ end
40
+ end
41
+
42
+ def output_state(model, value)
43
+ state[model.stream] = value
44
+ output type: :STATE, value: state
45
+ end
46
+
47
+ def get(model)
48
+ start_time = state[model.stream] || DEFAULT_START_TIME
49
+
50
+ Array(
51
+ connection.get(
52
+ "/api/v1.0/reporting/#{model.path}",
53
+ start_time: start_time,
54
+ limit: LIMIT
55
+ ).body
56
+ )
57
+ end
58
+
59
+ def connection
60
+ @connection ||= Faraday::Connection.new do |conn|
61
+ conn.authorization :Bearer, token
62
+ conn.headers['Accept-Encoding'] = 'application/json'
63
+ conn.response :json
64
+ conn.url_prefix = BASE_URL
65
+ conn.response :logger, ::Logger.new(STDERR), bodies: true if verbose
66
+ conn.adapter Faraday.default_adapter
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/inflector'
4
+ require_relative '../schema'
5
+
6
+ module TapRep
7
+ module Models
8
+ Base = Struct.new(:data, :client) do # rubocop:disable Metrics/BlockLength
9
+ def self.subclasses
10
+ ObjectSpace.each_object(Class).select { |klass| klass < self }
11
+ end
12
+
13
+ def self.path
14
+ name.demodulize.tableize
15
+ end
16
+
17
+ def self.stream
18
+ name.demodulize.tableize
19
+ end
20
+
21
+ def self.key_property
22
+ :id
23
+ end
24
+
25
+ def self.schema(&block)
26
+ @schema ||= ::TapRep::Schema.new(stream, key_property)
27
+ @schema.instance_eval(&block) if block_given?
28
+ @schema.to_hash
29
+ end
30
+
31
+ def transform
32
+ data.dup
33
+ end
34
+
35
+ def base_record
36
+ {
37
+ type: 'RECORD',
38
+ stream: self.class.stream,
39
+ record: transform
40
+ }
41
+ end
42
+
43
+ def extra_records
44
+ []
45
+ end
46
+
47
+ def records
48
+ [base_record] + extra_records.map(&:records)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module TapRep
6
+ module Models
7
+ # Models a Rep Session
8
+ class Session < Base
9
+ def self.key_property
10
+ :encrypted_id
11
+ end
12
+
13
+ schema do
14
+ string :encrypted_id, :not_null
15
+ datetime :first_customer_message_at
16
+ datetime :end_time
17
+ number :duration
18
+ string :responder
19
+ datetime :start_time
20
+ string :channel_name
21
+ object :customer
22
+ string :channel_type
23
+ array :categories
24
+ string :notes
25
+ datetime :first_agent_message_at
26
+ number :time_to_first_response
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+ require 'optparse'
5
+
6
+ require_relative 'client'
7
+ require_relative 'schema'
8
+ require_relative 'models/base'
9
+ require_relative 'models/session'
10
+
11
+ module TapRep
12
+ # Kicks off tap-rep process
13
+ class Runner
14
+ attr_reader :config_filename
15
+ attr_reader :state_filename
16
+ attr_reader :stream
17
+ attr_reader :verbose
18
+
19
+ def initialize(argv, stream: $stderr, config: nil, state: nil)
20
+ @stream = stream
21
+ @config = config
22
+ @state = state
23
+ parser.parse! argv
24
+ end
25
+
26
+ def perform
27
+ return stream.puts(parser) if config.keys.empty?
28
+ output_schemata
29
+ process_models
30
+ end
31
+
32
+ def config
33
+ @config ||= read_json(config_filename)
34
+ end
35
+
36
+ def state
37
+ @state ||= read_json(state_filename)
38
+ end
39
+
40
+ private
41
+
42
+ def output_schemata
43
+ TapRep::Models::Base.subclasses.each do |model|
44
+ client.output model.schema
45
+ end
46
+ end
47
+
48
+ def process_models
49
+ TapRep::Models::Base.subclasses.each do |model|
50
+ client.process model
51
+ end
52
+ end
53
+
54
+ def client
55
+ @client ||= TapRep::Client.new(
56
+ token: config['token'],
57
+ verbose: verbose,
58
+ state: Concurrent::Hash.new.merge!(state_minus_3_days),
59
+ stream: stream
60
+ )
61
+ end
62
+
63
+ # Per Rep, include a "buffer" when we kick off our process
64
+ # In other words, end_time != session modification time. As a result, just
65
+ # maintaining a high watermark has the potential to miss certain sessions,
66
+ # and the probability of missing sessions increases if there's a big gap
67
+ # between the time of last message in the session and the time it is closed
68
+ # out by an agent (like, on weekends).
69
+ #
70
+ # For example, if you most recently queried all sessions up until time T1,
71
+ # then set start_time to T1 - 3 days on the next run (and dedupe sessions
72
+ # based on encrypted_id, which is guaranteed to be unique). This should
73
+ # account for sessions that happened over the weekend, etc.
74
+ def state_minus_3_days
75
+ return state unless state['sessions']
76
+ state.merge(
77
+ 'sessions' => DateTime.parse(state['sessions']).prev_day(3).iso8601
78
+ )
79
+ end
80
+
81
+ def read_json(filename)
82
+ return JSON.parse(File.read(filename)) if filename
83
+ {}
84
+ end
85
+
86
+ def parser
87
+ @parser ||= OptionParser.new do |opts|
88
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
89
+ opts.on('-c', '--config filename', 'Set config file (json)') do |config|
90
+ @config_filename = config
91
+ end
92
+
93
+ opts.on('-s', '--state filename', 'Set state file (json)') do |state|
94
+ @state_filename = state
95
+ end
96
+
97
+ opts.on('-v', '--verbose', 'Enables verbose logging to STDERR') do
98
+ @verbose = true
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,65 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ module TapRep
5
+ # Models a JSON Schema required for Singer taps
6
+ class Schema
7
+ attr_reader :stream, :key_property
8
+
9
+ def initialize(stream, key_property)
10
+ @stream = stream
11
+ @key_property = key_property
12
+ end
13
+
14
+ # Models JSON Schema types
15
+ class Types
16
+ def self.method_missing(method, *args)
17
+ return super unless %I[
18
+ array
19
+ number
20
+ object
21
+ string
22
+ ].include?(method)
23
+
24
+ types = [method.to_sym]
25
+ types << :null unless args.include?(:not_null)
26
+
27
+ {
28
+ type: types.one? ? types.first : types
29
+ }.tap do |hash|
30
+ hash[:format] = 'date-time' if args.include?(:datetime)
31
+ end
32
+ end
33
+
34
+ def self.respond_to_missing?(_method, *_args)
35
+ true
36
+ end
37
+
38
+ def self.datetime(*args)
39
+ args << :datetime
40
+ string(*args)
41
+ end
42
+ end
43
+
44
+ %i[string number array object datetime].each do |type|
45
+ define_method type do |name, *options|
46
+ properties[name.to_sym] = Types.send(type, *options)
47
+ end
48
+ end
49
+
50
+ def properties
51
+ @properties ||= {}
52
+ end
53
+
54
+ def to_hash
55
+ {
56
+ type: :SCHEMA,
57
+ stream: stream,
58
+ key_properties: [key_property],
59
+ schema: {
60
+ properties: properties
61
+ }
62
+ }
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TapRep
4
+ VERSION = '0.0.1'
5
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tap-rep
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Joe Lind
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-06-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.1'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 5.1.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '5.1'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 5.1.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: concurrent-ruby
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 1.0.2
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '1.0'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 1.0.2
53
+ - !ruby/object:Gem::Dependency
54
+ name: faraday
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '0.12'
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 0.12.1
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '0.12'
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 0.12.1
73
+ - !ruby/object:Gem::Dependency
74
+ name: faraday_middleware
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '0.11'
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 0.11.0.1
83
+ type: :runtime
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.11'
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 0.11.0.1
93
+ description: Stream Rep records to a Singer target, such as Stitch
94
+ email: joelind@gmail.com
95
+ executables:
96
+ - tap-rep
97
+ extensions: []
98
+ extra_rdoc_files: []
99
+ files:
100
+ - LICENSE
101
+ - README.md
102
+ - bin/tap-rep
103
+ - lib/client.rb
104
+ - lib/models/base.rb
105
+ - lib/models/session.rb
106
+ - lib/runner.rb
107
+ - lib/schema.rb
108
+ - lib/tap_rep/version.rb
109
+ homepage: https://github.com/Follain/tap-rep
110
+ licenses:
111
+ - MIT
112
+ metadata: {}
113
+ post_install_message:
114
+ rdoc_options: []
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ requirements: []
128
+ rubyforge_project:
129
+ rubygems_version: 2.5.2
130
+ signing_key:
131
+ specification_version: 4
132
+ summary: Singer.io tap for Rep POS
133
+ test_files: []