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
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.
@@ -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]
@@ -0,0 +1,8 @@
1
+ # Class LinkedIn::MemberData::Error <a id="class-LinkedIn-MemberData-Error"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | StandardError |
6
+ | **Defined in** | lib/linkedin/member_data/errors.rb |
7
+
8
+ Base class of every error this gem raises on purpose.