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