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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +186 -0
- data/doc/CHANGELOG.md +9 -0
- data/doc/LinkedIn/MemberData/ApiError.md +52 -0
- data/doc/LinkedIn/MemberData/Authorization.md +30 -0
- data/doc/LinkedIn/MemberData/Changelog/Page.md +28 -0
- data/doc/LinkedIn/MemberData/Changelog.md +96 -0
- data/doc/LinkedIn/MemberData/Client.md +97 -0
- data/doc/LinkedIn/MemberData/ConfigurationError.md +8 -0
- data/doc/LinkedIn/MemberData/ConnectionError.md +15 -0
- data/doc/LinkedIn/MemberData/Domains.md +22 -0
- data/doc/LinkedIn/MemberData/Error.md +8 -0
- data/doc/LinkedIn/MemberData/Event.md +90 -0
- data/doc/LinkedIn/MemberData/Forbidden.md +8 -0
- data/doc/LinkedIn/MemberData/NotFound.md +8 -0
- data/doc/LinkedIn/MemberData/RateLimited.md +25 -0
- data/doc/LinkedIn/MemberData/ServerError.md +12 -0
- data/doc/LinkedIn/MemberData/Snapshot/Page.md +43 -0
- data/doc/LinkedIn/MemberData/Snapshot.md +90 -0
- data/doc/LinkedIn/MemberData/Unauthorized.md +8 -0
- data/doc/LinkedIn/MemberData/VersionError.md +8 -0
- data/doc/LinkedIn/MemberData.md +44 -0
- data/doc/LinkedIn.md +7 -0
- data/doc/README.md +186 -0
- data/exe/linkedin-member-data +6 -0
- data/lib/linkedin/member_data/authorization.rb +30 -0
- data/lib/linkedin/member_data/changelog/page.rb +28 -0
- data/lib/linkedin/member_data/changelog.rb +146 -0
- data/lib/linkedin/member_data/cli/changelog_command.rb +27 -0
- data/lib/linkedin/member_data/cli/command.rb +59 -0
- data/lib/linkedin/member_data/cli/global_options.rb +27 -0
- data/lib/linkedin/member_data/cli/simple_commands.rb +48 -0
- data/lib/linkedin/member_data/cli/since_parser.rb +36 -0
- data/lib/linkedin/member_data/cli/snapshot_command.rb +85 -0
- data/lib/linkedin/member_data/cli/support.rb +89 -0
- data/lib/linkedin/member_data/cli.rb +115 -0
- data/lib/linkedin/member_data/client.rb +107 -0
- data/lib/linkedin/member_data/connection.rb +165 -0
- data/lib/linkedin/member_data/domains.rb +43 -0
- data/lib/linkedin/member_data/errors.rb +154 -0
- data/lib/linkedin/member_data/event.rb +101 -0
- data/lib/linkedin/member_data/snapshot/page.rb +53 -0
- data/lib/linkedin/member_data/snapshot.rb +114 -0
- data/lib/linkedin/member_data/util.rb +52 -0
- data/lib/linkedin/member_data/version.rb +16 -0
- data/lib/linkedin/member_data.rb +12 -0
- data/llms.txt +44 -0
- metadata +95 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Class LinkedIn::MemberData::Event <a id="class-LinkedIn-MemberData-Event"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | Data |
|
|
6
|
+
| **Defined in** | lib/linkedin/member_data/event.rb |
|
|
7
|
+
|
|
8
|
+
One Member Changelog event. Immutable. Times are UTC. `activity` and friends
|
|
9
|
+
stay raw. Fields missing in the API response are `nil`.
|
|
10
|
+
|
|
11
|
+
`method` is the API field (`CREATE`, `UPDATE`, ...). It shadows Ruby's
|
|
12
|
+
`Object#method` on purpose, so `event.method(:name)` does not work on an
|
|
13
|
+
Event.
|
|
14
|
+
|
|
15
|
+
## Attributes
|
|
16
|
+
### `activity` [R] <a id="attribute-i-activity"></a> <a id="activity-instance_method"></a>
|
|
17
|
+
- **@return** [Hash, nil] raw activity payload.
|
|
18
|
+
|
|
19
|
+
### `activity_id` [R] <a id="attribute-i-activity_id"></a> <a id="activity_id-instance_method"></a>
|
|
20
|
+
- **@return** [String, nil] value as sent by the API.
|
|
21
|
+
|
|
22
|
+
### `activity_status` [R] <a id="attribute-i-activity_status"></a> <a id="activity_status-instance_method"></a>
|
|
23
|
+
- **@return** [String, nil] value as sent by the API.
|
|
24
|
+
|
|
25
|
+
### `actor` [R] <a id="attribute-i-actor"></a> <a id="actor-instance_method"></a>
|
|
26
|
+
- **@return** [String, nil] who did the action, as sent by the API.
|
|
27
|
+
|
|
28
|
+
### `captured_at` [R] <a id="attribute-i-captured_at"></a> <a id="captured_at-instance_method"></a>
|
|
29
|
+
- **@return** [Time, nil] when LinkedIn captured the event. UTC.
|
|
30
|
+
|
|
31
|
+
### `config_version` [R] <a id="attribute-i-config_version"></a> <a id="config_version-instance_method"></a>
|
|
32
|
+
- **@return** [Object, nil] value as sent by the API.
|
|
33
|
+
|
|
34
|
+
### `id` [R] <a id="attribute-i-id"></a> <a id="id-instance_method"></a>
|
|
35
|
+
- **@return** [String, nil] event id. Used to skip events seen on the previous page.
|
|
36
|
+
|
|
37
|
+
### `method` [R] <a id="attribute-i-method"></a> <a id="method-instance_method"></a>
|
|
38
|
+
- **@return** [String, nil] API method, for example `CREATE` or `UPDATE`.
|
|
39
|
+
|
|
40
|
+
### `method_name` [R] <a id="attribute-i-method_name"></a> <a id="method_name-instance_method"></a>
|
|
41
|
+
- **@return** [String, nil] value as sent by the API.
|
|
42
|
+
|
|
43
|
+
### `owner` [R] <a id="attribute-i-owner"></a> <a id="owner-instance_method"></a>
|
|
44
|
+
- **@return** [String, nil] owner of the event, as sent by the API.
|
|
45
|
+
|
|
46
|
+
### `parent_sibling_activities` [R] <a id="attribute-i-parent_sibling_activities"></a> <a id="parent_sibling_activities-instance_method"></a>
|
|
47
|
+
- **@return** [Array<Hash>, nil] raw parent sibling activities.
|
|
48
|
+
|
|
49
|
+
### `processed_activity` [R] <a id="attribute-i-processed_activity"></a> <a id="processed_activity-instance_method"></a>
|
|
50
|
+
- **@return** [Hash, nil] raw processed activity payload.
|
|
51
|
+
|
|
52
|
+
### `processed_at` [R] <a id="attribute-i-processed_at"></a> <a id="processed_at-instance_method"></a>
|
|
53
|
+
- **@return** [Time, nil] when LinkedIn processed the event. UTC. Used as the cursor.
|
|
54
|
+
|
|
55
|
+
### `raw` [R] <a id="attribute-i-raw"></a> <a id="raw-instance_method"></a>
|
|
56
|
+
- **@return** [Hash] the original event Hash from the API.
|
|
57
|
+
|
|
58
|
+
### `resource_id` [R] <a id="attribute-i-resource_id"></a> <a id="resource_id-instance_method"></a>
|
|
59
|
+
- **@return** [String, nil] value as sent by the API.
|
|
60
|
+
|
|
61
|
+
### `resource_name` [R] <a id="attribute-i-resource_name"></a> <a id="resource_name-instance_method"></a>
|
|
62
|
+
- **@return** [String, nil] kind of resource that changed.
|
|
63
|
+
|
|
64
|
+
### `resource_uri` [R] <a id="attribute-i-resource_uri"></a> <a id="resource_uri-instance_method"></a>
|
|
65
|
+
- **@return** [String, nil] value as sent by the API.
|
|
66
|
+
|
|
67
|
+
### `sibling_activities` [R] <a id="attribute-i-sibling_activities"></a> <a id="sibling_activities-instance_method"></a>
|
|
68
|
+
- **@return** [Array<Hash>, nil] raw sibling activities.
|
|
69
|
+
|
|
70
|
+
## Public Class Methods
|
|
71
|
+
### `from_api(hash)` <a id="method-c-from_api"></a> <a id="from_api-class_method"></a>
|
|
72
|
+
Builds an event from one element of the API response.
|
|
73
|
+
- **@param** `hash` [Hash] one element of `elements` from the changelog response.
|
|
74
|
+
- **@return** [Event]
|
|
75
|
+
|
|
76
|
+
**@example**
|
|
77
|
+
```ruby
|
|
78
|
+
event = LinkedIn::MemberData::Event.from_api(
|
|
79
|
+
"id" => "1", "method" => "CREATE", "processedAt" => 1_788_000_000_000
|
|
80
|
+
)
|
|
81
|
+
event.method # => "CREATE"
|
|
82
|
+
event.processed_at # => 2026-08-29 10:40:00 UTC
|
|
83
|
+
event.processed_at_ms # => 1788000000000
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Public Instance Methods
|
|
87
|
+
### `processed_at_ms()` <a id="method-i-processed_at_ms"></a> <a id="processed_at_ms-instance_method"></a>
|
|
88
|
+
Raw epoch milliseconds, used as the next `startTime` cursor. Pass it as
|
|
89
|
+
<code>since:</code> to `Client#changelog` to resume.
|
|
90
|
+
- **@return** [Integer, nil] `nil` when the event has no `processedAt`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Class LinkedIn::MemberData::Forbidden <a id="class-LinkedIn-MemberData-Forbidden"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | [LinkedIn::MemberData::ApiError](ApiError.md) |
|
|
6
|
+
| **Defined in** | lib/linkedin/member_data/errors.rb |
|
|
7
|
+
|
|
8
|
+
HTTP 403. The token is not allowed to read this data.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Class LinkedIn::MemberData::NotFound <a id="class-LinkedIn-MemberData-NotFound"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | [LinkedIn::MemberData::ApiError](ApiError.md) |
|
|
6
|
+
| **Defined in** | lib/linkedin/member_data/errors.rb |
|
|
7
|
+
|
|
8
|
+
HTTP 404. For snapshots it can mean "No data found for this memberId".
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Class LinkedIn::MemberData::RateLimited <a id="class-LinkedIn-MemberData-RateLimited"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | [LinkedIn::MemberData::ApiError](ApiError.md) |
|
|
6
|
+
| **Defined in** | lib/linkedin/member_data/errors.rb |
|
|
7
|
+
|
|
8
|
+
HTTP 429. Retryable.
|
|
9
|
+
|
|
10
|
+
## Attributes
|
|
11
|
+
### `retry_after` [R] <a id="attribute-i-retry_after"></a> <a id="retry_after-instance_method"></a>
|
|
12
|
+
Seconds to wait, from the <code>Retry-After</code> header.
|
|
13
|
+
- **@return** [Integer, nil] `nil` when the header is absent, is not a positive number, or is an HTTP date.
|
|
14
|
+
|
|
15
|
+
## Public Instance Methods
|
|
16
|
+
### `initialize(message, status:, code: = nil, body: = nil, retry_after: = nil)` <a id="method-i-initialize"></a> <a id="initialize-instance_method"></a>
|
|
17
|
+
- **@param** `message` [String] error message.
|
|
18
|
+
- **@param** `status` [Integer] HTTP status code.
|
|
19
|
+
- **@param** `code` [Integer, String, nil] error code from the body.
|
|
20
|
+
- **@param** `body` [Hash, nil] parsed response body.
|
|
21
|
+
- **@param** `retry_after` [Integer, nil] seconds from the `Retry-After` header.
|
|
22
|
+
- **@return** [RateLimited] a new instance of RateLimited
|
|
23
|
+
|
|
24
|
+
### `retryable?()` <a id="method-i-retryable-3F"></a> <a id="retryable?-instance_method"></a>
|
|
25
|
+
- **@return** [true] a rate limit is worth a retry.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Class LinkedIn::MemberData::ServerError <a id="class-LinkedIn-MemberData-ServerError"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | [LinkedIn::MemberData::ApiError](ApiError.md) |
|
|
6
|
+
| **Defined in** | lib/linkedin/member_data/errors.rb |
|
|
7
|
+
|
|
8
|
+
HTTP 5xx. Retryable.
|
|
9
|
+
|
|
10
|
+
## Public Instance Methods
|
|
11
|
+
### `retryable?()` <a id="method-i-retryable-3F"></a> <a id="retryable?-instance_method"></a>
|
|
12
|
+
- **@return** [true] server errors are worth a retry.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Class LinkedIn::MemberData::Snapshot::Page <a id="class-LinkedIn-MemberData-Snapshot-Page"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | Data |
|
|
6
|
+
| **Defined in** | lib/linkedin/member_data/snapshot/page.rb |
|
|
7
|
+
|
|
8
|
+
One response page of snapshot data. Immutable.
|
|
9
|
+
|
|
10
|
+
## Attributes
|
|
11
|
+
### `count` [R] <a id="attribute-i-count"></a> <a id="count-instance_method"></a>
|
|
12
|
+
- **@return** [Integer, nil] page size from the `paging` object.
|
|
13
|
+
|
|
14
|
+
### `domain` [R] <a id="attribute-i-domain"></a> <a id="domain-instance_method"></a>
|
|
15
|
+
- **@return** [String, nil] `snapshotDomain` of the first element. An all-domains call may return several.
|
|
16
|
+
|
|
17
|
+
### `has_next` [R] <a id="attribute-i-has_next"></a> <a id="has_next-instance_method"></a>
|
|
18
|
+
- **@return** [Boolean] true when `paging.links` has a link with rel `next`.
|
|
19
|
+
|
|
20
|
+
### `raw` [R] <a id="attribute-i-raw"></a> <a id="raw-instance_method"></a>
|
|
21
|
+
- **@return** [Hash] the parsed response body.
|
|
22
|
+
|
|
23
|
+
### `rows` [R] <a id="attribute-i-rows"></a> <a id="rows-instance_method"></a>
|
|
24
|
+
- **@return** [Array<Hash{String => Object}>] rows of every element on this page. Keys differ per domain.
|
|
25
|
+
|
|
26
|
+
### `start` [R] <a id="attribute-i-start"></a> <a id="start-instance_method"></a>
|
|
27
|
+
- **@return** [Integer, nil] page index from the `paging` object.
|
|
28
|
+
|
|
29
|
+
### `total` [R] <a id="attribute-i-total"></a> <a id="total-instance_method"></a>
|
|
30
|
+
- **@return** [Integer, nil] total number of items from the `paging` object.
|
|
31
|
+
|
|
32
|
+
## Public Class Methods
|
|
33
|
+
### `from_api(raw)` <a id="method-c-from_api"></a> <a id="from_api-class_method"></a>
|
|
34
|
+
Builds a page from a parsed response body. Missing keys give empty or nil
|
|
35
|
+
values.
|
|
36
|
+
- **@param** `raw` [Hash] parsed JSON body of the API response.
|
|
37
|
+
- **@return** [Snapshot::Page]
|
|
38
|
+
|
|
39
|
+
## Public Instance Methods
|
|
40
|
+
### `next?()` <a id="method-i-next-3F"></a> <a id="next?-instance_method"></a>
|
|
41
|
+
Tells if another page may follow. Same value as <code>#has_next</code>. The
|
|
42
|
+
last page can still carry a `next` link, so an empty next page is possible.
|
|
43
|
+
- **@return** [Boolean]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Class LinkedIn::MemberData::Snapshot <a id="class-LinkedIn-MemberData-Snapshot"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | Object |
|
|
6
|
+
| **Includes** | Enumerable |
|
|
7
|
+
| **Defined in** | lib/linkedin/member_data/snapshot.rb, lib/linkedin/member_data/snapshot/page.rb |
|
|
8
|
+
|
|
9
|
+
Lazy view over `GET /rest/memberSnapshotData` for one domain (or all). Nothing
|
|
10
|
+
is fetched until you iterate. Each iteration starts a new walk from the first
|
|
11
|
+
page.
|
|
12
|
+
|
|
13
|
+
Pages are indexed by `start` (0, 1, 2, ...). The API says to keep going until
|
|
14
|
+
it answers "No data found for this memberId", so that error ends iteration
|
|
15
|
+
instead of raising. A page with no rows also ends iteration.
|
|
16
|
+
|
|
17
|
+
It includes `Enumerable`, so `first`, `to_a`, `lazy`, `select` and the rest
|
|
18
|
+
work on rows. `first` fetches one page only.
|
|
19
|
+
|
|
20
|
+
**@example Walk all rows of one domain**
|
|
21
|
+
```ruby
|
|
22
|
+
snapshot = client.snapshot(:connections)
|
|
23
|
+
snapshot.each { |row| puts row["First Name"] }
|
|
24
|
+
snapshot.first # fetches one page
|
|
25
|
+
snapshot.lazy.select { |row| row["Company"] }.first(5)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Constants
|
|
29
|
+
### `NO_DATA_MESSAGE` <a id="constant-NO_DATA_MESSAGE"></a> <a id="NO_DATA_MESSAGE-constant"></a>
|
|
30
|
+
Text of the API error that means "no more data". It ends iteration.
|
|
31
|
+
- **@return** [String]
|
|
32
|
+
|
|
33
|
+
### `PATH` <a id="constant-PATH"></a> <a id="PATH-constant"></a>
|
|
34
|
+
API path of the snapshot resource.
|
|
35
|
+
- **@return** [String]
|
|
36
|
+
|
|
37
|
+
## Attributes
|
|
38
|
+
### `domain` [R] <a id="attribute-i-domain"></a> <a id="domain-instance_method"></a>
|
|
39
|
+
The domain name sent to the API. `nil` means all domains.
|
|
40
|
+
- **@return** [String, nil]
|
|
41
|
+
|
|
42
|
+
## Public Instance Methods
|
|
43
|
+
### `each(&block)` <a id="method-i-each"></a> <a id="each-instance_method"></a>
|
|
44
|
+
Yields every row of every page. Pages are fetched one by one while you
|
|
45
|
+
iterate. Rows are plain Hashes with the keys LinkedIn returns. Keys differ per
|
|
46
|
+
domain.
|
|
47
|
+
- **@raise** [ApiError] on a non-2xx response, except the "No data found" answer.
|
|
48
|
+
- **@raise** [ConnectionError] on a network failure after all retries.
|
|
49
|
+
- **@return** [Snapshot] self, when a block is given.
|
|
50
|
+
- **@return** [Enumerator<Hash>] when no block is given.
|
|
51
|
+
- **@yieldparam** `row` [Hash{String => Object}] one row of snapshot data.
|
|
52
|
+
|
|
53
|
+
**@example Block form**
|
|
54
|
+
```ruby
|
|
55
|
+
client.snapshot(:connections).each { |row| puts row["First Name"] }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**@example Without a block, you get an Enumerator**
|
|
59
|
+
```ruby
|
|
60
|
+
client.snapshot(:connections).each.first(3)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### `page(start)` <a id="method-i-page"></a> <a id="page-instance_method"></a>
|
|
64
|
+
Fetches one page by index. Does not walk. Does not hide the "No data found"
|
|
65
|
+
error.
|
|
66
|
+
- **@param** `start` [Integer] zero-based page index.
|
|
67
|
+
- **@raise** [NotFound] past the end of the data.
|
|
68
|
+
- **@raise** [ApiError] on any other non-2xx response.
|
|
69
|
+
- **@raise** [ConnectionError] on a network failure after all retries.
|
|
70
|
+
- **@return** [Snapshot::Page]
|
|
71
|
+
|
|
72
|
+
**@example**
|
|
73
|
+
```ruby
|
|
74
|
+
client.snapshot(:profile).page(0).rows
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `pages()` <a id="method-i-pages"></a> <a id="pages-instance_method"></a>
|
|
78
|
+
Lazy Enumerator of pages. The walk stops at the first page without a `next`
|
|
79
|
+
link. It also stops at an empty page or at the "No data found" answer. Any
|
|
80
|
+
other API error raises.
|
|
81
|
+
- **@raise** [ApiError] while iterating, on a non-2xx response except "No data found".
|
|
82
|
+
- **@raise** [ConnectionError] while iterating, on a network failure after all retries.
|
|
83
|
+
- **@return** [Enumerator<Snapshot::Page>]
|
|
84
|
+
|
|
85
|
+
**@example Inspect paging data**
|
|
86
|
+
```ruby
|
|
87
|
+
client.snapshot(:profile).pages.each do |page|
|
|
88
|
+
puts "#{page.domain}: #{page.rows.size} rows, total #{page.total}"
|
|
89
|
+
end
|
|
90
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Class LinkedIn::MemberData::Unauthorized <a id="class-LinkedIn-MemberData-Unauthorized"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | [LinkedIn::MemberData::ApiError](ApiError.md) |
|
|
6
|
+
| **Defined in** | lib/linkedin/member_data/errors.rb |
|
|
7
|
+
|
|
8
|
+
HTTP 401. The token is invalid or expired.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Class LinkedIn::MemberData::VersionError <a id="class-LinkedIn-MemberData-VersionError"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | [LinkedIn::MemberData::ApiError](ApiError.md) |
|
|
6
|
+
| **Defined in** | lib/linkedin/member_data/errors.rb |
|
|
7
|
+
|
|
8
|
+
HTTP 426. The API version sent by the gem is not supported.
|
|
@@ -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](MemberData/ApiError.md)
|
|
28
|
+
- [MemberData/Authorization.md](MemberData/Authorization.md)
|
|
29
|
+
- [MemberData/Changelog/Page.md](MemberData/Changelog/Page.md)
|
|
30
|
+
- [MemberData/Changelog.md](MemberData/Changelog.md)
|
|
31
|
+
- [MemberData/Client.md](MemberData/Client.md)
|
|
32
|
+
- [MemberData/ConfigurationError.md](MemberData/ConfigurationError.md)
|
|
33
|
+
- [MemberData/ConnectionError.md](MemberData/ConnectionError.md)
|
|
34
|
+
- [MemberData/Domains.md](MemberData/Domains.md)
|
|
35
|
+
- [MemberData/Error.md](MemberData/Error.md)
|
|
36
|
+
- [MemberData/Event.md](MemberData/Event.md)
|
|
37
|
+
- [MemberData/Forbidden.md](MemberData/Forbidden.md)
|
|
38
|
+
- [MemberData/NotFound.md](MemberData/NotFound.md)
|
|
39
|
+
- [MemberData/RateLimited.md](MemberData/RateLimited.md)
|
|
40
|
+
- [MemberData/ServerError.md](MemberData/ServerError.md)
|
|
41
|
+
- [MemberData/Snapshot/Page.md](MemberData/Snapshot/Page.md)
|
|
42
|
+
- [MemberData/Snapshot.md](MemberData/Snapshot.md)
|
|
43
|
+
- [MemberData/Unauthorized.md](MemberData/Unauthorized.md)
|
|
44
|
+
- [MemberData/VersionError.md](MemberData/VersionError.md)
|
data/doc/LinkedIn.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Module LinkedIn <a id="module-LinkedIn"></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
|
+
Namespace of the LinkedIn gems.
|
data/doc/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# linkedin-member-data
|
|
2
|
+
|
|
3
|
+
Ruby client and CLI for the LinkedIn [Member Data Portability (Member) API](https://learn.microsoft.com/en-us/linkedin/dma/member-data-portability/member-data-portability-member/). Download your own LinkedIn data: 66 snapshot domains (profile, connections, messages, posts, ...) and the changelog of your activity from the last 28 days.
|
|
4
|
+
|
|
5
|
+
Zero runtime dependencies. Ruby 3.2+.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bundle add linkedin-member-data
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install the CLI only:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install linkedin-member-data
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Getting a token
|
|
20
|
+
|
|
21
|
+
1. Create an app in the [LinkedIn Developer Portal](https://www.linkedin.com/developers/apps/) using the [Member Data Portability (Member) Default Company](https://www.linkedin.com/company/member-data-portability-member-default-company) page.
|
|
22
|
+
2. Under Products, request access to **Member Data Portability API (Member)**.
|
|
23
|
+
3. Open **Docs and tools > OAuth Token Tools**, create a token with scope `r_dma_portability_self_serve`, and consent.
|
|
24
|
+
|
|
25
|
+
Tokens last 60 days. Only EEA and Swiss members can consent today.
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require "linkedin/member_data"
|
|
31
|
+
|
|
32
|
+
client = LinkedIn::MemberData::Client.new(access_token: ENV["LINKEDIN_ACCESS_TOKEN"])
|
|
33
|
+
|
|
34
|
+
client.snapshot(:connections).each do |row|
|
|
35
|
+
puts row["First Name"], row["Company"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
client.changelog(since: Time.now - 7 * 86_400).each do |event|
|
|
39
|
+
puts "#{event.method} #{event.resource_name} at #{event.processed_at}"
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Row keys differ per domain. These are CONNECTIONS keys.
|
|
44
|
+
|
|
45
|
+
## Snapshot
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
snap = client.snapshot(:profile) # symbol is upcased: "PROFILE"
|
|
49
|
+
snap = client.snapshot("ALL_COMMENTS") # strings are sent as given
|
|
50
|
+
snap = client.snapshot # all domains
|
|
51
|
+
|
|
52
|
+
snap.first # first row, fetches one page
|
|
53
|
+
snap.to_a # every row, walks all pages
|
|
54
|
+
snap.lazy.select { |row| row["Company"] }.first(5) # Enumerable, so anything goes
|
|
55
|
+
|
|
56
|
+
snap.pages.each do |page|
|
|
57
|
+
page.domain; page.rows; page.start; page.count; page.total; page.next?; page.raw
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
snap.page(0) # one page by index, no walking. Raises past the end.
|
|
61
|
+
|
|
62
|
+
LinkedIn::MemberData::Domains::ALL # list of domain names
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Rows are plain Hashes with the keys LinkedIn returns, for example `"First Name"`. Keys differ per domain.
|
|
66
|
+
|
|
67
|
+
Pages are walked until LinkedIn answers "No data found for this memberId". That answer ends iteration and is not raised. A page with no rows also ends iteration.
|
|
68
|
+
|
|
69
|
+
## Changelog
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
log = client.changelog(since: Date.new(2026, 9, 1), count: 10) # since: Time, Date or epoch ms; count: 1..50
|
|
73
|
+
|
|
74
|
+
log.each do |event|
|
|
75
|
+
event.id; event.method; event.resource_name; event.resource_id
|
|
76
|
+
event.captured_at; event.processed_at # Time (UTC)
|
|
77
|
+
event.activity; event.processed_activity # Hash
|
|
78
|
+
event.raw # the original Hash
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
log.pages.each { |page| page.events; page.next_start_time }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The API returns the cursor event again on the next page. The gem skips events it has already seen. Iteration stops when a page has no new events, or when the last event has no `processedAt`.
|
|
85
|
+
|
|
86
|
+
`event.method` is the API field (`CREATE`, `UPDATE`, ...). It shadows Ruby's `Object#method` on purpose, so `event.method(:name)` does not work on an Event.
|
|
87
|
+
|
|
88
|
+
`count` defaults to 10. A value outside 1..50 raises `ArgumentError` before any request.
|
|
89
|
+
|
|
90
|
+
Event fields: `id, activity_id, activity_status, config_version, owner, actor, resource_name, resource_id, resource_uri, method, method_name, captured_at, processed_at, activity, processed_activity, sibling_activities, parent_sibling_activities, raw`.
|
|
91
|
+
|
|
92
|
+
Known limit: when more than `count` events share one `processedAt`, the cursor cannot reach the rest. Raise `count` (max 50) to reduce the chance.
|
|
93
|
+
|
|
94
|
+
To resume later, store the last `event.processed_at_ms` and pass it as `since:`.
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
last_seen = client.changelog.to_a.last&.processed_at_ms
|
|
98
|
+
client.changelog(since: last_seen).each { |event| puts event.id }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Authorization
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
client.authorization # => Authorization or nil
|
|
105
|
+
client.authorization.regulated_at
|
|
106
|
+
client.enable_changelog! # start changelog archiving (usually automatic)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`Authorization` also has `member`, `developer_application`, `scopes` and `raw`.
|
|
110
|
+
|
|
111
|
+
## Options
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
require "logger"
|
|
115
|
+
|
|
116
|
+
LinkedIn::MemberData::Client.new(
|
|
117
|
+
access_token: "...",
|
|
118
|
+
retries: 3, # retries on 429, 5xx and network errors, with backoff; 0 disables
|
|
119
|
+
timeout: 30, # seconds
|
|
120
|
+
logger: Logger.new($stderr) # logs "GET url -> status" at debug level
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Retries wait for `Retry-After` when it is present, capped at 60 seconds. Otherwise the wait grows with each try (exponential backoff).
|
|
125
|
+
|
|
126
|
+
## Errors
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
LinkedIn::MemberData::Error
|
|
130
|
+
ConfigurationError # missing token
|
|
131
|
+
ConnectionError # network failure after retries
|
|
132
|
+
ApiError # status, code, body
|
|
133
|
+
Unauthorized Forbidden NotFound VersionError RateLimited ServerError
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`ConnectionError` covers timeouts, reset connections, DNS and TLS failures. `retryable?` is true for `RateLimited`, `ServerError` and `ConnectionError`. `ApiError#code` comes from `serviceErrorCode` or `code` in the response body. `RateLimited#retry_after` is nil when the header is absent.
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
begin
|
|
140
|
+
client.snapshot(:profile).to_a
|
|
141
|
+
rescue LinkedIn::MemberData::Unauthorized
|
|
142
|
+
warn "Token is invalid or expired"
|
|
143
|
+
rescue LinkedIn::MemberData::ApiError => error
|
|
144
|
+
warn "#{error.status}: #{error.message}"
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## CLI
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
export LINKEDIN_ACCESS_TOKEN=...
|
|
152
|
+
|
|
153
|
+
linkedin-member-data snapshot CONNECTIONS --out connections.json
|
|
154
|
+
linkedin-member-data snapshot --all --out-dir ./export
|
|
155
|
+
linkedin-member-data changelog --since 2026-09-01 --count 50 --out changelog.json
|
|
156
|
+
linkedin-member-data domains
|
|
157
|
+
linkedin-member-data auth
|
|
158
|
+
linkedin-member-data version
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Use `-h` or `--help` to print usage. The DOMAIN argument is upcased, so `snapshot connections` works. The token comes from `--token TOKEN` (before or after the command) or `LINKEDIN_ACCESS_TOKEN`. Data goes to stdout or `--out FILE`. Progress and errors go to stderr. `--since` takes an ISO date (midnight UTC) or an ISO datetime.
|
|
162
|
+
|
|
163
|
+
`snapshot --all` writes one `<DOMAIN>.json` per domain. Without `--out-dir DIR` it is a usage error (exit 2). A failing domain is reported and the run continues. The exit code is 1 at the end. Unauthorized and Forbidden stop the run, because a bad token fails every domain.
|
|
164
|
+
|
|
165
|
+
`auth` exits 1 with "No authorization found for this token" when none exists.
|
|
166
|
+
|
|
167
|
+
Exit codes: 0 success, 1 API error, network error or file write error, 2 usage error.
|
|
168
|
+
|
|
169
|
+
## Development
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
bin/setup
|
|
173
|
+
bundle exec rake # tests + rubocop
|
|
174
|
+
bundle exec rake branchproof # MC/DC coverage (Ruby 4.0+)
|
|
175
|
+
bundle exec rake quality # quality_gate fast, verify, audit
|
|
176
|
+
bundle exec rake docs # YARD Markdown docs in doc/ and llms.txt
|
|
177
|
+
ruby bin/prepare_release # full gate, then builds pkg/<gem>.gem
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## For AI agents
|
|
181
|
+
|
|
182
|
+
The gem ships its API reference as Markdown. Start at `llms.txt` in the gem root, which links every page under `doc/`. The same files are inside the installed gem (`gem contents linkedin-member-data | grep doc/`).
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LinkedIn
|
|
4
|
+
module MemberData
|
|
5
|
+
# The member's authorization record from `memberAuthorizations`. Immutable.
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [r] member
|
|
8
|
+
# @return [String, nil] member, as sent by the API.
|
|
9
|
+
# @!attribute [r] developer_application
|
|
10
|
+
# @return [String, nil] developer application, as sent by the API.
|
|
11
|
+
# @!attribute [r] regulated_at
|
|
12
|
+
# @return [Time, nil] when the member gave consent. UTC.
|
|
13
|
+
# @!attribute [r] scopes
|
|
14
|
+
# @return [Array<String>] granted scopes. Empty when the API sends none.
|
|
15
|
+
# @!attribute [r] raw
|
|
16
|
+
# @return [Hash] the original element from the API.
|
|
17
|
+
Authorization = Data.define(:member, :developer_application, :regulated_at, :scopes, :raw) do
|
|
18
|
+
# Builds an authorization from one element of the API response.
|
|
19
|
+
#
|
|
20
|
+
# @param hash [Hash] one element of `elements` from the `memberAuthorizations` response.
|
|
21
|
+
# @return [Authorization]
|
|
22
|
+
def self.from_api(hash)
|
|
23
|
+
key = hash.fetch("memberComplianceAuthorizationKey", {})
|
|
24
|
+
new(member: key["member"], developer_application: key["developerApplication"],
|
|
25
|
+
regulated_at: Util.time_from_ms(hash["regulatedAt"]),
|
|
26
|
+
scopes: hash.fetch("memberComplianceScopes", []), raw: hash)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LinkedIn
|
|
4
|
+
module MemberData
|
|
5
|
+
class Changelog
|
|
6
|
+
# One response page of changelog events. Immutable.
|
|
7
|
+
#
|
|
8
|
+
# @!attribute [r] events
|
|
9
|
+
# @return [Array<Event>] events of this page, in API order (oldest first).
|
|
10
|
+
# @!attribute [r] raw
|
|
11
|
+
# @return [Hash] the parsed response body.
|
|
12
|
+
Page = Data.define(:events, :raw) do
|
|
13
|
+
# Builds a page from a parsed response body. A missing `elements` key gives no events.
|
|
14
|
+
#
|
|
15
|
+
# @param raw [Hash] parsed JSON body of the API response.
|
|
16
|
+
# @return [Changelog::Page]
|
|
17
|
+
def self.from_api(raw)
|
|
18
|
+
new(events: raw.fetch("elements", []).map { |element| Event.from_api(element) }, raw: raw)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Cursor for the next request: `processedAt` of the last event, in epoch milliseconds.
|
|
22
|
+
#
|
|
23
|
+
# @return [Integer, nil] `nil` when the page has no events or the last event has no `processedAt`.
|
|
24
|
+
def next_start_time = events.last&.processed_at_ms
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|