easydata-api 1.0.1__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EasyData
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.
@@ -0,0 +1,185 @@
1
+ Metadata-Version: 2.4
2
+ Name: easydata-api
3
+ Version: 1.0.1
4
+ Summary: Official Python client for the EasyData LinkedIn enrichment API
5
+ Author-email: EasyData Development <dev@easydata.win>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://easydata.win
8
+ Project-URL: Documentation, https://easydata.win/docs/api
9
+ Project-URL: Reference, https://easydata.win/openapi.yaml
10
+ Keywords: linkedin,enrichment,data,api,scraping
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Provides-Extra: ed25519
19
+ Requires-Dist: cryptography>=42; extra == "ed25519"
20
+ Dynamic: license-file
21
+
22
+ # EasyData for Python
23
+
24
+ The official client for the [EasyData](https://easydata.win) LinkedIn
25
+ enrichment API.
26
+
27
+ ```bash
28
+ pip install easydata-api
29
+ ```
30
+
31
+ Zero dependencies. The client is `urllib` and the webhook HMAC is
32
+ `hmac`/`hashlib`, both standard library - an enrichment script should not drag a
33
+ dependency tree in behind it. Only the asymmetric webhook scheme needs an extra
34
+ (`pip install easydata-api[ed25519]`), and most receivers never use it.
35
+
36
+ ## One record
37
+
38
+ ```python
39
+ from easydata_api import EasyData
40
+
41
+ ed = EasyData() # reads EASYDATA_API_KEY
42
+
43
+ r = ed.profiles_enrich.sync("https://linkedin.com/in/satyanadella")
44
+ print(r.result.data["full_name"])
45
+ ```
46
+
47
+ `.sync()` takes **one** target and answers with **one** record, in the response.
48
+ It costs double, it accepts no webhooks and no `enrich`, and for a paged
49
+ operation it returns one upstream page. Those bounds are refusals, not quiet
50
+ downgrades.
51
+
52
+ If the deadline expires first you get `complete=False` and a real `batch_id`,
53
+ never a 504 - so you keep the handle to results you may already have been
54
+ charged for.
55
+
56
+ ## A batch
57
+
58
+ Everything is a batch, including a batch of one. There is no ceiling to discover
59
+ between one target and fifty thousand.
60
+
61
+ ```python
62
+ batch = ed.profiles_enrich(urls, external_id="crm-sync", find_emails=True)
63
+
64
+ for entry in ed.results(batch.batch_id):
65
+ if entry.ok:
66
+ save(entry.data)
67
+ else:
68
+ log(entry.input, entry.error["type"]) # and credits_used is 0
69
+ ```
70
+
71
+ `results()` is a generator over the cursor. Results **stream**: it yields rows
72
+ while the batch is still processing, so a long batch starts producing
73
+ immediately rather than after it finishes. It holds the cursor open until the
74
+ batch reaches a terminal state, sleeping for the server's own poll interval
75
+ between empty reads.
76
+
77
+ ```python
78
+ list(ed.results(batch_id, wait=False)) # what is readable right now
79
+ ed.wait(batch_id, timeout=600) # the counters, not the rows
80
+ ed.profiles_enrich.collect(urls) # submit and drain, in one call
81
+ ```
82
+
83
+ ## Retries and double-billing
84
+
85
+ Retries are on by default and cover exactly what is safe to repeat: `429`, `5xx`
86
+ and a transport failure. A `4xx` is raised immediately, because repeating a
87
+ refusal only spends the rate budget on a certain no.
88
+
89
+ Every submission carries an `Idempotency-Key` that the client mints, so the
90
+ retry is free: a retried submit resolves to the batch the first attempt created
91
+ rather than creating a second one and charging for it. Pass your own
92
+ `idempotency_key=` if your caller's retry needs the same guarantee.
93
+
94
+ ```python
95
+ ed = EasyData(max_retries=5, timeout=180)
96
+ ed.rate_limits.remaining # read off the last response, including a 429
97
+ ```
98
+
99
+ `rate_limits` fields are `None` when the deployment publishes no ceiling. That
100
+ is what "no limit" looks like on the wire: an absent header, never a zero.
101
+
102
+ ## Errors
103
+
104
+ ```python
105
+ from easydata_api import QuotaExhausted, RateLimited, InvalidRequest, EasyDataError
106
+
107
+ try:
108
+ ed.profiles_enrich(urls)
109
+ except InvalidRequest as e:
110
+ print(e.field, e.request_id) # quote request_id at support
111
+ except QuotaExhausted:
112
+ ... # waiting for the month is the fix
113
+ except EasyDataError as e:
114
+ print(e.type, e.status)
115
+ ```
116
+
117
+ One class per `error.type`, all under `EasyDataError`. An `error.type` this
118
+ version has never heard of becomes a plain `EasyDataError` carrying that string
119
+ rather than a crash.
120
+
121
+ `EmailUnverified` is a `403` and is **not** a spent allowance: the default
122
+ allowance is gated on somebody having confirmed their email address, and
123
+ clicking the link fixes it.
124
+
125
+ ## Webhooks
126
+
127
+ ```python
128
+ from easydata_api import verify, VerificationError
129
+
130
+ @app.post("/webhooks/easydata")
131
+ def hook():
132
+ try:
133
+ d = verify(SECRET, request.headers, request.get_data())
134
+ except VerificationError:
135
+ return "", 400
136
+
137
+ if seen(d.id): # stable across every retry of the same delivery
138
+ return "", 200
139
+
140
+ if d.event == "batch.result":
141
+ handle(d.data["result"])
142
+ elif d.event == "batch.completed":
143
+ finish(d.data["batch_id"])
144
+ return "", 200
145
+ ```
146
+
147
+ Four events: `batch.started`, `batch.result` (one per row), `batch.completed`
148
+ and `batch.failed`. The fields are in `d.data`, one level down.
149
+
150
+ Two things that are silent when you get them wrong:
151
+
152
+ - **Verify against the raw body.** Re-serialising parsed JSON changes key order
153
+ and whitespace, and the signature will not match a body you rebuilt.
154
+ - **Never verify against `X-EasyData-Timestamp`.** It sits outside both signed
155
+ messages, so a replayed delivery can set it to anything. The `t` inside the
156
+ signature header is the only copy that cannot be edited.
157
+
158
+ For the asymmetric scheme, `webhook_id` is required - one key signs for every
159
+ customer, so checking `wid` is what stops another customer's genuine delivery
160
+ verifying against your receiver.
161
+
162
+ ```python
163
+ from easydata_api import verify_ed25519
164
+ d = verify_ed25519(PUBLIC_KEY, request.headers, body, webhook_id=MY_ENDPOINT_ID)
165
+ ```
166
+
167
+ ## Everything else
168
+
169
+ ```python
170
+ ed.batch(batch_id) # one batch's state
171
+ ed.batches(status="completed", external_id="crm") # your batches
172
+ ed.cancel(batch_id) # delivered rows stay charged
173
+ ed.account() # allowance, ceilings, secret
174
+ ed.usage(from_="2026-09-01", to="2026-09-30") # spend by day and operation
175
+ ```
176
+
177
+ Operations are attributes: `profiles_enrich`, `profiles_activity`,
178
+ `profiles_posts`, `profiles_comments`, `profiles_reactions`, `companies_enrich`,
179
+ `posts_enrich`, `sales_search_people`, `sales_search_companies`.
180
+
181
+ ## Reference
182
+
183
+ - [API reference](https://easydata.win/docs/api)
184
+ - [The batch model](https://easydata.win/docs/batches) - what a credit is, and why
185
+ - [OpenAPI 3.1](https://easydata.win/openapi.yaml)
@@ -0,0 +1,164 @@
1
+ # EasyData for Python
2
+
3
+ The official client for the [EasyData](https://easydata.win) LinkedIn
4
+ enrichment API.
5
+
6
+ ```bash
7
+ pip install easydata-api
8
+ ```
9
+
10
+ Zero dependencies. The client is `urllib` and the webhook HMAC is
11
+ `hmac`/`hashlib`, both standard library - an enrichment script should not drag a
12
+ dependency tree in behind it. Only the asymmetric webhook scheme needs an extra
13
+ (`pip install easydata-api[ed25519]`), and most receivers never use it.
14
+
15
+ ## One record
16
+
17
+ ```python
18
+ from easydata_api import EasyData
19
+
20
+ ed = EasyData() # reads EASYDATA_API_KEY
21
+
22
+ r = ed.profiles_enrich.sync("https://linkedin.com/in/satyanadella")
23
+ print(r.result.data["full_name"])
24
+ ```
25
+
26
+ `.sync()` takes **one** target and answers with **one** record, in the response.
27
+ It costs double, it accepts no webhooks and no `enrich`, and for a paged
28
+ operation it returns one upstream page. Those bounds are refusals, not quiet
29
+ downgrades.
30
+
31
+ If the deadline expires first you get `complete=False` and a real `batch_id`,
32
+ never a 504 - so you keep the handle to results you may already have been
33
+ charged for.
34
+
35
+ ## A batch
36
+
37
+ Everything is a batch, including a batch of one. There is no ceiling to discover
38
+ between one target and fifty thousand.
39
+
40
+ ```python
41
+ batch = ed.profiles_enrich(urls, external_id="crm-sync", find_emails=True)
42
+
43
+ for entry in ed.results(batch.batch_id):
44
+ if entry.ok:
45
+ save(entry.data)
46
+ else:
47
+ log(entry.input, entry.error["type"]) # and credits_used is 0
48
+ ```
49
+
50
+ `results()` is a generator over the cursor. Results **stream**: it yields rows
51
+ while the batch is still processing, so a long batch starts producing
52
+ immediately rather than after it finishes. It holds the cursor open until the
53
+ batch reaches a terminal state, sleeping for the server's own poll interval
54
+ between empty reads.
55
+
56
+ ```python
57
+ list(ed.results(batch_id, wait=False)) # what is readable right now
58
+ ed.wait(batch_id, timeout=600) # the counters, not the rows
59
+ ed.profiles_enrich.collect(urls) # submit and drain, in one call
60
+ ```
61
+
62
+ ## Retries and double-billing
63
+
64
+ Retries are on by default and cover exactly what is safe to repeat: `429`, `5xx`
65
+ and a transport failure. A `4xx` is raised immediately, because repeating a
66
+ refusal only spends the rate budget on a certain no.
67
+
68
+ Every submission carries an `Idempotency-Key` that the client mints, so the
69
+ retry is free: a retried submit resolves to the batch the first attempt created
70
+ rather than creating a second one and charging for it. Pass your own
71
+ `idempotency_key=` if your caller's retry needs the same guarantee.
72
+
73
+ ```python
74
+ ed = EasyData(max_retries=5, timeout=180)
75
+ ed.rate_limits.remaining # read off the last response, including a 429
76
+ ```
77
+
78
+ `rate_limits` fields are `None` when the deployment publishes no ceiling. That
79
+ is what "no limit" looks like on the wire: an absent header, never a zero.
80
+
81
+ ## Errors
82
+
83
+ ```python
84
+ from easydata_api import QuotaExhausted, RateLimited, InvalidRequest, EasyDataError
85
+
86
+ try:
87
+ ed.profiles_enrich(urls)
88
+ except InvalidRequest as e:
89
+ print(e.field, e.request_id) # quote request_id at support
90
+ except QuotaExhausted:
91
+ ... # waiting for the month is the fix
92
+ except EasyDataError as e:
93
+ print(e.type, e.status)
94
+ ```
95
+
96
+ One class per `error.type`, all under `EasyDataError`. An `error.type` this
97
+ version has never heard of becomes a plain `EasyDataError` carrying that string
98
+ rather than a crash.
99
+
100
+ `EmailUnverified` is a `403` and is **not** a spent allowance: the default
101
+ allowance is gated on somebody having confirmed their email address, and
102
+ clicking the link fixes it.
103
+
104
+ ## Webhooks
105
+
106
+ ```python
107
+ from easydata_api import verify, VerificationError
108
+
109
+ @app.post("/webhooks/easydata")
110
+ def hook():
111
+ try:
112
+ d = verify(SECRET, request.headers, request.get_data())
113
+ except VerificationError:
114
+ return "", 400
115
+
116
+ if seen(d.id): # stable across every retry of the same delivery
117
+ return "", 200
118
+
119
+ if d.event == "batch.result":
120
+ handle(d.data["result"])
121
+ elif d.event == "batch.completed":
122
+ finish(d.data["batch_id"])
123
+ return "", 200
124
+ ```
125
+
126
+ Four events: `batch.started`, `batch.result` (one per row), `batch.completed`
127
+ and `batch.failed`. The fields are in `d.data`, one level down.
128
+
129
+ Two things that are silent when you get them wrong:
130
+
131
+ - **Verify against the raw body.** Re-serialising parsed JSON changes key order
132
+ and whitespace, and the signature will not match a body you rebuilt.
133
+ - **Never verify against `X-EasyData-Timestamp`.** It sits outside both signed
134
+ messages, so a replayed delivery can set it to anything. The `t` inside the
135
+ signature header is the only copy that cannot be edited.
136
+
137
+ For the asymmetric scheme, `webhook_id` is required - one key signs for every
138
+ customer, so checking `wid` is what stops another customer's genuine delivery
139
+ verifying against your receiver.
140
+
141
+ ```python
142
+ from easydata_api import verify_ed25519
143
+ d = verify_ed25519(PUBLIC_KEY, request.headers, body, webhook_id=MY_ENDPOINT_ID)
144
+ ```
145
+
146
+ ## Everything else
147
+
148
+ ```python
149
+ ed.batch(batch_id) # one batch's state
150
+ ed.batches(status="completed", external_id="crm") # your batches
151
+ ed.cancel(batch_id) # delivered rows stay charged
152
+ ed.account() # allowance, ceilings, secret
153
+ ed.usage(from_="2026-09-01", to="2026-09-30") # spend by day and operation
154
+ ```
155
+
156
+ Operations are attributes: `profiles_enrich`, `profiles_activity`,
157
+ `profiles_posts`, `profiles_comments`, `profiles_reactions`, `companies_enrich`,
158
+ `posts_enrich`, `sales_search_people`, `sales_search_companies`.
159
+
160
+ ## Reference
161
+
162
+ - [API reference](https://easydata.win/docs/api)
163
+ - [The batch model](https://easydata.win/docs/batches) - what a credit is, and why
164
+ - [OpenAPI 3.1](https://easydata.win/openapi.yaml)
@@ -0,0 +1,79 @@
1
+ """EasyData - LinkedIn data enrichment.
2
+
3
+ from easydata_api import EasyData
4
+
5
+ ed = EasyData() # reads EASYDATA_API_KEY
6
+
7
+ # One record, in this call, at twice the credits.
8
+ r = ed.profiles_enrich.sync("https://linkedin.com/in/satyanadella")
9
+ print(r.result.data["full_name"])
10
+
11
+ # A batch of any size, streamed as it drains.
12
+ batch = ed.profiles_enrich(urls, external_id="crm-sync")
13
+ for entry in ed.results(batch.batch_id):
14
+ if entry.ok:
15
+ save(entry.data)
16
+
17
+ Everything is a batch, including a batch of one, and you are billed per record
18
+ that resolves: a failure costs nothing.
19
+ """
20
+
21
+ from .client import (
22
+ DEFAULT_BASE_URL,
23
+ OPERATIONS,
24
+ Batch,
25
+ EasyData,
26
+ RateLimits,
27
+ ResultEntry,
28
+ ResultsPage,
29
+ SyncResult,
30
+ )
31
+ from .errors import (
32
+ CapacityUnavailable,
33
+ Conflict,
34
+ EasyDataError,
35
+ EmailUnverified,
36
+ InternalError,
37
+ InvalidAPIKey,
38
+ InvalidRequest,
39
+ NotFound,
40
+ NotImplementedYet,
41
+ QuotaExhausted,
42
+ RateLimited,
43
+ TransportError,
44
+ UnprocessableTarget,
45
+ UpstreamTimeout,
46
+ )
47
+ from .webhooks import Delivery, VerificationError, verify, verify_ed25519
48
+
49
+ __version__ = "1.0.1"
50
+
51
+ __all__ = [
52
+ "EasyData",
53
+ "Batch",
54
+ "ResultEntry",
55
+ "ResultsPage",
56
+ "SyncResult",
57
+ "RateLimits",
58
+ "OPERATIONS",
59
+ "DEFAULT_BASE_URL",
60
+ "EasyDataError",
61
+ "InvalidRequest",
62
+ "InvalidAPIKey",
63
+ "QuotaExhausted",
64
+ "EmailUnverified",
65
+ "NotFound",
66
+ "Conflict",
67
+ "UnprocessableTarget",
68
+ "RateLimited",
69
+ "NotImplementedYet",
70
+ "InternalError",
71
+ "UpstreamTimeout",
72
+ "CapacityUnavailable",
73
+ "TransportError",
74
+ "verify",
75
+ "verify_ed25519",
76
+ "Delivery",
77
+ "VerificationError",
78
+ "__version__",
79
+ ]