parseapi 1.8.0__tar.gz → 1.9.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.
- {parseapi-1.8.0 → parseapi-1.9.0}/CHANGELOG.md +5 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/PKG-INFO +39 -4
- {parseapi-1.8.0 → parseapi-1.9.0}/README.md +38 -3
- {parseapi-1.8.0 → parseapi-1.9.0}/api/public.json +200 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/pyproject.toml +1 -1
- {parseapi-1.8.0 → parseapi-1.9.0}/src/parseapi/__init__.py +1 -1
- {parseapi-1.8.0 → parseapi-1.9.0}/src/parseapi/_client.py +116 -2
- parseapi-1.9.0/tests/test_company_directory.py +160 -0
- parseapi-1.9.0/tests/typing_company_directory.py +32 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/.github/workflows/publish.yml +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/.github/workflows/test.yml +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/.gitignore +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/LICENSE +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/scripts/public_api.py +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/smoke/smoke.py +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/src/parseapi/py.typed +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/tests/bank-fixtures.json +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/tests/test_bank.py +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/tests/test_card_dx.py +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/tests/test_client.py +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/tests/test_email_enrichment.py +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/tests/test_language.py +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/tests/test_name_formatting.py +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/tests/test_postal_localities.py +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/tests/test_public_api.py +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/tests/test_stack.py +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/tests/test_tariff.py +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/tests/test_time_complete.py +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/tests/test_time_gaps.py +0 -0
- {parseapi-1.8.0 → parseapi-1.9.0}/tests/test_version.py +0 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.0 — 2026-09-29
|
|
4
|
+
|
|
5
|
+
- Add Company stable-ID lookup, candidate search and coverage for the current public-company cohort.
|
|
6
|
+
- Preserve existing Company calls and all published compatibility methods.
|
|
7
|
+
|
|
3
8
|
## 1.8.0 — 2026-09-25
|
|
4
9
|
|
|
5
10
|
- Add Bank diagnostics, POST requests, requirements and explicit US ACH helpers; Card Core and optional Deep; Provider; Industry; and Vehicle.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: parseapi
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: Official ParseAPI client for Python. One key, minimal JSON, fast.
|
|
5
5
|
Project-URL: Homepage, https://parseapi.com
|
|
6
6
|
Project-URL: Documentation, https://parseapi.com/docs
|
|
@@ -36,7 +36,7 @@ Get a key at [parseapi.com](https://parseapi.com). The client also reads `PARSEA
|
|
|
36
36
|
|
|
37
37
|
## API versions
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
This SDK explicitly selects the API contract supported by this SDK. It sends `Parse-Version: 2.0.0` on every lookup so responses match the API contract supported by the package. Your key and the team's saved default stay the same.
|
|
40
40
|
|
|
41
41
|
Upgrade the dependency in staging, review the [release notes](https://parseapi.com/docs/releases), and test the application before deploying the same code and dependency version to production. Commit your dependency lockfile so the tested package travels with your deployment. Future major SDK upgrades can select a newer API contract.
|
|
42
42
|
|
|
@@ -66,6 +66,36 @@ Results are plain data. Pass a returned code or coordinate to another operation
|
|
|
66
66
|
|
|
67
67
|
Use `parse.postal("28202", country="US", deep=True)` for US ZIP tax references. `deep.tax` names the levy and `deep.tax_rate` is a percentage, so `7.9` means 7.9%. The state, county, city and special components explain that combined rate. An exact address can differ. Country and state lookups provide their own geographic reference rates, which should not be added to the ZIP rate. `None` means unknown and `0` means known zero. Country `deep.tax_id_format` and `deep.tax_id_regex` describe registration-number format only. Use `vat` for a metered registration check with `deep` explicitly enabled.
|
|
68
68
|
|
|
69
|
+
## Company directory
|
|
70
|
+
|
|
71
|
+
Find candidates, then fetch the profile you selected.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
candidates = parse.company.search(query="GitLab", country="US", limit=5)
|
|
75
|
+
selected_id = "co_caczn6wf36hj" # Explicitly chosen after reviewing candidates.
|
|
76
|
+
profile = parse.company.id(selected_id, deep=True)
|
|
77
|
+
if candidates["next"] is not None:
|
|
78
|
+
next_page = parse.company.search(
|
|
79
|
+
query="GitLab", country="US", limit=5, cursor=candidates["next"]
|
|
80
|
+
)
|
|
81
|
+
coverage = parse.company.coverage()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Use at most one selector: `query` for a name, `domain`, `ticker`, or `identifier`; country or exact industry filters also allow discovery without a selector. The API validates selectors and filters. Use `country` to scope candidates, `exchange` with a ticker, and `authority` with an identifier. Search returns `companies` and an opaque `next` cursor. Review candidate identity and match details before choosing a stable Company ID. Pass `next` as `cursor` with the same selector, filters and limit to continue that result set.
|
|
85
|
+
|
|
86
|
+
Directory profiles are plain JSON data. `deep` belongs to each company in search results and adds legal/reference detail plus nullable `description`, `logo`, `founded`, and the `social_profiles`, `phone_numbers`, `email_addresses`, `domains` and `sources` collections. A logo is a reported URL. `founded` has `value` and `precision`, distinct from incorporation. Sources identify the website, filing or business register, supported fields and observation/update timestamps. Employee observations retain count, measurement date, organization scope and approximation; null means unknown. Missing, null, empty and unknown fields retain their response values. An empty search is a successful result. Invalid inputs and unknown IDs raise the existing API error.
|
|
87
|
+
|
|
88
|
+
Company directory social profiles contain `platform`, `url` and `handle`; unknown metadata stays null. Phone and email records use `type` for the source-reported purpose. Earlier response fields remain readable by the client.
|
|
89
|
+
|
|
90
|
+
The recipe requests one candidate page, one explicitly chosen profile and directory coverage, plus a second page when a cursor is returned. Each operation uses the existing retry settings. The number-validation call remains unchanged. `lang` applies to national company-number lookup, while directory calls use the source labels.
|
|
91
|
+
|
|
92
|
+
`AsyncParseAPI` exposes the same `company`, `company.id`, `company.search` and `company.coverage` calls with `await`.
|
|
93
|
+
|
|
94
|
+
Discovery example: `parse.company.search(country="US", industry="0700", industry_type="sic")`
|
|
95
|
+
|
|
96
|
+
Supply `industry` and `industry_type` together. The supported namespace is `sic`, with an exact four-digit string such as `0700`; leading zeros are meaningful. Country-only discovery is also supported. Filters intersect and may narrow an existing selector. Country matches the profile country, not a headquarters or operating-presence claim. Unknown values do not match a requested filter. Filter-only candidates use `match.field: "filters"` and `match.value: null`; reuse the same filters and limit with a returned cursor. Counts describe this directory edition, not complete country coverage.
|
|
97
|
+
|
|
98
|
+
|
|
69
99
|
## Calls
|
|
70
100
|
|
|
71
101
|
One method per endpoint, named after the route.
|
|
@@ -89,6 +119,11 @@ parse.postal.distance("28202", "10001", country="US")
|
|
|
89
119
|
parse.address("1600 Pennsylvania Ave NW, Washington DC", country="US")
|
|
90
120
|
parse.address.search("1600 Pennsylvania", country="US", postal="20500")
|
|
91
121
|
parse.company("51 824 753 556", country="AU")
|
|
122
|
+
parse.company.id("co_caczn6wf36hj", deep=True)
|
|
123
|
+
parse.company.search(domain="about.gitlab.com")
|
|
124
|
+
parse.company.search(ticker="GTLB", exchange="Nasdaq")
|
|
125
|
+
parse.company.search(identifier="0001653482", authority="sec")
|
|
126
|
+
parse.company.coverage()
|
|
92
127
|
parse.city("charlotte", country="US")
|
|
93
128
|
parse.city.id("city_mb8mbqrkz8zb")
|
|
94
129
|
parse.city.search("char", country="US", limit=10)
|
|
@@ -166,7 +201,7 @@ Choose display names for one request:
|
|
|
166
201
|
parse.country("DE", lang="fr")
|
|
167
202
|
```
|
|
168
203
|
|
|
169
|
-
`lang` is optional on geography lookups and their lists/searches, Currency lookup, Language, Date, Time/Timezone, Emoji lookup/search, and unit discovery. IP, ASN, Company and NPI also accept it for their geographic labels. Codes, native names, quantities and response structure retain their meanings. Source coverage determines which labels are translated; unavailable labels use the API's documented fallback.
|
|
204
|
+
`lang` is optional on geography lookups and their lists/searches, Currency lookup, Language, Date, Time/Timezone, Emoji lookup/search, and unit discovery. IP, ASN, national Company number lookup and NPI also accept it for their geographic labels. Codes, native names, quantities and response structure retain their meanings. Source coverage determines which labels are translated; unavailable labels use the API's documented fallback.
|
|
170
205
|
|
|
171
206
|
The next call keeps its usual default unless it also supplies `lang`. Existing `deep` rules still apply. Date `format` and measurement `locale` remain explicit input-parsing controls.
|
|
172
207
|
|
|
@@ -389,7 +424,7 @@ before dispatch, accepted input is forwarded unchanged. Never send a full card n
|
|
|
389
424
|
|
|
390
425
|
## Optional detail
|
|
391
426
|
|
|
392
|
-
The default response answers the common task. Ask for `deep` when you need more detail about that same result. Core fields stay equal. City, Industry and Emoji searches put detail inside each result. Postal nearby and distance put metropolitan detail beside the entity it describes. Time conversion keeps target detail in `to.deep
|
|
427
|
+
The default response answers the common task. Ask for `deep` when you need more detail about that same result. Core fields stay equal. City, Company directory, Industry and Emoji searches put detail inside each result. Postal nearby and distance put metropolitan detail beside the entity it describes. Time conversion keeps target detail in `to.deep` or each `targets` item; only the source has `deep.next_dst`.
|
|
393
428
|
|
|
394
429
|
```python
|
|
395
430
|
basic = parse.time("America/New_York")
|
|
@@ -17,7 +17,7 @@ Get a key at [parseapi.com](https://parseapi.com). The client also reads `PARSEA
|
|
|
17
17
|
|
|
18
18
|
## API versions
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
This SDK explicitly selects the API contract supported by this SDK. It sends `Parse-Version: 2.0.0` on every lookup so responses match the API contract supported by the package. Your key and the team's saved default stay the same.
|
|
21
21
|
|
|
22
22
|
Upgrade the dependency in staging, review the [release notes](https://parseapi.com/docs/releases), and test the application before deploying the same code and dependency version to production. Commit your dependency lockfile so the tested package travels with your deployment. Future major SDK upgrades can select a newer API contract.
|
|
23
23
|
|
|
@@ -47,6 +47,36 @@ Results are plain data. Pass a returned code or coordinate to another operation
|
|
|
47
47
|
|
|
48
48
|
Use `parse.postal("28202", country="US", deep=True)` for US ZIP tax references. `deep.tax` names the levy and `deep.tax_rate` is a percentage, so `7.9` means 7.9%. The state, county, city and special components explain that combined rate. An exact address can differ. Country and state lookups provide their own geographic reference rates, which should not be added to the ZIP rate. `None` means unknown and `0` means known zero. Country `deep.tax_id_format` and `deep.tax_id_regex` describe registration-number format only. Use `vat` for a metered registration check with `deep` explicitly enabled.
|
|
49
49
|
|
|
50
|
+
## Company directory
|
|
51
|
+
|
|
52
|
+
Find candidates, then fetch the profile you selected.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
candidates = parse.company.search(query="GitLab", country="US", limit=5)
|
|
56
|
+
selected_id = "co_caczn6wf36hj" # Explicitly chosen after reviewing candidates.
|
|
57
|
+
profile = parse.company.id(selected_id, deep=True)
|
|
58
|
+
if candidates["next"] is not None:
|
|
59
|
+
next_page = parse.company.search(
|
|
60
|
+
query="GitLab", country="US", limit=5, cursor=candidates["next"]
|
|
61
|
+
)
|
|
62
|
+
coverage = parse.company.coverage()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Use at most one selector: `query` for a name, `domain`, `ticker`, or `identifier`; country or exact industry filters also allow discovery without a selector. The API validates selectors and filters. Use `country` to scope candidates, `exchange` with a ticker, and `authority` with an identifier. Search returns `companies` and an opaque `next` cursor. Review candidate identity and match details before choosing a stable Company ID. Pass `next` as `cursor` with the same selector, filters and limit to continue that result set.
|
|
66
|
+
|
|
67
|
+
Directory profiles are plain JSON data. `deep` belongs to each company in search results and adds legal/reference detail plus nullable `description`, `logo`, `founded`, and the `social_profiles`, `phone_numbers`, `email_addresses`, `domains` and `sources` collections. A logo is a reported URL. `founded` has `value` and `precision`, distinct from incorporation. Sources identify the website, filing or business register, supported fields and observation/update timestamps. Employee observations retain count, measurement date, organization scope and approximation; null means unknown. Missing, null, empty and unknown fields retain their response values. An empty search is a successful result. Invalid inputs and unknown IDs raise the existing API error.
|
|
68
|
+
|
|
69
|
+
Company directory social profiles contain `platform`, `url` and `handle`; unknown metadata stays null. Phone and email records use `type` for the source-reported purpose. Earlier response fields remain readable by the client.
|
|
70
|
+
|
|
71
|
+
The recipe requests one candidate page, one explicitly chosen profile and directory coverage, plus a second page when a cursor is returned. Each operation uses the existing retry settings. The number-validation call remains unchanged. `lang` applies to national company-number lookup, while directory calls use the source labels.
|
|
72
|
+
|
|
73
|
+
`AsyncParseAPI` exposes the same `company`, `company.id`, `company.search` and `company.coverage` calls with `await`.
|
|
74
|
+
|
|
75
|
+
Discovery example: `parse.company.search(country="US", industry="0700", industry_type="sic")`
|
|
76
|
+
|
|
77
|
+
Supply `industry` and `industry_type` together. The supported namespace is `sic`, with an exact four-digit string such as `0700`; leading zeros are meaningful. Country-only discovery is also supported. Filters intersect and may narrow an existing selector. Country matches the profile country, not a headquarters or operating-presence claim. Unknown values do not match a requested filter. Filter-only candidates use `match.field: "filters"` and `match.value: null`; reuse the same filters and limit with a returned cursor. Counts describe this directory edition, not complete country coverage.
|
|
78
|
+
|
|
79
|
+
|
|
50
80
|
## Calls
|
|
51
81
|
|
|
52
82
|
One method per endpoint, named after the route.
|
|
@@ -70,6 +100,11 @@ parse.postal.distance("28202", "10001", country="US")
|
|
|
70
100
|
parse.address("1600 Pennsylvania Ave NW, Washington DC", country="US")
|
|
71
101
|
parse.address.search("1600 Pennsylvania", country="US", postal="20500")
|
|
72
102
|
parse.company("51 824 753 556", country="AU")
|
|
103
|
+
parse.company.id("co_caczn6wf36hj", deep=True)
|
|
104
|
+
parse.company.search(domain="about.gitlab.com")
|
|
105
|
+
parse.company.search(ticker="GTLB", exchange="Nasdaq")
|
|
106
|
+
parse.company.search(identifier="0001653482", authority="sec")
|
|
107
|
+
parse.company.coverage()
|
|
73
108
|
parse.city("charlotte", country="US")
|
|
74
109
|
parse.city.id("city_mb8mbqrkz8zb")
|
|
75
110
|
parse.city.search("char", country="US", limit=10)
|
|
@@ -147,7 +182,7 @@ Choose display names for one request:
|
|
|
147
182
|
parse.country("DE", lang="fr")
|
|
148
183
|
```
|
|
149
184
|
|
|
150
|
-
`lang` is optional on geography lookups and their lists/searches, Currency lookup, Language, Date, Time/Timezone, Emoji lookup/search, and unit discovery. IP, ASN, Company and NPI also accept it for their geographic labels. Codes, native names, quantities and response structure retain their meanings. Source coverage determines which labels are translated; unavailable labels use the API's documented fallback.
|
|
185
|
+
`lang` is optional on geography lookups and their lists/searches, Currency lookup, Language, Date, Time/Timezone, Emoji lookup/search, and unit discovery. IP, ASN, national Company number lookup and NPI also accept it for their geographic labels. Codes, native names, quantities and response structure retain their meanings. Source coverage determines which labels are translated; unavailable labels use the API's documented fallback.
|
|
151
186
|
|
|
152
187
|
The next call keeps its usual default unless it also supplies `lang`. Existing `deep` rules still apply. Date `format` and measurement `locale` remain explicit input-parsing controls.
|
|
153
188
|
|
|
@@ -370,7 +405,7 @@ before dispatch, accepted input is forwarded unchanged. Never send a full card n
|
|
|
370
405
|
|
|
371
406
|
## Optional detail
|
|
372
407
|
|
|
373
|
-
The default response answers the common task. Ask for `deep` when you need more detail about that same result. Core fields stay equal. City, Industry and Emoji searches put detail inside each result. Postal nearby and distance put metropolitan detail beside the entity it describes. Time conversion keeps target detail in `to.deep
|
|
408
|
+
The default response answers the common task. Ask for `deep` when you need more detail about that same result. Core fields stay equal. City, Company directory, Industry and Emoji searches put detail inside each result. Postal nearby and distance put metropolitan detail beside the entity it describes. Time conversion keeps target detail in `to.deep` or each `targets` item; only the source has `deep.next_dst`.
|
|
374
409
|
|
|
375
410
|
```python
|
|
376
411
|
basic = parse.time("America/New_York")
|
|
@@ -453,6 +453,106 @@
|
|
|
453
453
|
],
|
|
454
454
|
"returns": "Json"
|
|
455
455
|
},
|
|
456
|
+
"company.coverage": {
|
|
457
|
+
"async": true,
|
|
458
|
+
"parameters": [],
|
|
459
|
+
"returns": "Json"
|
|
460
|
+
},
|
|
461
|
+
"company.id": {
|
|
462
|
+
"async": true,
|
|
463
|
+
"parameters": [
|
|
464
|
+
{
|
|
465
|
+
"kind": "POSITIONAL_OR_KEYWORD",
|
|
466
|
+
"name": "id",
|
|
467
|
+
"type": "str"
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
"default": false,
|
|
471
|
+
"kind": "KEYWORD_ONLY",
|
|
472
|
+
"name": "deep",
|
|
473
|
+
"type": "bool"
|
|
474
|
+
}
|
|
475
|
+
],
|
|
476
|
+
"returns": "Json"
|
|
477
|
+
},
|
|
478
|
+
"company.search": {
|
|
479
|
+
"async": true,
|
|
480
|
+
"parameters": [
|
|
481
|
+
{
|
|
482
|
+
"default": null,
|
|
483
|
+
"kind": "KEYWORD_ONLY",
|
|
484
|
+
"name": "query",
|
|
485
|
+
"type": "Optional[str]"
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
"default": null,
|
|
489
|
+
"kind": "KEYWORD_ONLY",
|
|
490
|
+
"name": "domain",
|
|
491
|
+
"type": "Optional[str]"
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
"default": null,
|
|
495
|
+
"kind": "KEYWORD_ONLY",
|
|
496
|
+
"name": "ticker",
|
|
497
|
+
"type": "Optional[str]"
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
"default": null,
|
|
501
|
+
"kind": "KEYWORD_ONLY",
|
|
502
|
+
"name": "identifier",
|
|
503
|
+
"type": "Optional[str]"
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
"default": null,
|
|
507
|
+
"kind": "KEYWORD_ONLY",
|
|
508
|
+
"name": "country",
|
|
509
|
+
"type": "Optional[str]"
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
"default": null,
|
|
513
|
+
"kind": "KEYWORD_ONLY",
|
|
514
|
+
"name": "exchange",
|
|
515
|
+
"type": "Optional[str]"
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
"default": null,
|
|
519
|
+
"kind": "KEYWORD_ONLY",
|
|
520
|
+
"name": "authority",
|
|
521
|
+
"type": "Optional[str]"
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
"default": null,
|
|
525
|
+
"kind": "KEYWORD_ONLY",
|
|
526
|
+
"name": "limit",
|
|
527
|
+
"type": "Optional[int]"
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
"default": null,
|
|
531
|
+
"kind": "KEYWORD_ONLY",
|
|
532
|
+
"name": "cursor",
|
|
533
|
+
"type": "Optional[str]"
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
"default": false,
|
|
537
|
+
"kind": "KEYWORD_ONLY",
|
|
538
|
+
"name": "deep",
|
|
539
|
+
"type": "bool"
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
"default": null,
|
|
543
|
+
"kind": "KEYWORD_ONLY",
|
|
544
|
+
"name": "industry",
|
|
545
|
+
"type": "Optional[str]"
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
"default": null,
|
|
549
|
+
"kind": "KEYWORD_ONLY",
|
|
550
|
+
"name": "industry_type",
|
|
551
|
+
"type": "Optional[str]"
|
|
552
|
+
}
|
|
553
|
+
],
|
|
554
|
+
"returns": "Json"
|
|
555
|
+
},
|
|
456
556
|
"constructor": {
|
|
457
557
|
"async": false,
|
|
458
558
|
"parameters": [
|
|
@@ -2345,6 +2445,106 @@
|
|
|
2345
2445
|
],
|
|
2346
2446
|
"returns": "Json"
|
|
2347
2447
|
},
|
|
2448
|
+
"company.coverage": {
|
|
2449
|
+
"async": false,
|
|
2450
|
+
"parameters": [],
|
|
2451
|
+
"returns": "Json"
|
|
2452
|
+
},
|
|
2453
|
+
"company.id": {
|
|
2454
|
+
"async": false,
|
|
2455
|
+
"parameters": [
|
|
2456
|
+
{
|
|
2457
|
+
"kind": "POSITIONAL_OR_KEYWORD",
|
|
2458
|
+
"name": "id",
|
|
2459
|
+
"type": "str"
|
|
2460
|
+
},
|
|
2461
|
+
{
|
|
2462
|
+
"default": false,
|
|
2463
|
+
"kind": "KEYWORD_ONLY",
|
|
2464
|
+
"name": "deep",
|
|
2465
|
+
"type": "bool"
|
|
2466
|
+
}
|
|
2467
|
+
],
|
|
2468
|
+
"returns": "Json"
|
|
2469
|
+
},
|
|
2470
|
+
"company.search": {
|
|
2471
|
+
"async": false,
|
|
2472
|
+
"parameters": [
|
|
2473
|
+
{
|
|
2474
|
+
"default": null,
|
|
2475
|
+
"kind": "KEYWORD_ONLY",
|
|
2476
|
+
"name": "query",
|
|
2477
|
+
"type": "Optional[str]"
|
|
2478
|
+
},
|
|
2479
|
+
{
|
|
2480
|
+
"default": null,
|
|
2481
|
+
"kind": "KEYWORD_ONLY",
|
|
2482
|
+
"name": "domain",
|
|
2483
|
+
"type": "Optional[str]"
|
|
2484
|
+
},
|
|
2485
|
+
{
|
|
2486
|
+
"default": null,
|
|
2487
|
+
"kind": "KEYWORD_ONLY",
|
|
2488
|
+
"name": "ticker",
|
|
2489
|
+
"type": "Optional[str]"
|
|
2490
|
+
},
|
|
2491
|
+
{
|
|
2492
|
+
"default": null,
|
|
2493
|
+
"kind": "KEYWORD_ONLY",
|
|
2494
|
+
"name": "identifier",
|
|
2495
|
+
"type": "Optional[str]"
|
|
2496
|
+
},
|
|
2497
|
+
{
|
|
2498
|
+
"default": null,
|
|
2499
|
+
"kind": "KEYWORD_ONLY",
|
|
2500
|
+
"name": "country",
|
|
2501
|
+
"type": "Optional[str]"
|
|
2502
|
+
},
|
|
2503
|
+
{
|
|
2504
|
+
"default": null,
|
|
2505
|
+
"kind": "KEYWORD_ONLY",
|
|
2506
|
+
"name": "exchange",
|
|
2507
|
+
"type": "Optional[str]"
|
|
2508
|
+
},
|
|
2509
|
+
{
|
|
2510
|
+
"default": null,
|
|
2511
|
+
"kind": "KEYWORD_ONLY",
|
|
2512
|
+
"name": "authority",
|
|
2513
|
+
"type": "Optional[str]"
|
|
2514
|
+
},
|
|
2515
|
+
{
|
|
2516
|
+
"default": null,
|
|
2517
|
+
"kind": "KEYWORD_ONLY",
|
|
2518
|
+
"name": "limit",
|
|
2519
|
+
"type": "Optional[int]"
|
|
2520
|
+
},
|
|
2521
|
+
{
|
|
2522
|
+
"default": null,
|
|
2523
|
+
"kind": "KEYWORD_ONLY",
|
|
2524
|
+
"name": "cursor",
|
|
2525
|
+
"type": "Optional[str]"
|
|
2526
|
+
},
|
|
2527
|
+
{
|
|
2528
|
+
"default": false,
|
|
2529
|
+
"kind": "KEYWORD_ONLY",
|
|
2530
|
+
"name": "deep",
|
|
2531
|
+
"type": "bool"
|
|
2532
|
+
},
|
|
2533
|
+
{
|
|
2534
|
+
"default": null,
|
|
2535
|
+
"kind": "KEYWORD_ONLY",
|
|
2536
|
+
"name": "industry",
|
|
2537
|
+
"type": "Optional[str]"
|
|
2538
|
+
},
|
|
2539
|
+
{
|
|
2540
|
+
"default": null,
|
|
2541
|
+
"kind": "KEYWORD_ONLY",
|
|
2542
|
+
"name": "industry_type",
|
|
2543
|
+
"type": "Optional[str]"
|
|
2544
|
+
}
|
|
2545
|
+
],
|
|
2546
|
+
"returns": "Json"
|
|
2547
|
+
},
|
|
2348
2548
|
"constructor": {
|
|
2349
2549
|
"async": false,
|
|
2350
2550
|
"parameters": [
|
|
@@ -7,12 +7,12 @@ import random
|
|
|
7
7
|
import re
|
|
8
8
|
import time
|
|
9
9
|
from email.utils import parsedate_to_datetime
|
|
10
|
-
from typing import Any, Dict, Optional, overload
|
|
10
|
+
from typing import Any, Dict, Optional, Protocol, Union, overload
|
|
11
11
|
from urllib.parse import quote
|
|
12
12
|
|
|
13
13
|
import httpx
|
|
14
14
|
|
|
15
|
-
VERSION = "1.
|
|
15
|
+
VERSION = "1.9.0"
|
|
16
16
|
_API_VERSION = "2.0.0"
|
|
17
17
|
DEFAULT_BASE_URL = "https://api.parseapi.com"
|
|
18
18
|
DEFAULT_TIMEOUT = 10.0
|
|
@@ -129,6 +129,54 @@ class _Config:
|
|
|
129
129
|
return {"X-API-Key": self.api_key, "User-Agent": f"parseapi-python/{VERSION}", "Parse-Version": _API_VERSION}
|
|
130
130
|
|
|
131
131
|
|
|
132
|
+
class _CompanySyncClassCall(Protocol):
|
|
133
|
+
def __call__(self, client: ParseAPI, /, number: str, *, country: Optional[str] = None,
|
|
134
|
+
deep: bool = False, lang: Optional[str] = None) -> Json: ...
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class _CompanyAsyncClassCall(Protocol):
|
|
138
|
+
async def __call__(self, client: AsyncParseAPI, /, number: str, *, country: Optional[str] = None,
|
|
139
|
+
deep: bool = False, lang: Optional[str] = None) -> Json: ...
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# A typed, non-data descriptor keeps the published class-level number method
|
|
143
|
+
# while exposing a callable namespace to instances and static type checkers.
|
|
144
|
+
class _CompanySyncDescriptor:
|
|
145
|
+
def __init__(self, lookup: _CompanySyncClassCall):
|
|
146
|
+
self._lookup = lookup
|
|
147
|
+
|
|
148
|
+
@overload
|
|
149
|
+
def __get__(self, instance: None, owner: Optional[type[Any]] = None) -> _CompanySyncClassCall: ...
|
|
150
|
+
|
|
151
|
+
@overload
|
|
152
|
+
def __get__(self, instance: ParseAPI, owner: Optional[type[Any]] = None) -> _CompanySync: ...
|
|
153
|
+
|
|
154
|
+
def __get__(self, instance: Optional[ParseAPI], owner: Optional[type[Any]] = None) -> Union[_CompanySyncClassCall, _CompanySync]:
|
|
155
|
+
if instance is None:
|
|
156
|
+
return self._lookup
|
|
157
|
+
namespace = _CompanySync(instance, self._lookup)
|
|
158
|
+
instance.__dict__["company"] = namespace
|
|
159
|
+
return namespace
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class _CompanyAsyncDescriptor:
|
|
163
|
+
def __init__(self, lookup: _CompanyAsyncClassCall):
|
|
164
|
+
self._lookup = lookup
|
|
165
|
+
|
|
166
|
+
@overload
|
|
167
|
+
def __get__(self, instance: None, owner: Optional[type[Any]] = None) -> _CompanyAsyncClassCall: ...
|
|
168
|
+
|
|
169
|
+
@overload
|
|
170
|
+
def __get__(self, instance: AsyncParseAPI, owner: Optional[type[Any]] = None) -> _CompanyAsync: ...
|
|
171
|
+
|
|
172
|
+
def __get__(self, instance: Optional[AsyncParseAPI], owner: Optional[type[Any]] = None) -> Union[_CompanyAsyncClassCall, _CompanyAsync]:
|
|
173
|
+
if instance is None:
|
|
174
|
+
return self._lookup
|
|
175
|
+
namespace = _CompanyAsync(instance, self._lookup)
|
|
176
|
+
instance.__dict__["company"] = namespace
|
|
177
|
+
return namespace
|
|
178
|
+
|
|
179
|
+
|
|
132
180
|
class ParseAPI:
|
|
133
181
|
"""Synchronous client. `parse = ParseAPI()` reads PARSEAPI_KEY from the env."""
|
|
134
182
|
|
|
@@ -316,6 +364,7 @@ class ParseAPI:
|
|
|
316
364
|
def vin(self, vin: str, *, deep: bool = False) -> Json:
|
|
317
365
|
return self._get(f"/vin/{_seg(vin)}", {"deep": deep})
|
|
318
366
|
|
|
367
|
+
@_CompanySyncDescriptor
|
|
319
368
|
def company(self, number: str, *, country: Optional[str] = None, deep: bool = False, lang: Optional[str] = None) -> Json:
|
|
320
369
|
return self._get(f"/company/{_seg(number)}", {"country": country, "deep": deep, "lang": lang})
|
|
321
370
|
|
|
@@ -735,6 +784,7 @@ class AsyncParseAPI:
|
|
|
735
784
|
async def vin(self, vin: str, *, deep: bool = False) -> Json:
|
|
736
785
|
return await self._get(f"/vin/{_seg(vin)}", {"deep": deep})
|
|
737
786
|
|
|
787
|
+
@_CompanyAsyncDescriptor
|
|
738
788
|
async def company(self, number: str, *, country: Optional[str] = None, deep: bool = False, lang: Optional[str] = None) -> Json:
|
|
739
789
|
return await self._get(f"/company/{_seg(number)}", {"country": country, "deep": deep, "lang": lang})
|
|
740
790
|
|
|
@@ -1106,6 +1156,70 @@ class _MeasureAsync:
|
|
|
1106
1156
|
"""Discover reviewed units. unit filters compatible conversion targets."""
|
|
1107
1157
|
return await self._client._get("/measure/units", {"q": query, "type": type, "unit": unit, "lang": lang})
|
|
1108
1158
|
|
|
1159
|
+
|
|
1160
|
+
class _CompanySync:
|
|
1161
|
+
def __init__(self, client: ParseAPI, lookup: _CompanySyncClassCall):
|
|
1162
|
+
self._client = client
|
|
1163
|
+
self._number_lookup = lookup
|
|
1164
|
+
|
|
1165
|
+
def __call__(self, number: str, *, country: Optional[str] = None, deep: bool = False, lang: Optional[str] = None) -> Json:
|
|
1166
|
+
return self._number_lookup(self._client, number, country=country, deep=deep, lang=lang)
|
|
1167
|
+
|
|
1168
|
+
def id(self, id: str, *, deep: bool = False) -> Json:
|
|
1169
|
+
"""Fetch a directory profile by its stable Company ID. Deep adds profile detail."""
|
|
1170
|
+
return self._client._get(f"/company/id/{_seg(id)}", {"deep": deep})
|
|
1171
|
+
|
|
1172
|
+
def search(self, *, query: Optional[str] = None, domain: Optional[str] = None,
|
|
1173
|
+
ticker: Optional[str] = None, identifier: Optional[str] = None, country: Optional[str] = None,
|
|
1174
|
+
exchange: Optional[str] = None, authority: Optional[str] = None, limit: Optional[int] = None,
|
|
1175
|
+
cursor: Optional[str] = None, deep: bool = False, industry: Optional[str] = None,
|
|
1176
|
+
industry_type: Optional[str] = None) -> Json:
|
|
1177
|
+
"""Use at most one selector, or discover by country, industry or selected registration.
|
|
1178
|
+
Pair a four-digit SIC industry string with industry_type="sic".
|
|
1179
|
+
Administrative registration status does not establish current business activity.
|
|
1180
|
+
The API validates selectors and filters. Review candidates before choosing an ID.
|
|
1181
|
+
Pass a returned cursor with the same selector and filters. Deep belongs to each result.
|
|
1182
|
+
"""
|
|
1183
|
+
return self._client._get("/company", {"q": query, "domain": domain, "ticker": ticker,
|
|
1184
|
+
"identifier": identifier, "country": country, "exchange": exchange, "authority": authority,
|
|
1185
|
+
"limit": limit, "cursor": cursor, "deep": deep, "industry": industry, "industry_type": industry_type})
|
|
1186
|
+
|
|
1187
|
+
def coverage(self) -> Json:
|
|
1188
|
+
"""Read the directory edition and source coverage."""
|
|
1189
|
+
return self._client._get("/company/directory/coverage")
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
class _CompanyAsync:
|
|
1193
|
+
def __init__(self, client: AsyncParseAPI, lookup: _CompanyAsyncClassCall):
|
|
1194
|
+
self._client = client
|
|
1195
|
+
self._number_lookup = lookup
|
|
1196
|
+
|
|
1197
|
+
async def __call__(self, number: str, *, country: Optional[str] = None, deep: bool = False, lang: Optional[str] = None) -> Json:
|
|
1198
|
+
return await self._number_lookup(self._client, number, country=country, deep=deep, lang=lang)
|
|
1199
|
+
|
|
1200
|
+
async def id(self, id: str, *, deep: bool = False) -> Json:
|
|
1201
|
+
"""Fetch a directory profile by its stable Company ID. Deep adds profile detail."""
|
|
1202
|
+
return await self._client._get(f"/company/id/{_seg(id)}", {"deep": deep})
|
|
1203
|
+
|
|
1204
|
+
async def search(self, *, query: Optional[str] = None, domain: Optional[str] = None,
|
|
1205
|
+
ticker: Optional[str] = None, identifier: Optional[str] = None, country: Optional[str] = None,
|
|
1206
|
+
exchange: Optional[str] = None, authority: Optional[str] = None, limit: Optional[int] = None,
|
|
1207
|
+
cursor: Optional[str] = None, deep: bool = False, industry: Optional[str] = None,
|
|
1208
|
+
industry_type: Optional[str] = None) -> Json:
|
|
1209
|
+
"""Use at most one selector, or discover by country, industry or selected registration.
|
|
1210
|
+
Pair a four-digit SIC industry string with industry_type="sic".
|
|
1211
|
+
Administrative registration status does not establish current business activity.
|
|
1212
|
+
The API validates selectors and filters. Review candidates before choosing an ID.
|
|
1213
|
+
Pass a returned cursor with the same selector and filters. Deep belongs to each result.
|
|
1214
|
+
"""
|
|
1215
|
+
return await self._client._get("/company", {"q": query, "domain": domain, "ticker": ticker,
|
|
1216
|
+
"identifier": identifier, "country": country, "exchange": exchange, "authority": authority,
|
|
1217
|
+
"limit": limit, "cursor": cursor, "deep": deep, "industry": industry, "industry_type": industry_type})
|
|
1218
|
+
|
|
1219
|
+
async def coverage(self) -> Json:
|
|
1220
|
+
"""Read the directory edition and source coverage."""
|
|
1221
|
+
return await self._client._get("/company/directory/coverage")
|
|
1222
|
+
|
|
1109
1223
|
def _time_source(timezone, **values):
|
|
1110
1224
|
primary = [values.get(key) for key in ("ip", "city", "iata", "icao", "unlocode", "address") if values.get(key) is not None]
|
|
1111
1225
|
present = any(value is not None for value in values.values())
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import pytest
|
|
9
|
+
from parseapi import ParseAPI, AsyncParseAPI, ParseAPIError
|
|
10
|
+
|
|
11
|
+
BASE = {"id": "co_caczn6wf36hj", "name": "GitLab", "country": "US", "website": None, "listings": [], "address": None, "future": {"unknown": None}}
|
|
12
|
+
RICH = {"description": None, "logo": "https://example.com/logo.svg", "socials": [], "founded": {"value": "2011", "precision": "future_precision"}, "sources": [{"type": "future_source", "url": "https://example.com/", "fields": ["logo"], "observed_at": None, "updated_at": None, "future": True}], "parent": None, "future": [None, {}]}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def exercise(async_client, operations, responses, **config):
|
|
16
|
+
calls = []
|
|
17
|
+
queue = list(responses)
|
|
18
|
+
def handler(request):
|
|
19
|
+
calls.append(request)
|
|
20
|
+
status, body = queue.pop(0)
|
|
21
|
+
return httpx.Response(status, json=body, headers={"Retry-After": "0"})
|
|
22
|
+
async def run():
|
|
23
|
+
cls = AsyncParseAPI if async_client else ParseAPI
|
|
24
|
+
client = cls("test_company", transport=httpx.MockTransport(handler), **config)
|
|
25
|
+
results = []
|
|
26
|
+
try:
|
|
27
|
+
for operation in operations:
|
|
28
|
+
value = operation(client)
|
|
29
|
+
results.append(await value if inspect.isawaitable(value) else value)
|
|
30
|
+
finally:
|
|
31
|
+
value = client.close()
|
|
32
|
+
if inspect.isawaitable(value):
|
|
33
|
+
await value
|
|
34
|
+
return results
|
|
35
|
+
return asyncio.run(run()), calls
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.parametrize("async_client", [False, True])
|
|
39
|
+
@pytest.mark.parametrize("operation,path,params", [
|
|
40
|
+
(lambda p: p.company.id("co_/é?+", deep=True), "/company/id/co_%2F%C3%A9%3F%2B", {"deep": "true"}),
|
|
41
|
+
(lambda p: p.company.id("co_caczn6wf36hj"), "/company/id/co_caczn6wf36hj", {}),
|
|
42
|
+
(lambda p: p.company.search(query="Café & Co", country="us", limit=2, cursor="a+/=?&", deep=True), "/company", {"q": "Café & Co", "country": "us", "limit": "2", "cursor": "a+/=?&", "deep": "true"}),
|
|
43
|
+
(lambda p: p.company.search(domain="WWW.Example.COM."), "/company", {"domain": "WWW.Example.COM."}),
|
|
44
|
+
(lambda p: p.company.search(ticker="BRK/B", exchange="X/Y"), "/company", {"ticker": "BRK/B", "exchange": "X/Y"}),
|
|
45
|
+
(lambda p: p.company.search(identifier="0000/123", authority="US/SEC", country="US"), "/company", {"identifier": "0000/123", "authority": "US/SEC", "country": "US"}),
|
|
46
|
+
(lambda p: p.company.search(query="", limit=0, cursor=""), "/company", {"q": "", "limit": "0", "cursor": ""}),
|
|
47
|
+
(lambda p: p.company.search(country="US"), "/company", {"country": "US"}),
|
|
48
|
+
(lambda p: p.company.search(industry="0700", industry_type="sic"), "/company", {"industry": "0700", "industry_type": "sic"}),
|
|
49
|
+
(lambda p: p.company.search(country="US", industry="0700", industry_type="sic", cursor="opaque+/=", limit=2, deep=True), "/company", {"country": "US", "industry": "0700", "industry_type": "sic", "cursor": "opaque+/=", "limit": "2", "deep": "true"}),
|
|
50
|
+
(lambda p: p.company.search(query="Example", industry="0700", industry_type="sic", deep=False), "/company", {"q": "Example", "industry": "0700", "industry_type": "sic"}),
|
|
51
|
+
(lambda p: p.company.coverage(), "/company/directory/coverage", {}),
|
|
52
|
+
])
|
|
53
|
+
def test_routes_encoding_and_direct_request(async_client, operation, path, params):
|
|
54
|
+
body = {"companies": [], "next": None, "future": None}
|
|
55
|
+
results, calls = exercise(async_client, [operation], [(200, body)], timeout=1.25)
|
|
56
|
+
assert results == [body]
|
|
57
|
+
assert len(calls) == 1
|
|
58
|
+
assert calls[0].url.raw_path.split(b"?")[0].decode() == path
|
|
59
|
+
assert dict(calls[0].url.params) == params
|
|
60
|
+
assert calls[0].headers["Parse-Version"] == "2.0.0"
|
|
61
|
+
assert calls[0].headers["X-API-Key"] == "test_company"
|
|
62
|
+
assert calls[0].extensions["timeout"]["read"] == 1.25
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize("async_client", [False, True])
|
|
66
|
+
def test_plain_profiles_deep_triad_pagination_and_coverage(async_client):
|
|
67
|
+
profiles = [BASE, {**BASE, "deep": {}}, {**BASE, "deep": RICH}, {**BASE, "deep": {"description": None, "logo": None, "socials": None, "founded": None, "sources": None}}]
|
|
68
|
+
page = {"companies": [{**p, "match": {"field": "future_field", "value": None}} for p in profiles], "next": "opaque+/=", "future": None}
|
|
69
|
+
empty = {"companies": [], "next": None}
|
|
70
|
+
coverage = {"scope": "future_scope", "companies": 0, "countries": [], "future": None}
|
|
71
|
+
operations = [lambda p: p.company.id(BASE["id"]), lambda p: p.company.id(BASE["id"], deep=True), lambda p: p.company.id(BASE["id"], deep=True), lambda p: p.company.id(BASE["id"], deep=True), lambda p: p.company.search(query="GitLab", deep=True, limit=4), lambda p: p.company.search(query="GitLab", deep=True, limit=4, cursor=page["next"]), lambda p: p.company.coverage()]
|
|
72
|
+
expected = profiles + [page, empty, coverage]
|
|
73
|
+
results, calls = exercise(async_client, operations, [(200, item) for item in expected])
|
|
74
|
+
assert results == expected
|
|
75
|
+
assert len(calls) == len(operations)
|
|
76
|
+
assert dict(calls[5].url.params) == {"q": "GitLab", "limit": "4", "cursor": "opaque+/=", "deep": "true"}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.parametrize("async_client", [False, True])
|
|
80
|
+
@pytest.mark.parametrize("status,operation", [(400, lambda p: p.company.search(query="A", domain="B")), (400, lambda p: p.company.search()), (404, lambda p: p.company.id("co_unknown"))])
|
|
81
|
+
def test_errors_remain_errors_with_api_validation(async_client, status, operation):
|
|
82
|
+
code = "invalid_request" if status == 400 else "not_found"
|
|
83
|
+
with pytest.raises(ParseAPIError) as error:
|
|
84
|
+
exercise(async_client, [operation], [(status, {"code": code, "message": "source error", "docs": None, "request_id": "r1"})])
|
|
85
|
+
assert (error.value.status, error.value.code, error.value.request_id) == (status, code, "r1")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.parametrize("async_client", [False, True])
|
|
89
|
+
def test_company_number_callable_and_class_method_are_preserved(async_client):
|
|
90
|
+
cls = AsyncParseAPI if async_client else ParseAPI
|
|
91
|
+
body = {"valid": True, "company": "51824753556", "deep": {"activity": None}}
|
|
92
|
+
results, calls = exercise(async_client, [lambda p: p.company("51 824 753 556", country="AU", deep=True, lang="fr"), lambda p: cls.company(p, "51 824 753 556", country="AU", deep=True, lang="fr")], [(200, body)] * 2)
|
|
93
|
+
assert results == [body, body]
|
|
94
|
+
assert str(calls[0].url) == str(calls[1].url) == "https://api.parseapi.com/company/51%20824%20753%20556?country=AU&deep=true&lang=fr"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.mark.parametrize("async_client", [False, True])
|
|
98
|
+
def test_directory_uses_existing_retry_controls(async_client):
|
|
99
|
+
result, calls = exercise(async_client, [lambda p: p.company.id(BASE["id"], deep=True)], [(503, {}), (503, {}), (200, BASE)])
|
|
100
|
+
assert result == [BASE] and len(calls) == 3
|
|
101
|
+
with pytest.raises(ParseAPIError):
|
|
102
|
+
exercise(async_client, [lambda p: p.company.coverage()], [(503, {})], retries=0)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_sync_async_signatures_and_no_directory_lang():
|
|
106
|
+
with ParseAPI("fixture") as sync:
|
|
107
|
+
assert sync.company is sync.company
|
|
108
|
+
async_client = AsyncParseAPI("fixture")
|
|
109
|
+
try:
|
|
110
|
+
assert async_client.company is async_client.company
|
|
111
|
+
for name in ("__call__", "id", "search", "coverage"):
|
|
112
|
+
assert inspect.signature(getattr(sync.company, name)) == inspect.signature(getattr(async_client.company, name))
|
|
113
|
+
for method, arguments in [(sync.company.id, (BASE["id"],)), (sync.company.search, ())]:
|
|
114
|
+
with pytest.raises(TypeError):
|
|
115
|
+
method(*arguments, lang="fr")
|
|
116
|
+
finally:
|
|
117
|
+
asyncio.run(async_client.close())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_readme_recipe_uses_explicit_selection_and_returned_cursor():
|
|
121
|
+
section = (Path(__file__).resolve().parents[1] / "README.md").read_text().split("## Company directory\n", 1)[1]
|
|
122
|
+
code = re.search(r"```python\n(.*?)```", section, re.S).group(1)
|
|
123
|
+
responses = [{"companies": [BASE], "next": "opaque+/="}, {**BASE, "deep": RICH}, {"companies": [], "next": None}, {"scope": "sample"}]
|
|
124
|
+
calls = []
|
|
125
|
+
def handler(request):
|
|
126
|
+
calls.append(request)
|
|
127
|
+
return httpx.Response(200, json=responses.pop(0))
|
|
128
|
+
with ParseAPI("fixture", transport=httpx.MockTransport(handler)) as parse:
|
|
129
|
+
exec(compile(code, "README.md", "exec"), {"parse": parse})
|
|
130
|
+
assert len(calls) == 4
|
|
131
|
+
assert calls[1].url.path == "/company/id/co_caczn6wf36hj"
|
|
132
|
+
assert calls[2].url.params["cursor"] == "opaque+/="
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@pytest.mark.parametrize("async_client", [False, True])
|
|
136
|
+
def test_employee_observations_preserve_zero_false_dates_scope_and_missing(async_client):
|
|
137
|
+
values = [
|
|
138
|
+
{}, {"employees": None},
|
|
139
|
+
{"employees": {"count": 0, "as_of": "2025-12-31", "scope": "legal_entity", "method": "reported", "approximate": False}},
|
|
140
|
+
{"employees": {"count": 12500, "as_of": "2026-06-30", "scope": "consolidated_group", "method": "reported", "approximate": True}},
|
|
141
|
+
{"employees": {"count": 7, "as_of": "2026-01-15", "scope": "future_scope", "method": "future_method", "approximate": False, "future": None}},
|
|
142
|
+
]
|
|
143
|
+
for deep in values:
|
|
144
|
+
profile = {**BASE, "deep": deep}
|
|
145
|
+
page = {"companies": [{**profile, "match": {"field": "name", "value": BASE["name"]}}], "next": None}
|
|
146
|
+
operations = [lambda p: p.company.id(BASE["id"], deep=True), lambda p: p.company.search(query=BASE["name"], deep=True)]
|
|
147
|
+
results, calls = exercise(async_client, operations, [(200, profile), (200, page)])
|
|
148
|
+
assert results == [profile, page]
|
|
149
|
+
assert ("employees" in results[0]["deep"]) == ("employees" in deep)
|
|
150
|
+
assert len(calls) == 2
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@pytest.mark.parametrize("async_client", [False, True])
|
|
154
|
+
def test_registrations_preserve_source_roles_nulls_and_leading_zeros(async_client):
|
|
155
|
+
registration = json.loads(r'{"authority":"RA000599","number":"0001234567","jurisdiction":{"country":"US","state":"CO"},"role":"domestic","legal_form":{"code":"DNC","name":"Domestic Non-profit Corporation"},"status":"Good Standing","formation_date":"2004-02-29","address":{"kind":"principal","line1":"12 Main St.","line2":"Suite 2","city":"Example","state":"CO","postal":"00123-0001","country_raw":"US"},"future":"retained"}')
|
|
156
|
+
for deep in [{}, {"registrations": None}, {"registrations": []}, {"registrations": [registration]}, {"registrations": [{**registration, "role": "future_role", "formation_date": None, "address": None}]}]:
|
|
157
|
+
profile = {**BASE, "deep": deep}
|
|
158
|
+
page = {"companies": [{**profile, "match": {"field": "identifier", "value": registration["number"]}}], "next": None}
|
|
159
|
+
results, calls = exercise(async_client, [lambda p: p.company.id(BASE["id"], deep=True), lambda p: p.company.search(identifier=registration["number"], authority=registration["authority"], deep=True)], [(200, profile), (200, page)])
|
|
160
|
+
assert results == [profile, page] and len(calls) == 2
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Compile-only consumer for the typed callable namespace and previous methods.
|
|
2
|
+
|
|
3
|
+
pyright --pythonpath .venv/bin/python tests/typing_company_directory.py
|
|
4
|
+
No functions in this fixture are invoked.
|
|
5
|
+
"""
|
|
6
|
+
from parseapi import ParseAPI, AsyncParseAPI
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def sync_directory(parse: ParseAPI) -> None:
|
|
10
|
+
number_lookup = ParseAPI.company
|
|
11
|
+
number_lookup(parse, "51 824 753 556", country="AU", deep=True, lang="fr")
|
|
12
|
+
lookup = parse.company
|
|
13
|
+
lookup("51 824 753 556", country="AU", deep=True, lang="fr")
|
|
14
|
+
lookup.id("co_caczn6wf36hj", deep=True)
|
|
15
|
+
lookup.search(query="GitLab", country="US", limit=5, cursor="opaque", deep=True)
|
|
16
|
+
lookup.search(domain="about.gitlab.com")
|
|
17
|
+
lookup.search(ticker="GTLB", exchange="Nasdaq")
|
|
18
|
+
lookup.search(identifier="0001653482", authority="sec")
|
|
19
|
+
lookup.coverage()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def async_directory(parse: AsyncParseAPI) -> None:
|
|
23
|
+
number_lookup = AsyncParseAPI.company
|
|
24
|
+
await number_lookup(parse, "51 824 753 556", country="AU", deep=True, lang="fr")
|
|
25
|
+
lookup = parse.company
|
|
26
|
+
await lookup("51 824 753 556", country="AU", deep=True, lang="fr")
|
|
27
|
+
await lookup.id("co_caczn6wf36hj", deep=True)
|
|
28
|
+
await lookup.search(query="GitLab", country="US", limit=5, cursor="opaque", deep=True)
|
|
29
|
+
await lookup.search(domain="about.gitlab.com")
|
|
30
|
+
await lookup.search(ticker="GTLB", exchange="Nasdaq")
|
|
31
|
+
await lookup.search(identifier="0001653482", authority="sec")
|
|
32
|
+
await lookup.coverage()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|