developer_cliques 0.5.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 +7 -0
- data/bin/devcliques +32 -0
- data/config/app_logger.rb +22 -0
- data/config/github_config.rb +7 -0
- data/config/twitter_config.rb +13 -0
- data/lib/developer_cliques/connected_developers.rb +82 -0
- data/lib/developer_cliques/graph.rb +45 -0
- data/lib/developer_cliques.rb +36 -0
- metadata +51 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7c3671bced3ec1f9c2dad3c41542e37fa78378235e0a0e5edc452e7f82449da9
|
4
|
+
data.tar.gz: 710b910a669260612d682d4af97422e19b5a5543cc0ced7b332fde72588afdb7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 563afcf91b96c60d415699e3848a85a8354594258ea3e841f48093c66978be235be8bae77fe780a9a82e87c6335b2487a2740bbb4ac0e916abf4bd347f4c2aa6
|
7
|
+
data.tar.gz: 00bd75cf2688dfe664aff807502067f79b64c6ee217c462f63113e043196c4378d325d77b244ce235451c6ad0ddcf38bf602b92cc629d174f494dad9807ef1b7
|
data/bin/devcliques
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'dotenv'
|
5
|
+
require_relative '../lib/developer_cliques'
|
6
|
+
|
7
|
+
Dotenv.load('.env')
|
8
|
+
|
9
|
+
begin
|
10
|
+
opt_parser = OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: developer_cliques.rb [options]"
|
12
|
+
|
13
|
+
opts.on("-f", "--file USERNAMES", "Calculates maximal cliques") do |file|
|
14
|
+
DeveloperCliques.new(file: file).max_cliques.each { |clique| puts clique.join(' ') }
|
15
|
+
end
|
16
|
+
|
17
|
+
opts.on_tail("-e", "--env ENVIRONMENT_VARS", "Calculates maximal cliques") do |env|
|
18
|
+
Dotenv.load(env) if env
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on_tail("-h", "--help", "Show this help") do
|
22
|
+
puts opts
|
23
|
+
exit
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
opt_parser.parse!
|
28
|
+
|
29
|
+
rescue OptionParser::MissingArgument => error
|
30
|
+
puts error
|
31
|
+
puts opt_parser
|
32
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
class AppLogger
|
5
|
+
|
6
|
+
LEVELS = ["info", "warn", "debug", "error", "fatal"]
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
def logger
|
11
|
+
Logger.new('application.log', 'monthly')
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
LEVELS.each do |level|
|
17
|
+
define_singleton_method level.to_sym do |message|
|
18
|
+
logger.send level.to_sym, message
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'twitter'
|
2
|
+
|
3
|
+
class TwitterClient
|
4
|
+
def self.get
|
5
|
+
Twitter::REST::Client.new do |config|
|
6
|
+
config.consumer_key = ENV["TWITTER_CONSUMER_KEY"]
|
7
|
+
config.consumer_secret = ENV["TWITTER_CONSUMER_SECRET"]
|
8
|
+
config.access_token = ENV["TWITTER_ACCESS_TOKEN"]
|
9
|
+
config.access_token_secret = ENV["TWITTER_ACCESS_TOKEN_SECRET"]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
|
2
|
+
require 'twitter'
|
3
|
+
require 'octokit'
|
4
|
+
require_relative '../../config/app_logger'
|
5
|
+
|
6
|
+
class ConnectedDevelopers
|
7
|
+
|
8
|
+
attr_reader :developers
|
9
|
+
|
10
|
+
def initialize developers:, twitter_client:, github_client:
|
11
|
+
@twitter_client = twitter_client
|
12
|
+
@github_client = github_client
|
13
|
+
@developers = developers
|
14
|
+
@organizations = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def graph
|
18
|
+
@graph ||= generate_graph
|
19
|
+
end
|
20
|
+
|
21
|
+
def friends user_name
|
22
|
+
friends_list = twitter_retry { @twitter_client.friends(user_name).entries.map{ |u| u.screen_name } }
|
23
|
+
AppLogger.debug "FRIENDS: #{friends_list}"
|
24
|
+
friends_list
|
25
|
+
end
|
26
|
+
|
27
|
+
def followers user_name
|
28
|
+
follow_list = twitter_retry { @twitter_client.followers(user_name).entries.map{ |u| u.screen_name } }
|
29
|
+
AppLogger.debug "FRIENDS: #{follow_list}"
|
30
|
+
follow_list
|
31
|
+
end
|
32
|
+
|
33
|
+
def organizations user_name
|
34
|
+
@organizations[user_name] ||= @github_client.organizations(user_name).map{ |u| u.login }
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def generate_graph
|
40
|
+
developers_graph = {}
|
41
|
+
excluded = []
|
42
|
+
|
43
|
+
@developers.each do |developer|
|
44
|
+
AppLogger.debug "GITHUB USER: #{developer}"
|
45
|
+
AppLogger.debug "ORGANIZATIONS: #{organizations(developer)}"
|
46
|
+
|
47
|
+
excluded << developer
|
48
|
+
developers_graph[developer] ||= []
|
49
|
+
|
50
|
+
#Bidireccional relationship. It excludes iterated developers and itself. We look in one way only
|
51
|
+
(@developers-excluded).each do |developer2|
|
52
|
+
# Intersection between organizations if there are some organization in common goes in
|
53
|
+
unless (organizations(developer) & organizations(developer2)).empty?
|
54
|
+
#It creates relationship in one direction
|
55
|
+
developers_graph[developer] << developer2
|
56
|
+
# And reverse direction
|
57
|
+
developers_graph[developer2] ||= []
|
58
|
+
developers_graph[developer2] << developer
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
#Intersection between friends, followers and current relationships by organizations in common
|
63
|
+
developers_graph[developer] &= friends(developer) & followers(developer) unless developers_graph[developer].empty?
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
developers_graph
|
68
|
+
end
|
69
|
+
|
70
|
+
def twitter_retry &block
|
71
|
+
begin
|
72
|
+
yield
|
73
|
+
rescue Twitter::Error::TooManyRequests => error
|
74
|
+
# NOTE: Your process could go to sleep for up to 15 minutes but if you
|
75
|
+
# retry any sooner, it will almost certainly fail with the same exception.
|
76
|
+
AppLogger.debug "TOO MANY REQUEST: #{error}. Must wait #{error.rate_limit.reset_in/60} min"
|
77
|
+
sleep error.rate_limit.reset_in + 1
|
78
|
+
retry
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
|
2
|
+
class Graph
|
3
|
+
|
4
|
+
attr_reader :edges, :nodes
|
5
|
+
|
6
|
+
def initialize edges: {}
|
7
|
+
@edges = edges
|
8
|
+
@nodes = edges.keys
|
9
|
+
end
|
10
|
+
|
11
|
+
def neighbours node:
|
12
|
+
@edges[node] || []
|
13
|
+
end
|
14
|
+
|
15
|
+
def max_cliques
|
16
|
+
@cliques ||= []
|
17
|
+
bron_kerbosch(possibles: nodes) if @cliques.empty?
|
18
|
+
@cliques
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Bron-Kerbosch Algorithm with Pivoting
|
24
|
+
# @param result
|
25
|
+
# is the set with the nodes of a maximal clique
|
26
|
+
# @param possibles
|
27
|
+
# is the set of the possible nodes to look
|
28
|
+
# @param excluded
|
29
|
+
# is the set of the excluded nodes
|
30
|
+
def bron_kerbosch result: [], possibles: [], excluded: []
|
31
|
+
@cliques << result if possibles.empty? and excluded.empty?
|
32
|
+
|
33
|
+
#Pivoting: it takes first element of possibles as pivot
|
34
|
+
(possibles - neighbours(node: possibles.first)).each do |n|
|
35
|
+
# Recursive call
|
36
|
+
bron_kerbosch result: result | [n],
|
37
|
+
possibles: possibles & neighbours(node: n),
|
38
|
+
excluded: excluded & neighbours(node: n)
|
39
|
+
|
40
|
+
possibles -= [n]
|
41
|
+
excluded |= [n]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
require_relative 'developer_cliques/connected_developers'
|
3
|
+
require_relative 'developer_cliques/graph'
|
4
|
+
require_relative '../config/twitter_config'
|
5
|
+
require_relative '../config/github_config'
|
6
|
+
|
7
|
+
|
8
|
+
class DeveloperCliques
|
9
|
+
|
10
|
+
def initialize file:
|
11
|
+
@file = file
|
12
|
+
end
|
13
|
+
|
14
|
+
def developers
|
15
|
+
@developers ||= read_file
|
16
|
+
end
|
17
|
+
|
18
|
+
def max_cliques
|
19
|
+
developers_graph = Graph.new edges: connected_developers.graph
|
20
|
+
developers_graph.max_cliques
|
21
|
+
end
|
22
|
+
|
23
|
+
def connected_developers
|
24
|
+
ConnectedDevelopers.new developers: developers,
|
25
|
+
twitter_client: TwitterClient.get,
|
26
|
+
github_client: GithubClient.get
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def read_file
|
32
|
+
File.readlines(@file).map{ |line| line.strip }
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
metadata
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: developer_cliques
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adrián Cepillo
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-10-04 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Calculates maximal cliques from developers social connections
|
14
|
+
email: adrian.cepillo@gmail.com
|
15
|
+
executables:
|
16
|
+
- devcliques
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- bin/devcliques
|
21
|
+
- config/app_logger.rb
|
22
|
+
- config/github_config.rb
|
23
|
+
- config/twitter_config.rb
|
24
|
+
- lib/developer_cliques.rb
|
25
|
+
- lib/developer_cliques/connected_developers.rb
|
26
|
+
- lib/developer_cliques/graph.rb
|
27
|
+
homepage: http://rubygems.org/gems/developer_cliques
|
28
|
+
licenses:
|
29
|
+
- MIT
|
30
|
+
metadata: {}
|
31
|
+
post_install_message:
|
32
|
+
rdoc_options: []
|
33
|
+
require_paths:
|
34
|
+
- lib
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
requirements: []
|
46
|
+
rubyforge_project:
|
47
|
+
rubygems_version: 2.7.7
|
48
|
+
signing_key:
|
49
|
+
specification_version: 4
|
50
|
+
summary: Calculates maximal cliques from developers social connections
|
51
|
+
test_files: []
|