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