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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: fffdb2d428be752a12be1d391449f8572a2b4d50be3209ef9fed96b3feadf6db
|
|
4
|
+
data.tar.gz: 6a0912ba3cefa423ef0de9c86d328f7186b1be45e687a3c1cd9bee95d6adea6d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1c4e4d4bdfb3a8e4d43e5b08aae421dbd7813d81d332ae055749f4dfc054cfb8922f2105cdc954d4e152fc5828458a5a19a43240c929b27c671a75a7ed451969
|
|
7
|
+
data.tar.gz: 1102dc284b5355f43ddbcd0ef4d1606cb1cd98473e1a197ec79b884d1a5dc650860c03a8a7551bbbe498f849c7da511540f6eb023e63c2fecade8471ce51f637
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-10-06
|
|
4
|
+
|
|
5
|
+
- Initial release: Client, Snapshot, Changelog, Authorization, CLI.
|
|
6
|
+
- Retries on 429, 5xx and network errors. `Retry-After` is capped at 60 seconds.
|
|
7
|
+
- `ConnectionError` for network failures after retries.
|
|
8
|
+
- `Changelog` skips the overlap event and resumes with `Event#processed_at_ms`.
|
|
9
|
+
- CLI exit codes: 0 success, 1 API error, 2 usage error.
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"linkedin-member-data" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at [lucianghinda@users.noreply.github.com](mailto:lucianghinda@users.noreply.github.com).
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lucian Ghinda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/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
|
data/doc/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-10-06
|
|
4
|
+
|
|
5
|
+
- Initial release: Client, Snapshot, Changelog, Authorization, CLI.
|
|
6
|
+
- Retries on 429, 5xx and network errors. `Retry-After` is capped at 60 seconds.
|
|
7
|
+
- `ConnectionError` for network failures after retries.
|
|
8
|
+
- `Changelog` skips the overlap event and resumes with `Event#processed_at_ms`.
|
|
9
|
+
- CLI exit codes: 0 success, 1 API error, 2 usage error.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Class LinkedIn::MemberData::ApiError <a id="class-LinkedIn-MemberData-ApiError"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | [LinkedIn::MemberData::Error](Error.md) |
|
|
6
|
+
| **Defined in** | lib/linkedin/member_data/errors.rb |
|
|
7
|
+
|
|
8
|
+
Defined last because the classes must exist first.
|
|
9
|
+
|
|
10
|
+
## Constants
|
|
11
|
+
### `STATUS_CLASSES` <a id="constant-STATUS_CLASSES"></a> <a id="STATUS_CLASSES-constant"></a>
|
|
12
|
+
HTTP status => error class. Other statuses use `ServerError` (5xx) or
|
|
13
|
+
`ApiError`.
|
|
14
|
+
- **@api** private
|
|
15
|
+
|
|
16
|
+
## Attributes
|
|
17
|
+
### `body` [R] <a id="attribute-i-body"></a> <a id="body-instance_method"></a>
|
|
18
|
+
Details of the failed response. `status` is the HTTP status code (Integer).
|
|
19
|
+
`code` is `serviceErrorCode` or `code` from the body (Integer or String, nil
|
|
20
|
+
when the body has neither). `body` is the parsed JSON body (nil when the body
|
|
21
|
+
is empty, not JSON, or not an object).
|
|
22
|
+
- **@return** [Integer, String, Hash, nil] `status`, `code` or `body`.
|
|
23
|
+
|
|
24
|
+
### `code` [R] <a id="attribute-i-code"></a> <a id="code-instance_method"></a>
|
|
25
|
+
Details of the failed response. `status` is the HTTP status code (Integer).
|
|
26
|
+
`code` is `serviceErrorCode` or `code` from the body (Integer or String, nil
|
|
27
|
+
when the body has neither). `body` is the parsed JSON body (nil when the body
|
|
28
|
+
is empty, not JSON, or not an object).
|
|
29
|
+
- **@return** [Integer, String, Hash, nil] `status`, `code` or `body`.
|
|
30
|
+
|
|
31
|
+
### `status` [R] <a id="attribute-i-status"></a> <a id="status-instance_method"></a>
|
|
32
|
+
Details of the failed response. `status` is the HTTP status code (Integer).
|
|
33
|
+
`code` is `serviceErrorCode` or `code` from the body (Integer or String, nil
|
|
34
|
+
when the body has neither). `body` is the parsed JSON body (nil when the body
|
|
35
|
+
is empty, not JSON, or not an object).
|
|
36
|
+
- **@return** [Integer, String, Hash, nil] `status`, `code` or `body`.
|
|
37
|
+
|
|
38
|
+
## Public Instance Methods
|
|
39
|
+
### `initialize(message, status:, code: = nil, body: = nil)` <a id="method-i-initialize"></a> <a id="initialize-instance_method"></a>
|
|
40
|
+
- **@param** `message` [String] `message` from the body, or "HTTP <status>".
|
|
41
|
+
- **@param** `status` [Integer] HTTP status code.
|
|
42
|
+
- **@param** `code` [Integer, String, nil] error code from the body.
|
|
43
|
+
- **@param** `body` [Hash, nil] parsed response body.
|
|
44
|
+
- **@return** [ApiError] a new instance of ApiError
|
|
45
|
+
|
|
46
|
+
### `retry_after()` <a id="method-i-retry_after"></a> <a id="retry_after-instance_method"></a>
|
|
47
|
+
Only RateLimited knows a Retry-After. Others let the backoff decide.
|
|
48
|
+
- **@return** [nil]
|
|
49
|
+
|
|
50
|
+
### `retryable?()` <a id="method-i-retryable-3F"></a> <a id="retryable?-instance_method"></a>
|
|
51
|
+
Tells if a retry may help. False for most API errors.
|
|
52
|
+
- **@return** [Boolean]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Class LinkedIn::MemberData::Authorization <a id="class-LinkedIn-MemberData-Authorization"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | Data |
|
|
6
|
+
| **Defined in** | lib/linkedin/member_data/authorization.rb |
|
|
7
|
+
|
|
8
|
+
The member's authorization record from `memberAuthorizations`. Immutable.
|
|
9
|
+
|
|
10
|
+
## Attributes
|
|
11
|
+
### `developer_application` [R] <a id="attribute-i-developer_application"></a> <a id="developer_application-instance_method"></a>
|
|
12
|
+
- **@return** [String, nil] developer application, as sent by the API.
|
|
13
|
+
|
|
14
|
+
### `member` [R] <a id="attribute-i-member"></a> <a id="member-instance_method"></a>
|
|
15
|
+
- **@return** [String, nil] member, as sent by the API.
|
|
16
|
+
|
|
17
|
+
### `raw` [R] <a id="attribute-i-raw"></a> <a id="raw-instance_method"></a>
|
|
18
|
+
- **@return** [Hash] the original element from the API.
|
|
19
|
+
|
|
20
|
+
### `regulated_at` [R] <a id="attribute-i-regulated_at"></a> <a id="regulated_at-instance_method"></a>
|
|
21
|
+
- **@return** [Time, nil] when the member gave consent. UTC.
|
|
22
|
+
|
|
23
|
+
### `scopes` [R] <a id="attribute-i-scopes"></a> <a id="scopes-instance_method"></a>
|
|
24
|
+
- **@return** [Array<String>] granted scopes. Empty when the API sends none.
|
|
25
|
+
|
|
26
|
+
## Public Class Methods
|
|
27
|
+
### `from_api(hash)` <a id="method-c-from_api"></a> <a id="from_api-class_method"></a>
|
|
28
|
+
Builds an authorization from one element of the API response.
|
|
29
|
+
- **@param** `hash` [Hash] one element of `elements` from the `memberAuthorizations` response.
|
|
30
|
+
- **@return** [Authorization]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Class LinkedIn::MemberData::Changelog::Page <a id="class-LinkedIn-MemberData-Changelog-Page"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | Data |
|
|
6
|
+
| **Defined in** | lib/linkedin/member_data/changelog/page.rb |
|
|
7
|
+
|
|
8
|
+
One response page of changelog events. Immutable.
|
|
9
|
+
|
|
10
|
+
## Attributes
|
|
11
|
+
### `events` [R] <a id="attribute-i-events"></a> <a id="events-instance_method"></a>
|
|
12
|
+
- **@return** [Array<Event>] events of this page, in API order (oldest first).
|
|
13
|
+
|
|
14
|
+
### `raw` [R] <a id="attribute-i-raw"></a> <a id="raw-instance_method"></a>
|
|
15
|
+
- **@return** [Hash] the parsed response body.
|
|
16
|
+
|
|
17
|
+
## Public Class Methods
|
|
18
|
+
### `from_api(raw)` <a id="method-c-from_api"></a> <a id="from_api-class_method"></a>
|
|
19
|
+
Builds a page from a parsed response body. A missing `elements` key gives no
|
|
20
|
+
events.
|
|
21
|
+
- **@param** `raw` [Hash] parsed JSON body of the API response.
|
|
22
|
+
- **@return** [Changelog::Page]
|
|
23
|
+
|
|
24
|
+
## Public Instance Methods
|
|
25
|
+
### `next_start_time()` <a id="method-i-next_start_time"></a> <a id="next_start_time-instance_method"></a>
|
|
26
|
+
Cursor for the next request: `processedAt` of the last event, in epoch
|
|
27
|
+
milliseconds.
|
|
28
|
+
- **@return** [Integer, nil] `nil` when the page has no events or the last event has no `processedAt`.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Class LinkedIn::MemberData::Changelog <a id="class-LinkedIn-MemberData-Changelog"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | Object |
|
|
6
|
+
| **Includes** | Enumerable |
|
|
7
|
+
| **Defined in** | lib/linkedin/member_data/changelog.rb, lib/linkedin/member_data/changelog/page.rb |
|
|
8
|
+
|
|
9
|
+
Lazy view over `GET /rest/memberChangeLogs` (last 28 days). Nothing is fetched
|
|
10
|
+
until you iterate. Each iteration starts a new walk.
|
|
11
|
+
|
|
12
|
+
The cursor is `startTime` = last `processedAt` seen. LinkedIn returns the
|
|
13
|
+
cursor event again on the next page, so events already seen are skipped and
|
|
14
|
+
iteration stops when a page brings nothing new, or when the last event has no
|
|
15
|
+
processedAt (the cursor cannot move).
|
|
16
|
+
|
|
17
|
+
Known limit: if more than `count` events share one processedAt, the ones
|
|
18
|
+
beyond the first page are not reachable through the cursor. A higher `count`
|
|
19
|
+
(max 50) makes this less likely.
|
|
20
|
+
|
|
21
|
+
It includes `Enumerable`, so `first`, `to_a`, `lazy`, `select` and the rest
|
|
22
|
+
work on events.
|
|
23
|
+
|
|
24
|
+
**@example Print recent events**
|
|
25
|
+
```ruby
|
|
26
|
+
client.changelog(since: Time.now - 7 * 86_400).each do |event|
|
|
27
|
+
puts "#{event.method} #{event.resource_name} at #{event.processed_at}"
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Constants
|
|
32
|
+
### `COUNT_RANGE` <a id="constant-COUNT_RANGE"></a> <a id="COUNT_RANGE-constant"></a>
|
|
33
|
+
Allowed values for `count`.
|
|
34
|
+
- **@return** [Range<Integer>]
|
|
35
|
+
|
|
36
|
+
### `DEFAULT_COUNT` <a id="constant-DEFAULT_COUNT"></a> <a id="DEFAULT_COUNT-constant"></a>
|
|
37
|
+
Default for `count`.
|
|
38
|
+
- **@return** [Integer]
|
|
39
|
+
|
|
40
|
+
### `PATH` <a id="constant-PATH"></a> <a id="PATH-constant"></a>
|
|
41
|
+
API path of the changelog resource.
|
|
42
|
+
- **@return** [String]
|
|
43
|
+
|
|
44
|
+
## Attributes
|
|
45
|
+
### `count` [R] <a id="attribute-i-count"></a> <a id="count-instance_method"></a>
|
|
46
|
+
Settings of this view. `since` is the first `processedAt` to fetch, in epoch
|
|
47
|
+
milliseconds (`nil` starts at the oldest event). `count` is the number of
|
|
48
|
+
events per request, from 1 to 50.
|
|
49
|
+
- **@return** [Integer, nil] `since` or `count`. `count` is never nil.
|
|
50
|
+
|
|
51
|
+
### `since` [R] <a id="attribute-i-since"></a> <a id="since-instance_method"></a>
|
|
52
|
+
Settings of this view. `since` is the first `processedAt` to fetch, in epoch
|
|
53
|
+
milliseconds (`nil` starts at the oldest event). `count` is the number of
|
|
54
|
+
events per request, from 1 to 50.
|
|
55
|
+
- **@return** [Integer, nil] `since` or `count`. `count` is never nil.
|
|
56
|
+
|
|
57
|
+
## Public Instance Methods
|
|
58
|
+
### `each(&block)` <a id="method-i-each"></a> <a id="each-instance_method"></a>
|
|
59
|
+
Yields every new event of every page, oldest first. Pages are fetched one by
|
|
60
|
+
one while you iterate.
|
|
61
|
+
- **@raise** [ApiError] on a non-2xx response.
|
|
62
|
+
- **@raise** [ConnectionError] on a network failure after all retries.
|
|
63
|
+
- **@return** [Changelog] self, when a block is given.
|
|
64
|
+
- **@return** [Enumerator<Event>] when no block is given.
|
|
65
|
+
- **@yieldparam** `event` [Event] one changelog event. Events already seen in the overlap are skipped.
|
|
66
|
+
|
|
67
|
+
**@example**
|
|
68
|
+
```ruby
|
|
69
|
+
client.changelog(count: 50).each { |event| puts event.id }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**@example Without a block, you get an Enumerator**
|
|
73
|
+
```ruby
|
|
74
|
+
client.changelog.each.first(5)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `page(start_time)` <a id="method-i-page"></a> <a id="page-instance_method"></a>
|
|
78
|
+
Fetches one raw page. No overlap handling, so the cursor event comes back
|
|
79
|
+
again.
|
|
80
|
+
- **@param** `start_time` [Integer, nil] cursor in epoch milliseconds. `nil` starts at the oldest event.
|
|
81
|
+
- **@raise** [ApiError] on a non-2xx response.
|
|
82
|
+
- **@raise** [ConnectionError] on a network failure after all retries.
|
|
83
|
+
- **@return** [Changelog::Page]
|
|
84
|
+
|
|
85
|
+
### `pages()` <a id="method-i-pages"></a> <a id="pages-instance_method"></a>
|
|
86
|
+
Lazy Enumerator of pages with already-seen events removed. Stops on the first
|
|
87
|
+
page with no new events, or when the last event has no `processedAt`. A
|
|
88
|
+
yielded Page has filtered `events` but the unfiltered `raw` response.
|
|
89
|
+
- **@raise** [ApiError] while iterating, on a non-2xx response.
|
|
90
|
+
- **@raise** [ConnectionError] while iterating, on a network failure after all retries.
|
|
91
|
+
- **@return** [Enumerator<Changelog::Page>]
|
|
92
|
+
|
|
93
|
+
**@example Cursor of each page**
|
|
94
|
+
```ruby
|
|
95
|
+
client.changelog.pages.each { |page| puts "#{page.events.size} events, next #{page.next_start_time}" }
|
|
96
|
+
```
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Class LinkedIn::MemberData::Client <a id="class-LinkedIn-MemberData-Client"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | Object |
|
|
6
|
+
| **Defined in** | lib/linkedin/member_data/client.rb |
|
|
7
|
+
|
|
8
|
+
Entry point of the gem. Holds one `Connection`.
|
|
9
|
+
|
|
10
|
+
**@example Create a client and read a snapshot**
|
|
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
|
+
### `AUTHORIZATIONS_PATH` <a id="constant-AUTHORIZATIONS_PATH"></a> <a id="AUTHORIZATIONS_PATH-constant"></a>
|
|
18
|
+
API path of the member authorizations resource.
|
|
19
|
+
- **@return** [String]
|
|
20
|
+
|
|
21
|
+
## Public Instance Methods
|
|
22
|
+
### `authorization()` <a id="method-i-authorization"></a> <a id="authorization-instance_method"></a>
|
|
23
|
+
Fetches the authorization record of the token. Sends one request.
|
|
24
|
+
- **@raise** [ApiError] on a non-2xx response, for example `Unauthorized` for a bad token.
|
|
25
|
+
- **@raise** [ConnectionError] on a network failure after all retries.
|
|
26
|
+
- **@return** [Authorization, nil] `nil` when LinkedIn returns no authorization.
|
|
27
|
+
|
|
28
|
+
**@example**
|
|
29
|
+
```ruby
|
|
30
|
+
authorization = client.authorization
|
|
31
|
+
puts authorization.regulated_at if authorization
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### `changelog(since: = nil, count: = Changelog::DEFAULT_COUNT)` <a id="method-i-changelog"></a> <a id="changelog-instance_method"></a>
|
|
35
|
+
Returns a lazy view over the changelog of the last 28 days. No request is sent
|
|
36
|
+
until you iterate.
|
|
37
|
+
- **@param** `since` [Time, Date, Integer, nil] first `processedAt` to fetch.
|
|
38
|
+
Integer is epoch milliseconds. A Date is midnight UTC. `nil` starts at the oldest event.
|
|
39
|
+
- **@param** `count` [Integer] events per request, from 1 to 50.
|
|
40
|
+
- **@raise** [ArgumentError] when `count` is outside 1..50 or `since` has an unsupported type.
|
|
41
|
+
Raised before any request.
|
|
42
|
+
- **@return** [Changelog]
|
|
43
|
+
|
|
44
|
+
**@example Events of the last 7 days**
|
|
45
|
+
```ruby
|
|
46
|
+
client.changelog(since: Time.now - 7 * 86_400).each do |event|
|
|
47
|
+
puts "#{event.method} #{event.resource_name} at #{event.processed_at}"
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**@example Resume from the last event you saw**
|
|
52
|
+
```ruby
|
|
53
|
+
last_seen = client.changelog.to_a.last&.processed_at_ms
|
|
54
|
+
client.changelog(since: last_seen).each { |event| puts event.id }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `enable_changelog!()` <a id="method-i-enable_changelog-21"></a> <a id="enable_changelog!-instance_method"></a>
|
|
58
|
+
Starts changelog archiving for the member. LinkedIn usually does this by
|
|
59
|
+
itself.
|
|
60
|
+
- **@raise** [ApiError] on a non-2xx response.
|
|
61
|
+
- **@raise** [ConnectionError] on a network failure after all retries.
|
|
62
|
+
- **@return** [true] always true. API failures raise.
|
|
63
|
+
|
|
64
|
+
**@example**
|
|
65
|
+
```ruby
|
|
66
|
+
client.enable_changelog! # => true
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### `initialize(access_token:, retries: = 3, timeout: = 30, logger: = nil, connection: = nil)` <a id="method-i-initialize"></a> <a id="initialize-instance_method"></a>
|
|
70
|
+
Builds a client. No request is sent.
|
|
71
|
+
- **@param** `access_token` [String] OAuth token with scope `r_dma_portability_self_serve`.
|
|
72
|
+
Leading and trailing spaces are removed.
|
|
73
|
+
- **@param** `retries` [Integer] retries on 429, 5xx and network errors, with backoff. `0` turns retries off.
|
|
74
|
+
- **@param** `timeout` [Integer, Float] open, read and write timeout in seconds.
|
|
75
|
+
- **@param** `logger` [Logger, nil] gets one debug line per request: `GET url -> status`.
|
|
76
|
+
- **@param** `connection` [Connection, nil] ready-made connection. When given, `retries`, `timeout`
|
|
77
|
+
and `logger` are not used. Meant for tests.
|
|
78
|
+
- **@raise** [ConfigurationError] when `access_token` is nil or blank.
|
|
79
|
+
- **@return** [Client] a new instance of Client
|
|
80
|
+
|
|
81
|
+
### `snapshot(domain = nil)` <a id="method-i-snapshot"></a> <a id="snapshot-instance_method"></a>
|
|
82
|
+
Returns a lazy view over snapshot data. No request is sent until you iterate.
|
|
83
|
+
- **@param** `domain` [Symbol, String, nil] a name from `Domains::ALL`. A Symbol is upcased.
|
|
84
|
+
A String is sent as given, because the API is case sensitive. `nil` means all domains.
|
|
85
|
+
- **@raise** [ArgumentError] when `domain` is not a Symbol, a String or nil.
|
|
86
|
+
- **@return** [Snapshot]
|
|
87
|
+
|
|
88
|
+
**@example One domain, symbol or string**
|
|
89
|
+
```ruby
|
|
90
|
+
client.snapshot(:profile).first # symbol is upcased: "PROFILE"
|
|
91
|
+
client.snapshot("ALL_COMMENTS").to_a
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**@example All domains**
|
|
95
|
+
```ruby
|
|
96
|
+
client.snapshot.each { |row| puts row.keys.inspect }
|
|
97
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Class LinkedIn::MemberData::ConfigurationError <a id="class-LinkedIn-MemberData-ConfigurationError"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | [LinkedIn::MemberData::Error](Error.md) |
|
|
6
|
+
| **Defined in** | lib/linkedin/member_data/errors.rb |
|
|
7
|
+
|
|
8
|
+
Missing or empty access token.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Class LinkedIn::MemberData::ConnectionError <a id="class-LinkedIn-MemberData-ConnectionError"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | [LinkedIn::MemberData::Error](Error.md) |
|
|
6
|
+
| **Defined in** | lib/linkedin/member_data/errors.rb |
|
|
7
|
+
|
|
8
|
+
Network failure after all retries (timeouts, reset connections).
|
|
9
|
+
|
|
10
|
+
## Public Instance Methods
|
|
11
|
+
### `retry_after()` <a id="method-i-retry_after"></a> <a id="retry_after-instance_method"></a>
|
|
12
|
+
- **@return** [nil] there is no Retry-After header, so the backoff decides.
|
|
13
|
+
|
|
14
|
+
### `retryable?()` <a id="method-i-retryable-3F"></a> <a id="retryable?-instance_method"></a>
|
|
15
|
+
- **@return** [true] network errors are worth a retry.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Module LinkedIn::MemberData::Domains <a id="module-LinkedIn-MemberData-Domains"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Defined in** | lib/linkedin/member_data/domains.rb |
|
|
6
|
+
|
|
7
|
+
Snapshot domains documented at
|
|
8
|
+
https://learn.microsoft.com/en-us/linkedin/dma/member-data-portability/shared/
|
|
9
|
+
snapshot-domain The API is case sensitive. Strings are sent as given; symbols
|
|
10
|
+
are upcased.
|
|
11
|
+
|
|
12
|
+
## Constants
|
|
13
|
+
### `ALL` <a id="constant-ALL"></a> <a id="ALL-constant"></a>
|
|
14
|
+
Names of all snapshot domains. Pass one to `Client#snapshot`.
|
|
15
|
+
- **@return** [Array<String>]
|
|
16
|
+
|
|
17
|
+
## Public Class Methods
|
|
18
|
+
### `normalize(domain)` <a id="method-c-normalize"></a> <a id="normalize-class_method"></a>
|
|
19
|
+
Turns a domain argument into the name sent to the API.
|
|
20
|
+
- **@param** `domain` [Symbol, String, nil] a Symbol is upcased. A String is returned as given. `nil` stays `nil`.
|
|
21
|
+
- **@raise** [ArgumentError] when `domain` is not a Symbol, a String or nil.
|
|
22
|
+
- **@return** [String, nil]
|