companyenrich 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +327 -0
- data/lib/companyenrich/client.rb +41 -0
- data/lib/companyenrich/errors.rb +88 -0
- data/lib/companyenrich/http_client.rb +257 -0
- data/lib/companyenrich/ndjson_decoder.rb +40 -0
- data/lib/companyenrich/resources/batch_job.rb +62 -0
- data/lib/companyenrich/resources/batches.rb +79 -0
- data/lib/companyenrich/resources/companies.rb +110 -0
- data/lib/companyenrich/resources/geo.rb +69 -0
- data/lib/companyenrich/resources/people.rb +74 -0
- data/lib/companyenrich/resources/reference.rb +30 -0
- data/lib/companyenrich/resources/support.rb +36 -0
- data/lib/companyenrich/version.rb +5 -0
- data/lib/companyenrich.rb +9 -0
- metadata +60 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b29a5f9bca5c9a4988c19c84b4c879d7ff44dcbffdb2c29bfc495e27740a0960
|
|
4
|
+
data.tar.gz: 628e62829990c88d4fbf9c725587232db9418a5bd7c0edc345dfcc74d397b27f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: dc6253692f6c67c9c843a14e376bfc5d25da701545fedb4406e10ac17cbd80da57bcd3ab6edf914e05a1ea8893addfc8d9dbc39c81c7a37e6456b764b003fb6e
|
|
7
|
+
data.tar.gz: c133eff3296f9938f5ad1fc5da81ba3c5a11a48add44f3ce240b33289b3468d2e93bb0ef310db3fd79e375bfa66b78e7e98eb28189fcf7a68d282a436fcb5256
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 STDIO Ltd.
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# CompanyEnrich Ruby SDK
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/companyenrich)
|
|
4
|
+
|
|
5
|
+
The official Ruby client for the [CompanyEnrich API](https://companyenrich.com): company and people enrichment, filter, semantic and lookalike search, work emails, and bulk jobs.
|
|
6
|
+
|
|
7
|
+
It requires Ruby 3.1 or newer and has no runtime dependencies.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
gem install companyenrich
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or add it to your `Gemfile`:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
gem "companyenrich"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
Create an API key in your [dashboard](https://companyenrich.com/dashboard) and keep it secret.
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
require "companyenrich"
|
|
27
|
+
|
|
28
|
+
# Reads COMPANYENRICH_API_KEY from the environment, or pass api_key: "your-api-key".
|
|
29
|
+
client = CompanyEnrich::Client.new
|
|
30
|
+
|
|
31
|
+
company = client.companies.enrich("stripe.com")
|
|
32
|
+
puts company["name"], company["employees"]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Responses are plain hashes with the API's field names as string keys. Optional fields can be absent or `nil`.
|
|
36
|
+
|
|
37
|
+
Method arguments are `snake_case` keywords. Hashes you pass, such as `filter`, use the API's own field names.
|
|
38
|
+
|
|
39
|
+
## Companies
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
# Add paid data to a lookup. Each expansion costs extra credits.
|
|
43
|
+
detailed = client.companies.enrich("stripe.com", expand: %w[technologies workforce])
|
|
44
|
+
|
|
45
|
+
# Up to 50 domains at once. Check each item's status.
|
|
46
|
+
batch = client.companies.enrich_batch(["stripe.com", "adyen.com"])
|
|
47
|
+
batch["items"].each do |item|
|
|
48
|
+
puts item["domain"], item.dig("company", "name") if item["status"] == "found"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Filter search
|
|
52
|
+
page = client.companies.search(
|
|
53
|
+
filter: { country: "US", employees: ["51-200", "201-500"], categories: "saas" },
|
|
54
|
+
page_size: 25
|
|
55
|
+
)
|
|
56
|
+
puts page["items"].size, page["nextCursor"]
|
|
57
|
+
|
|
58
|
+
# Free count of the same query
|
|
59
|
+
total = client.companies.count(filter: { country: "US" })
|
|
60
|
+
puts total["count"]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## People
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
results = client.people.search(
|
|
67
|
+
filter: { domain: "stripe.com", seniority: %w[c-suite founder] },
|
|
68
|
+
page_size: 10
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
person = client.people.enrich("jane@stripe.com")
|
|
72
|
+
|
|
73
|
+
lookup = client.people.find_email(person["id"], domain: "stripe.com")
|
|
74
|
+
puts lookup["email"], lookup["certainty"] if lookup["status"] == "found"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Semantic and lookalike search
|
|
78
|
+
|
|
79
|
+
Semantic search finds companies or people from a plain-language description. Lookalike search finds companies similar to ones you already know. A company search can use either mode but not both, and either can be combined with `filter`. `search_all` accepts the same arguments as `search`.
|
|
80
|
+
|
|
81
|
+
| Search | Credits per result |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| Companies by filters | 1 |
|
|
84
|
+
| Companies by description | 2 |
|
|
85
|
+
| Lookalike companies | 5 |
|
|
86
|
+
| People, in any mode | 2 |
|
|
87
|
+
|
|
88
|
+
Paid expansions cost extra, and an empty result costs one result's base price. To try a query for free, pass the same arguments to `companies.preview` or `people.preview`, which need a plan with previews.
|
|
89
|
+
|
|
90
|
+
### Companies by description
|
|
91
|
+
|
|
92
|
+
By default, the API reads attributes such as location and size from your query and applies them. `interpretation` shows what it inferred.
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
results = client.companies.search(
|
|
96
|
+
semantic: { query: "saas companies in Turkey" },
|
|
97
|
+
page_size: 25
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
results["items"].each do |company|
|
|
101
|
+
puts company["domain"], company["description"]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
p results.dig("interpretation", "filter") # Inferred filters, such as the country
|
|
105
|
+
p results.dig("interpretation", "boost") # Inferred ranking boosts, such as the category
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Filters you pass take priority over inferred values of the same kind. Set `autoFilter: false` to turn inference off, or set `weight` (0 to 1) to control how much semantic similarity affects ranking.
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
filtered = client.companies.search(
|
|
112
|
+
semantic: { query: "AI tools for hospitals", autoFilter: false },
|
|
113
|
+
filter: { country: "US", employees: ["51-200", "201-500"] },
|
|
114
|
+
page_size: 25
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Lookalike companies
|
|
119
|
+
|
|
120
|
+
Pass 1 to 10 seed domains. Use `minScore` (0 to 1) to require a minimum similarity.
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
similar = client.companies.search(
|
|
124
|
+
lookalike: { domains: ["stripe.com", "adyen.com"] },
|
|
125
|
+
filter: { country: "US", employees: ["51-200", "201-500"] },
|
|
126
|
+
page_size: 25
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
scores = similar["scores"] || {} # Scores are not probabilities
|
|
130
|
+
similar["items"].each do |company|
|
|
131
|
+
puts company["domain"], scores[company["id"]]
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Keep results in the order returned rather than sorting them by score.
|
|
136
|
+
|
|
137
|
+
### People by description
|
|
138
|
+
|
|
139
|
+
The API turns your query into person filters, such as role, seniority and country, plus a description of their current companies. `interpretation` shows both. A query it can't infer any filters from is rejected with an error, so include something concrete like a role or location.
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
people = client.people.search(
|
|
143
|
+
semantic: { query: "marketing leaders at Series B fintech startups in London" },
|
|
144
|
+
page_size: 25
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
people["items"].each do |person|
|
|
148
|
+
puts person["name"], person["position"]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
p people["interpretation"] # The inferred filters and company description
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### People at matching companies
|
|
155
|
+
|
|
156
|
+
Use `company` to find people by the kind of company they work at. These searches cover people at the 1,000 best-matching companies.
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
# Executives at companies matching a description
|
|
160
|
+
executives = client.people.search(
|
|
161
|
+
filter: { seniority: %w[c-suite vp] },
|
|
162
|
+
company: { semantic: { query: "AI tools for hospitals" } },
|
|
163
|
+
page_size: 25
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Executives at companies similar to your best customers
|
|
167
|
+
similar_teams = client.people.search(
|
|
168
|
+
filter: { seniority: %w[c-suite vp] },
|
|
169
|
+
company: { lookalike: { domains: ["stripe.com", "adyen.com"] } },
|
|
170
|
+
page_size: 25
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Pagination
|
|
175
|
+
|
|
176
|
+
`search` returns one page. The `*_all` methods take a block, or return a lazy `Enumerator` that requests pages as you consume them.
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
client.companies.search_all(filter: { country: "TR" }, page_size: 100) do |company|
|
|
180
|
+
puts company["domain"]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
first_ten = client.people.search_all(filter: { domain: "stripe.com" }, page_size: 10).first(10)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
These are available as `companies.search_all`, `people.search_all`, `batches.list_all`, and `geo.search_countries_all`, `geo.search_states_all` and `geo.search_cities_all`.
|
|
187
|
+
|
|
188
|
+
Search credits are charged for every result on a fetched page, even if you stop iterating partway through it. Set `page_size` close to the number of results you plan to use.
|
|
189
|
+
|
|
190
|
+
## Bulk jobs
|
|
191
|
+
|
|
192
|
+
Bulk enrichment and search exports run in the background.
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
job = client.companies.bulk_enrichment.create(["stripe.com", "adyen.com"])
|
|
196
|
+
|
|
197
|
+
client.batches.wait_for_completion(job["id"]) do |batch|
|
|
198
|
+
puts "#{batch['progress']['succeeded']}/#{batch['progress']['total']}"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
client.companies.bulk_enrichment.stream(job["id"]) do |record|
|
|
202
|
+
puts record["domain"], record.dig("company", "name") if record["success"]
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`companies.bulk_enrichment`, `companies.search_export` and `people.search_export` share the same methods:
|
|
207
|
+
|
|
208
|
+
| Method | Returns |
|
|
209
|
+
| --- | --- |
|
|
210
|
+
| `create(...)` | The new batch's status |
|
|
211
|
+
| `stream(id)` | Each record in turn, best for large jobs |
|
|
212
|
+
| `results(id)` | One JSON document with every record |
|
|
213
|
+
| `csv(id)` | The CSV as a string |
|
|
214
|
+
| `csv_chunks(id)` | The CSV as raw bytes, for writing to a file |
|
|
215
|
+
|
|
216
|
+
## Errors
|
|
217
|
+
|
|
218
|
+
Failed requests raise a subclass of `CompanyEnrich::APIError`, which carries `status`, `title`, `detail`, `errors` (field-level validation messages) and `trace_id` (include it when contacting support).
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
begin
|
|
222
|
+
client.companies.enrich("example.invalid")
|
|
223
|
+
rescue CompanyEnrich::NotFoundError
|
|
224
|
+
puts "No company found"
|
|
225
|
+
rescue CompanyEnrich::PaymentRequiredError
|
|
226
|
+
puts "Out of credits, or not included in this plan"
|
|
227
|
+
rescue CompanyEnrich::APIError => e
|
|
228
|
+
puts e.status, e.detail, e.trace_id
|
|
229
|
+
end
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
| Status | Class |
|
|
233
|
+
| --- | --- |
|
|
234
|
+
| 400 | `BadRequestError` |
|
|
235
|
+
| 401 | `AuthenticationError` |
|
|
236
|
+
| 402 | `PaymentRequiredError` |
|
|
237
|
+
| 403 | `PermissionDeniedError` |
|
|
238
|
+
| 404 | `NotFoundError` |
|
|
239
|
+
| 422 | `ValidationError` |
|
|
240
|
+
| 429 | `RateLimitError` |
|
|
241
|
+
| 5xx | `ServerError` |
|
|
242
|
+
| Other | `APIError` |
|
|
243
|
+
|
|
244
|
+
`APIConnectionError` and `APITimeoutError` cover requests that never received a response or were cut off. Everything above extends `CompanyEnrich::Error`.
|
|
245
|
+
|
|
246
|
+
## Retries and timeouts
|
|
247
|
+
|
|
248
|
+
Failed requests are retried up to twice with exponential backoff.
|
|
249
|
+
|
|
250
|
+
| Failure | GET and free POST endpoints | Other POST endpoints |
|
|
251
|
+
| --- | --- | --- |
|
|
252
|
+
| 429 rate limit | Retried | Retried |
|
|
253
|
+
| 408, 5xx, connection error, timeout | Retried | Not retried |
|
|
254
|
+
|
|
255
|
+
POST requests that consume credits or start a job are not repeated after an ambiguous failure, because the first attempt may already have been charged or created a batch. A download that fails partway through is never restarted, so you won't receive duplicate records.
|
|
256
|
+
|
|
257
|
+
## Configuration
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
configured = CompanyEnrich::Client.new(api_key: "your-api-key", timeout: 30, max_retries: 4)
|
|
261
|
+
|
|
262
|
+
# Every method accepts per-request options.
|
|
263
|
+
configured.me(options: { timeout: 5, max_retries: 0 })
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
| Option | Default | Description |
|
|
267
|
+
| --- | --- | --- |
|
|
268
|
+
| `api_key` | `COMPANYENRICH_API_KEY` | Your API key |
|
|
269
|
+
| `base_url` | `https://api.companyenrich.com` | API origin, for proxies |
|
|
270
|
+
| `timeout` | `120` | Timeout in seconds. `nil` disables it |
|
|
271
|
+
| `max_retries` | `2` | Retries after the first attempt |
|
|
272
|
+
| `default_headers` | none | Headers added to every request |
|
|
273
|
+
|
|
274
|
+
Each request uses its own connection, so one client can be shared across threads.
|
|
275
|
+
|
|
276
|
+
## API reference
|
|
277
|
+
|
|
278
|
+
| SDK method | Endpoint |
|
|
279
|
+
| --- | --- |
|
|
280
|
+
| `companies.get(id)` | `GET /v2/companies/{companyId}` |
|
|
281
|
+
| `companies.enrich(domain)` | `GET /v2/companies/enrich` |
|
|
282
|
+
| `companies.enrich_by_properties(...)` | `POST /v2/companies/enrich` |
|
|
283
|
+
| `companies.enrich_batch(domains)` | `POST /v2/companies/enrich/batch` |
|
|
284
|
+
| `companies.autocomplete(query)` | `GET /v2/companies/autocomplete` |
|
|
285
|
+
| `companies.workforce(...)` | `GET /v2/companies/workforce` |
|
|
286
|
+
| `companies.search(...)` | `POST /v2/companies/search` |
|
|
287
|
+
| `companies.count(...)` | `POST /v2/companies/search/count` |
|
|
288
|
+
| `companies.preview(...)` | `POST /v2/companies/search/preview` |
|
|
289
|
+
| `companies.bulk_enrichment.*` | `/v2/companies/enrich/bulk` |
|
|
290
|
+
| `companies.search_export.*` | `/v2/companies/search/export` |
|
|
291
|
+
| `people.get(id)` | `GET /v2/people/{personId}` |
|
|
292
|
+
| `people.enrich(email)` | `POST /v2/people/enrich` |
|
|
293
|
+
| `people.find_email(id)` | `GET /v2/people/{personId}/email` |
|
|
294
|
+
| `people.search(...)` | `POST /v2/people/search` |
|
|
295
|
+
| `people.count(...)` | `POST /v2/people/search/count` |
|
|
296
|
+
| `people.preview(...)` | `POST /v2/people/search/preview` |
|
|
297
|
+
| `people.search_export.*` | `/v2/people/search/export` |
|
|
298
|
+
| `batches.list(...)` | `GET /v2/batches` |
|
|
299
|
+
| `batches.get(id)` | `GET /v2/batches/{id}` |
|
|
300
|
+
| `batches.cancel(id)` | `POST /v2/batches/{id}/cancel` |
|
|
301
|
+
| `batches.raw_results(id)` | `GET /v2/batches/{id}/results` |
|
|
302
|
+
| `batches.wait_for_completion(id)` | Polls `GET /v2/batches/{id}` |
|
|
303
|
+
| `geo.regions` | `GET /v2/geo/regions` |
|
|
304
|
+
| `geo.country(code)` | `GET /v2/geo/countries/{countryCode}` |
|
|
305
|
+
| `geo.search_countries(...)` | `POST /v2/geo/countries` |
|
|
306
|
+
| `geo.search_states(...)` | `POST /v2/geo/states` |
|
|
307
|
+
| `geo.search_cities(...)` | `POST /v2/geo/cities` |
|
|
308
|
+
| `industries.list` | `GET /v2/industries` |
|
|
309
|
+
| `keywords.autocomplete(query)` | `GET /v2/keywords/autocomplete` |
|
|
310
|
+
| `technologies.autocomplete(query)` | `GET /v2/technologies/autocomplete` |
|
|
311
|
+
| `positions.autocomplete(query)` | `GET /v2/positions/autocomplete` |
|
|
312
|
+
| `me` | `GET /v2/me` |
|
|
313
|
+
|
|
314
|
+
See the [API documentation](https://api.companyenrich.com/openapi/v2.json) for parameters and credit costs.
|
|
315
|
+
|
|
316
|
+
## Development
|
|
317
|
+
|
|
318
|
+
```sh
|
|
319
|
+
bundle install
|
|
320
|
+
bundle exec rake
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
To release, bump `VERSION` in `lib/companyenrich/version.rb` and push to `main`. The publish workflow runs the checks and publishes that version to RubyGems if it is new.
|
|
324
|
+
|
|
325
|
+
## License
|
|
326
|
+
|
|
327
|
+
MIT
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "http_client"
|
|
4
|
+
require_relative "resources/batches"
|
|
5
|
+
require_relative "resources/companies"
|
|
6
|
+
require_relative "resources/geo"
|
|
7
|
+
require_relative "resources/people"
|
|
8
|
+
require_relative "resources/reference"
|
|
9
|
+
|
|
10
|
+
module CompanyEnrich
|
|
11
|
+
# Client for the CompanyEnrich API.
|
|
12
|
+
class Client
|
|
13
|
+
attr_reader :companies, :people, :batches, :geo, :industries, :keywords, :technologies, :positions
|
|
14
|
+
|
|
15
|
+
# @param api_key [String, nil] defaults to the +COMPANYENRICH_API_KEY+ environment variable
|
|
16
|
+
# @param base_url [String] API origin, for proxies
|
|
17
|
+
# @param timeout [Numeric, nil] timeout in seconds for each attempt. +nil+ disables it.
|
|
18
|
+
# @param max_retries [Integer] how many times to retry a failed request
|
|
19
|
+
# @param default_headers [Hash, nil] headers sent with every request
|
|
20
|
+
def initialize(api_key: nil, base_url: HTTPClient::DEFAULT_BASE_URL, timeout: HTTPClient::DEFAULT_TIMEOUT,
|
|
21
|
+
max_retries: HTTPClient::DEFAULT_MAX_RETRIES, default_headers: nil)
|
|
22
|
+
@http = HTTPClient.new(
|
|
23
|
+
api_key: api_key, base_url: base_url, timeout: timeout,
|
|
24
|
+
max_retries: max_retries, default_headers: default_headers
|
|
25
|
+
)
|
|
26
|
+
@companies = Resources::Companies.new(@http)
|
|
27
|
+
@people = Resources::People.new(@http)
|
|
28
|
+
@batches = Resources::Batches.new(@http)
|
|
29
|
+
@geo = Resources::Geo.new(@http)
|
|
30
|
+
@industries = Resources::Industries.new(@http)
|
|
31
|
+
@keywords = Resources::Autocomplete.new(@http, "/v2/keywords/autocomplete")
|
|
32
|
+
@technologies = Resources::Autocomplete.new(@http, "/v2/technologies/autocomplete")
|
|
33
|
+
@positions = Resources::Autocomplete.new(@http, "/v2/positions/autocomplete")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the authenticated user, including credit balance and plan capabilities. Free.
|
|
37
|
+
def me(options: nil)
|
|
38
|
+
@http.request("GET", "/v2/me", options: options)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CompanyEnrich
|
|
4
|
+
# Base class for every error raised by this SDK.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# The request never produced a response, for example because of a DNS or socket failure.
|
|
8
|
+
class APIConnectionError < Error; end
|
|
9
|
+
|
|
10
|
+
# The request, or a +batches.wait_for_completion+ call, ran out of time.
|
|
11
|
+
class APITimeoutError < Error; end
|
|
12
|
+
|
|
13
|
+
# The API responded with a non-2xx status.
|
|
14
|
+
class APIError < Error
|
|
15
|
+
# @return [Integer] the HTTP status
|
|
16
|
+
attr_reader :status
|
|
17
|
+
# @return [Hash{String => String}] response headers, with lowercase names
|
|
18
|
+
attr_reader :headers
|
|
19
|
+
# @return [Object] the parsed response body, or the raw text if it was not JSON
|
|
20
|
+
attr_reader :body
|
|
21
|
+
# @return [Hash{String => Array<String>}, nil] field-level validation messages
|
|
22
|
+
attr_reader :errors
|
|
23
|
+
# @return [String, nil] value of the +x-trace-id+ header. Include it in support requests.
|
|
24
|
+
attr_reader :trace_id
|
|
25
|
+
attr_reader :type, :title, :detail, :instance
|
|
26
|
+
|
|
27
|
+
def initialize(status:, headers:, body:)
|
|
28
|
+
problem = body.is_a?(Hash) ? body : {}
|
|
29
|
+
super(self.class.format_message(status, problem))
|
|
30
|
+
@status = status
|
|
31
|
+
@headers = headers
|
|
32
|
+
@body = body
|
|
33
|
+
@type = problem["type"]
|
|
34
|
+
@title = problem["title"]
|
|
35
|
+
@detail = problem["detail"]
|
|
36
|
+
@instance = problem["instance"]
|
|
37
|
+
@errors = problem["errors"]
|
|
38
|
+
@trace_id = headers["x-trace-id"]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.format_message(status, problem)
|
|
42
|
+
summary = [problem["title"], problem["detail"]].compact.reject(&:empty?).join(": ")
|
|
43
|
+
fields = (problem["errors"] || {}).map { |field, messages| "#{field}: #{messages.join(", ")}" }.join("; ")
|
|
44
|
+
[status.to_s, summary, fields.empty? ? nil : "(#{fields})"].compact.reject(&:empty?).join(" ")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# +400+: the request was malformed.
|
|
49
|
+
class BadRequestError < APIError; end
|
|
50
|
+
|
|
51
|
+
# +401+: the API key is missing or invalid.
|
|
52
|
+
class AuthenticationError < APIError; end
|
|
53
|
+
|
|
54
|
+
# +402+: the account is out of credits or its plan does not include this request.
|
|
55
|
+
class PaymentRequiredError < APIError; end
|
|
56
|
+
|
|
57
|
+
# +403+: the API key may not perform this request.
|
|
58
|
+
class PermissionDeniedError < APIError; end
|
|
59
|
+
|
|
60
|
+
# +404+: the company, person, or batch does not exist.
|
|
61
|
+
class NotFoundError < APIError; end
|
|
62
|
+
|
|
63
|
+
# +422+: the request failed validation. See {APIError#errors}.
|
|
64
|
+
class ValidationError < APIError; end
|
|
65
|
+
|
|
66
|
+
# +429+: a rate limit was exceeded and automatic retries did not resolve it.
|
|
67
|
+
class RateLimitError < APIError; end
|
|
68
|
+
|
|
69
|
+
# +5xx+: the API failed to process the request.
|
|
70
|
+
class ServerError < APIError; end
|
|
71
|
+
|
|
72
|
+
ERRORS_BY_STATUS = {
|
|
73
|
+
400 => BadRequestError,
|
|
74
|
+
401 => AuthenticationError,
|
|
75
|
+
402 => PaymentRequiredError,
|
|
76
|
+
403 => PermissionDeniedError,
|
|
77
|
+
404 => NotFoundError,
|
|
78
|
+
422 => ValidationError,
|
|
79
|
+
429 => RateLimitError
|
|
80
|
+
}.freeze
|
|
81
|
+
private_constant :ERRORS_BY_STATUS
|
|
82
|
+
|
|
83
|
+
# @api private
|
|
84
|
+
def self.api_error(status:, headers:, body:)
|
|
85
|
+
fallback = status >= 500 ? ServerError : APIError
|
|
86
|
+
ERRORS_BY_STATUS.fetch(status, fallback).new(status: status, headers: headers, body: body)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "time"
|
|
7
|
+
require "uri"
|
|
8
|
+
require "zlib"
|
|
9
|
+
|
|
10
|
+
require_relative "errors"
|
|
11
|
+
require_relative "ndjson_decoder"
|
|
12
|
+
require_relative "version"
|
|
13
|
+
|
|
14
|
+
module CompanyEnrich
|
|
15
|
+
# Sends requests, applies the retry policy, and maps failures to SDK errors.
|
|
16
|
+
# @api private
|
|
17
|
+
class HTTPClient
|
|
18
|
+
DEFAULT_BASE_URL = "https://api.companyenrich.com"
|
|
19
|
+
DEFAULT_TIMEOUT = 120
|
|
20
|
+
DEFAULT_MAX_RETRIES = 2
|
|
21
|
+
|
|
22
|
+
BASE_BACKOFF = 0.5
|
|
23
|
+
MAX_BACKOFF = 8.0
|
|
24
|
+
MAX_SERVER_REQUESTED_WAIT = 60.0
|
|
25
|
+
RETRYABLE_STATUSES = [408, 500, 502, 503, 504].freeze
|
|
26
|
+
TIMESPAN = /\A(\d+):(\d{2}):(\d{2}(?:\.\d+)?)\z/
|
|
27
|
+
|
|
28
|
+
TIMEOUT_ERRORS = [Timeout::Error, Errno::ETIMEDOUT].freeze
|
|
29
|
+
TRANSPORT_ERRORS = [
|
|
30
|
+
*TIMEOUT_ERRORS, SocketError, SystemCallError, IOError, OpenSSL::SSL::SSLError,
|
|
31
|
+
Net::ProtocolError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Zlib::Error
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
def initialize(api_key:, base_url:, timeout:, max_retries:, default_headers:)
|
|
35
|
+
@api_key = api_key || ENV.fetch("COMPANYENRICH_API_KEY", nil)
|
|
36
|
+
if @api_key.nil? || @api_key.empty?
|
|
37
|
+
raise Error, "Missing API key. Pass `api_key:` or set the COMPANYENRICH_API_KEY environment variable."
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@base_uri = URI.parse(base_url.sub(%r{/+\z}, ""))
|
|
41
|
+
@timeout = timeout
|
|
42
|
+
@max_retries = max_retries
|
|
43
|
+
@default_headers = default_headers
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Keeps the API key out of logs and error messages.
|
|
47
|
+
def inspect
|
|
48
|
+
"#<#{self.class} base_url=#{@base_uri}>"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Sends a request and parses the JSON response.
|
|
52
|
+
def request(method, path, query: nil, body: nil, retry_safe: nil, options: nil)
|
|
53
|
+
execute(method, path, query, body, retry_safe, options, accept: "application/json") do |response|
|
|
54
|
+
parse_json(response.read_body, response.code.to_i)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Sends a GET request and yields the response body as it arrives.
|
|
59
|
+
def stream_chunks(path, query: nil, options: nil, &block)
|
|
60
|
+
execute("GET", path, query, nil, nil, options, accept: "*/*", stream: true) do |response|
|
|
61
|
+
read_chunks(response, &block)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Sends a GET request to an NDJSON endpoint and yields one parsed record per line.
|
|
66
|
+
def stream_ndjson(path, query: nil, options: nil)
|
|
67
|
+
decoder = NdjsonDecoder.new
|
|
68
|
+
stream_chunks(path, query: query, options: options) do |chunk|
|
|
69
|
+
decoder.feed(chunk).each { |record| yield record }
|
|
70
|
+
end
|
|
71
|
+
decoder.flush.each { |record| yield record }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def pause(seconds)
|
|
75
|
+
sleep(seconds)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def execute(method, path, query, body, retry_safe, options, accept:, stream: false)
|
|
81
|
+
options ||= {}
|
|
82
|
+
max_retries = options.fetch(:max_retries, @max_retries)
|
|
83
|
+
safe = retry_safe.nil? ? method == "GET" : retry_safe
|
|
84
|
+
attempt = 0
|
|
85
|
+
streaming = false
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
connect(options.fetch(:timeout, @timeout)) do |http|
|
|
89
|
+
http.request(build_request(method, path, query, body, options, accept)) do |response|
|
|
90
|
+
raise_for_status(response)
|
|
91
|
+
# Once records have been handed over, repeating the download would duplicate them.
|
|
92
|
+
streaming = stream
|
|
93
|
+
return yield(response)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
rescue APIError, *TRANSPORT_ERRORS => e
|
|
97
|
+
raise if streaming
|
|
98
|
+
|
|
99
|
+
failure = normalize_failure(e)
|
|
100
|
+
delay = retry_delay(failure, attempt, safe)
|
|
101
|
+
raise failure if delay.nil? || attempt >= max_retries
|
|
102
|
+
|
|
103
|
+
pause(delay)
|
|
104
|
+
attempt += 1
|
|
105
|
+
retry
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def connect(timeout, &)
|
|
110
|
+
Net::HTTP.start(
|
|
111
|
+
@base_uri.host, @base_uri.port,
|
|
112
|
+
use_ssl: @base_uri.scheme == "https",
|
|
113
|
+
open_timeout: timeout, read_timeout: timeout, write_timeout: timeout,
|
|
114
|
+
# Retries follow this SDK's policy only, so a request is never repeated behind its back.
|
|
115
|
+
max_retries: 0,
|
|
116
|
+
# By default Net::HTTP accepts a truncated body as complete, which would silently lose records.
|
|
117
|
+
ignore_eof: false,
|
|
118
|
+
&
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def build_request(method, path, query, body, options, accept)
|
|
123
|
+
request_class = method == "POST" ? Net::HTTP::Post : Net::HTTP::Get
|
|
124
|
+
request = request_class.new(request_target(path, query), request_headers(accept, body, options))
|
|
125
|
+
if body
|
|
126
|
+
request.body = JSON.generate(body)
|
|
127
|
+
elsif method == "POST"
|
|
128
|
+
request["Content-Length"] = "0"
|
|
129
|
+
# Net::HTTP would otherwise label an empty POST as a submitted form.
|
|
130
|
+
request["Content-Type"] = "application/json"
|
|
131
|
+
end
|
|
132
|
+
request
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def request_target(path, query)
|
|
136
|
+
target = "#{@base_uri.path}#{path}"
|
|
137
|
+
pairs = (query || {}).flat_map do |key, value|
|
|
138
|
+
next [] if value.nil?
|
|
139
|
+
|
|
140
|
+
Array(value).map { |item| [key.to_s, item.to_s] }
|
|
141
|
+
end
|
|
142
|
+
pairs.empty? ? target : "#{target}?#{URI.encode_www_form(pairs)}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def request_headers(accept, body, options)
|
|
146
|
+
headers = {
|
|
147
|
+
"accept" => accept,
|
|
148
|
+
"authorization" => "Bearer #{@api_key}",
|
|
149
|
+
"user-agent" => "companyenrich-ruby/#{VERSION}"
|
|
150
|
+
}
|
|
151
|
+
headers["content-type"] = "application/json" if body
|
|
152
|
+
[@default_headers, options[:headers]].compact.each do |extra|
|
|
153
|
+
extra.each { |name, value| headers[name.to_s.downcase] = value }
|
|
154
|
+
end
|
|
155
|
+
headers
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def raise_for_status(response)
|
|
159
|
+
status = response.code.to_i
|
|
160
|
+
return if status.between?(200, 299)
|
|
161
|
+
|
|
162
|
+
raise CompanyEnrich.api_error(
|
|
163
|
+
status: status, headers: response_headers(response), body: parse_error_body(response.read_body)
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def response_headers(response)
|
|
168
|
+
response.to_hash.transform_values { |values| values.join(", ") }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Hands each chunk to the caller. Network failures become SDK errors, while exceptions
|
|
172
|
+
# raised by the caller's own block pass through unchanged.
|
|
173
|
+
def read_chunks(response)
|
|
174
|
+
caller_failure = nil
|
|
175
|
+
response.read_body do |chunk|
|
|
176
|
+
yield chunk
|
|
177
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
178
|
+
caller_failure = e
|
|
179
|
+
raise
|
|
180
|
+
end
|
|
181
|
+
rescue *TRANSPORT_ERRORS => e
|
|
182
|
+
raise if e.equal?(caller_failure)
|
|
183
|
+
raise APITimeoutError, "Download timed out: #{e.message}" if timeout_error?(e)
|
|
184
|
+
|
|
185
|
+
raise APIConnectionError, "Download was interrupted: #{e.message}"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def normalize_failure(error)
|
|
189
|
+
return error if error.is_a?(APIError)
|
|
190
|
+
|
|
191
|
+
if timeout_error?(error)
|
|
192
|
+
APITimeoutError.new("Request timed out: #{error.message}")
|
|
193
|
+
else
|
|
194
|
+
APIConnectionError.new("Could not reach the CompanyEnrich API: #{error.message}")
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def timeout_error?(error)
|
|
199
|
+
TIMEOUT_ERRORS.any? { |type| error.is_a?(type) }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# @return [Numeric, nil] how long to wait before retrying, or nil if the request must not be retried
|
|
203
|
+
def retry_delay(failure, attempt, safe)
|
|
204
|
+
return safe ? backoff(attempt) : nil unless failure.is_a?(APIError)
|
|
205
|
+
|
|
206
|
+
status = failure.status
|
|
207
|
+
# A 429 is rejected before any work happens, so it is safe to repeat for every request.
|
|
208
|
+
return nil unless status == 429 || (safe && RETRYABLE_STATUSES.include?(status))
|
|
209
|
+
|
|
210
|
+
requested = parse_retry_after(failure.headers["retry-after"])
|
|
211
|
+
requested ||= parse_timespan(failure.headers["x-ratelimit-window"]) if status == 429
|
|
212
|
+
return backoff(attempt) if requested.nil?
|
|
213
|
+
|
|
214
|
+
requested <= MAX_SERVER_REQUESTED_WAIT ? requested : nil
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def backoff(attempt)
|
|
218
|
+
ceiling = [MAX_BACKOFF, BASE_BACKOFF * (2**attempt)].min
|
|
219
|
+
ceiling * (0.75 + (rand * 0.25))
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def parse_retry_after(value)
|
|
223
|
+
return nil if value.nil? || value.empty?
|
|
224
|
+
|
|
225
|
+
[Float(value), 0.0].max
|
|
226
|
+
rescue ArgumentError
|
|
227
|
+
begin
|
|
228
|
+
[Time.httpdate(value) - Time.now, 0.0].max
|
|
229
|
+
rescue ArgumentError
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Parses a .NET TimeSpan such as 00:00:35.7551671 into seconds.
|
|
235
|
+
def parse_timespan(value)
|
|
236
|
+
match = TIMESPAN.match(value.to_s)
|
|
237
|
+
return nil unless match
|
|
238
|
+
|
|
239
|
+
(match[1].to_i * 3600) + (match[2].to_i * 60) + match[3].to_f
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def parse_json(content, status)
|
|
243
|
+
return nil if content.nil? || content.empty?
|
|
244
|
+
|
|
245
|
+
JSON.parse(content)
|
|
246
|
+
rescue JSON::ParserError
|
|
247
|
+
raise Error, "The API returned an invalid JSON response (status #{status})"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def parse_error_body(content)
|
|
251
|
+
text = content.to_s.dup.force_encoding(Encoding::UTF_8).scrub
|
|
252
|
+
JSON.parse(text)
|
|
253
|
+
rescue JSON::ParserError
|
|
254
|
+
text
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
|
|
7
|
+
module CompanyEnrich
|
|
8
|
+
# Splits a byte stream into JSON records, keeping partial lines between chunks.
|
|
9
|
+
# @api private
|
|
10
|
+
class NdjsonDecoder
|
|
11
|
+
def initialize
|
|
12
|
+
@buffer = String.new(encoding: Encoding::BINARY)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [Array] the records completed by this chunk
|
|
16
|
+
def feed(chunk)
|
|
17
|
+
@buffer << chunk.b
|
|
18
|
+
lines = @buffer.split("\n", -1)
|
|
19
|
+
@buffer = lines.pop || String.new(encoding: Encoding::BINARY)
|
|
20
|
+
parse(lines)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Array] the final record, if the stream did not end with a newline
|
|
24
|
+
def flush
|
|
25
|
+
rest = @buffer
|
|
26
|
+
@buffer = String.new(encoding: Encoding::BINARY)
|
|
27
|
+
parse([rest])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def parse(lines)
|
|
33
|
+
lines.reject { |line| line.strip.empty? }.map do |line|
|
|
34
|
+
JSON.parse(line.dup.force_encoding(Encoding::UTF_8))
|
|
35
|
+
rescue JSON::ParserError
|
|
36
|
+
raise Error, "The API returned an invalid NDJSON record"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support"
|
|
4
|
+
|
|
5
|
+
module CompanyEnrich
|
|
6
|
+
module Resources
|
|
7
|
+
# An asynchronous job: create it, wait with +batches.wait_for_completion+, then download its results.
|
|
8
|
+
class BatchJob
|
|
9
|
+
# @api private
|
|
10
|
+
def initialize(http, path)
|
|
11
|
+
@http = http
|
|
12
|
+
@path = path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Downloads every settled record as one JSON document.
|
|
16
|
+
def results(batch_id, sig: nil, options: nil)
|
|
17
|
+
@http.request("GET", results_path(batch_id), query: { format: "json", sig: sig }, options: options)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Streams settled records one at a time. Prefer this for large jobs.
|
|
21
|
+
# @return [Enumerator] when no block is given
|
|
22
|
+
def stream(batch_id, sig: nil, options: nil, &block)
|
|
23
|
+
enumerator = Enumerator.new do |yielder|
|
|
24
|
+
query = { format: "ndjson", sig: sig }
|
|
25
|
+
@http.stream_ndjson(results_path(batch_id), query: query, options: options) do |record|
|
|
26
|
+
yielder << record
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
Support.iterate(enumerator, block)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Downloads the results as CSV text. Use {#csv_chunks} for large jobs.
|
|
33
|
+
# @return [String]
|
|
34
|
+
def csv(batch_id, sig: nil, options: nil)
|
|
35
|
+
csv_chunks(batch_id, sig: sig, options: options).to_a.join
|
|
36
|
+
.force_encoding(Encoding::UTF_8).delete_prefix("")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Yields the CSV download as raw bytes, for example to write it to a file.
|
|
40
|
+
# @return [Enumerator] when no block is given
|
|
41
|
+
def csv_chunks(batch_id, sig: nil, options: nil, &block)
|
|
42
|
+
enumerator = Enumerator.new do |yielder|
|
|
43
|
+
query = { format: "csv", sig: sig }
|
|
44
|
+
@http.stream_chunks(results_path(batch_id), query: query, options: options) do |chunk|
|
|
45
|
+
yielder << chunk
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
Support.iterate(enumerator, block)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def create_job(body, options)
|
|
54
|
+
@http.request("POST", @path, body: body.compact, options: options)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def results_path(batch_id)
|
|
58
|
+
"#{@path}/#{Support.escape(batch_id)}/results"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
require_relative "support"
|
|
5
|
+
|
|
6
|
+
module CompanyEnrich
|
|
7
|
+
module Resources
|
|
8
|
+
class Batches
|
|
9
|
+
DEFAULT_POLL_INTERVAL = 2.0
|
|
10
|
+
|
|
11
|
+
# @api private
|
|
12
|
+
def initialize(http)
|
|
13
|
+
@http = http
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns one page of your batches, newest first.
|
|
17
|
+
def list(kind: nil, page_size: nil, cursor: nil, options: nil)
|
|
18
|
+
@http.request("GET", "/v2/batches", query: { kind: kind, pageSize: page_size, cursor: cursor },
|
|
19
|
+
options: options)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Iterates over all of your batches, newest first.
|
|
23
|
+
# @return [Enumerator] when no block is given
|
|
24
|
+
def list_all(kind: nil, page_size: nil, cursor: nil, options: nil, &block)
|
|
25
|
+
enumerator = Support.paginate(cursor) do |next_cursor|
|
|
26
|
+
list(kind: kind, page_size: page_size, cursor: next_cursor, options: options)
|
|
27
|
+
end
|
|
28
|
+
Support.iterate(enumerator, block)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Retrieves a batch's status and progress.
|
|
32
|
+
def get(batch_id, options: nil)
|
|
33
|
+
@http.request("GET", "/v2/batches/#{Support.escape(batch_id)}", options: options)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Stops remaining work. Results that were already charged stay downloadable.
|
|
37
|
+
def cancel(batch_id, options: nil)
|
|
38
|
+
@http.request("POST", "/v2/batches/#{Support.escape(batch_id)}/cancel", retry_safe: true, options: options)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Streams the raw HTTP outcome of each request in the batch.
|
|
42
|
+
# @return [Enumerator] when no block is given
|
|
43
|
+
def raw_results(batch_id, sig: nil, options: nil, &block)
|
|
44
|
+
enumerator = Enumerator.new do |yielder|
|
|
45
|
+
path = "/v2/batches/#{Support.escape(batch_id)}/results"
|
|
46
|
+
@http.stream_ndjson(path, query: { sig: sig }, options: options) { |record| yielder << record }
|
|
47
|
+
end
|
|
48
|
+
Support.iterate(enumerator, block)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Polls until the batch is +completed+ or +cancelled+, then returns its final status.
|
|
52
|
+
# Yields the latest status after every check. Raises {APITimeoutError} if +timeout+ seconds
|
|
53
|
+
# pass first, and waits indefinitely by default.
|
|
54
|
+
def wait_for_completion(batch_id, poll_interval: DEFAULT_POLL_INTERVAL, timeout: nil, options: nil)
|
|
55
|
+
deadline = timeout && (now + timeout)
|
|
56
|
+
while true # rubocop:disable Style/InfiniteLoop
|
|
57
|
+
batch = get(batch_id, options: options)
|
|
58
|
+
yield batch if block_given?
|
|
59
|
+
return batch if %w[completed cancelled].include?(batch["status"])
|
|
60
|
+
|
|
61
|
+
wait = poll_interval
|
|
62
|
+
if deadline
|
|
63
|
+
remaining = deadline - now
|
|
64
|
+
raise APITimeoutError, "Batch #{batch_id} did not finish within #{timeout} seconds" if remaining <= 0
|
|
65
|
+
|
|
66
|
+
wait = [poll_interval, remaining].min
|
|
67
|
+
end
|
|
68
|
+
@http.pause(wait)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def now
|
|
75
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "batch_job"
|
|
4
|
+
require_relative "support"
|
|
5
|
+
|
|
6
|
+
module CompanyEnrich
|
|
7
|
+
module Resources
|
|
8
|
+
# Asynchronous enrichment of up to 10,000 domains.
|
|
9
|
+
class CompanyBulkEnrichment < BatchJob
|
|
10
|
+
# Starts the job. It runs in the background and consumes credits as it progresses.
|
|
11
|
+
def create(domains, expand: nil, wait_for_enrichment: nil, options: nil)
|
|
12
|
+
create_job({ domains: domains, expand: expand, waitForEnrichment: wait_for_enrichment }, options)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Asynchronous export of a company search.
|
|
17
|
+
class CompanySearchExport < BatchJob
|
|
18
|
+
# Starts the export. It runs in the background and consumes credits as it progresses.
|
|
19
|
+
def create(limit, filter: nil, semantic: nil, lookalike: nil, expand: nil, options: nil)
|
|
20
|
+
create_job({ limit: limit, filter: filter, semantic: semantic, lookalike: lookalike, expand: expand }, options)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Companies
|
|
25
|
+
# @return [CompanyBulkEnrichment]
|
|
26
|
+
attr_reader :bulk_enrichment
|
|
27
|
+
# @return [CompanySearchExport]
|
|
28
|
+
attr_reader :search_export
|
|
29
|
+
|
|
30
|
+
# @api private
|
|
31
|
+
def initialize(http)
|
|
32
|
+
@http = http
|
|
33
|
+
@bulk_enrichment = CompanyBulkEnrichment.new(http, "/v2/companies/enrich/bulk")
|
|
34
|
+
@search_export = CompanySearchExport.new(http, "/v2/companies/search/export")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Retrieves a stored company by its CompanyEnrich ID.
|
|
38
|
+
def get(company_id, expand: nil, options: nil)
|
|
39
|
+
@http.request("GET", "/v2/companies/#{Support.escape(company_id)}", query: { expand: expand }, options: options)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Enriches a company by domain. Raises {NotFoundError} when there is no match.
|
|
43
|
+
def enrich(domain, expand: nil, wait_for_enrichment: nil, options: nil)
|
|
44
|
+
query = { domain: domain, expand: expand, waitForEnrichment: wait_for_enrichment }
|
|
45
|
+
@http.request("GET", "/v2/companies/enrich", query: query, options: options)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Enriches a company from identifying properties such as its name or LinkedIn URL.
|
|
49
|
+
def enrich_by_properties(name: nil, linkedin_url: nil, linkedin_id: nil, twitter_url: nil, facebook_url: nil,
|
|
50
|
+
instagram_url: nil, youtube_url: nil, expand: nil, options: nil)
|
|
51
|
+
body = {
|
|
52
|
+
name: name, linkedinUrl: linkedin_url, linkedinId: linkedin_id, twitterUrl: twitter_url,
|
|
53
|
+
facebookUrl: facebook_url, instagramUrl: instagram_url, youtubeUrl: youtube_url, expand: expand
|
|
54
|
+
}.compact
|
|
55
|
+
@http.request("POST", "/v2/companies/enrich", body: body, options: options)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Enriches up to 50 domains in one request.
|
|
59
|
+
def enrich_batch(domains, expand: nil, wait_for_enrichment: nil, options: nil)
|
|
60
|
+
body = { domains: domains, expand: expand, waitForEnrichment: wait_for_enrichment }.compact
|
|
61
|
+
@http.request("POST", "/v2/companies/enrich/batch", body: body, options: options)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Suggests up to 10 companies whose domain matches +query+.
|
|
65
|
+
def autocomplete(query, options: nil)
|
|
66
|
+
@http.request("GET", "/v2/companies/autocomplete", query: { query: query }, options: options)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Retrieves workforce insights for exactly one of +company_id+ or +domain+.
|
|
70
|
+
# Pass +nextCursor+ from the response as +cursor+ to go back another 12 months.
|
|
71
|
+
def workforce(company_id: nil, domain: nil, cursor: nil, options: nil)
|
|
72
|
+
raise ArgumentError, "Provide exactly one of `company_id:` or `domain:`." if company_id.nil? == domain.nil?
|
|
73
|
+
|
|
74
|
+
query = { id: company_id, domain: domain, cursor: cursor }
|
|
75
|
+
@http.request("GET", "/v2/companies/workforce", query: query, options: options)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns one page of companies matching filters, a semantic query, or lookalike seeds.
|
|
79
|
+
def search(filter: nil, semantic: nil, lookalike: nil, page_size: nil, cursor: nil, expand: nil, options: nil)
|
|
80
|
+
body = {
|
|
81
|
+
filter: filter, semantic: semantic, lookalike: lookalike,
|
|
82
|
+
pageSize: page_size, cursor: cursor, expand: expand
|
|
83
|
+
}.compact
|
|
84
|
+
@http.request("POST", "/v2/companies/search", body: body, options: options)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Iterates over every matching company, fetching pages as needed. Accepts the same arguments as {#search}.
|
|
88
|
+
# Credits are charged per page, so set +page_size+ close to what you will consume.
|
|
89
|
+
# @return [Enumerator] when no block is given
|
|
90
|
+
def search_all(cursor: nil, options: nil, **params, &block)
|
|
91
|
+
enumerator = Support.paginate(cursor) do |next_cursor|
|
|
92
|
+
search(**params, cursor: next_cursor, options: options)
|
|
93
|
+
end
|
|
94
|
+
Support.iterate(enumerator, block)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Counts matching companies. Free.
|
|
98
|
+
def count(filter: nil, semantic: nil, lookalike: nil, options: nil)
|
|
99
|
+
body = { filter: filter, semantic: semantic, lookalike: lookalike }.compact
|
|
100
|
+
@http.request("POST", "/v2/companies/search/count", body: body, retry_safe: true, options: options)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Returns up to 25 company summaries. Free, but requires a plan with previews.
|
|
104
|
+
def preview(filter: nil, semantic: nil, lookalike: nil, options: nil)
|
|
105
|
+
body = { filter: filter, semantic: semantic, lookalike: lookalike }.compact
|
|
106
|
+
@http.request("POST", "/v2/companies/search/preview", body: body, retry_safe: true, options: options)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support"
|
|
4
|
+
|
|
5
|
+
module CompanyEnrich
|
|
6
|
+
module Resources
|
|
7
|
+
# Geographic reference data for search filters. Every endpoint here is free.
|
|
8
|
+
class Geo
|
|
9
|
+
# @api private
|
|
10
|
+
def initialize(http)
|
|
11
|
+
@http = http
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Lists all regions and their country codes.
|
|
15
|
+
def regions(options: nil)
|
|
16
|
+
@http.request("GET", "/v2/geo/regions", options: options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Retrieves a country by its two-letter code.
|
|
20
|
+
def country(country_code, options: nil)
|
|
21
|
+
@http.request("GET", "/v2/geo/countries/#{Support.escape(country_code)}", options: options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def search_countries(query: nil, page_size: nil, cursor: nil, options: nil)
|
|
25
|
+
search("/v2/geo/countries", { query: query, pageSize: page_size, cursor: cursor }, options)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Enumerator] when no block is given
|
|
29
|
+
def search_countries_all(cursor: nil, options: nil, **params, &block)
|
|
30
|
+
enumerator = Support.paginate(cursor) do |next_cursor|
|
|
31
|
+
search_countries(**params, cursor: next_cursor, options: options)
|
|
32
|
+
end
|
|
33
|
+
Support.iterate(enumerator, block)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def search_states(country_codes: nil, query: nil, page_size: nil, cursor: nil, options: nil)
|
|
37
|
+
body = { countryCodes: country_codes, query: query, pageSize: page_size, cursor: cursor }
|
|
38
|
+
search("/v2/geo/states", body, options)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Enumerator] when no block is given
|
|
42
|
+
def search_states_all(cursor: nil, options: nil, **params, &block)
|
|
43
|
+
enumerator = Support.paginate(cursor) do |next_cursor|
|
|
44
|
+
search_states(**params, cursor: next_cursor, options: options)
|
|
45
|
+
end
|
|
46
|
+
Support.iterate(enumerator, block)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def search_cities(state_ids: nil, country_codes: nil, query: nil, page_size: nil, cursor: nil, options: nil)
|
|
50
|
+
body = { stateIds: state_ids, countryCodes: country_codes, query: query, pageSize: page_size, cursor: cursor }
|
|
51
|
+
search("/v2/geo/cities", body, options)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Enumerator] when no block is given
|
|
55
|
+
def search_cities_all(cursor: nil, options: nil, **params, &block)
|
|
56
|
+
enumerator = Support.paginate(cursor) do |next_cursor|
|
|
57
|
+
search_cities(**params, cursor: next_cursor, options: options)
|
|
58
|
+
end
|
|
59
|
+
Support.iterate(enumerator, block)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def search(path, body, options)
|
|
65
|
+
@http.request("POST", path, body: body.compact, retry_safe: true, options: options)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "batch_job"
|
|
4
|
+
require_relative "support"
|
|
5
|
+
|
|
6
|
+
module CompanyEnrich
|
|
7
|
+
module Resources
|
|
8
|
+
# Asynchronous export of a people search.
|
|
9
|
+
class PersonSearchExport < BatchJob
|
|
10
|
+
# Starts the export. It runs in the background and consumes credits as it progresses.
|
|
11
|
+
def create(limit, filter: nil, semantic: nil, company: nil, expand: nil, options: nil)
|
|
12
|
+
create_job({ limit: limit, filter: filter, semantic: semantic, company: company, expand: expand }, options)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class People
|
|
17
|
+
# @return [PersonSearchExport]
|
|
18
|
+
attr_reader :search_export
|
|
19
|
+
|
|
20
|
+
# @api private
|
|
21
|
+
def initialize(http)
|
|
22
|
+
@http = http
|
|
23
|
+
@search_export = PersonSearchExport.new(http, "/v2/people/search/export")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Retrieves a person by their CompanyEnrich ID.
|
|
27
|
+
def get(person_id, expand: nil, options: nil)
|
|
28
|
+
@http.request("GET", "/v2/people/#{Support.escape(person_id)}", query: { expand: expand }, options: options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Identifies a person from an email. Raises {NotFoundError} if there is no match.
|
|
32
|
+
def enrich(email, expand: nil, options: nil)
|
|
33
|
+
@http.request("POST", "/v2/people/enrich", body: { email: email, expand: expand }.compact, options: options)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Finds a person's work email. Check +status+: only +found+ includes +email+.
|
|
37
|
+
def find_email(person_id, domain: nil, options: nil)
|
|
38
|
+
@http.request("GET", "/v2/people/#{Support.escape(person_id)}/email", query: { domain: domain },
|
|
39
|
+
options: options)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns one page of people matching filters, a semantic query, or a company query.
|
|
43
|
+
def search(filter: nil, semantic: nil, company: nil, page_size: nil, cursor: nil, expand: nil, options: nil)
|
|
44
|
+
body = {
|
|
45
|
+
filter: filter, semantic: semantic, company: company,
|
|
46
|
+
pageSize: page_size, cursor: cursor, expand: expand
|
|
47
|
+
}.compact
|
|
48
|
+
@http.request("POST", "/v2/people/search", body: body, options: options)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Iterates over every matching person, fetching pages as needed. Accepts the same arguments as {#search}.
|
|
52
|
+
# Credits are charged per page, so set +page_size+ close to what you will consume.
|
|
53
|
+
# @return [Enumerator] when no block is given
|
|
54
|
+
def search_all(cursor: nil, options: nil, **params, &block)
|
|
55
|
+
enumerator = Support.paginate(cursor) do |next_cursor|
|
|
56
|
+
search(**params, cursor: next_cursor, options: options)
|
|
57
|
+
end
|
|
58
|
+
Support.iterate(enumerator, block)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Counts matching people. Free.
|
|
62
|
+
def count(filter: nil, semantic: nil, company: nil, options: nil)
|
|
63
|
+
body = { filter: filter, semantic: semantic, company: company }.compact
|
|
64
|
+
@http.request("POST", "/v2/people/search/count", body: body, retry_safe: true, options: options)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns up to 10 person summaries. Free, but requires a plan with previews.
|
|
68
|
+
def preview(filter: nil, semantic: nil, company: nil, options: nil)
|
|
69
|
+
body = { filter: filter, semantic: semantic, company: company }.compact
|
|
70
|
+
@http.request("POST", "/v2/people/search/preview", body: body, retry_safe: true, options: options)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CompanyEnrich
|
|
4
|
+
module Resources
|
|
5
|
+
class Industries
|
|
6
|
+
# @api private
|
|
7
|
+
def initialize(http)
|
|
8
|
+
@http = http
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Lists all industries and their NAICS code prefixes. Free.
|
|
12
|
+
def list(options: nil)
|
|
13
|
+
@http.request("GET", "/v2/industries", options: options)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Suggests up to 10 values for a search filter, ordered by popularity. Free.
|
|
18
|
+
class Autocomplete
|
|
19
|
+
# @api private
|
|
20
|
+
def initialize(http, path)
|
|
21
|
+
@http = http
|
|
22
|
+
@path = path
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def autocomplete(query, options: nil)
|
|
26
|
+
@http.request("GET", @path, query: { query: query }, options: options)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
|
|
5
|
+
module CompanyEnrich
|
|
6
|
+
module Resources
|
|
7
|
+
# Helpers shared by the resource classes.
|
|
8
|
+
# @api private
|
|
9
|
+
module Support
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def escape(value)
|
|
13
|
+
ERB::Util.url_encode(value.to_s)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Runs the enumerator with the caller's block, or returns it for lazy use.
|
|
17
|
+
def iterate(enumerator, block)
|
|
18
|
+
block ? enumerator.each(&block) : enumerator
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Builds a lazy enumerator that requests a page only when it is needed.
|
|
22
|
+
def paginate(first_cursor, &fetch_page)
|
|
23
|
+
Enumerator.new do |yielder|
|
|
24
|
+
cursor = first_cursor
|
|
25
|
+
fetching = true
|
|
26
|
+
while fetching
|
|
27
|
+
page = fetch_page.call(cursor)
|
|
28
|
+
page.fetch("items").each { |item| yielder << item }
|
|
29
|
+
cursor = page["nextCursor"]
|
|
30
|
+
fetching = !(cursor.nil? || cursor.empty?)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: companyenrich
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- CompanyEnrich
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Company and people enrichment, filter, semantic and lookalike search,
|
|
13
|
+
work emails, and bulk jobs through the CompanyEnrich API.
|
|
14
|
+
email:
|
|
15
|
+
- support@companyenrich.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/companyenrich.rb
|
|
23
|
+
- lib/companyenrich/client.rb
|
|
24
|
+
- lib/companyenrich/errors.rb
|
|
25
|
+
- lib/companyenrich/http_client.rb
|
|
26
|
+
- lib/companyenrich/ndjson_decoder.rb
|
|
27
|
+
- lib/companyenrich/resources/batch_job.rb
|
|
28
|
+
- lib/companyenrich/resources/batches.rb
|
|
29
|
+
- lib/companyenrich/resources/companies.rb
|
|
30
|
+
- lib/companyenrich/resources/geo.rb
|
|
31
|
+
- lib/companyenrich/resources/people.rb
|
|
32
|
+
- lib/companyenrich/resources/reference.rb
|
|
33
|
+
- lib/companyenrich/resources/support.rb
|
|
34
|
+
- lib/companyenrich/version.rb
|
|
35
|
+
homepage: https://github.com/CompanyEnrich/companyenrich-ruby-sdk
|
|
36
|
+
licenses:
|
|
37
|
+
- MIT
|
|
38
|
+
metadata:
|
|
39
|
+
homepage_uri: https://companyenrich.com
|
|
40
|
+
source_code_uri: https://github.com/CompanyEnrich/companyenrich-ruby-sdk
|
|
41
|
+
bug_tracker_uri: https://github.com/CompanyEnrich/companyenrich-ruby-sdk/issues
|
|
42
|
+
rubygems_mfa_required: 'true'
|
|
43
|
+
rdoc_options: []
|
|
44
|
+
require_paths:
|
|
45
|
+
- lib
|
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - ">="
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '3.1'
|
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '0'
|
|
56
|
+
requirements: []
|
|
57
|
+
rubygems_version: 3.6.9
|
|
58
|
+
specification_version: 4
|
|
59
|
+
summary: Official Ruby SDK for the CompanyEnrich API.
|
|
60
|
+
test_files: []
|