async_linkedinlib 0.2.0__py3-none-any.whl

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 (64) hide show
  1. async_linkedinlib/__init__.py +187 -0
  2. async_linkedinlib/__main__.py +3 -0
  3. async_linkedinlib/auth/__init__.py +5 -0
  4. async_linkedinlib/auth/service.py +77 -0
  5. async_linkedinlib/cli.py +37 -0
  6. async_linkedinlib/client.py +93 -0
  7. async_linkedinlib/config/__init__.py +6 -0
  8. async_linkedinlib/config/options.py +15 -0
  9. async_linkedinlib/config/profile.py +56 -0
  10. async_linkedinlib/exceptions.py +75 -0
  11. async_linkedinlib/models/__init__.py +155 -0
  12. async_linkedinlib/models/_json.py +65 -0
  13. async_linkedinlib/models/comments.py +145 -0
  14. async_linkedinlib/models/content.py +207 -0
  15. async_linkedinlib/models/feed.py +124 -0
  16. async_linkedinlib/models/jobs.py +215 -0
  17. async_linkedinlib/models/jobs_apply.py +424 -0
  18. async_linkedinlib/models/member.py +14 -0
  19. async_linkedinlib/models/pagination.py +52 -0
  20. async_linkedinlib/models/posts.py +141 -0
  21. async_linkedinlib/models/posts_results.py +120 -0
  22. async_linkedinlib/models/search.py +307 -0
  23. async_linkedinlib/models/search_filters.py +339 -0
  24. async_linkedinlib/models/search_jobs.py +213 -0
  25. async_linkedinlib/py.typed +0 -0
  26. async_linkedinlib/resources/__init__.py +12 -0
  27. async_linkedinlib/resources/account.py +35 -0
  28. async_linkedinlib/resources/comments/__init__.py +5 -0
  29. async_linkedinlib/resources/comments/resource.py +227 -0
  30. async_linkedinlib/resources/comments/urns.py +43 -0
  31. async_linkedinlib/resources/feed/__init__.py +5 -0
  32. async_linkedinlib/resources/feed/queries.py +57 -0
  33. async_linkedinlib/resources/feed/resource.py +225 -0
  34. async_linkedinlib/resources/feed/saved.py +49 -0
  35. async_linkedinlib/resources/feed/urns.py +68 -0
  36. async_linkedinlib/resources/jobs/__init__.py +5 -0
  37. async_linkedinlib/resources/jobs/answers.py +239 -0
  38. async_linkedinlib/resources/jobs/resource.py +212 -0
  39. async_linkedinlib/resources/jobs/upload.py +78 -0
  40. async_linkedinlib/resources/mentions.py +58 -0
  41. async_linkedinlib/resources/paging.py +88 -0
  42. async_linkedinlib/resources/posts/__init__.py +6 -0
  43. async_linkedinlib/resources/posts/media.py +173 -0
  44. async_linkedinlib/resources/posts/payloads.py +98 -0
  45. async_linkedinlib/resources/posts/resource.py +254 -0
  46. async_linkedinlib/resources/reactions.py +89 -0
  47. async_linkedinlib/resources/search/__init__.py +5 -0
  48. async_linkedinlib/resources/search/queries.py +79 -0
  49. async_linkedinlib/resources/search/results.py +85 -0
  50. async_linkedinlib/resources/search/search.py +284 -0
  51. async_linkedinlib/resources/threads.py +30 -0
  52. async_linkedinlib/session/__init__.py +5 -0
  53. async_linkedinlib/session/cookies.py +52 -0
  54. async_linkedinlib/session/lease.py +30 -0
  55. async_linkedinlib/session/state.py +129 -0
  56. async_linkedinlib/transport/__init__.py +6 -0
  57. async_linkedinlib/transport/http.py +183 -0
  58. async_linkedinlib/transport/restli.py +46 -0
  59. async_linkedinlib/transport/voyager.py +151 -0
  60. async_linkedinlib-0.2.0.dist-info/METADATA +478 -0
  61. async_linkedinlib-0.2.0.dist-info/RECORD +64 -0
  62. async_linkedinlib-0.2.0.dist-info/WHEEL +4 -0
  63. async_linkedinlib-0.2.0.dist-info/entry_points.txt +2 -0
  64. async_linkedinlib-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,478 @@
1
+ Metadata-Version: 2.5
2
+ Name: async_linkedinlib
3
+ Version: 0.2.0
4
+ Summary: An async Python client for LinkedIn's Android API: sessions, feed, posts, comments, search and jobs.
5
+ Project-URL: Homepage, https://github.com/Anandesh-Sharma/async_linkedinlib
6
+ Project-URL: Repository, https://github.com/Anandesh-Sharma/async_linkedinlib
7
+ Project-URL: Issues, https://github.com/Anandesh-Sharma/async_linkedinlib/issues
8
+ Project-URL: Documentation, https://github.com/Anandesh-Sharma/async_linkedinlib#readme
9
+ Author: Anandesh Sharma
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api-client,asyncio,httpx,linkedin,python
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: AsyncIO
15
+ Classifier: Operating System :: POSIX
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: httpx[http2]<1,>=0.28
20
+ Description-Content-Type: text/markdown
21
+
22
+ <p align="center">
23
+ <img src="https://raw.githubusercontent.com/Anandesh-Sharma/async_linkedinlib/main/assets/banner.png" alt="async_linkedinlib — Async Python for LinkedIn" width="100%">
24
+ </p>
25
+
26
+ # async_linkedinlib
27
+
28
+ [![CI](https://github.com/Anandesh-Sharma/async_linkedinlib/actions/workflows/ci.yml/badge.svg)](https://github.com/Anandesh-Sharma/async_linkedinlib/actions/workflows/ci.yml)
29
+ [![PyPI](https://img.shields.io/pypi/v/async_linkedinlib?color=111111)](https://pypi.org/project/async_linkedinlib/)
30
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-111111?logo=python&logoColor=white)
31
+ [![MIT license](https://img.shields.io/badge/license-MIT-111111)](https://github.com/Anandesh-Sharma/async_linkedinlib/blob/main/LICENSE)
32
+ ![Typed](https://img.shields.io/badge/typing-PEP%20561-111111)
33
+
34
+ Async Python client for LinkedIn's private Android API (app 4.1.1267). One saved session
35
+ drives everything: sign-in, the feed, your posts, comments, search and jobs with Easy Apply.
36
+ Connections and messages are not built yet. Python 3.11+ is required.
37
+
38
+ ## At a glance
39
+
40
+ | Area | What you can do |
41
+ | --- | --- |
42
+ | Sessions | Import your own cookies, persist sessions and verify your account |
43
+ | Feed | Home, profile and company feeds, reactions, saves and polls |
44
+ | Publishing | Posts, media uploads, comments and replies |
45
+ | Search | People, companies, content, jobs and other supported search categories |
46
+ | Jobs | Search, details, saved jobs and explicit Easy Apply steps |
47
+
48
+ Connections and messages are not implemented. Google sign-in exchanges a caller-supplied
49
+ fresh token; it does not acquire one. See [feature status](#feature-status) for the
50
+ boundary between offline tests and previously recorded live verification.
51
+
52
+ [Sessions](#saved-sessions) · [Feed](#feed-linkedinfeed) · [Posts](#posts-linkedinposts) ·
53
+ [Search](#search-linkedinsearch) · [Jobs](#jobs-and-easy-apply-linkedinjobs) ·
54
+ [Feature status](#feature-status) · [Development](#development)
55
+
56
+ One account, one async client, one shared connection pool. Pagination and writes
57
+ are explicit; session files preserve account state between runs.
58
+
59
+ This is an independent, unofficial client for LinkedIn's private API. It is not
60
+ affiliated with or endorsed by LinkedIn. Private endpoints can change without notice.
61
+ Saved-session ownership uses POSIX file locks; use macOS or Linux.
62
+
63
+ ## Install
64
+
65
+ Requires Python 3.11 or newer. Install from PyPI:
66
+
67
+ ```sh
68
+ pip install async_linkedinlib
69
+ ```
70
+
71
+ For development and the `uv run` examples below:
72
+
73
+ ```sh
74
+ git clone https://github.com/Anandesh-Sharma/async_linkedinlib.git
75
+ cd async_linkedinlib
76
+ uv sync --locked
77
+ ```
78
+
79
+ For reproducible deployments, pin a PyPI version or install a reviewed Git commit: `git+https://github.com/Anandesh-Sharma/async_linkedinlib.git@<commit-sha>`.
80
+
81
+ ## Saved sessions
82
+
83
+ `li_at` is the primary session credential. The client also keeps `JSESSIONID` and
84
+ sends its unquoted value as `csrf-token`. Import your own cookie pair through hidden
85
+ prompts, verify it with LinkedIn, and save it outside the repository:
86
+
87
+ ```sh
88
+ uv run async-linkedinlib import-session \
89
+ --session ~/.local/share/async_linkedinlib/account.session.json
90
+ uv run async-linkedinlib me \
91
+ --session ~/.local/share/async_linkedinlib/account.session.json
92
+ ```
93
+
94
+ ```python
95
+ import asyncio
96
+ from pathlib import Path
97
+ from async_linkedinlib import LinkedIn
98
+
99
+ async def main():
100
+ path = Path.home() / ".local/share/async_linkedinlib/account.session.json"
101
+ async with LinkedIn.from_session_file(path) as linkedin:
102
+ me = await linkedin.account.me()
103
+ print(me.full_name, me.id)
104
+
105
+ asyncio.run(main())
106
+ ```
107
+
108
+ For credentials obtained through your own secure input, use
109
+ `Session.from_cookies(li_at, jsessionid)` with `LinkedIn(session)`. The explicit
110
+ constructor uses in-memory state; call `session.save(path)` when persistence is
111
+ needed. `Session.load(path)` restores state without checking server validity.
112
+ `session.authenticated` only checks local credential presence and expiration.
113
+ `account.me()` checks server acceptance and returns a `Member`.
114
+
115
+ Session files are written atomically with mode 0600. Cookies retain domain, path,
116
+ secure flag and expiry. Device identity and app profile persist with them.
117
+ The file-backed client checkpoints cookie changes and saves on close without
118
+ logging out. It owns a POSIX sidecar lock until closed; a second owner raises
119
+ `SessionInUse`. Do not remove the lock file while another process may own it.
120
+ One file per account; share its active client among tasks.
121
+
122
+ ## Google sign-in
123
+
124
+ ```sh
125
+ uv run async-linkedinlib login-google \
126
+ --session ~/.local/share/async_linkedinlib/google.session.json
127
+ ```
128
+
129
+ The hidden prompt expects a **fresh Google ID token issued for LinkedIn's Android
130
+ sign-in flow**. An arbitrary token issued for your own Google OAuth client is not
131
+ equivalent. The SDK does not sign into Google, obtain that token, or replay the
132
+ recorded token. In Python, call `await linkedin.auth.login_google(id_token)` on a
133
+ client without an existing authenticated session.
134
+
135
+ The captured sequence is:
136
+
137
+ 1. `GET /mob/tracking` obtains fresh server cookies. The app uses a generated
138
+ `JSESSIONID` in `ajax:<19 digits>` form; the client generates a fresh value when
139
+ the bootstrap does not supply one.
140
+ 2. `POST /uas/authenticate` submits a URL-encoded Google token with the recorded
141
+ `soogleLoginRequestType=ONE_TAP_AUTO_LOGIN` spelling, native captcha capability,
142
+ CSRF cookie and fresh bootstrap cookies.
143
+ 3. A `login_result=PASS` response sets `li_at` and rotates `JSESSIONID`.
144
+ 4. `GET /mob/sso/you` verifies the session and reads the member identity.
145
+
146
+ The login method returns the verified `Member`. It neither stores the Google token
147
+ nor retries the exchange. A challenge URL is available through
148
+ `ChallengeRequired.challenge_url` but is not followed or included in its message.
149
+ Complete challenges in the official app. Email/password login, Google token
150
+ acquisition, MFA continuation and remote logout are not implemented.
151
+
152
+ ## How requests are made
153
+
154
+ The app draws feed, post, comment, search and job screens with server-driven protobuf. The
155
+ same APK still ships the classic Voyager operations, and Voyager answers plain JSON on the
156
+ same session, so every feature calls those: persisted GraphQL queries and mutations
157
+ (`/voyager/api/graphql`) and rest.li routes (`/voyager/api/<route>`), with variables encoded
158
+ exactly as the app encodes them ([transport/voyager.py](https://github.com/Anandesh-Sharma/async_linkedinlib/blob/main/src/async_linkedinlib/transport/voyager.py),
159
+ [transport/restli.py](https://github.com/Anandesh-Sharma/async_linkedinlib/blob/main/src/async_linkedinlib/transport/restli.py)). Query IDs, variable names,
160
+ enums and bodies come from the app's code; each was then checked against the live service.
161
+
162
+ Every resource shares the client's one session and connection pool. Lists return a
163
+ `Page` with `items`, `total` (when LinkedIn gives one) and `next`, a `Cursor` that continues
164
+ only the operation, query and session that made it; pass it back as `cursor=`. Paging
165
+ follows the app's pager (next start = `paging.start + paging.count`, until `paging.total`
166
+ or an empty page), and nothing fetches the next page by itself. `count` and `cursor` are
167
+ keyword arguments everywhere. Writes are sent once and never retried. URNs and IDs are
168
+ strings. Every public type is importable from `async_linkedinlib`. Models keep the provider
169
+ object in `raw` (left out of repr).
170
+
171
+ ```python
172
+ from async_linkedinlib import FeedSort, LinkedIn, ReactionType
173
+
174
+ async with LinkedIn.from_session_file(path) as linkedin:
175
+ page = await linkedin.feed.home(count=10, sort=FeedSort.RECENT)
176
+ post = page.items[0]
177
+ await linkedin.feed.react(post.urn, ReactionType.CELEBRATE)
178
+ comments = await linkedin.comments.list(post.urn)
179
+ more = await linkedin.feed.home(count=10, sort=FeedSort.RECENT, cursor=page.next)
180
+ ```
181
+
182
+ ### Feed (`linkedin.feed`)
183
+
184
+ | Operation | Method |
185
+ | --- | --- |
186
+ | Main feed, top or recent | `home(count, sort, cursor)` |
187
+ | One post by activity, ugcPost or share URN | `get(urn)` |
188
+ | A member's posts, or posts they commented on or reacted to | `profile(profile_urn, activity, count, cursor)` |
189
+ | A company page's posts (all, images, videos, documents, articles) | `company(company_urn, content, count, cursor)` |
190
+ | Who reacted, by type and order | `reactions(urn, reaction, sort, count, cursor)` |
191
+ | React, change, remove | `react(urn, reaction)`, `change_reaction`, `unreact` |
192
+ | Save, unsave, saved posts | `save(activity_urn)`, `unsave`, `saved(count, cursor)` |
193
+ | Hide a post and undo | `hide(post.update_urn)`, `undo_hide` |
194
+ | Vote on a poll and withdraw | `vote(option.urn)`, `unvote` |
195
+
196
+ `Post.urn` is the post's social thread (reactions and comments use it); `activity_urn` is the
197
+ feed activity, which differs for "X likes this" updates. Recommendation modules are not posts,
198
+ so `items` can be shorter than `count` (`page.raw` keeps them). `save` takes the post's own
199
+ activity URN. Profile feeds give no total. No read shows whether a post is hidden.
200
+
201
+ ### Posts (`linkedin.posts`)
202
+
203
+ ```python
204
+ from async_linkedinlib import CommentScope, Image, MediaKind, Mention, Visibility
205
+
206
+ text = "Hello from Ada Lovelace"
207
+ shared = await linkedin.posts.create(
208
+ text, visibility=Visibility.CONNECTIONS_ONLY, comments=CommentScope.ANYONE,
209
+ mentions=[Mention("urn:li:fsd_profile:<id>", text.index("Ada"), 12)],
210
+ )
211
+ photo = await linkedin.posts.media.upload("photo.png", MediaKind.IMAGE)
212
+ await linkedin.posts.create("With a photo", media=Image(photo))
213
+ await linkedin.posts.edit(shared.share_urn, "Hello again")
214
+ await linkedin.posts.delete(shared.share_urn)
215
+ ```
216
+
217
+ | Operation | Method | Verified |
218
+ | --- | --- | --- |
219
+ | Text post with visibility, comment control, member/company mentions | `create` | live |
220
+ | Schedule, list, reschedule, cancel | `create(publish_at=)`, `scheduled`, `reschedule`, `cancel_scheduled` | live |
221
+ | Edit text and mentions; delete | `edit`, `delete` | live |
222
+ | Who may comment on a published post | `set_comment_scope` | live |
223
+ | Instant repost and undo | `repost`, `undo_repost` | live |
224
+ | Upload image, video, document; processing status; link preview | `media.upload`, `media.status`, `media.link_preview` | live |
225
+ | Image, gallery, video, document, poll and link posts; repost with thoughts | `create(media=...)`, `repost_with_thoughts` | offline (see limits) |
226
+
227
+ Mention offsets count Unicode code points; the server silently drops a wrong offset.
228
+ Hashtags are plain `#text`. After publishing only text and mentions can be edited. A gallery
229
+ needs `session.member_id` (call `account.me()` once). Galleries and polls are created before
230
+ the post and remain if the post is refused. Uploads must come back as single-request
231
+ uploads. Uploaded media cannot be deleted. Unverified accounts get a small daily posting
232
+ limit, raised as `OperationRejected`; the test account hit it before the media post kinds
233
+ could be published, so those shapes are tested offline only.
234
+
235
+ ### Comments (`linkedin.comments`)
236
+
237
+ | Operation | Method |
238
+ | --- | --- |
239
+ | Top-level comments, relevant or recent, with reply previews | `list(post_urn, sort, count, cursor)` |
240
+ | Replies, oldest first; one comment | `replies(comment_urn, count, cursor)`, `get(comment_urn)` |
241
+ | Comment, reply (with member mentions) | `create(post_urn, text, mentions)`, `reply(comment_urn, text, mentions)` |
242
+ | Edit, delete own comment | `edit(comment_urn, text, mentions)`, `delete(comment_urn)` |
243
+ | React, change, remove; who reacted (`Reactor`s, as for posts) | `react`, `change_reaction`, `unreact`, `reactions(comment_urn, reaction, sort, count, cursor)` |
244
+ | Hide another member's comment on your post | `hide(comment)`, `unhide(comment)` (offline only) |
245
+
246
+ Comment URNs may be `urn:li:comment:(activity:<post>,<id>)` or
247
+ `urn:li:fsd_comment:(<id>,urn:li:activity:<post>)`. A reply to a reply attaches to its
248
+ top-level comment. Text only: no image, GIF or company mentions; no pinning.
249
+
250
+ ### Search (`linkedin.search`)
251
+
252
+ ```python
253
+ from async_linkedinlib import JobFilters, PeopleFilters, TypeaheadKind
254
+
255
+ berlin = (await linkedin.search.typeahead(TypeaheadKind.GEO, "Berlin", count=1))[0]
256
+ people = await linkedin.search.people("python developer", PeopleFilters(locations=[berlin.id]))
257
+ jobs = await linkedin.search.jobs("python", JobFilters(location=berlin.id, easy_apply=True))
258
+ job = await linkedin.jobs.get(jobs.items[0].id)
259
+ ```
260
+
261
+ | Operation | Returns |
262
+ | --- | --- |
263
+ | `all(keywords)` | `Page[ResultGroup]`: People, Posts, Jobs, Companies blocks |
264
+ | `people`, `services` (with `PeopleFilters`, `ServiceFilters`) | `Page[PersonResult]` |
265
+ | `posts` (with `PostFilters`) | `Page[Post]` |
266
+ | `companies`, `groups`, `schools`, `events`, `courses`, `products` | `Page[SearchResult]` |
267
+ | `jobs` (with `JobFilters`) | `Page[JobResult]`; `id` goes to `linkedin.jobs` |
268
+ | `filters(vertical, keywords)`, `job_filters(keywords)` | available filters and suggested values |
269
+ | `typeahead(kind, text)`, `suggest(text)` | `Suggestion`s; `id` is the filter value |
270
+
271
+ Filters are typed dataclasses per vertical; ID fields take IDs or simple URNs, and `extra`
272
+ passes any other key that `filters()` lists. People filters cover connection degree,
273
+ locations, current and past companies, schools, industries, profile languages, connections
274
+ of, followers of, service categories, open to volunteering and name/title/company/school
275
+ keywords. Job filters cover location and distance, date posted, Easy Apply, under 10
276
+ applicants, in my network, verified, workplace, experience, job type, company, place,
277
+ salary, industry, function, title and commitments; `workplace` takes the same
278
+ `WorkplaceType` that `Job` reports. Searches never add to search history.
279
+ People, company, group and service results stop at 1,000; `total` is an estimate.
280
+
281
+ ### Jobs and Easy Apply (`linkedin.jobs`)
282
+
283
+ ```python
284
+ from pathlib import Path
285
+
286
+ form = await linkedin.jobs.application(job.id) # pages, questions, offered resumes
287
+ for question in form.questions:
288
+ print(question.label, question.kind, [o.label for o in question.options])
289
+ resume = await linkedin.jobs.upload_file(
290
+ Path("resume.pdf"), max_size=form.file("Resume").max_size # refuse before storing
291
+ )
292
+ submission = linkedin.jobs.submission(
293
+ form, {"Mobile phone number": "5550100"}, files={"Resume": resume}, job=job
294
+ )
295
+ for label, values in submission.answers: # checked locally; nothing sent yet
296
+ print(label, values)
297
+ status = await linkedin.jobs.submit(submission) # applies; sent once
298
+ ```
299
+
300
+ | Operation | Method | Verified |
301
+ | --- | --- | --- |
302
+ | Job details (title, company, place, workplace, type, description, applicants, Easy Apply, state) | `get(job)` | live |
303
+ | Save, unsave; My jobs lists (saved, in progress, applied, archived) | `save`, `unsave`, `tracked(status, count, cursor)` | live |
304
+ | Easy Apply form with typed questions and file slots | `application(job)` | live |
305
+ | Store a .pdf/.doc/.docx file | `upload_file(file)` | live |
306
+ | Check answers and build the app's submission | `submission(form, answers, files)` | live, not sent |
307
+ | Send the application | `submit(submission)` | offline only |
308
+
309
+ Answers are checked against question kinds, options and limits; unanswered questions keep
310
+ LinkedIn's prefill. Date, location and typeahead questions keep their prefill, conditional
311
+ rules are not evaluated, and forms that need a data-consent step or have repeatable sections
312
+ are refused. Following the company is off unless `follow_company=True`. Uploaded files cannot
313
+ be deleted, and the app has no withdraw. `submit` has not been sent to the live service:
314
+ the one real application is left to the account owner.
315
+
316
+ ## Failures and transport
317
+
318
+ Expected failures inherit from `LinkedInError`. Authentication rejection, required
319
+ verification, HTTP failures, network failures, rate limits, unexpected protocol responses
320
+ and refused operations have separate exception classes. `OperationRejected.reasons` holds
321
+ LinkedIn's GraphQL messages (for example a validation error or the daily posting limit);
322
+ `HTTPError.reason` holds rest.li's one-line client-error message when there is one.
323
+ `UnconfirmedWrite` (a `ProtocolError`) means LinkedIn accepted a write but its reply could
324
+ not be read: the change may exist, so read the state back before sending it again.
325
+ Errors do not attach raw HTTP responses or include credentials in messages. A 403 is conservatively reported as requiring
326
+ review in the app; it does not prove which verification or restriction occurred.
327
+
328
+ HTTP 401 clears the local `li_at` credential. HTTP 429 records the `Retry-After`
329
+ cooldown and blocks further requests locally until it expires. Without a valid
330
+ server hint, the local fallback is 60 seconds. Cooldowns persist with the session.
331
+ No network operation is automatically retried, and redirects are not followed.
332
+ Requests are serialized per client so cookie rotation is applied in order.
333
+
334
+ `ClientOptions(timeout=30, max_connections=1)` configures the HTTPX pool.
335
+ TLS verification stays enabled and ambient proxy environment variables are ignored.
336
+ The SDK contacts only `https://www.linkedin.com` (media and resume uploads go to its
337
+ `/dms-uploads/` and `/ambry/` paths); it needs neither an emulator nor the capture proxy at
338
+ runtime. The low-level HTTPX transport can be injected
339
+ for tests. Do not enable HTTP wire logging when using real credentials.
340
+
341
+ ## Files and verification
342
+
343
+ ```text
344
+ src/async_linkedinlib/
345
+ client.py public client, lifecycle and resource construction
346
+ cli.py interactive login, import-session and me commands
347
+ exceptions.py public, credential-safe failures
348
+ auth/service.py Google login orchestration and fresh bootstrap
349
+ config/ connection options, versioned Android app metadata
350
+ session/ identity, scoped cookies, cooldowns, atomic persistence, lease
351
+ transport/
352
+ http.py shared HTTPX pool, headers, cookie rotation and errors
353
+ voyager.py GraphQL queries/mutations and rest.li calls, error decoding
354
+ restli.py the app's rest.li variable encoding
355
+ resources/
356
+ account.py current-member read and verification
357
+ paging.py the app's pager: collections, pages and session-bound cursors
358
+ threads.py post and comment thread URNs, social-detail URNs
359
+ mentions.py member/company mentions in post and comment text
360
+ reactions.py react, change, remove and list reactions on posts and comments
361
+ feed/ home, single post, profile/company feeds, saves, hides, polls
362
+ posts/ create, edit, delete, schedule, repost; media uploads
363
+ comments/ list, replies, create, reply, edit, delete, reactions
364
+ search/ verticals, job search, filters, typeahead
365
+ jobs/ job details, saved jobs, tracker, Easy Apply
366
+ models/ typed results and inputs per feature; content.py holds Post,
367
+ Author and ReactionType; pagination.py holds Page and Cursor
368
+ tests/ one folder per area, synthetic data, httpx.MockTransport;
369
+ restli_decode.py reads what a test request sent
370
+ examples/ account, feed, posts, comments, search, jobs
371
+ AGENTS.md library architecture and contribution rules
372
+ ```
373
+
374
+ The call path is `LinkedIn resource → Voyager → Transport → LinkedIn HTTP endpoint`.
375
+ All resources use the client's one session and transport. Models contain data;
376
+ resources own feature behavior; the client assembles them and manages lifetime.
377
+ See [AGENTS.md](https://github.com/Anandesh-Sharma/async_linkedinlib/blob/main/AGENTS.md) before adding functionality.
378
+
379
+ ### Feature status
380
+
381
+ | Client group | Status |
382
+ | --- | --- |
383
+ | `auth` | Google-token exchange; tested locally, fresh exchange not live-verified |
384
+ | `account` | live verified |
385
+ | `feed`, `comments`, `search` | live verified, except comment hide/unhide (offline) |
386
+ | `posts` | live verified, except posts with media, polls, links and repost with thoughts (offline; daily posting limit) |
387
+ | `jobs` | live verified up to Easy Apply submit; `submit` offline only |
388
+ | `connections`, `messages` | not built |
389
+
390
+ ```sh
391
+ uv run pytest -q
392
+ uv run ruff check src tests examples
393
+ uv run ruff format --check src tests examples
394
+ uv run mypy src
395
+ uv build
396
+ ```
397
+
398
+ Evidence comes from a private Android research lab (not distributed here): the
399
+ `login-20261031-055813` capture for sign-in; recordings of every feature flow in the app;
400
+ and the decompiled 4.1.1267 APK, whose Voyager GraphQL clients, routes and request builders
401
+ give each operation's query ID, variables and body. Each module names its query IDs.
402
+ The login capture contains a successful Google exchange and account verification and no password login. Raw captures,
403
+ Google tokens and real account cookies remain outside this package. The four
404
+ unavailable response-body exports were streaming/unrelated records; the login and
405
+ account-verification bodies were exported successfully.
406
+
407
+ Postman CLI 1.45.0 lacks the simulation command required by the installed skill.
408
+ Verification uses synthetic protocol tests and a real loopback HTTP server instead.
409
+
410
+ Verified on 2026-10-31:
411
+
412
+ - The official Android app completed Google sign-in and returned `li_at` and
413
+ `JSESSIONID` in the captured success response.
414
+ - A fresh Python bootstrap to `/mob/tracking` returned HTTP 200 and server cookies.
415
+ - The SDK imported only the captured `li_at`/`JSESSIONID` credential pair and called
416
+ `account.me()` successfully; the member matched the captured account response.
417
+ - Reopening the saved session and reading the account again succeeded without login.
418
+ - Synthetic protocol and real loopback HTTP tests cover the Google exchange, cookie
419
+ rotation/deletion, session persistence, malformed responses, challenge reporting,
420
+ rejected sessions, redirects, network failures and persisted rate limits.
421
+
422
+ Feature verification on 2026-10-31 used the test account through the SDK itself: each
423
+ operation in the tables above marked live was run once, every write was undone or deleted,
424
+ and the cleanup was confirmed with a read. Five test media uploads, one gallery container,
425
+ one poll container and one Easy Apply resume file remain stored on LinkedIn because the app
426
+ offers no way to delete them; none is attached to a post, profile or application.
427
+
428
+ A fresh programmatic Google ID-token exchange has not been live-verified. The
429
+ captured token was not replayed.
430
+
431
+ ## Development
432
+
433
+ The normal suite uses synthetic data and local HTTP servers. It does not need a
434
+ LinkedIn account. Run these checks before opening a pull request:
435
+
436
+ ```sh
437
+ uv sync --locked
438
+ uv run pytest -q
439
+ uv run ruff check src tests examples
440
+ uv run ruff format --check src tests examples
441
+ uv run mypy src
442
+ uv build
443
+ ```
444
+
445
+ CI runs these checks on Linux and macOS with Python 3.11 and 3.13. Historical live
446
+ verification is documented above; CI does not verify current private API behavior.
447
+
448
+ Keep changes focused, include a synthetic regression test for behavior changes,
449
+ and update this README when a public method changes. Open a
450
+ [bug report or feature request](https://github.com/Anandesh-Sharma/async_linkedinlib/issues/new/choose) with a minimal example,
451
+ Python/package versions, and a redacted error. Keep cookies, passwords, tokens,
452
+ session files, raw captures and real account data out of issues and pull requests.
453
+
454
+ ## Related library
455
+
456
+ [async_instalib](https://github.com/Anandesh-Sharma/async_instalib) is the companion async Python client for Instagram. Each package is independent.
457
+
458
+ ## License
459
+
460
+ [MIT](https://github.com/Anandesh-Sharma/async_linkedinlib/blob/main/LICENSE) © 2026 Anandesh Sharma.
461
+
462
+ ## Releases
463
+
464
+ Publishing a GitHub release runs the complete CI matrix, builds a wheel and source
465
+ archive, validates package metadata, and uploads both to PyPI with
466
+ [trusted publishing](https://docs.pypi.org/trusted-publishers/). No stored PyPI token
467
+ is needed. The publisher is bound to `Anandesh-Sharma/async_linkedinlib`, `release.yml`, and
468
+ the GitHub environment `pypi`.
469
+
470
+ 1. Update `project.version` in `pyproject.toml`, run `uv lock`, and commit to `main`.
471
+ 2. Publish a GitHub release with the matching tag `v<version>` from that commit.
472
+ 3. Check the [publishing workflow](https://github.com/Anandesh-Sharma/async_linkedinlib/actions/workflows/release.yml) and
473
+ [PyPI project](https://pypi.org/project/async_linkedinlib/).
474
+
475
+ Tags must match the package version exactly and point to a commit on `main`.
476
+ A draft release does not publish. A published prerelease does; use a matching
477
+ PEP 440 prerelease version such as `0.3.0rc1`. Re-running a release skips files
478
+ already present only after checking their SHA-256 hashes match the build.
@@ -0,0 +1,64 @@
1
+ async_linkedinlib/__init__.py,sha256=lz7R5w5EE1sbk7SCyYxUISunqYN86omXcTVJrqyspAk,7688
2
+ async_linkedinlib/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30
3
+ async_linkedinlib/cli.py,sha256=BTXdjEtGGpS07fUXMfLOKnCamH0nhmhQ3hz45fu-MW4,1511
4
+ async_linkedinlib/client.py,sha256=l24__hsrTDKsCZAHdw7w9ZlkI3MGQE0zkAAmij9or-0,2955
5
+ async_linkedinlib/exceptions.py,sha256=vSavA34U8wnQgdFeNlPFp-2byodrqm2O3d2U_DbZzXc,2527
6
+ async_linkedinlib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ async_linkedinlib/auth/__init__.py,sha256=phRgYDPZM7cheUmlr7d6zVTpoctmcL65dFk6RJ8u3Jk,121
8
+ async_linkedinlib/auth/service.py,sha256=-qPIMAz5r4YGFQp9Tvdii-fw3Bb9v8o7UKaDjQTMQyc,3348
9
+ async_linkedinlib/config/__init__.py,sha256=2cTrGAqF6EvldJ5zDMNfeCgGpz2habd3zTAoPeBrJsI,202
10
+ async_linkedinlib/config/options.py,sha256=sFaqdNY8ZbQRjhcGG4Bb0KvBgvz3VqjaFif9HN_3FvE,438
11
+ async_linkedinlib/config/profile.py,sha256=V0sQhpJshZDw42V_y_IYWUA2w7tRNyXYge9nLdQeGoU,1982
12
+ async_linkedinlib/models/__init__.py,sha256=WTPExSLDnOg_AULF71TBNgHjhOZiXZGoyJHbQPIDlZ8,6378
13
+ async_linkedinlib/models/_json.py,sha256=XnWBVYxD6pPdEE5R8qZVvd_QpDmnyC9UJZcb1V-d338,2113
14
+ async_linkedinlib/models/comments.py,sha256=lmHrUyfezsmE46ZuaTVJVV7WHakEC0iJP_ePMkn_4h0,5662
15
+ async_linkedinlib/models/content.py,sha256=Dp_NjVvAdxykTY5ZQTp3-1IYv7bJnEm-4HswoDbXJpw,7922
16
+ async_linkedinlib/models/feed.py,sha256=9BvYw2C7HbyAJRQZ1QBA3PZ1VcrI7oHNq89BXu2Bm68,4153
17
+ async_linkedinlib/models/jobs.py,sha256=jh9YQ4NnBlema4r63CsFhSt_KZkIDF5S3BBSxVtVStU,8460
18
+ async_linkedinlib/models/jobs_apply.py,sha256=A9zeehFmDNPBnb95uJD1coaZgJw5DRcSr0pq-jr9sLY,16188
19
+ async_linkedinlib/models/member.py,sha256=kf5j73ZIFWHM2WHXLPm_dJV2mGxidTawsifhxcjtSeI,312
20
+ async_linkedinlib/models/pagination.py,sha256=HvHo3M5OUrzQbkd7-Av8TdR6jwUrnmk7XA69cZm4Ttg,1905
21
+ async_linkedinlib/models/posts.py,sha256=t6OZzPlxZKcVeYcXeD8i0Ub9Y4atQm-4Dx25fe-DISA,4316
22
+ async_linkedinlib/models/posts_results.py,sha256=VzZ9QF9LG1b8WZ_dgh0mjotcAPdXEg_0grufTO8ItRc,4200
23
+ async_linkedinlib/models/search.py,sha256=Fu9Hlnk9NOpIG80cCAzqdfL8hOn6i0p_A9z-Gm9sjf0,10806
24
+ async_linkedinlib/models/search_filters.py,sha256=TZQu7IjsqlYxoMMBENcmB3VTdplOJH9py4KWmU-NZpE,12146
25
+ async_linkedinlib/models/search_jobs.py,sha256=gpBLxJF5awuY1S5ZDn5F8eT3-Lhll6qDwVRdjyy3m5E,7650
26
+ async_linkedinlib/resources/__init__.py,sha256=DIgzbR5JlP7qYVLacO6HC4Gvf7GUMaM1x5JEpan6V9U,710
27
+ async_linkedinlib/resources/account.py,sha256=fDLuue5Q9M4KQBkMXDXL8zP1h0qevrs7zPBQMRvWcTo,1666
28
+ async_linkedinlib/resources/mentions.py,sha256=0Hj7T4nJ0_0DuMKcS0btCb7L4BEoH1mIBtyaP1F4EVs,2346
29
+ async_linkedinlib/resources/paging.py,sha256=tz1uRyXi0m0QKLpKUj6v_y9_pz1TTOHcE2i5s0G0p7M,3162
30
+ async_linkedinlib/resources/reactions.py,sha256=4Zn8UCAb4GyGneb2MxP80L09YHNvFXexG62tykWoUkc,3431
31
+ async_linkedinlib/resources/threads.py,sha256=WSb2wmH3blYqYDkXtarY_751qbAkyLbAULGjFMB1rwI,1194
32
+ async_linkedinlib/resources/comments/__init__.py,sha256=IIKHMhJUbB_RwPyAcFk5eNvwfNhxdTpveJ2BeuRXL8M,129
33
+ async_linkedinlib/resources/comments/resource.py,sha256=PKNxVAEf41bvhmleLjt2Tngop0mnpn9jufAqzX07VP0,9439
34
+ async_linkedinlib/resources/comments/urns.py,sha256=jxTvLMhtjjQZmjVmVSWYidF_s0NtuolKLKB0jKtBXj4,1524
35
+ async_linkedinlib/resources/feed/__init__.py,sha256=zSEwt1Wpbzb-g5DmC1zdlv5wAhsp5NqEVUQ9BO_v0z4,118
36
+ async_linkedinlib/resources/feed/queries.py,sha256=SLW0RimtQGiccW6LvY4HAHMHCJ-W5EXltWgz9MTmxPQ,2358
37
+ async_linkedinlib/resources/feed/resource.py,sha256=i8eNK23WBuECmEvVFZoL0il-UxhKlBKM_LjLYOYIlL0,9032
38
+ async_linkedinlib/resources/feed/saved.py,sha256=SzvS7y5MT74jOcuNdZjb9nCC8i3c4t_T1Yd-ieHZpyE,1713
39
+ async_linkedinlib/resources/feed/urns.py,sha256=TrzSe5Ld8jgfrDjqcFIjqsWneIreSkxe1EIlzmaJjo0,2581
40
+ async_linkedinlib/resources/jobs/__init__.py,sha256=AuVTTooN3gSRpUaQBVSKm0Wyv9w-OLwnF7Tsct6HOBA,133
41
+ async_linkedinlib/resources/jobs/answers.py,sha256=ladpOypMTsY8LmsmyhRZewvrYDMGIvSkgcUMidEpUYo,10859
42
+ async_linkedinlib/resources/jobs/resource.py,sha256=pzaKQRJj13gF8n5DoTttMxw5YgLdjO7BSGIcfFK_R5Q,8750
43
+ async_linkedinlib/resources/jobs/upload.py,sha256=M7yYWPJvHTYfUzyjTeumFCkTdNcMX1jcNrlbZZtuB64,3145
44
+ async_linkedinlib/resources/posts/__init__.py,sha256=GPb5zfQU7Adp11r94PDzH74ezLIKGqvZksZVw_zd42I,237
45
+ async_linkedinlib/resources/posts/media.py,sha256=-v3GJqKkD_UiO6uJSDm6hUpqGx3dFPjA6eOMYyTV0Bg,7220
46
+ async_linkedinlib/resources/posts/payloads.py,sha256=ZNiEowsWx9Go0BiY4pJ3CqSfOSPGvObGCxRh7R6apCc,3179
47
+ async_linkedinlib/resources/posts/resource.py,sha256=TdctnLIS_4YgEQ9Pb8rPK2K8pgumRAah04tpDHHKo-E,10870
48
+ async_linkedinlib/resources/search/__init__.py,sha256=SALXeXqWrOvuI5A3aIjB_u5EFuRFs6uCpGQkqQePGq4,148
49
+ async_linkedinlib/resources/search/queries.py,sha256=6dn4_nIoGRd48ViR7naroZxVeGrDoKdETpJM0zCQo9Y,3209
50
+ async_linkedinlib/resources/search/results.py,sha256=k-EpFdrg0jZTyHEYDhVRAinU6MCsFUJAUxwYZY1FjBg,2849
51
+ async_linkedinlib/resources/search/search.py,sha256=Mdme6UcBjNF2Cz4x-6QzhyyemnWeR8bS6ZDAvFM9iPU,11163
52
+ async_linkedinlib/session/__init__.py,sha256=ZIDDsV1GKDOvTIlOxkZLpBRHcddi43mUV-aarQan5ZI,123
53
+ async_linkedinlib/session/cookies.py,sha256=lMDPwwBh10o9cPxy7kqWDMDns6EQobkzO2jBB82AhsM,1642
54
+ async_linkedinlib/session/lease.py,sha256=IV4q32FtdA2c8JOWvRD6jVveGDOOUOG9ZiVrcMEbsT0,927
55
+ async_linkedinlib/session/state.py,sha256=7EJ1tjnrYOzOOCUfa0K0hXQDeHtIZhQP7IotAYhSOA4,5043
56
+ async_linkedinlib/transport/__init__.py,sha256=AiNNLA66jHf6aUFHs5xTJ-6MsBnYNZaPQMLnXcacc8M,228
57
+ async_linkedinlib/transport/http.py,sha256=em4ZSxlv1yzMDP6GcxucKg2k7WlspGWfy-etf0v_Kow,6960
58
+ async_linkedinlib/transport/restli.py,sha256=vmf681z6W-o8xWVBEw28jdIwsuowFww9XuqlabABQ78,1994
59
+ async_linkedinlib/transport/voyager.py,sha256=HkR0dnqKLupBy0_BGmjYdH91PWXX-Zex_rFhJ5B12HM,6098
60
+ async_linkedinlib-0.2.0.dist-info/METADATA,sha256=y1OvhdxTtq6rwkAma7jhl7R219osQK5xRT_aHhCildM,25661
61
+ async_linkedinlib-0.2.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
62
+ async_linkedinlib-0.2.0.dist-info/entry_points.txt,sha256=wY1cHAT0kY-iH3lx8kIkZTcv40lgRNa1hn4bq4bfjQ8,65
63
+ async_linkedinlib-0.2.0.dist-info/licenses/LICENSE,sha256=61MZy4hjyfZS9m3-44VvnGBI2mi4iBM7KDrvG5ATKwU,1072
64
+ async_linkedinlib-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ async-linkedinlib = async_linkedinlib.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anandesh Sharma
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 all
13
+ 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 THE
21
+ SOFTWARE.