onena 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.gitignore +5 -0
- data/.rdoc_options +16 -0
- data/.rspec +1 -0
- data/.travis.yml +4 -0
- data/ChangeLog.md +4 -0
- data/Gemfile +3 -0
- data/LICENSE.md +30 -0
- data/README.md +140 -0
- data/Rakefile +25 -0
- data/bin/onena +6 -0
- data/lib/onena.rb +5 -0
- data/lib/onena/client.rb +145 -0
- data/lib/onena/error.rb +12 -0
- data/lib/onena/protocol.rb +26 -0
- data/lib/onena/util.rb +24 -0
- data/lib/onena/version.rb +3 -0
- data/onena.gemspec +43 -0
- data/spec/client_spec.rb +112 -0
- data/spec/onena_spec.rb +8 -0
- data/spec/protocol_spec.rb +32 -0
- data/spec/spec_helper.rb +35 -0
- data/spec/util_spec.rb +24 -0
- data/spec/vcr/Onena_Client.yml +847 -0
- metadata +215 -0
checksums.yaml
ADDED
@@ -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
|
data/.document
ADDED
data/.gitignore
ADDED
data/.rdoc_options
ADDED
@@ -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
|
data/.travis.yml
ADDED
data/ChangeLog.md
ADDED
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/bin/onena
ADDED
data/lib/onena.rb
ADDED
data/lib/onena/client.rb
ADDED
@@ -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
|
data/lib/onena/error.rb
ADDED
@@ -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
|