renidly 0.1.0__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,28 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ cache: pip
21
+ - run: python -m pip install --upgrade pip
22
+ - run: pip install -e ".[dev]"
23
+ - name: Lint
24
+ run: ruff check src tests
25
+ - name: Type-check
26
+ run: mypy src/renidly
27
+ - name: Test
28
+ run: pytest -q
@@ -0,0 +1,20 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ id-token: write # PyPI Trusted Publishing (OIDC) — no token needed
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+ - run: python -m pip install --upgrade build
19
+ - run: python -m build
20
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .mypy_cache/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .env
11
+ .env.*
12
+ scripts/*
renidly-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,429 @@
1
+ Metadata-Version: 2.4
2
+ Name: renidly
3
+ Version: 0.1.0
4
+ Summary: The official Python SDK for the Renidly B2B professional data APIs (Data, Live, Email, Account).
5
+ Project-URL: Homepage, https://renidly.com
6
+ Project-URL: Source, https://github.com/renidly/renidly-python
7
+ Author: Renidly
8
+ License: MIT
9
+ Keywords: b2b,email verification,enrichment,professional data,renidly,sdk
10
+ Requires-Python: >=3.9
11
+ Requires-Dist: httpx>=0.27
12
+ Requires-Dist: pydantic>=2.6
13
+ Requires-Dist: typing-extensions>=4.7
14
+ Provides-Extra: dev
15
+ Requires-Dist: build>=1; extra == 'dev'
16
+ Requires-Dist: mypy>=1.10; extra == 'dev'
17
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
18
+ Requires-Dist: pytest>=8; extra == 'dev'
19
+ Requires-Dist: pyyaml>=6; extra == 'dev'
20
+ Requires-Dist: respx>=0.21; extra == 'dev'
21
+ Requires-Dist: ruff>=0.5; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Renidly Python SDK
25
+
26
+ The official Python SDK for the [Renidly](https://renidly.com) B2B professional data APIs — resolve, search, enrich, and verify professional identities (people, organizations, institutions, skills, professional activity, job opportunities, and business email) through one clean, typed client.
27
+
28
+ ```python
29
+ from renidly import Renidly
30
+
31
+ renidly = Renidly("rnd-...")
32
+
33
+ person = renidly.data.people.retrieve(handle="ryanroslansky")
34
+ company = renidly.data.companies.retrieve(slug="stripe")
35
+ email = renidly.emails.verify("sundar@google.com")
36
+
37
+ print(person.headline, company.name, email.deliverable)
38
+ ```
39
+
40
+ - **One client, four products** — `data`, `live`, `emails`, `account`, all off the same key.
41
+ - **Fully typed** — method names, parameters, and returns autocomplete in your IDE (ships `py.typed`).
42
+ - **Batteries included** — automatic retries, transparent pagination, batch jobs, a typed error hierarchy, and an optional self-tuning rate limiter.
43
+ - **Sync and async** — identical surface; just `await`.
44
+
45
+ ---
46
+
47
+ ## Table of contents
48
+
49
+ - [Install](#install)
50
+ - [Quickstart](#quickstart)
51
+ - [Authentication](#authentication)
52
+ - [The four products](#the-four-products)
53
+ - [Pagination](#pagination)
54
+ - [Batch jobs](#batch-jobs)
55
+ - [Errors](#errors)
56
+ - [Configuration](#configuration)
57
+ - [Automatic rate limiting](#automatic-rate-limiting)
58
+ - [Async](#async)
59
+ - [Response objects](#response-objects)
60
+ - [Advanced](#advanced)
61
+ - [Requirements & support](#requirements--support)
62
+
63
+ ---
64
+
65
+ ## Install
66
+
67
+ ```sh
68
+ pip install renidly
69
+ ```
70
+
71
+ Requires **Python 3.9+**.
72
+
73
+ ---
74
+
75
+ ## Quickstart
76
+
77
+ ```python
78
+ from renidly import Renidly
79
+
80
+ renidly = Renidly("rnd-...") # or Renidly() and set RENIDLY_API_KEY
81
+
82
+ # Retrieve a single record (returns None if nothing resolved)
83
+ person = renidly.data.people.retrieve(id="prsn_...")
84
+ if person:
85
+ print(person.first_name, person.headline)
86
+
87
+ # Search with any filters — they autocomplete in your editor
88
+ for p in renidly.data.people.search(title="cto", current_only=True).auto_paging_iter():
89
+ print(p.headline)
90
+
91
+ # Verify an email
92
+ v = renidly.emails.verify("sundar@google.com")
93
+ print(v.deliverable, v.reason)
94
+ ```
95
+
96
+ That's the whole feel: `renidly.<product>.<resource>.<action>(...)`.
97
+
98
+ ---
99
+
100
+ ## Authentication
101
+
102
+ Pass your key positionally, via config, or through the environment — whichever you prefer.
103
+
104
+ ```python
105
+ Renidly("rnd-...") # positional
106
+ Renidly() # reads RENIDLY_API_KEY
107
+ Renidly(config=RenidlyConfig(api_key="rnd-...")) # inside the config object
108
+ ```
109
+
110
+ Per-request override (e.g. multi-tenant apps):
111
+
112
+ ```python
113
+ renidly.data.people.search(title="cto", options={"api_key": "rnd-tenant-key"})
114
+ ```
115
+
116
+ Grab your key from [Workspace → API Keys](https://renidly.com/dashboard/key).
117
+
118
+ ---
119
+
120
+ ## The four products
121
+
122
+ ### `data` — clean, queryable records
123
+
124
+ Deduplicated professional records addressable by stable opaque IDs (`prsn_`, `org_`, `inst_`, `skl_`) or rich filters.
125
+
126
+ ```python
127
+ # People
128
+ renidly.data.people.retrieve(id="prsn_...") # or handle="..."
129
+ renidly.data.people.search(title="cto", skills="python", geo_country_code="US")
130
+ renidly.data.people.enrich_batch(handles=[...], ids=[...], live=True) # bulk (see Batch jobs)
131
+
132
+ # Companies
133
+ renidly.data.companies.retrieve(slug="google") # or id="org_..."
134
+ renidly.data.companies.search(name="stripe", staff_count_min=100)
135
+ renidly.data.companies.employees("google", title="engineer", current_only=True)
136
+ renidly.data.companies.enrich_batch(ids=[...]) # bulk (see Batch jobs)
137
+
138
+ # Institutions
139
+ renidly.data.institutions.retrieve("stanford") # by normalized name
140
+ renidly.data.institutions.search("stanford")
141
+ renidly.data.institutions.alumni("stanford", degree="MBA")
142
+
143
+ # Skills
144
+ renidly.data.skills.retrieve("skl_...")
145
+ renidly.data.skills.search("python")
146
+
147
+ # Job changes — trigger-based prospecting
148
+ renidly.data.job_changes.search(event_type="joined", days_ago=30)
149
+ ```
150
+
151
+ ### `live` — freshest snapshot on demand
152
+
153
+ Resolve a single subject or run a discovery search.
154
+
155
+ ```python
156
+ # People — resolve a public handle to a stable id once, then reuse it
157
+ eid = renidly.live.people.resolve_handle("williamhgates").entityId
158
+ renidly.live.people.enrich(eid) # or handle="..."
159
+ renidly.live.people.employment_history(eid)
160
+ renidly.live.people.endorsements(eid)
161
+ renidly.live.people.lookalikes(eid)
162
+ renidly.live.people.interests(eid)
163
+
164
+ # Organizations
165
+ oid = renidly.live.organizations.resolve_slug("google").id
166
+ renidly.live.organizations.enrich(oid)
167
+ renidly.live.organizations.headcount(oid)
168
+ renidly.live.organizations.similar(oid)
169
+ renidly.live.organizations.affiliated(oid)
170
+ renidly.live.organizations.activities(oid)
171
+ renidly.live.organizations.opportunities("1441,1035") # comma-separated org ids
172
+
173
+ # Opportunities (job postings)
174
+ renidly.live.opportunities.retrieve("4019200001")
175
+ renidly.live.opportunities.similar("4019200001")
176
+ renidly.live.opportunities.related_views("4019200001")
177
+ renidly.live.opportunities.hiring_team("4019200001")
178
+ renidly.live.opportunities.by_person(eid)
179
+
180
+ # Activity
181
+ renidly.live.activities.feed(eid)
182
+ renidly.live.activities.retrieve(activity_id)
183
+ renidly.live.activities.reactions(activity_id)
184
+ renidly.live.activities.replies(activity_id, sort_by="date_posted")
185
+ renidly.live.activities.replies_by_author(eid)
186
+
187
+ # Discover (search)
188
+ renidly.live.discover.people(keyword="cto", count=25)
189
+ renidly.live.discover.organizations(keyword="fintech", headcountRange="51-200")
190
+ renidly.live.discover.opportunities(keyword="python", workplaceTypes="remote")
191
+ ```
192
+
193
+ ### `emails` — verify, find, and resolve
194
+
195
+ ```python
196
+ renidly.emails.verify("sundar@google.com")
197
+ renidly.emails.find(first_name="Patrick", last_name="Collison", domain="stripe.com")
198
+ renidly.emails.find_by_url("https://example.com/in/someone") # from a professional profile URL
199
+ renidly.emails.reverse("john@acme.com") # who is behind this business email
200
+ renidly.emails.prospects("acme.com", kind="verified_only") # known contacts for a domain
201
+
202
+ # Bulk (see Batch jobs)
203
+ renidly.emails.verify_batch(["a@x.com", "b@y.com"])
204
+ renidly.emails.find_batch([{"first_name": "A", "last_name": "B", "domain": "acme.com"}])
205
+ ```
206
+
207
+ ### `account` — balance, tier, and pricing
208
+
209
+ ```python
210
+ renidly.account.balance().balance
211
+ renidly.account.tier().current_tier.limit_per_minute
212
+ renidly.account.enterprise_balance() # for an Enterprise workspace
213
+ renidly.account.tiers() # public tier ladder (no key needed)
214
+ renidly.account.route_costs() # per-endpoint credit costs (no key needed)
215
+ ```
216
+
217
+ ---
218
+
219
+ ## Pagination
220
+
221
+ Every `search`/list method returns a list you can use directly **and** page through transparently.
222
+
223
+ ```python
224
+ page = renidly.data.people.search(title="cto", limit=25)
225
+
226
+ len(page) # items on this page
227
+ page[0] # index it
228
+ for p in page: ... # iterate this page
229
+ page.has_more # is there more?
230
+
231
+ # ...or walk EVERY page lazily (fetches as it goes, one page in memory at a time)
232
+ for person in renidly.data.people.search(title="cto").auto_paging_iter():
233
+ print(person.headline)
234
+ ```
235
+
236
+ ---
237
+
238
+ ## Batch jobs
239
+
240
+ Process up to 1000 items in one async job. Submit returns a handle instantly.
241
+
242
+ ```python
243
+ job = renidly.data.people.enrich_batch(handles=["ryanroslansky", "williamhgates"], live=True)
244
+
245
+ # block until done and collect everything
246
+ result = job.wait(on_progress=lambda n: print("resolved", n))
247
+ print(result.status, result.resolved, "/", result.total)
248
+ for row in result.results:
249
+ print(row.matched_input, "->", row.headline)
250
+ print("not found:", result.not_found)
251
+
252
+ # ...or stream results as they resolve
253
+ for row in renidly.emails.verify_batch(["a@x.com", "b@y.com"]).stream():
254
+ print(row.email, row.deliverable)
255
+ ```
256
+
257
+ Available on `data.people.enrich_batch`, `data.companies.enrich_batch`, `emails.verify_batch`, `emails.find_batch`.
258
+
259
+ ---
260
+
261
+ ## Errors
262
+
263
+ Every failure raises a specific subclass of `RenidlyError`, and the message tells you exactly what went wrong.
264
+
265
+ ```python
266
+ from renidly import (
267
+ RenidlyError, AuthenticationError, InvalidRequestError,
268
+ InsufficientCreditsError, NotFoundError, RateLimitError,
269
+ PermissionDeniedError, ServiceUnavailableError,
270
+ )
271
+
272
+ try:
273
+ renidly.emails.find(first_name="A", last_name="B", domain="bad")
274
+ except InvalidRequestError as e:
275
+ print(e.message) # "Validation failed"
276
+ print(e.field_errors) # {"domain": "must be a bare hostname"}
277
+ except RateLimitError as e:
278
+ print(e.tier, e.limit, e.retry_after)
279
+ except InsufficientCreditsError:
280
+ ...
281
+ except RenidlyError as e: # catch-all
282
+ print(e.status_code, e.error_code, e.message, e.errors)
283
+ ```
284
+
285
+ The string form includes the detail, so an uncaught error is self-explanatory:
286
+
287
+ ```
288
+ InvalidRequestError: Validation failed — domain: must be a bare hostname (VALIDATION_ERROR, HTTP 400)
289
+ ```
290
+
291
+ | Exception | When |
292
+ |---|---|
293
+ | `AuthenticationError` | missing / invalid key |
294
+ | `PermissionDeniedError` | key valid but not allowed here |
295
+ | `InvalidRequestError` | bad input (see `.field_errors`) |
296
+ | `InsufficientCreditsError` | not enough credits |
297
+ | `NotFoundError` | job not found / expired |
298
+ | `RateLimitError` | per-minute limit hit (`.tier`, `.limit`, `.retry_after`) |
299
+ | `ServiceUnavailableError` | temporary — retry shortly |
300
+ | `APIConnectionError` | network / timeout |
301
+
302
+ > **Not-found lookups:** a single `retrieve(...)` that resolves nothing returns `None` by default (not an exception). Set `raise_on_not_found=True` to raise instead.
303
+
304
+ ---
305
+
306
+ ## Configuration
307
+
308
+ All options live on one object, `RenidlyConfig`:
309
+
310
+ ```python
311
+ from renidly import Renidly, RenidlyConfig
312
+
313
+ renidly = Renidly("rnd-...", config=RenidlyConfig(
314
+ timeout=30,
315
+ max_retries=3, # auto-retry on 429 / 503 / connection errors (backoff + jitter)
316
+ unwrap_data_obj=True, # return the data model (False -> the full envelope)
317
+ raise_on_not_found=False, # single lookups return None when empty (True -> raise)
318
+ raise_on_api_error=True, # map failures to typed exceptions
319
+ auto_rate_limit=False, # see below
320
+ ))
321
+ ```
322
+
323
+ | Option | Default | Meaning |
324
+ |---|---|---|
325
+ | `api_key` | `RENIDLY_API_KEY` env | Your key (positional arg overrides this). |
326
+ | `timeout` | `30` | Per-request timeout (seconds). |
327
+ | `max_retries` | `2` | Retries on transient failures. |
328
+ | `backoff_factor` | `0.5` | Base seconds for exponential backoff. |
329
+ | `base_url` | `https://renidly.com` | Override the API host. |
330
+ | `proxy` | `None` | HTTP(S) proxy URL. |
331
+ | `default_headers` | `{}` | Extra headers on every request. |
332
+ | `unwrap_data_obj` | `True` | Return `data` vs the full envelope. |
333
+ | `raise_on_not_found` | `False` | `None` vs `NotFoundError` on empty lookups. |
334
+ | `raise_on_api_error` | `True` | Raise vs return `None` on API errors. |
335
+ | `auto_rate_limit` | `False` | Self-throttle to your tier's limit. |
336
+ | `rate_limit_per_minute` | `None` | Fixed limit (required for enterprise keys). |
337
+ | `rate_limit_safety` | `1.0` | Fraction of the limit to target (e.g. `0.9`). |
338
+
339
+ ---
340
+
341
+ ## Automatic rate limiting
342
+
343
+ Turn it on and the SDK keeps you under your per-minute limit automatically — no limiter to build.
344
+
345
+ ```python
346
+ # Regular key: the limit is read from your tier and refreshed automatically.
347
+ Renidly("rnd-...", config=RenidlyConfig(auto_rate_limit=True))
348
+
349
+ # Enterprise key: the limit is fixed — supply it.
350
+ Renidly("enterprise-...", config=RenidlyConfig(auto_rate_limit=True, rate_limit_per_minute=550))
351
+ ```
352
+
353
+ It uses a sliding 60-second window so you never exceed the limit, and re-reads your tier after a `429`.
354
+
355
+ ---
356
+
357
+ ## Async
358
+
359
+ Same surface, same names — just `await` (and use it as a context manager to auto-close the connection pool).
360
+
361
+ ```python
362
+ import asyncio
363
+ from renidly import AsyncRenidly
364
+
365
+ async def main():
366
+ async with AsyncRenidly("rnd-...") as renidly:
367
+ company = await renidly.data.companies.retrieve(slug="stripe")
368
+ print(company.name)
369
+
370
+ async for p in renidly.data.people.search(title="cto").auto_paging_iter():
371
+ print(p.headline)
372
+
373
+ asyncio.run(main())
374
+ ```
375
+
376
+ ---
377
+
378
+ ## Response objects
379
+
380
+ Responses are dynamic and drill-able — access any field (nested included) as an attribute, no schema classes required.
381
+
382
+ ```python
383
+ t = renidly.account.tier()
384
+ t.current_tier.name # nested attribute access, arbitrarily deep
385
+ t.model_dump() # convert to a plain dict anytime
386
+
387
+ # HTTP metadata is attached to every object
388
+ t.last_response.status_code
389
+ t.last_response.request_id
390
+ ```
391
+
392
+ Prefer the raw envelope? Set `unwrap_data_obj=False` and every call returns `APIResponse(success=..., data=..., message=...)`.
393
+
394
+ ---
395
+
396
+ ## Advanced
397
+
398
+ **Per-request options** override config for a single call:
399
+
400
+ ```python
401
+ renidly.data.skills.search("python", options={"timeout": 5, "api_key": "rnd-other"})
402
+ ```
403
+
404
+ **Escape hatch** — call any endpoint directly:
405
+
406
+ ```python
407
+ env = renidly.raw_request("GET", "/people/search", service="data", params={"title": "cto"})
408
+ print(env.success, env.data)
409
+ ```
410
+
411
+ **Bring your own HTTP client** (connection pools, custom timeouts, mounts):
412
+
413
+ ```python
414
+ import httpx
415
+ Renidly("rnd-...", http_client=httpx.Client(limits=httpx.Limits(max_connections=50)))
416
+ ```
417
+
418
+ ---
419
+
420
+ ## Requirements & support
421
+
422
+ - Python **3.9+**
423
+ - Depends only on [`httpx`](https://www.python-httpx.org/) and [`pydantic`](https://docs.pydantic.dev/) v2.
424
+
425
+ Questions or issues? Open one on [GitHub](https://github.com/renidly/renidly-python/issues).
426
+
427
+ ## License
428
+
429
+ MIT