messenger_pigeon 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
+ SHA1:
3
+ metadata.gz: 9fdb8c895d8bb1bcbe17ca6b03c1c5b9d5817f5a
4
+ data.tar.gz: 00b928c5bdb4bbdeda14f782c444ebb3c41579b4
5
+ SHA512:
6
+ metadata.gz: 99d56f47b366c771b0cd3cb1fe5c22943d2807dd26fd863e0a785aa0636fb08f0165b8738374ea5da40a383034422497ff319db63aa1bb5c2aefba1e2a226dd0
7
+ data.tar.gz: 23e2a014dd88653a1e4f511f841221036fc254ce62e58d612aac538f02b44f3a09551cb4092d4bf862605a5c22a46e13b642bbd61db0b486cb30dafdcdf8efea
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Chris Mann
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,227 @@
1
+ # MessengerPigeon
2
+
3
+ A means of getting data from one location to another.
4
+
5
+ The goal of MessengerPigeon is to provide a highly-configurable and adaptable
6
+ animal to take data from any number of sources, and copy that data to any
7
+ number of destinations. A pigeon may modify, drop or add to your data while
8
+ in transit, but should only do these things when you ask it nicely.
9
+
10
+ MessengerPigeon is under heavy development.
11
+
12
+ ## Configuration
13
+ Example Configuration demonstrating some features:
14
+
15
+ * CSV source to an OrgMode target
16
+ * Setting the data format for a target
17
+ * Globbed file selection
18
+ * Source hooks
19
+ * Data Filtering
20
+ * Pre and Post-filter data transformations
21
+
22
+ ~/.messengerpigeonrc:
23
+ ```ruby
24
+ {
25
+ targets: {
26
+ 'OrgMode' => {
27
+ type: MessengerPigeon::OrgMode,
28
+ options: {
29
+ file: 'clocks.org',
30
+ refile_target: '%{project}',
31
+ data_format: proc do |d|
32
+ clock = ' CLOCK: [%{date} %{start_time}]--[%{date} %{end_time}] => %{duration}'
33
+ clock % d
34
+ end
35
+ }
36
+ }
37
+ },
38
+ sources: {
39
+ 'TimeTracker' => {
40
+ type: MessengerPigeon::CSV,
41
+ options: {
42
+ file_glob: 'test.csv',
43
+ on_complete: :archive
44
+ }
45
+ }
46
+ },
47
+ pigeons: {
48
+ 'Gemima' => {
49
+ source: 'TimeTracker',
50
+ filters: {
51
+ project: "Work"
52
+ },
53
+ transforms: {
54
+ pre_filter: {
55
+ date: proc { |a| Date.parse(a).strftime('%Y-%m-%d') },
56
+ duration: proc do |a|
57
+ r = format('%.2f', a.to_i + (a.to_f - a.to_i) / 100 * 60)
58
+ r.sub(/\./, ':')
59
+ end
60
+ },
61
+ post_filter: {}
62
+ },
63
+ target: 'OrgMode'
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ ## Modules
70
+
71
+ ### CSV
72
+ #### Source
73
+ ```ruby
74
+ ...
75
+ sources: {
76
+ 'TimeTracker' => {
77
+ type: MessengerPigeon::CSV,
78
+ options: {
79
+ file_glob: 'test.csv',
80
+ on_complete: :archive
81
+ }
82
+ }
83
+ }
84
+ ...
85
+ ```
86
+ #### Target
87
+ Not yet implemented
88
+
89
+ ### Console
90
+ #### Source
91
+ Not yet implemented.
92
+ #### Target
93
+ ```ruby
94
+ ...
95
+ targets: {
96
+ 'Console => {
97
+ type: MessengerPigeon::Console,
98
+ options: {}
99
+ }
100
+ }
101
+ ...
102
+ ```
103
+ ### OrgMode
104
+ #### Source
105
+ Not yet implemented
106
+ #### Target
107
+ ```ruby
108
+ ...
109
+ targets: {
110
+ 'OrgMode' => {
111
+ type: MessengerPigeon::OrgMode,
112
+ options: {
113
+ file: 'clocks.org',
114
+ refile_target: '%{project}',
115
+ data_format: proc do |d|
116
+ clock = ' CLOCK: [%{date} %{start_time}]--[%{date} %{end_time}] => %{duration}'
117
+ clock % d
118
+ end
119
+ }
120
+ }
121
+ }
122
+ ...
123
+ ```
124
+ ### Redmine
125
+ Configuration should be self-explanatory except for perhaps the 'resource'
126
+ option. This is required option that corresponds to a capitalised,
127
+ camel-cased and singular equivalent to the resources listed here:
128
+
129
+ http://www.redmine.org/projects/redmine/wiki/Rest_api
130
+
131
+ E.g., 'time_entries' becomes 'TimeEntry'
132
+ #### Source
133
+ 'mode' is either ':all', or ':specific'.
134
+
135
+ Example for :all to get all issues in Project 2.
136
+ ```ruby
137
+ ...
138
+ sources: {
139
+ 'Redmine' => {
140
+ type: MessengerPigeon::Redmine,
141
+ options: {
142
+ resource: "Issue",
143
+ site: 'https://url.to.redmine.org',
144
+ user: 'username',
145
+ password: 's3kr1t'
146
+ mode: :all,
147
+ params: {
148
+ status_id: '*',
149
+ project_id: '2'
150
+ }
151
+ }
152
+ }
153
+ },
154
+ ...
155
+ ```
156
+
157
+ Example for :specific to get the issue with ID = 30
158
+ ```ruby
159
+ ...
160
+ sources: {
161
+ 'Redmine' => {
162
+ type: MessengerPigeon::Redmine,
163
+ options: {
164
+ resource: "Issue",
165
+ site: 'https://url.to.redmine.org',
166
+ user: 'username',
167
+ password: 's3kr1t'
168
+ mode: :specific,
169
+ key: 30
170
+ }
171
+ }
172
+ },
173
+ ...
174
+ ```
175
+
176
+ #### Target
177
+ Note: The generators section of the configuration creates keys in the hash. This
178
+ should give a reasonable overview of what is required. See the description of
179
+ keys required on the particular resource page for more information.
180
+
181
+ ```ruby
182
+ ...
183
+ targets: {
184
+ 'Redmine' => {
185
+ type: MessengerPigeon::Redmine,
186
+ options: {
187
+ resource: "TimeEntry",
188
+ site: 'https://url.to.redmine.org',
189
+ user: 'username',
190
+ password: 's3kr1t'
191
+ }
192
+ },
193
+ },
194
+ pigeons: {
195
+ 'Gemima' => {
196
+ filters: {
197
+ issue_id: proc { |a| !a.nil? },
198
+ activity_id: proc { |a| !a.nil? }
199
+ },
200
+ generators: {
201
+ activity_id: proc { |a| a[:activity_id] || '9' },
202
+ issue_id: proc do |a|
203
+ if a[:description]
204
+ m = a[:description].match(/\#(?<id>[0-9]+)/)
205
+ m[:id]
206
+ end
207
+ end,
208
+ hours: proc { |a| a[:duration] },
209
+ spent_on: proc { |a| a[:date] },
210
+ comments: proc do |a|
211
+ a[:description].sub(/\#[0-9]+ */, '') if a[:description]
212
+ end
213
+ },
214
+ transforms: {
215
+ pre_filter: {
216
+ date: proc { |a| Date.parse(a).strftime('%Y-%m-%d') }
217
+ },
218
+ post_filter: {}
219
+ },
220
+ source: 'MySource',
221
+ target: 'Redmine'
222
+ }
223
+ }
224
+ ...
225
+ ```
226
+ ### And more?
227
+ Suggestions / contributions of more modules are welcome
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'messenger_pigeon'
4
+
5
+ opts = MessengerPigeon::CLI.parse_options
6
+ MessengerPigeon.flock(opts)
@@ -0,0 +1,20 @@
1
+ module MessengerPigeon
2
+ module CLI
3
+ module_function
4
+
5
+ def parse_options
6
+ options = {
7
+ config: File.expand_path(ENV['MESSENGER_PIGEON_RC'] ||
8
+ '~/.messenger-pigeon.rc')
9
+ }
10
+ OptionParser.new do |opts|
11
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
12
+ opts.on('-c', '--config file', 'Configuration file') do |f|
13
+ options[:config] = f
14
+ end
15
+ end.parse!
16
+ Config.pigeons = ARGV
17
+ options
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ module MessengerPigeon
2
+ # MessengerPigeon Configuration
3
+ module Config
4
+ extend self
5
+
6
+ attr_reader :conf
7
+ attr_accessor :pigeons
8
+
9
+ def load(config)
10
+ @conf ||= {}
11
+ @conf.merge!(binding.eval(File.read(config)))
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+
2
+ module MessengerPigeon
3
+ # Console source/target module
4
+ module Console
5
+ # Source definition
6
+ class Source
7
+ def initialize
8
+ fail 'Not Implemented'
9
+ end
10
+ end
11
+
12
+ # Target definition
13
+ class Target
14
+ def initialize(_options)
15
+ end
16
+
17
+ def update(data)
18
+ puts data
19
+ end
20
+
21
+ def write
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,52 @@
1
+ require 'csv'
2
+ CSVLib = CSV
3
+ module MessengerPigeon
4
+ # CSV source/target module
5
+ module CSV
6
+ # Source definition
7
+ class Source
8
+ def initialize(options)
9
+ # :file_glob
10
+ @files = Dir.glob(options[:file_glob])
11
+ @on_complete = options[:on_complete]
12
+ end
13
+
14
+ def read
15
+ @data = @files.collect { |f| read_file f }.flatten
16
+ end
17
+
18
+ def complete
19
+ case @on_complete
20
+ when :archive
21
+ archive_files
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def archive_files
28
+ @files.each do |f|
29
+ archive_dir = (File.dirname f) + '/archive/'
30
+ Dir.mkdir archive_dir unless Dir.exist? archive_dir
31
+ File.rename f, archive_dir + (File.basename f)
32
+ end
33
+ end
34
+
35
+ def read_file(file)
36
+ d = CSVLib.read file
37
+ headings = d.first.collect do |v|
38
+ title = v.gsub(/[^a-z]+/i, '_')
39
+ title.downcase.intern
40
+ end
41
+ d[1..-1].collect { |v| (headings.zip v).to_h }
42
+ end
43
+ end
44
+
45
+ # Target definition
46
+ class Target
47
+ def initialize
48
+ fail 'Not Implemented'
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,56 @@
1
+
2
+
3
+ module MessengerPigeon
4
+ # OrgMode source/target module
5
+ module OrgMode
6
+ # Source definition
7
+ class Source
8
+ def initialize(_options = {})
9
+ fail 'Not Implemented'
10
+ end
11
+ end
12
+
13
+ # Target definition
14
+ class Target
15
+ def initialize(options = nil)
16
+ @options = options
17
+ if File.exist? options[:file]
18
+ @fd = File.open(options[:file], 'r+')
19
+ @fd.seek 0
20
+ @filedata = @fd.read nil
21
+ else
22
+ @fd = File.open(options[:file], 'w')
23
+ @filedata = ''
24
+ end
25
+ @known_headings = known_headings
26
+ end
27
+
28
+ def known_headings
29
+ res = []
30
+ @filedata.each_line do |l|
31
+ /^\*+\ (?<heading>.+?)(:[^ ]*:)?$/ =~ l
32
+ res.push heading if heading
33
+ end
34
+ res
35
+ end
36
+
37
+ def update(data)
38
+ heading = @options[:refile_target] % data
39
+ data_string = @options[:data_format].call data
40
+ if @known_headings.include? heading
41
+ @filedata.sub!(/^(\* #{heading}.*)$/,
42
+ "\\1\n#{data_string}")
43
+ else
44
+ @filedata += "* #{heading}\n#{data_string}\n"
45
+ @known_headings.push heading
46
+ end
47
+ end
48
+
49
+ def write
50
+ @fd.seek 0
51
+ @fd.write @filedata
52
+ @fd.close
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,59 @@
1
+
2
+ module MessengerPigeon
3
+ # Check and apply data matches
4
+ class Pigeon
5
+ def initialize(source, target, pigeon_config)
6
+ @source = source
7
+ @target = target
8
+ @filters = pigeon_config[:filters]
9
+ @transforms = pigeon_config[:transforms]
10
+ @generators = pigeon_config[:generators]
11
+ end
12
+
13
+ def fly
14
+ @source.read.each do |d|
15
+ transforms :pre_filter, d
16
+ generators d
17
+ @target.update(transforms :post_filter, d) if filter d
18
+ end
19
+ self
20
+ end
21
+
22
+ def finalise
23
+ @target.write
24
+ @source.complete
25
+ end
26
+
27
+ private
28
+
29
+ def filter(data)
30
+ return if @filters.nil?
31
+ @filters.each do |k, v|
32
+ if v.class == Regexp
33
+ return false unless data[k] =~ v
34
+ elsif v.class == String
35
+ return false unless data[k] == v
36
+ elsif v.class == Proc
37
+ return false unless v.call data[k]
38
+ end
39
+ end
40
+ true
41
+ end
42
+
43
+ def transforms(type, data)
44
+ return data if @transforms.nil? || @transforms[:type].nil?
45
+ @transforms[type].each do |k, v|
46
+ data[k] = v.call data[k]
47
+ end
48
+ data
49
+ end
50
+
51
+ def generators(data)
52
+ return data if @generators.nil?
53
+ @generators.each do |k, v|
54
+ data[k] = v.call data
55
+ end
56
+ data
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,78 @@
1
+ require 'active_resource'
2
+
3
+ module MessengerPigeon
4
+ # Redmine source/target module
5
+ module Redmine
6
+ # Handle JSON returned by Redmine
7
+ class RedmineFormatter
8
+ include ActiveResource::Formats::JsonFormat
9
+
10
+ def decode(json)
11
+ remove_root(ActiveSupport::JSON.decode(json))
12
+ end
13
+
14
+ private
15
+
16
+ def remove_root(data)
17
+ # Just return the first value in the hash
18
+ data.each do |_k, v|
19
+ return v
20
+ end
21
+ end
22
+ end
23
+
24
+ # Source definition
25
+ class Source
26
+ def initialize(options)
27
+ @options = options
28
+ ar_cl = Class.new ActiveResource::Base
29
+ o = Object.const_set(options[:resource], ar_cl)
30
+ o.site = options[:site]
31
+ o.user = options[:user]
32
+ o.password = options[:password]
33
+ o.include_root_in_json = true
34
+ o.format = RedmineFormatter.new
35
+ @resource = o
36
+ end
37
+
38
+ def read
39
+ if @options[:mode] == :specific
40
+ [@resource.find(@options[:key]).attributes]
41
+ elsif @options[:mode] == :all
42
+ res = @resource.find(:all, params: @options[:params])
43
+ res.map do |r|
44
+ attrs = r.attributes
45
+ attrs.each do |k, v|
46
+ attrs[k] = v.attributes if v.respond_to? :attributes
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ def complete
53
+ end
54
+ end
55
+
56
+ # Target definition
57
+ class Target
58
+ def initialize(options)
59
+ @options = options
60
+ ar_cl = Class.new ActiveResource::Base
61
+ o = Object.const_set(options[:resource], ar_cl)
62
+ o.site = options[:site]
63
+ o.user = options[:user]
64
+ o.password = options[:password]
65
+ o.include_root_in_json = true
66
+ @resource = o
67
+ end
68
+
69
+ def update(data)
70
+ m = @resource.new data
71
+ $stderr.puts issue.errors.full_messages unless m.save
72
+ end
73
+
74
+ def write
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,3 @@
1
+ module MessengerPigeon
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,38 @@
1
+ require 'optparse'
2
+
3
+ require 'messenger_pigeon/version'
4
+ require 'messenger_pigeon/pigeon'
5
+ require 'messenger_pigeon/orgmode'
6
+ require 'messenger_pigeon/csv'
7
+ require 'messenger_pigeon/redmine'
8
+ require 'messenger_pigeon/console'
9
+ require 'messenger_pigeon/config'
10
+ require 'messenger_pigeon/cli'
11
+
12
+ # Messenger Pigeon entry
13
+ module MessengerPigeon
14
+ module_function
15
+
16
+ def flock(opts)
17
+ Config.load(opts[:config])
18
+ pigeons = []
19
+ Config.conf[:pigeons].each do |name, instr|
20
+ next unless Config.pigeons.empty? || Config.pigeons.include?(name)
21
+ s = prepare_endpoint :sources, instr[:source]
22
+ t = prepare_endpoint :targets, instr[:target]
23
+ pigeon = Pigeon.new s, t, instr
24
+ pigeons << pigeon.fly
25
+ end
26
+ pigeons.each(&:finalise)
27
+ end
28
+
29
+ def prepare_endpoint(endpoint, name)
30
+ v = Config.conf[endpoint][name]
31
+ m = v[:type]
32
+ if endpoint == :sources
33
+ m::Source.new v[:options]
34
+ elsif endpoint == :targets
35
+ m::Target.new v[:options]
36
+ end
37
+ end
38
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: messenger_pigeon
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Mann
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-06-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activeresource
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.0'
55
+ description: The goal of MessengerPigeon is to provide a highly-configurable and general
56
+ mechanism to take data from any number of sources, and copy that data to any number
57
+ of destinations. Possibly after some modest filtering and transformations.
58
+ email:
59
+ - chris@bitpattern.com.au
60
+ executables:
61
+ - messenger-pigeon
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - LICENSE.txt
66
+ - README.md
67
+ - bin/messenger-pigeon
68
+ - lib/messenger_pigeon.rb
69
+ - lib/messenger_pigeon/cli.rb
70
+ - lib/messenger_pigeon/config.rb
71
+ - lib/messenger_pigeon/console.rb
72
+ - lib/messenger_pigeon/csv.rb
73
+ - lib/messenger_pigeon/orgmode.rb
74
+ - lib/messenger_pigeon/pigeon.rb
75
+ - lib/messenger_pigeon/redmine.rb
76
+ - lib/messenger_pigeon/version.rb
77
+ homepage: https://github.com/cshclm/MessengerPigeon
78
+ licenses:
79
+ - MIT
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project:
97
+ rubygems_version: 2.4.5
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: A means of getting data from one location to another.
101
+ test_files: []
102
+ has_rdoc: