onena 0.1.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: 0f8a1565f05c0850fb2fe02bc4a94a47de4a20ad
4
+ data.tar.gz: 9ec380884063a814f4f29e792acea6a5a0e600f7
5
+ SHA512:
6
+ metadata.gz: d72319d589fbdff4a9d1a50e8e50d79e2b7c6630a816994b1e9e8ae32361671bc4be9fdad7a02b2e26739a5dfdb86b9887196dc9ef0e1b9f0392ee17cd4a5cb2
7
+ data.tar.gz: e61181c1c3198c50b2ee50db466573d8a1dffdb40d4e81ce83af973318bb58089d362101784072e45c6f90e9849f1dfc26f10295814431e6932bb8276ce186c7
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ README.md
3
+ ChangeLog.md
4
+
5
+ LICENSE.txt
@@ -0,0 +1,5 @@
1
+ /.bundle
2
+ /Gemfile.lock
3
+ /html/
4
+ /pkg/
5
+ /vendor/cache/*.gem
@@ -0,0 +1,16 @@
1
+ --- !ruby/object:RDoc::Options
2
+ encoding: UTF-8
3
+ static_path: []
4
+ rdoc_include:
5
+ - .
6
+ charset: UTF-8
7
+ exclude:
8
+ hyperlink_all: false
9
+ line_numbers: false
10
+ main_page: README.md
11
+ markup: markdown
12
+ show_hash: false
13
+ tab_width: 8
14
+ title: onena Documentation
15
+ visibility: :protected
16
+ webcvs:
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour --format documentation
@@ -0,0 +1,4 @@
1
+ ---
2
+ language: ruby
3
+ rvm:
4
+ - 2.2
@@ -0,0 +1,4 @@
1
+ ### 0.1.1 / 2016-02-28
2
+
3
+ * Initial release
4
+ * Based on Samwise
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,30 @@
1
+ This project is in the public domain within the United States.
2
+
3
+ Additionally, we waive copyright and related rights in the work
4
+ worldwide through the CC0 1.0 Universal public domain dedication.
5
+
6
+ ## CC0 1.0 Universal Summary
7
+
8
+ This is a human-readable summary of the [Legal Code (read the full text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode).
9
+
10
+ ### No Copyright
11
+
12
+ The person who associated a work with this deed has dedicated the work to
13
+ the public domain by waiving all of his or her rights to the work worldwide
14
+ under copyright law, including all related and neighboring rights, to the
15
+ extent allowed by law.
16
+
17
+ You can copy, modify, distribute and perform the work, even for commercial
18
+ purposes, all without asking permission.
19
+
20
+ ### Other Information
21
+
22
+ In no way are the patent or trademark rights of any person affected by CC0,
23
+ nor are the rights that other persons may have in the work or in how the
24
+ work is used, such as publicity or privacy rights.
25
+
26
+ Unless expressly stated otherwise, the person who associated a work with
27
+ this deed makes no warranties about the work, and disclaims liability for
28
+ all uses of the work, to the fullest extent permitted by applicable law.
29
+ When using or citing the work, you should not imply endorsement by the
30
+ author or the affirmer.
@@ -0,0 +1,140 @@
1
+ # onena
2
+
3
+ ## Description
4
+
5
+ Ruby cli tool to reconcile [Tock](https://github.com/18F/tock) and [Float](https://www.float.com/) data.
6
+
7
+ Onena will return possible matches between Tock and Float users, clients,
8
+ projects, and project/client combinations. Exact matches are excluded.
9
+
10
+ Possible matches are returned with [Levenshtein
11
+ distance](https://en.wikipedia.org/wiki/Levenshtein_distance) and [White
12
+ similarity](http://www.catalysoft.com/articles/StrikeAMatch.html).
13
+
14
+ ## Usage
15
+
16
+ To get started, you'll need Tock and [Float API](https://github.com/floatschedule/api) keys.
17
+
18
+ For flexibility, all possible matches are returned as JSON, and filtering is done by a
19
+ separate tool. The examples below using [jq](https://stedolan.github.io/jq/)
20
+ to filter the results.
21
+
22
+ ### Configuration
23
+
24
+ Set the Tock API key as the environment variable `TOCK_API_KEY` and the Float
25
+ API Key as `FLOAT_API_KEY`, or pass the keys as arguments. You may also set
26
+ `TOCK_API_ENDPOINT` to override the default endpoint,
27
+ `https://tock.18f.gov/api/`.
28
+
29
+ ```shell
30
+ $ export TOCK_API_ENDPOINT=http://192.168.33.10/api
31
+ $ export TOCK_API_KEY=...
32
+ $ export FLOAT_API_KEY=...
33
+ ```
34
+
35
+ ### Get possible project matches with a Levenshtein distance of 4 or less
36
+
37
+ ```shell
38
+ $ onena | jq 'select(.type == "project" and .distance <= 4)'
39
+ {
40
+ "float": "First Proj",
41
+ "tock": "First Project",
42
+ "distance": 3,
43
+ "similarity": 0.8235294117647058,
44
+ "type": "project"
45
+ }
46
+ ```
47
+
48
+ ### Get possible user matches with a White similarity of 0.8 or greater
49
+
50
+ ```shell
51
+ $ onena | jq 'select(.type == "user" and .similarity >= 0.8)'
52
+ {
53
+ "float": "Christian Warden",
54
+ "tock": "Christian G. Warden",
55
+ "distance": 3,
56
+ "similarity": 0.9629629629629629,
57
+ "type": "user"
58
+ }
59
+ ```
60
+
61
+ ### Get possible client matches with a Levenshtein distance of 3 or less
62
+ ```shell
63
+ $ onena | jq 'select(.type == "client" and .distance <= 3)'
64
+ {
65
+ "float": "Acme!",
66
+ "tock": "Acme",
67
+ "distance": 1,
68
+ "similarity": 0.8571428571428571,
69
+ "type": "client"
70
+ }
71
+ ```
72
+
73
+ ### Get possible project->client matches with a Levenshtein distance of 8 or less
74
+
75
+ ```shell
76
+ $ onena | jq 'select(.type == "project-client" and .distance <= 8)'
77
+ {
78
+ "float": "First Proj -> Acme!",
79
+ "tock": "First Project -> Acme",
80
+ "distance": 4,
81
+ "similarity": 0.8461538461538461,
82
+ "type": "project-client"
83
+ }
84
+ ```
85
+
86
+ ### Library Usage
87
+
88
+ Onena can also be used a ruby library.
89
+
90
+ ```ruby
91
+ require 'onena'
92
+
93
+ client = Onena::Client.new(tock_api_key: 'tock key ...', float_api_key: 'float key ...', tock_api_endpoint: 'http://192.168.33.10/api')
94
+
95
+ # if you set the 'TOCK_API_KEY' and 'FLOAT_API_KEY' env vars, just use:
96
+ client = Onena::Client.new
97
+
98
+ client.possible_client_matches.select { |match| match[:distance] <= 4 }
99
+ => [{:float=>"Acme!", :tock=>"Acme", :distance=>1, :similarity=>0.8571428571428571}]
100
+ client.possible_project_matches.select { |match| match[:distance] <= 4 }
101
+ => [{:float=>"First Proj", :tock=>"First Project", :distance=>3, :similarity=>0.8235294117647058}]
102
+ client.possible_project_client_matches.select { |match| match[:distance] <= 4 }
103
+ => [{:float=>"First Proj -> Acme!", :tock=>"First Project -> Acme", :distance=>4, :similarity=>0.8461538461538461}]
104
+ client.possible_user_matches.select { |match| match[:distance] <= 4 }
105
+ => [{:float=>"Christian Warden", :tock=>"Christian G. Warden", :distance=>3, :similarity=>0.9629629629629629}]
106
+ ```
107
+
108
+ ## Install
109
+
110
+ From the command-line:
111
+
112
+ ```shell
113
+ $ gem install onena
114
+ ```
115
+
116
+ or in your Gemfile:
117
+
118
+ ```ruby
119
+ gem 'onena', github: 'cwarden/onena'
120
+ ```
121
+
122
+ ## History
123
+
124
+ This tool was developed by [Christian G. Warden](https://github.com/cwarden) as
125
+ a [micro-purchase by 18F](https://micropurchase.18f.gov/auctions/11). For
126
+ consistency with other 18F projects (and because the author has almost nil
127
+ experience with Ruby) the [samwise](https://github.com/18F/samwise) project by
128
+ [Alan deLevie](https://github.com/adelevie) was used as a template for the
129
+ application structure.
130
+
131
+ ## Public Domain
132
+
133
+ This project is in the worldwide [public domain](LICENSE.md).
134
+
135
+ > This project is in the public domain within the United States, and copyright
136
+ > and related rights in the work worldwide are waived through the [CC0 1.0 > Universal public domain > dedication](https://creativecommons.org/publicdomain/zero/1.0/).
137
+ >
138
+ > All contributions to this project will be released under the CC0 dedication.
139
+ > By submitting a pull request, you are agreeing to comply with this waiver of
140
+ > copyright interest.
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+
5
+ begin
6
+ require 'bundler/setup'
7
+ rescue LoadError => e
8
+ abort e.message
9
+ end
10
+
11
+ require 'rake'
12
+
13
+
14
+ require 'rubygems/tasks'
15
+ Gem::Tasks.new
16
+
17
+ require 'rdoc/task'
18
+ RDoc::Task.new
19
+ task :doc => :rdoc
20
+
21
+ require 'rspec/core/rake_task'
22
+ RSpec::Core::RakeTask.new
23
+
24
+ task :test => :spec
25
+ task :default => :spec
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'onena'
4
+
5
+ client = Onena::Client.new
6
+ client.print_possible_matches
@@ -0,0 +1,5 @@
1
+ require 'onena/version'
2
+ require 'onena/error'
3
+ require 'onena/util'
4
+ require 'onena/protocol'
5
+ require 'onena/client'
@@ -0,0 +1,145 @@
1
+ require 'curb'
2
+ require 'json'
3
+
4
+ module Onena
5
+ class Client
6
+ def initialize(tock_api_key: nil, float_api_key: nil, tock_api_endpoint: nil)
7
+ @tock_api_key = tock_api_key || ENV['TOCK_API_KEY']
8
+ @float_api_key = float_api_key || ENV['FLOAT_API_KEY']
9
+ @tock_api_endpoint = tock_api_endpoint || ENV['TOCK_API_ENDPOINT'] || Onena::Protocol::TOCK_API_BASE_URL
10
+ fail Onena::Error::ArgumentMissing, 'Float API key is missing' if @float_api_key.nil?
11
+ end
12
+
13
+ def float_user_names
14
+ float_users = get_float_users
15
+ float_users['people'].map { |user| user['name'] }
16
+ end
17
+
18
+ def tock_user_names
19
+ tock_users = get_tock_users
20
+ tock_users['results'].map { |user| [user['first_name'], user['last_name']].join(' ').strip }
21
+ end
22
+
23
+ def float_project_names
24
+ float_projects = get_float_projects
25
+ float_projects['projects'].map { |project| project['project_name'] }
26
+ end
27
+
28
+ def tock_project_names
29
+ tock_projects = get_tock_projects
30
+ tock_projects['results'].map { |project| project['name'] }
31
+ end
32
+
33
+ def float_client_names
34
+ float_projects = get_float_projects
35
+ clients = float_projects['projects'].map { |project| project['client_name'] }
36
+ clients.uniq.compact
37
+ end
38
+
39
+ def tock_client_names
40
+ tock_projects = get_tock_projects
41
+ clients = tock_projects['results'].map { |project| project['client'] }
42
+ clients.uniq
43
+ end
44
+
45
+ def float_project_client_names
46
+ float_projects = get_float_projects
47
+ project_clients = float_projects['projects'].map { |project| "#{project['project_name']} -> #{project['client_name']}" }
48
+ project_clients.uniq.compact
49
+ end
50
+
51
+ def tock_project_client_names
52
+ tock_projects = get_tock_projects
53
+ project_clients = tock_projects['results'].map { |project| "#{project['name']} -> #{project['client']}" }
54
+ project_clients.uniq
55
+ end
56
+
57
+ def possible_user_matches
58
+ Onena::Util.matches(tock_list: tock_user_names, float_list: float_user_names)
59
+ end
60
+
61
+ def possible_project_matches
62
+ Onena::Util.matches(tock_list: tock_project_names, float_list: float_project_names)
63
+ end
64
+
65
+ def possible_client_matches
66
+ Onena::Util.matches(tock_list: tock_client_names, float_list: float_client_names)
67
+ end
68
+
69
+ def possible_project_client_matches
70
+ Onena::Util.matches(tock_list: tock_project_client_names, float_list: float_project_client_names)
71
+ end
72
+
73
+ def tagged_possible_matches
74
+ [tag_matches(matches: possible_user_matches, type: :user),
75
+ tag_matches(matches: possible_project_matches, type: :project),
76
+ tag_matches(matches: possible_client_matches, type: :client),
77
+ tag_matches(matches: possible_project_client_matches, type: :"project-client")]
78
+ .flatten
79
+ .compact
80
+ end
81
+
82
+ def print_possible_matches
83
+ tagged_possible_matches.each { |match| $stdout.puts match.to_json }
84
+ end
85
+
86
+ private
87
+
88
+ def get_float_users
89
+ response = fetch_float_users
90
+ JSON.parse(response.body_str)
91
+ end
92
+
93
+ def get_tock_users
94
+ response = fetch_tock_users
95
+ JSON.parse(response.body_str)
96
+ end
97
+
98
+ def get_float_projects
99
+ response = fetch_float_projects
100
+ JSON.parse(response.body_str)
101
+ end
102
+
103
+ def get_tock_projects
104
+ response = fetch_tock_projects
105
+ JSON.parse(response.body_str)
106
+ end
107
+
108
+ def tock_request(url)
109
+ Curl.get(url) do |http|
110
+ # TODO: Confirm how authentication is done against production
111
+ # endpoint, https://tock.18f.gov/api/
112
+ http.headers['Cookie'] = '_oauth2_proxy=' + @tock_api_key unless @tock_api_key.nil?
113
+ end
114
+ end
115
+
116
+ def fetch_tock_users
117
+ url = Onena::Protocol.tock_users_url(endpoint: @tock_api_endpoint)
118
+ tock_request(url)
119
+ end
120
+
121
+ def fetch_float_users
122
+ url = Onena::Protocol.float_users_url
123
+ Curl.get(url) do |http|
124
+ http.headers['Authorization'] = @float_api_key
125
+ end
126
+ end
127
+
128
+ def fetch_tock_projects
129
+ url = Onena::Protocol.tock_projects_url(endpoint: @tock_api_endpoint)
130
+ tock_request(url)
131
+ end
132
+
133
+ def fetch_float_projects
134
+ url = Onena::Protocol.float_projects_url
135
+ Curl.get(url) do |http|
136
+ http.headers['Authorization'] = @float_api_key
137
+ end
138
+ end
139
+
140
+ def tag_matches(matches: [], type: nil)
141
+ matches.map { |match| match.merge(type: type) }
142
+ end
143
+
144
+ end
145
+ end
@@ -0,0 +1,12 @@
1
+ module Onena
2
+ module Error
3
+ class InvalidFormat < StandardError
4
+ def initialize(message: 'Invalid format')
5
+ super(message)
6
+ end
7
+ end
8
+
9
+ class ArgumentMissing < StandardError
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,26 @@
1
+ module Onena
2
+ module Protocol
3
+ TOCK_API_BASE_URL = 'https://tock.18f.gov/api/'
4
+ FLOAT_API_BASE_URL = 'https://api.floatschedule.com/api'
5
+ FLOAT_API_VERSION = 'v1'
6
+
7
+ def self.tock_users_url(endpoint: nil)
8
+ fail Onena::Error::ArgumentMissing, 'Tock endpoint is missing' if endpoint.nil?
9
+ "#{endpoint}/users.json?page_size=100000"
10
+ end
11
+
12
+ def self.tock_projects_url(endpoint: nil)
13
+ fail Onena::Error::ArgumentMissing, 'Tock endpoint is missing' if endpoint.nil?
14
+ "#{endpoint}/projects.json?page_size=100000"
15
+ end
16
+
17
+ def self.float_users_url
18
+ "#{FLOAT_API_BASE_URL}/#{FLOAT_API_VERSION}/people"
19
+ end
20
+
21
+ def self.float_projects_url
22
+ "#{FLOAT_API_BASE_URL}/#{FLOAT_API_VERSION}/projects"
23
+ end
24
+
25
+ end
26
+ end