roseflow-proxycurl 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/.rspec +3 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +10 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +49 -0
- data/Rakefile +10 -0
- data/lib/roseflow/linkedin/company/lookup_query.rb +58 -0
- data/lib/roseflow/linkedin/company/object.rb +82 -0
- data/lib/roseflow/linkedin/company/profile_query.rb +50 -0
- data/lib/roseflow/linkedin/company.rb +38 -0
- data/lib/roseflow/linkedin/job/object.rb +77 -0
- data/lib/roseflow/linkedin/job/profile_query.rb +32 -0
- data/lib/roseflow/linkedin/job/search_query.rb +48 -0
- data/lib/roseflow/linkedin/job.rb +38 -0
- data/lib/roseflow/linkedin/job_list_entry.rb +20 -0
- data/lib/roseflow/linkedin/person/experience.rb +73 -0
- data/lib/roseflow/linkedin/person/lookup_query.rb +57 -0
- data/lib/roseflow/linkedin/person/object.rb +69 -0
- data/lib/roseflow/linkedin/person/profile_query.rb +53 -0
- data/lib/roseflow/linkedin/person/role_query.rb +48 -0
- data/lib/roseflow/linkedin/person.rb +50 -0
- data/lib/roseflow/proxycurl/client.rb +27 -0
- data/lib/roseflow/proxycurl/config.rb +17 -0
- data/lib/roseflow/proxycurl/object.rb +11 -0
- data/lib/roseflow/proxycurl/version.rb +18 -0
- data/lib/roseflow/proxycurl.rb +10 -0
- data/roseflow-proxycurl.gemspec +49 -0
- data/sig/roseflow/proxycurl.rbs +6 -0
- metadata +244 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Types
|
4
|
+
DateOrNil = Date | Nil
|
5
|
+
StringOrNil = String | Nil
|
6
|
+
end
|
7
|
+
|
8
|
+
module Functions
|
9
|
+
extend Dry::Transformer::Registry
|
10
|
+
|
11
|
+
register :to_date, ->(v) { Date.parse("#{v.dig(:year)}-#{v.dig(:month)}-#{v.dig(:day)}") unless v.nil? }
|
12
|
+
end
|
13
|
+
|
14
|
+
module Roseflow
|
15
|
+
module LinkedIn
|
16
|
+
class Person
|
17
|
+
class ExperienceMapper < Dry::Transformer::Pipe
|
18
|
+
import Dry::Transformer::HashTransformations
|
19
|
+
|
20
|
+
def custom_t(*args)
|
21
|
+
Functions[*args]
|
22
|
+
end
|
23
|
+
|
24
|
+
define! do
|
25
|
+
deep_symbolize_keys
|
26
|
+
rename_keys company_linkedin_profile_url: :company_profile_url, starts_at: :started_on, ends_at: :ended_on
|
27
|
+
|
28
|
+
map_value :started_on, ->(v) { Date.parse("#{v.dig(:year)}-#{v.dig(:month)}-#{v.dig(:day)}") unless v.nil? }
|
29
|
+
map_value :ended_on, ->(v) { Date.parse("#{v.dig(:year)}-#{v.dig(:month)}-#{v.dig(:day)}") unless v.nil? }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Experience < Dry::Struct
|
34
|
+
defines :contract_object
|
35
|
+
|
36
|
+
class ExperienceContract < Dry::Validation::Contract
|
37
|
+
params do
|
38
|
+
required(:title).filled(:string)
|
39
|
+
required(:company).filled(:string)
|
40
|
+
optional(:company_profile_url).filled(:string)
|
41
|
+
optional(:location)
|
42
|
+
optional(:description)
|
43
|
+
optional(:started_on).filled(:date)
|
44
|
+
optional(:ended_on)
|
45
|
+
end
|
46
|
+
|
47
|
+
def call(input)
|
48
|
+
transformed = ExperienceMapper.new.call(input)
|
49
|
+
super(transformed)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
attribute :title, Types::String
|
54
|
+
attribute :company, Types::String
|
55
|
+
attribute? :company_profile_url, Types::StringOrNil
|
56
|
+
attribute? :location, Types::StringOrNil
|
57
|
+
attribute? :description, Types::StringOrNil
|
58
|
+
attribute :started_on, Types::Date
|
59
|
+
attribute? :ended_on, Types::DateOrNil
|
60
|
+
|
61
|
+
contract_object ExperienceContract
|
62
|
+
|
63
|
+
schema schema.strict
|
64
|
+
|
65
|
+
def self.new(input)
|
66
|
+
validation = self.contract_object.new.call(input)
|
67
|
+
raise ArgumentError, validation.errors.to_h unless validation.success?
|
68
|
+
super(validation.to_h)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry-validation"
|
4
|
+
require "roseflow/proxycurl/object"
|
5
|
+
require "roseflow/linkedin/person"
|
6
|
+
require "roseflow/types"
|
7
|
+
|
8
|
+
module Roseflow
|
9
|
+
module LinkedIn
|
10
|
+
class Person
|
11
|
+
class LookupQuery < Proxycurl::ProxycurlObject
|
12
|
+
class LookupQueryContract < Dry::Validation::Contract
|
13
|
+
params do
|
14
|
+
required(:domain).filled(:string)
|
15
|
+
required(:first_name).filled(:string)
|
16
|
+
optional(:last_name).filled(:string)
|
17
|
+
optional(:title).filled(:string)
|
18
|
+
optional(:location).filled(:string)
|
19
|
+
optional(:enrich).filled(:bool)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
contract_object LookupQueryContract
|
24
|
+
|
25
|
+
schema schema.strict
|
26
|
+
|
27
|
+
attribute :domain, Types::String
|
28
|
+
attribute :first_name, Types::String
|
29
|
+
attribute? :last_name, Types::String
|
30
|
+
attribute? :title, Types::String
|
31
|
+
attribute? :location, Types::String
|
32
|
+
attribute? :enrich, Types::Bool.default(false)
|
33
|
+
|
34
|
+
def self.new(input)
|
35
|
+
validation = self.contract_object.new.call(input)
|
36
|
+
raise ArgumentError, validation.errors.to_h.inspect unless validation.success?
|
37
|
+
super(input)
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_request_params
|
41
|
+
params = {
|
42
|
+
company_domain: domain,
|
43
|
+
first_name: first_name,
|
44
|
+
last_name: last_name,
|
45
|
+
title: title,
|
46
|
+
location: location,
|
47
|
+
enrich_profile: enrich == true ? "enrich" : "skip",
|
48
|
+
}
|
49
|
+
|
50
|
+
params.each_pair do |key, value|
|
51
|
+
params.delete(key) unless value
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry-transformer"
|
4
|
+
|
5
|
+
require "roseflow/linkedin/person/experience"
|
6
|
+
|
7
|
+
module Roseflow
|
8
|
+
module LinkedIn
|
9
|
+
class Person
|
10
|
+
class Object < Dry::Struct
|
11
|
+
defines :contract_object
|
12
|
+
|
13
|
+
class PersonMapper < Dry::Transformer::Pipe
|
14
|
+
import Dry::Transformer::HashTransformations
|
15
|
+
|
16
|
+
define! do
|
17
|
+
deep_symbolize_keys
|
18
|
+
rename_keys public_identifier: :id, profile_pic_url: :profile_picture_url, follower_count: :followers
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class PersonContract < Dry::Validation::Contract
|
23
|
+
params do
|
24
|
+
required(:id).filled(:string)
|
25
|
+
required(:first_name).filled(:string)
|
26
|
+
required(:last_name).filled(:string)
|
27
|
+
required(:full_name).filled(:string)
|
28
|
+
required(:profile_url).filled(:string)
|
29
|
+
required(:profile_picture_url).filled(:string)
|
30
|
+
required(:headline).filled(:string)
|
31
|
+
optional(:followers)
|
32
|
+
optional(:connections)
|
33
|
+
optional(:occupation)
|
34
|
+
optional(:experiences).filled(:array)
|
35
|
+
end
|
36
|
+
|
37
|
+
def call(input)
|
38
|
+
transformed = PersonMapper.new.call(input)
|
39
|
+
super(transformed)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
attribute :id, Types::String
|
44
|
+
attribute :first_name, Types::String
|
45
|
+
attribute :last_name, Types::String
|
46
|
+
attribute :full_name, Types::String
|
47
|
+
attribute :profile_url, Types::String
|
48
|
+
attribute :profile_picture_url, Types::String
|
49
|
+
attribute :headline, Types::String
|
50
|
+
attribute :followers, Types::Integer.optional
|
51
|
+
attribute :connections, Types::Integer.optional
|
52
|
+
attribute :occupation, Types::String
|
53
|
+
attribute :experiences, Types::Array.of(Experience).optional
|
54
|
+
|
55
|
+
contract_object PersonContract
|
56
|
+
|
57
|
+
schema schema.strict
|
58
|
+
|
59
|
+
def self.new(input)
|
60
|
+
experiences = input.delete("experiences")
|
61
|
+
input[:experiences] = experiences.map { |experience| Experience.new(experience) } if experiences
|
62
|
+
validation = self.contract_object.new.call(input)
|
63
|
+
raise ArgumentError, validation.errors.to_h.inspect unless validation.success?
|
64
|
+
super(validation.to_h)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry-validation"
|
4
|
+
require "roseflow/proxycurl/object"
|
5
|
+
require "roseflow/linkedin/person"
|
6
|
+
require "roseflow/types"
|
7
|
+
|
8
|
+
module Roseflow
|
9
|
+
module LinkedIn
|
10
|
+
class Person
|
11
|
+
end
|
12
|
+
|
13
|
+
class Person::ProfileQuery < Proxycurl::ProxycurlObject
|
14
|
+
class ProfileQueryContract < Dry::Validation::Contract
|
15
|
+
params do
|
16
|
+
required(:url).filled(:string)
|
17
|
+
end
|
18
|
+
|
19
|
+
rule (:url) do
|
20
|
+
unless URI.parse(value).is_a?(URI::HTTP)
|
21
|
+
key.failure("must be a valid URL")
|
22
|
+
end
|
23
|
+
|
24
|
+
unless value.match?(/linkedin\.com\/in\/\w+/)
|
25
|
+
key.failure("must be a valid LinkedIn profile URL")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
contract_object ProfileQueryContract
|
31
|
+
|
32
|
+
schema schema.strict
|
33
|
+
|
34
|
+
attribute :url, Types::String
|
35
|
+
attribute :fallback_to_cache, Types::String.default("on-error")
|
36
|
+
attribute :use_cache, Types::String.default("if-present")
|
37
|
+
attribute :skills, Types::String.default("exclude")
|
38
|
+
attribute :inferred_salary, Types::String.default("exclude")
|
39
|
+
attribute :personal_email, Types::String.default("exclude")
|
40
|
+
attribute :personal_contact_number, Types::String.default("exclude")
|
41
|
+
attribute :twitter_profile_id, Types::String.default("exclude")
|
42
|
+
attribute :facebook_profile_id, Types::String.default("exclude")
|
43
|
+
attribute :github_profile_id, Types::String.default("exclude")
|
44
|
+
attribute :extra, Types::String.default("exclude")
|
45
|
+
|
46
|
+
def self.new(input)
|
47
|
+
validation = self.contract_object.new.call(input)
|
48
|
+
raise ArgumentError, validation.errors.to_h.inspect unless validation.success?
|
49
|
+
super(input)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry-validation"
|
4
|
+
require "roseflow/proxycurl/object"
|
5
|
+
require "roseflow/linkedin/person"
|
6
|
+
require "roseflow/types"
|
7
|
+
|
8
|
+
module Roseflow
|
9
|
+
module LinkedIn
|
10
|
+
class Person
|
11
|
+
class RoleQuery < Proxycurl::ProxycurlObject
|
12
|
+
class RoleQueryContract < Dry::Validation::Contract
|
13
|
+
params do
|
14
|
+
required(:role).filled(:string)
|
15
|
+
required(:company_name).filled(:string)
|
16
|
+
optional(:enrich).filled(:bool)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
contract_object RoleQueryContract
|
21
|
+
|
22
|
+
schema schema.strict
|
23
|
+
|
24
|
+
attribute :role, Types::String
|
25
|
+
attribute :company_name, Types::String
|
26
|
+
attribute? :enrich, Types::Bool.default(false)
|
27
|
+
|
28
|
+
def self.new(input)
|
29
|
+
validation = self.contract_object.new.call(input)
|
30
|
+
raise ArgumentError, validation.errors.to_h.inspect unless validation.success?
|
31
|
+
super(input)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_request_params
|
35
|
+
params = {
|
36
|
+
company_name: company_name,
|
37
|
+
role: role,
|
38
|
+
enrich_profile: enrich == true ? "enrich" : "skip",
|
39
|
+
}
|
40
|
+
|
41
|
+
params.each_pair do |key, value|
|
42
|
+
params.delete(key) unless value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roseflow/linkedin/person/lookup_query"
|
4
|
+
require "roseflow/linkedin/person/profile_query"
|
5
|
+
require "roseflow/linkedin/person/role_query"
|
6
|
+
|
7
|
+
module Roseflow
|
8
|
+
module LinkedIn
|
9
|
+
class Person
|
10
|
+
def initialize(connection)
|
11
|
+
@connection = connection
|
12
|
+
end
|
13
|
+
|
14
|
+
def find(url, **options)
|
15
|
+
query = ProfileQuery.new(url: url, **options)
|
16
|
+
response = @connection.get("v2/linkedin", query.to_h)
|
17
|
+
return Person::Object.new(JSON.parse(response.body).merge(profile_url: url)) if person_found?(response)
|
18
|
+
return nil if person_not_found?(response)
|
19
|
+
end
|
20
|
+
|
21
|
+
def lookup(query)
|
22
|
+
query = LookupQuery.new(query)
|
23
|
+
response = @connection.get("linkedin/profile/resolve", query.to_request_params)
|
24
|
+
return JSON.parse(response.body).dig("url") if person_found?(response)
|
25
|
+
return nil if person_not_found?(response)
|
26
|
+
end
|
27
|
+
|
28
|
+
def role(query)
|
29
|
+
query = RoleQuery.new(query)
|
30
|
+
response = @connection.get("find/company/role/", query.to_request_params)
|
31
|
+
if person_found?(response)
|
32
|
+
url = JSON.parse(response.body).dig("linkedin_profile_url")
|
33
|
+
return find(url)
|
34
|
+
end
|
35
|
+
|
36
|
+
return nil if person_not_found?(response)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def person_found?(response)
|
42
|
+
response.success? && response.status == 200
|
43
|
+
end
|
44
|
+
|
45
|
+
def person_not_found?(response)
|
46
|
+
response.success? && response.status == 404
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roseflow
|
4
|
+
module Proxycurl
|
5
|
+
class Client
|
6
|
+
def initialize(config = Config.new)
|
7
|
+
@config = config
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def connection
|
13
|
+
@connection ||= Faraday.new(
|
14
|
+
url: proxycurl_base_url,
|
15
|
+
) do |faraday|
|
16
|
+
faraday.request :authorization, :Bearer, -> { config.api_key }
|
17
|
+
faraday.request :url_encoded
|
18
|
+
faraday.adapter Faraday.default_adapter
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def proxycurl_base_url
|
23
|
+
@config.base_url || Config::DEFAULT_BASE_URL
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anyway_config"
|
4
|
+
|
5
|
+
module Roseflow
|
6
|
+
module Proxycurl
|
7
|
+
class Config < Anyway::Config
|
8
|
+
DEFAULT_BASE_URL = "https://nubela.co/proxycurl/api/"
|
9
|
+
|
10
|
+
config_name :proxycurl
|
11
|
+
|
12
|
+
attr_config :api_key, :base_url
|
13
|
+
|
14
|
+
required :api_key
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roseflow
|
4
|
+
module Proxycurl
|
5
|
+
def self.gem_version
|
6
|
+
Gem::Version.new(VERSION::STRING)
|
7
|
+
end
|
8
|
+
|
9
|
+
module VERSION
|
10
|
+
MAJOR = 0
|
11
|
+
MINOR = 5
|
12
|
+
PATCH = 0
|
13
|
+
PRE = nil
|
14
|
+
|
15
|
+
STRING = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/roseflow/proxycurl/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "roseflow-proxycurl"
|
7
|
+
spec.version = Roseflow::Proxycurl.gem_version
|
8
|
+
spec.authors = ["Lauri Jutila"]
|
9
|
+
spec.email = ["ljuti@roseflow.ai"]
|
10
|
+
|
11
|
+
spec.summary = "Proxycurl integration for Roseflow."
|
12
|
+
spec.description = "Proxycurl integration for Roseflow."
|
13
|
+
spec.homepage = "https://github.com/roseflow-ai/roseflow-proxycurl"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 3.2.0"
|
16
|
+
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/roseflow-ai/roseflow-proxycurl"
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/roseflow-ai/roseflow-proxycurl/blob/master/CHANGELOG.md"
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
(File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
spec.bindir = "exe"
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ["lib"]
|
31
|
+
|
32
|
+
# Uncomment to register a new dependency of your gem
|
33
|
+
spec.add_dependency "activesupport", "~> 6.0"
|
34
|
+
spec.add_dependency "anyway_config", "~> 2.0"
|
35
|
+
spec.add_dependency "dry-struct", "~> 1.6"
|
36
|
+
spec.add_dependency "dry-transformer", "~> 1.0"
|
37
|
+
spec.add_dependency "dry-validation", "~> 1.10"
|
38
|
+
spec.add_dependency "faraday", "~> 2.0"
|
39
|
+
spec.add_dependency "faraday-retry", "~> 2.0"
|
40
|
+
|
41
|
+
spec.add_development_dependency "awesome_print"
|
42
|
+
spec.add_development_dependency "pry"
|
43
|
+
spec.add_development_dependency "webmock"
|
44
|
+
spec.add_development_dependency "vcr"
|
45
|
+
spec.add_development_dependency "roseflow"
|
46
|
+
|
47
|
+
# For more information and examples about making a new gem, check out our
|
48
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
49
|
+
end
|