onena 0.1.1

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.
@@ -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