linkedin-member-data 0.1.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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +186 -0
  6. data/doc/CHANGELOG.md +9 -0
  7. data/doc/LinkedIn/MemberData/ApiError.md +52 -0
  8. data/doc/LinkedIn/MemberData/Authorization.md +30 -0
  9. data/doc/LinkedIn/MemberData/Changelog/Page.md +28 -0
  10. data/doc/LinkedIn/MemberData/Changelog.md +96 -0
  11. data/doc/LinkedIn/MemberData/Client.md +97 -0
  12. data/doc/LinkedIn/MemberData/ConfigurationError.md +8 -0
  13. data/doc/LinkedIn/MemberData/ConnectionError.md +15 -0
  14. data/doc/LinkedIn/MemberData/Domains.md +22 -0
  15. data/doc/LinkedIn/MemberData/Error.md +8 -0
  16. data/doc/LinkedIn/MemberData/Event.md +90 -0
  17. data/doc/LinkedIn/MemberData/Forbidden.md +8 -0
  18. data/doc/LinkedIn/MemberData/NotFound.md +8 -0
  19. data/doc/LinkedIn/MemberData/RateLimited.md +25 -0
  20. data/doc/LinkedIn/MemberData/ServerError.md +12 -0
  21. data/doc/LinkedIn/MemberData/Snapshot/Page.md +43 -0
  22. data/doc/LinkedIn/MemberData/Snapshot.md +90 -0
  23. data/doc/LinkedIn/MemberData/Unauthorized.md +8 -0
  24. data/doc/LinkedIn/MemberData/VersionError.md +8 -0
  25. data/doc/LinkedIn/MemberData.md +44 -0
  26. data/doc/LinkedIn.md +7 -0
  27. data/doc/README.md +186 -0
  28. data/exe/linkedin-member-data +6 -0
  29. data/lib/linkedin/member_data/authorization.rb +30 -0
  30. data/lib/linkedin/member_data/changelog/page.rb +28 -0
  31. data/lib/linkedin/member_data/changelog.rb +146 -0
  32. data/lib/linkedin/member_data/cli/changelog_command.rb +27 -0
  33. data/lib/linkedin/member_data/cli/command.rb +59 -0
  34. data/lib/linkedin/member_data/cli/global_options.rb +27 -0
  35. data/lib/linkedin/member_data/cli/simple_commands.rb +48 -0
  36. data/lib/linkedin/member_data/cli/since_parser.rb +36 -0
  37. data/lib/linkedin/member_data/cli/snapshot_command.rb +85 -0
  38. data/lib/linkedin/member_data/cli/support.rb +89 -0
  39. data/lib/linkedin/member_data/cli.rb +115 -0
  40. data/lib/linkedin/member_data/client.rb +107 -0
  41. data/lib/linkedin/member_data/connection.rb +165 -0
  42. data/lib/linkedin/member_data/domains.rb +43 -0
  43. data/lib/linkedin/member_data/errors.rb +154 -0
  44. data/lib/linkedin/member_data/event.rb +101 -0
  45. data/lib/linkedin/member_data/snapshot/page.rb +53 -0
  46. data/lib/linkedin/member_data/snapshot.rb +114 -0
  47. data/lib/linkedin/member_data/util.rb +52 -0
  48. data/lib/linkedin/member_data/version.rb +16 -0
  49. data/lib/linkedin/member_data.rb +12 -0
  50. data/llms.txt +44 -0
  51. metadata +95 -0
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "snapshot/page"
4
+
5
+ module LinkedIn
6
+ module MemberData
7
+ # Lazy view over `GET /rest/memberSnapshotData` for one domain (or all).
8
+ # Nothing is fetched until you iterate. Each iteration starts a new walk from the first page.
9
+ #
10
+ # Pages are indexed by `start` (0, 1, 2, ...). The API says to keep going
11
+ # until it answers "No data found for this memberId", so that error ends
12
+ # iteration instead of raising. A page with no rows also ends iteration.
13
+ #
14
+ # It includes `Enumerable`, so `first`, `to_a`, `lazy`, `select` and the rest work on rows.
15
+ # `first` fetches one page only.
16
+ #
17
+ # @example Walk all rows of one domain
18
+ # snapshot = client.snapshot(:connections)
19
+ # snapshot.each { |row| puts row["First Name"] }
20
+ # snapshot.first # fetches one page
21
+ # snapshot.lazy.select { |row| row["Company"] }.first(5)
22
+ class Snapshot
23
+ include Enumerable
24
+
25
+ # API path of the snapshot resource.
26
+ # @return [String]
27
+ PATH = "/rest/memberSnapshotData"
28
+
29
+ # Text of the API error that means "no more data". It ends iteration.
30
+ # @return [String]
31
+ NO_DATA_MESSAGE = "No data found for this memberId"
32
+
33
+ # The domain name sent to the API. `nil` means all domains.
34
+ # @return [String, nil]
35
+ attr_reader :domain
36
+
37
+ # @api private
38
+ # @param connection [Connection] used for every request.
39
+ # @param domain [String, nil] domain name as sent to the API. `nil` means all domains.
40
+ def initialize(connection, domain)
41
+ @connection = connection
42
+ @domain = domain
43
+ end
44
+
45
+ # Yields every row of every page. Pages are fetched one by one while you iterate.
46
+ # Rows are plain Hashes with the keys LinkedIn returns. Keys differ per domain.
47
+ #
48
+ # @example Block form
49
+ # client.snapshot(:connections).each { |row| puts row["First Name"] }
50
+ # @example Without a block, you get an Enumerator
51
+ # client.snapshot(:connections).each.first(3)
52
+ # @yieldparam row [Hash{String => Object}] one row of snapshot data.
53
+ # @return [Snapshot] self, when a block is given.
54
+ # @return [Enumerator<Hash>] when no block is given.
55
+ # @raise [ApiError] on a non-2xx response, except the "No data found" answer.
56
+ # @raise [ConnectionError] on a network failure after all retries.
57
+ def each(&block)
58
+ return enum_for(:each) unless block
59
+
60
+ pages.each { |page| page.rows.each(&block) }
61
+ self
62
+ end
63
+
64
+ # Lazy Enumerator of pages. The walk stops at the first page without a `next` link.
65
+ # It also stops at an empty page or at the "No data found" answer. Any other API error raises.
66
+ #
67
+ # @example Inspect paging data
68
+ # client.snapshot(:profile).pages.each do |page|
69
+ # puts "#{page.domain}: #{page.rows.size} rows, total #{page.total}"
70
+ # end
71
+ # @return [Enumerator<Snapshot::Page>]
72
+ # @raise [ApiError] while iterating, on a non-2xx response except "No data found".
73
+ # @raise [ConnectionError] while iterating, on a network failure after all retries.
74
+ def pages = Enumerator.new { |yielder| each_page(yielder) }
75
+
76
+ # Fetches one page by index. Does not walk. Does not hide the "No data found" error.
77
+ #
78
+ # @example
79
+ # client.snapshot(:profile).page(0).rows
80
+ # @param start [Integer] zero-based page index.
81
+ # @return [Snapshot::Page]
82
+ # @raise [NotFound] past the end of the data.
83
+ # @raise [ApiError] on any other non-2xx response.
84
+ # @raise [ConnectionError] on a network failure after all retries.
85
+ def page(start)
86
+ Page.from_api(@connection.get(PATH, q: "criteria", start: start, domain: domain))
87
+ end
88
+
89
+ private
90
+
91
+ def each_page(yielder)
92
+ (0..).each do |start|
93
+ page = usable_page(start)
94
+ yielder << page unless page.nil?
95
+ break unless page&.next?
96
+ end
97
+ end
98
+
99
+ # nil ends the walk. An empty page counts as the end even with a next
100
+ # link, because `next` links can appear on the last page.
101
+ def usable_page(start)
102
+ page_or_nil(start)&.then { |page| page unless page.rows.empty? }
103
+ end
104
+
105
+ def page_or_nil(start)
106
+ page(start)
107
+ rescue ApiError => error
108
+ raise unless error.message.include?(NO_DATA_MESSAGE)
109
+
110
+ nil
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module LinkedIn
6
+ module MemberData
7
+ # Conversions between epoch milliseconds (LinkedIn wire format) and Time.
8
+ # @api private
9
+ module Util
10
+ module_function
11
+
12
+ # Converts epoch milliseconds to a UTC Time. Keeps sub-second precision.
13
+ #
14
+ # @param milliseconds [Integer, nil] epoch milliseconds.
15
+ # @return [Time, nil] UTC Time, or `nil` when `milliseconds` is `nil`.
16
+ def time_from_ms(milliseconds)
17
+ return nil if milliseconds.nil?
18
+
19
+ Time.at(Rational(milliseconds, 1000)).utc
20
+ end
21
+
22
+ # Converts a Time, Date or Integer to epoch milliseconds. Rounds down.
23
+ #
24
+ # @example
25
+ # LinkedIn::MemberData::Util.epoch_ms(Time.utc(2026, 9, 1)) # => 1788220800000
26
+ # LinkedIn::MemberData::Util.epoch_ms(Date.new(2026, 9, 1)) # => 1788220800000 (midnight UTC)
27
+ # LinkedIn::MemberData::Util.epoch_ms(1_788_220_800_000) # => 1788220800000
28
+ # LinkedIn::MemberData::Util.epoch_ms(nil) # => nil
29
+ # @param value [Time, Date, Integer, nil] an Integer is taken as epoch milliseconds.
30
+ # A Date counts as midnight UTC.
31
+ # @return [Integer, nil] epoch milliseconds, or `nil` when `value` is `nil`.
32
+ # @raise [ArgumentError] when `value` is any other type.
33
+ def epoch_ms(value)
34
+ case value
35
+ when nil, Integer then value
36
+ when Time, Date then (to_time(value).to_r * 1000).floor
37
+ else raise ArgumentError, "expected Time, Date or Integer epoch milliseconds, got #{value.class}"
38
+ end
39
+ end
40
+
41
+ # A Date counts as midnight UTC.
42
+ def to_time(value)
43
+ case value
44
+ when DateTime then value.to_time
45
+ when Date then Time.utc(value.year, value.month, value.day)
46
+ else value
47
+ end
48
+ end
49
+ private_class_method :to_time
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Namespace of the LinkedIn gems.
4
+ module LinkedIn
5
+ # Ruby client and CLI for the LinkedIn Member Data Portability (Member) API.
6
+ # Start with `Client`.
7
+ #
8
+ # @example
9
+ # client = LinkedIn::MemberData::Client.new(access_token: ENV["LINKEDIN_ACCESS_TOKEN"])
10
+ # client.snapshot(:connections).each { |row| puts row["First Name"] }
11
+ module MemberData
12
+ # Gem version.
13
+ # @return [String]
14
+ VERSION = "0.1.0"
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "member_data/version"
4
+ require_relative "member_data/errors"
5
+ require_relative "member_data/util"
6
+ require_relative "member_data/domains"
7
+ require_relative "member_data/connection"
8
+ require_relative "member_data/authorization"
9
+ require_relative "member_data/event"
10
+ require_relative "member_data/snapshot"
11
+ require_relative "member_data/changelog"
12
+ require_relative "member_data/client"
data/llms.txt ADDED
@@ -0,0 +1,44 @@
1
+ # Module LinkedIn::MemberData <a id="module-LinkedIn-MemberData"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Defined in** | lib/linkedin/member_data/version.rb, lib/linkedin/member_data/cli.rb, lib/linkedin/member_data/util.rb, lib/linkedin/member_data/event.rb, lib/linkedin/member_data/client.rb, lib/linkedin/member_data/errors.rb, lib/linkedin/member_data/domains.rb, lib/linkedin/member_data/snapshot.rb, lib/linkedin/member_data/changelog.rb, lib/linkedin/member_data/connection.rb, lib/linkedin/member_data/cli/command.rb, lib/linkedin/member_data/cli/support.rb, lib/linkedin/member_data/authorization.rb, lib/linkedin/member_data/snapshot/page.rb, lib/linkedin/member_data/changelog/page.rb, lib/linkedin/member_data/cli/since_parser.rb, lib/linkedin/member_data/cli/global_options.rb, lib/linkedin/member_data/cli/simple_commands.rb, lib/linkedin/member_data/cli/snapshot_command.rb, lib/linkedin/member_data/cli/changelog_command.rb |
6
+
7
+ Ruby client and CLI for the LinkedIn Member Data Portability (Member) API.
8
+ Start with `Client`.
9
+
10
+ **@example**
11
+ ```ruby
12
+ client = LinkedIn::MemberData::Client.new(access_token: ENV["LINKEDIN_ACCESS_TOKEN"])
13
+ client.snapshot(:connections).each { |row| puts row["First Name"] }
14
+ ```
15
+
16
+ ## Constants
17
+ ### `EVENT_KEYS` <a id="constant-EVENT_KEYS"></a> <a id="EVENT_KEYS-constant"></a>
18
+ Data member name => JSON key. Time fields are handled separately.
19
+ - **@api** private
20
+
21
+ ### `VERSION` <a id="constant-VERSION"></a> <a id="VERSION-constant"></a>
22
+ Gem version.
23
+ - **@return** [String]
24
+
25
+ # Documentation
26
+
27
+ - [MemberData/ApiError.md](doc/LinkedIn/MemberData/ApiError.md)
28
+ - [MemberData/Authorization.md](doc/LinkedIn/MemberData/Authorization.md)
29
+ - [MemberData/Changelog/Page.md](doc/LinkedIn/MemberData/Changelog/Page.md)
30
+ - [MemberData/Changelog.md](doc/LinkedIn/MemberData/Changelog.md)
31
+ - [MemberData/Client.md](doc/LinkedIn/MemberData/Client.md)
32
+ - [MemberData/ConfigurationError.md](doc/LinkedIn/MemberData/ConfigurationError.md)
33
+ - [MemberData/ConnectionError.md](doc/LinkedIn/MemberData/ConnectionError.md)
34
+ - [MemberData/Domains.md](doc/LinkedIn/MemberData/Domains.md)
35
+ - [MemberData/Error.md](doc/LinkedIn/MemberData/Error.md)
36
+ - [MemberData/Event.md](doc/LinkedIn/MemberData/Event.md)
37
+ - [MemberData/Forbidden.md](doc/LinkedIn/MemberData/Forbidden.md)
38
+ - [MemberData/NotFound.md](doc/LinkedIn/MemberData/NotFound.md)
39
+ - [MemberData/RateLimited.md](doc/LinkedIn/MemberData/RateLimited.md)
40
+ - [MemberData/ServerError.md](doc/LinkedIn/MemberData/ServerError.md)
41
+ - [MemberData/Snapshot/Page.md](doc/LinkedIn/MemberData/Snapshot/Page.md)
42
+ - [MemberData/Snapshot.md](doc/LinkedIn/MemberData/Snapshot.md)
43
+ - [MemberData/Unauthorized.md](doc/LinkedIn/MemberData/Unauthorized.md)
44
+ - [MemberData/VersionError.md](doc/LinkedIn/MemberData/VersionError.md)
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: linkedin-member-data
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Lucian Ghinda
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Download your own LinkedIn data (snapshot domains and changelog events)
13
+ through the Member Data Portability API. Zero runtime dependencies.
14
+ email:
15
+ - lucianghinda@users.noreply.github.com
16
+ executables:
17
+ - linkedin-member-data
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - CODE_OF_CONDUCT.md
23
+ - LICENSE.txt
24
+ - README.md
25
+ - doc/CHANGELOG.md
26
+ - doc/LinkedIn.md
27
+ - doc/LinkedIn/MemberData.md
28
+ - doc/LinkedIn/MemberData/ApiError.md
29
+ - doc/LinkedIn/MemberData/Authorization.md
30
+ - doc/LinkedIn/MemberData/Changelog.md
31
+ - doc/LinkedIn/MemberData/Changelog/Page.md
32
+ - doc/LinkedIn/MemberData/Client.md
33
+ - doc/LinkedIn/MemberData/ConfigurationError.md
34
+ - doc/LinkedIn/MemberData/ConnectionError.md
35
+ - doc/LinkedIn/MemberData/Domains.md
36
+ - doc/LinkedIn/MemberData/Error.md
37
+ - doc/LinkedIn/MemberData/Event.md
38
+ - doc/LinkedIn/MemberData/Forbidden.md
39
+ - doc/LinkedIn/MemberData/NotFound.md
40
+ - doc/LinkedIn/MemberData/RateLimited.md
41
+ - doc/LinkedIn/MemberData/ServerError.md
42
+ - doc/LinkedIn/MemberData/Snapshot.md
43
+ - doc/LinkedIn/MemberData/Snapshot/Page.md
44
+ - doc/LinkedIn/MemberData/Unauthorized.md
45
+ - doc/LinkedIn/MemberData/VersionError.md
46
+ - doc/README.md
47
+ - exe/linkedin-member-data
48
+ - lib/linkedin/member_data.rb
49
+ - lib/linkedin/member_data/authorization.rb
50
+ - lib/linkedin/member_data/changelog.rb
51
+ - lib/linkedin/member_data/changelog/page.rb
52
+ - lib/linkedin/member_data/cli.rb
53
+ - lib/linkedin/member_data/cli/changelog_command.rb
54
+ - lib/linkedin/member_data/cli/command.rb
55
+ - lib/linkedin/member_data/cli/global_options.rb
56
+ - lib/linkedin/member_data/cli/simple_commands.rb
57
+ - lib/linkedin/member_data/cli/since_parser.rb
58
+ - lib/linkedin/member_data/cli/snapshot_command.rb
59
+ - lib/linkedin/member_data/cli/support.rb
60
+ - lib/linkedin/member_data/client.rb
61
+ - lib/linkedin/member_data/connection.rb
62
+ - lib/linkedin/member_data/domains.rb
63
+ - lib/linkedin/member_data/errors.rb
64
+ - lib/linkedin/member_data/event.rb
65
+ - lib/linkedin/member_data/snapshot.rb
66
+ - lib/linkedin/member_data/snapshot/page.rb
67
+ - lib/linkedin/member_data/util.rb
68
+ - lib/linkedin/member_data/version.rb
69
+ - llms.txt
70
+ homepage: https://github.com/lucianghinda/linkedin-member-data
71
+ licenses:
72
+ - MIT
73
+ metadata:
74
+ homepage_uri: https://github.com/lucianghinda/linkedin-member-data
75
+ source_code_uri: https://github.com/lucianghinda/linkedin-member-data/tree/main
76
+ changelog_uri: https://github.com/lucianghinda/linkedin-member-data/blob/main/CHANGELOG.md
77
+ rubygems_mfa_required: 'true'
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 3.2.0
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 4.0.19
93
+ specification_version: 4
94
+ summary: Ruby client and CLI for the LinkedIn Member Data Portability (Member) API.
95
+ test_files: []