roseflow-proxycurl 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-struct"
4
+
5
+ module Roseflow
6
+ module Proxycurl
7
+ class ProxycurlObject < Dry::Struct
8
+ defines :contract_object
9
+ end
10
+ end
11
+ 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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "proxycurl/version"
4
+
5
+ module Roseflow
6
+ module Proxycurl
7
+ class Error < StandardError; end
8
+ # Your code goes here...
9
+ end
10
+ 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
@@ -0,0 +1,6 @@
1
+ module Roseflow
2
+ module Proxycurl
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end