licensy 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- licensy-0.1.0/LICENSE +21 -0
- licensy-0.1.0/PKG-INFO +231 -0
- licensy-0.1.0/README.md +200 -0
- licensy-0.1.0/licensy/__init__.py +75 -0
- licensy-0.1.0/licensy/client.py +385 -0
- licensy-0.1.0/licensy/errors.py +54 -0
- licensy-0.1.0/licensy/models.py +146 -0
- licensy-0.1.0/licensy.egg-info/PKG-INFO +231 -0
- licensy-0.1.0/licensy.egg-info/SOURCES.txt +13 -0
- licensy-0.1.0/licensy.egg-info/dependency_links.txt +1 -0
- licensy-0.1.0/licensy.egg-info/requires.txt +9 -0
- licensy-0.1.0/licensy.egg-info/top_level.txt +1 -0
- licensy-0.1.0/pyproject.toml +47 -0
- licensy-0.1.0/setup.cfg +4 -0
- licensy-0.1.0/tests/test_client.py +283 -0
licensy-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Licensy AI
|
|
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.
|
licensy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: licensy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Licensy — real-time U.S. medical-license verification across all 50 states + DC, sourced from state medical boards.
|
|
5
|
+
Author-email: Licensy <engineering@licensy.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://licensy.ai
|
|
8
|
+
Project-URL: Documentation, https://licensy.ai/docs
|
|
9
|
+
Project-URL: API Reference, https://mcp.licensy.ai/.well-known/oauth-protected-resource
|
|
10
|
+
Project-URL: Source, https://github.com/licensyai/licensy-sdk-python
|
|
11
|
+
Keywords: healthcare,credentialing,medical-license,verification,physician,NPI
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: httpx>=0.25
|
|
25
|
+
Requires-Dist: typing-extensions>=4.0; python_version < "3.11"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
29
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# Licensy Python SDK
|
|
33
|
+
|
|
34
|
+
Real-time U.S. medical-license verification across all 50 states + DC,
|
|
35
|
+
sourced directly from state medical boards. Free tier returns basic
|
|
36
|
+
status / expiration; paid tiers (Pro / Enterprise) unlock board-source
|
|
37
|
+
screenshots, bulk verification, disciplinary actions, point-in-time
|
|
38
|
+
history, and webhook subscriptions.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install licensy
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quickstart
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from licensy import LicensyClient
|
|
50
|
+
|
|
51
|
+
client = LicensyClient(api_key="sk_live_...")
|
|
52
|
+
|
|
53
|
+
result = client.verify_license(npi="1700013174", state="CA")
|
|
54
|
+
if result.license_active:
|
|
55
|
+
print(f"Active through {result.expiration_date}")
|
|
56
|
+
else:
|
|
57
|
+
print(f"Inactive: {result.status}")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Async
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from licensy import AsyncLicensyClient
|
|
64
|
+
|
|
65
|
+
async with AsyncLicensyClient(api_key="sk_live_...") as client:
|
|
66
|
+
result = await client.verify_license(npi="1700013174", state="CA")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## All tools
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# Single state, single physician
|
|
73
|
+
client.verify_license(npi="...", state="CA")
|
|
74
|
+
|
|
75
|
+
# All states for one physician
|
|
76
|
+
result = client.list_physician_licenses(npi="...", include_inactive=False)
|
|
77
|
+
for license in result:
|
|
78
|
+
print(license.state, license.status)
|
|
79
|
+
|
|
80
|
+
# Cheapest call — just the status word
|
|
81
|
+
client.get_license_status(npi="...", state="CA") # → "active"
|
|
82
|
+
|
|
83
|
+
# Audit-grade screenshot of the state-board page (Paid only)
|
|
84
|
+
client.get_screenshot_url(npi="...", state="CA")
|
|
85
|
+
|
|
86
|
+
# Bulk verification (Paid only)
|
|
87
|
+
client.bulk_verify([
|
|
88
|
+
{"npi": "1700013174", "state": "CA"},
|
|
89
|
+
{"npi": "1700013174", "state": "NY"},
|
|
90
|
+
])
|
|
91
|
+
|
|
92
|
+
# Find disciplinary actions (Paid only)
|
|
93
|
+
client.search_disciplinary_actions(npi="...")
|
|
94
|
+
|
|
95
|
+
# Was this physician licensed on YYYY-MM-DD? (Paid only)
|
|
96
|
+
client.get_license_history(npi="...", state="CA", as_of_date="2024-08-12")
|
|
97
|
+
|
|
98
|
+
# Subscribe to status-change webhooks (Paid only)
|
|
99
|
+
sub = client.subscribe_to_changes(
|
|
100
|
+
npis=["1700013174"],
|
|
101
|
+
webhook_url="https://your-app.com/webhooks/licensy",
|
|
102
|
+
)
|
|
103
|
+
client.unsubscribe(subscription_id=sub.subscription_id)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Error handling
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from licensy import (
|
|
110
|
+
LicensyClient,
|
|
111
|
+
AuthError,
|
|
112
|
+
NotFoundError,
|
|
113
|
+
RateLimitError,
|
|
114
|
+
TierUpgradeRequired,
|
|
115
|
+
ValidationError,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
client.get_screenshot_url(npi="...", state="CA")
|
|
120
|
+
except TierUpgradeRequired as e:
|
|
121
|
+
print(f"Need {e.required_tier} plan for screenshots")
|
|
122
|
+
except RateLimitError as e:
|
|
123
|
+
print(f"Slow down — retry in {e.retry_after_seconds}s")
|
|
124
|
+
except NotFoundError:
|
|
125
|
+
print("No license on file for that NPI/state")
|
|
126
|
+
except AuthError:
|
|
127
|
+
print("Check your API key")
|
|
128
|
+
except ValidationError as e:
|
|
129
|
+
print(f"Bad input: {e.details}")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Configuration
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
client = LicensyClient(
|
|
136
|
+
api_key="sk_live_...",
|
|
137
|
+
base_url="https://mcp.licensy.ai", # default; override for staging
|
|
138
|
+
timeout=30.0,
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
You can also bring your own `httpx.Client` for advanced proxying / retry
|
|
143
|
+
behaviour:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
import httpx
|
|
147
|
+
client = LicensyClient(api_key="...", http_client=httpx.Client(proxy="..."))
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Real-world recipes
|
|
151
|
+
|
|
152
|
+
### Daily roster check (cron-friendly)
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
import csv, datetime, os, sys
|
|
156
|
+
from licensy import LicensyClient, NotFoundError
|
|
157
|
+
|
|
158
|
+
client = LicensyClient(api_key=os.environ["LICENSY_API_KEY"])
|
|
159
|
+
today = datetime.date.today()
|
|
160
|
+
warn_in = datetime.timedelta(days=60)
|
|
161
|
+
|
|
162
|
+
with open("roster.csv") as f, open("alerts.csv", "w") as out:
|
|
163
|
+
reader = csv.DictReader(f)
|
|
164
|
+
writer = csv.writer(out)
|
|
165
|
+
writer.writerow(["npi", "state", "issue"])
|
|
166
|
+
for row in reader:
|
|
167
|
+
try:
|
|
168
|
+
r = client.verify_license(npi=row["npi"], state=row["state"])
|
|
169
|
+
except NotFoundError:
|
|
170
|
+
writer.writerow([row["npi"], row["state"], "no record"])
|
|
171
|
+
continue
|
|
172
|
+
if not r.license_active:
|
|
173
|
+
writer.writerow([row["npi"], row["state"], f"inactive: {r.status}"])
|
|
174
|
+
elif r.expiration_date and (r.expiration_date - today) < warn_in:
|
|
175
|
+
writer.writerow([row["npi"], row["state"], f"expires {r.expiration_date}"])
|
|
176
|
+
|
|
177
|
+
print("Done — see alerts.csv", file=sys.stderr)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Webhook receiver (FastAPI) with signature verification
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
import hmac, hashlib, os
|
|
184
|
+
from fastapi import FastAPI, Header, HTTPException, Request
|
|
185
|
+
|
|
186
|
+
SECRET = os.environ["LICENSY_WEBHOOK_SECRET"].encode()
|
|
187
|
+
app = FastAPI()
|
|
188
|
+
|
|
189
|
+
@app.post("/webhooks/licensy")
|
|
190
|
+
async def receive(request: Request, x_licensy_signature: str = Header(...)):
|
|
191
|
+
body = await request.body()
|
|
192
|
+
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
|
|
193
|
+
if not hmac.compare_digest(expected, x_licensy_signature):
|
|
194
|
+
raise HTTPException(401, "bad signature")
|
|
195
|
+
event = await request.json()
|
|
196
|
+
# event.type ∈ {"license.expired", "license.status_changed", ...}
|
|
197
|
+
print(f"{event['type']} for NPI {event['data']['npi']}/{event['data']['state']}")
|
|
198
|
+
return {"ok": True}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Signing-secret format and snippets in 5 languages at <https://licensy.ai/docs#webhook-signing>.
|
|
202
|
+
|
|
203
|
+
### Bulk verification with retry
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
import time
|
|
207
|
+
from licensy import LicensyClient, RateLimitError
|
|
208
|
+
|
|
209
|
+
client = LicensyClient(api_key=os.environ["LICENSY_API_KEY"])
|
|
210
|
+
pairs = [{"npi": n, "state": s} for n, s in roster]
|
|
211
|
+
|
|
212
|
+
while True:
|
|
213
|
+
try:
|
|
214
|
+
results = client.bulk_verify(pairs)
|
|
215
|
+
break
|
|
216
|
+
except RateLimitError as e:
|
|
217
|
+
time.sleep(e.retry_after_seconds)
|
|
218
|
+
|
|
219
|
+
inactive = [r for r in results if not r.license_active]
|
|
220
|
+
print(f"{len(inactive)} inactive of {len(results)}")
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Versioning
|
|
224
|
+
|
|
225
|
+
`licensy.__version__` is the package version. The wire protocol is
|
|
226
|
+
versioned at `/api/v1/...`; minor SDK releases stay backward-compatible
|
|
227
|
+
on the API.
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT.
|
licensy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Licensy Python SDK
|
|
2
|
+
|
|
3
|
+
Real-time U.S. medical-license verification across all 50 states + DC,
|
|
4
|
+
sourced directly from state medical boards. Free tier returns basic
|
|
5
|
+
status / expiration; paid tiers (Pro / Enterprise) unlock board-source
|
|
6
|
+
screenshots, bulk verification, disciplinary actions, point-in-time
|
|
7
|
+
history, and webhook subscriptions.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install licensy
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from licensy import LicensyClient
|
|
19
|
+
|
|
20
|
+
client = LicensyClient(api_key="sk_live_...")
|
|
21
|
+
|
|
22
|
+
result = client.verify_license(npi="1700013174", state="CA")
|
|
23
|
+
if result.license_active:
|
|
24
|
+
print(f"Active through {result.expiration_date}")
|
|
25
|
+
else:
|
|
26
|
+
print(f"Inactive: {result.status}")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Async
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from licensy import AsyncLicensyClient
|
|
33
|
+
|
|
34
|
+
async with AsyncLicensyClient(api_key="sk_live_...") as client:
|
|
35
|
+
result = await client.verify_license(npi="1700013174", state="CA")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## All tools
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
# Single state, single physician
|
|
42
|
+
client.verify_license(npi="...", state="CA")
|
|
43
|
+
|
|
44
|
+
# All states for one physician
|
|
45
|
+
result = client.list_physician_licenses(npi="...", include_inactive=False)
|
|
46
|
+
for license in result:
|
|
47
|
+
print(license.state, license.status)
|
|
48
|
+
|
|
49
|
+
# Cheapest call — just the status word
|
|
50
|
+
client.get_license_status(npi="...", state="CA") # → "active"
|
|
51
|
+
|
|
52
|
+
# Audit-grade screenshot of the state-board page (Paid only)
|
|
53
|
+
client.get_screenshot_url(npi="...", state="CA")
|
|
54
|
+
|
|
55
|
+
# Bulk verification (Paid only)
|
|
56
|
+
client.bulk_verify([
|
|
57
|
+
{"npi": "1700013174", "state": "CA"},
|
|
58
|
+
{"npi": "1700013174", "state": "NY"},
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
# Find disciplinary actions (Paid only)
|
|
62
|
+
client.search_disciplinary_actions(npi="...")
|
|
63
|
+
|
|
64
|
+
# Was this physician licensed on YYYY-MM-DD? (Paid only)
|
|
65
|
+
client.get_license_history(npi="...", state="CA", as_of_date="2024-08-12")
|
|
66
|
+
|
|
67
|
+
# Subscribe to status-change webhooks (Paid only)
|
|
68
|
+
sub = client.subscribe_to_changes(
|
|
69
|
+
npis=["1700013174"],
|
|
70
|
+
webhook_url="https://your-app.com/webhooks/licensy",
|
|
71
|
+
)
|
|
72
|
+
client.unsubscribe(subscription_id=sub.subscription_id)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Error handling
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from licensy import (
|
|
79
|
+
LicensyClient,
|
|
80
|
+
AuthError,
|
|
81
|
+
NotFoundError,
|
|
82
|
+
RateLimitError,
|
|
83
|
+
TierUpgradeRequired,
|
|
84
|
+
ValidationError,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
client.get_screenshot_url(npi="...", state="CA")
|
|
89
|
+
except TierUpgradeRequired as e:
|
|
90
|
+
print(f"Need {e.required_tier} plan for screenshots")
|
|
91
|
+
except RateLimitError as e:
|
|
92
|
+
print(f"Slow down — retry in {e.retry_after_seconds}s")
|
|
93
|
+
except NotFoundError:
|
|
94
|
+
print("No license on file for that NPI/state")
|
|
95
|
+
except AuthError:
|
|
96
|
+
print("Check your API key")
|
|
97
|
+
except ValidationError as e:
|
|
98
|
+
print(f"Bad input: {e.details}")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
client = LicensyClient(
|
|
105
|
+
api_key="sk_live_...",
|
|
106
|
+
base_url="https://mcp.licensy.ai", # default; override for staging
|
|
107
|
+
timeout=30.0,
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
You can also bring your own `httpx.Client` for advanced proxying / retry
|
|
112
|
+
behaviour:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
import httpx
|
|
116
|
+
client = LicensyClient(api_key="...", http_client=httpx.Client(proxy="..."))
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Real-world recipes
|
|
120
|
+
|
|
121
|
+
### Daily roster check (cron-friendly)
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import csv, datetime, os, sys
|
|
125
|
+
from licensy import LicensyClient, NotFoundError
|
|
126
|
+
|
|
127
|
+
client = LicensyClient(api_key=os.environ["LICENSY_API_KEY"])
|
|
128
|
+
today = datetime.date.today()
|
|
129
|
+
warn_in = datetime.timedelta(days=60)
|
|
130
|
+
|
|
131
|
+
with open("roster.csv") as f, open("alerts.csv", "w") as out:
|
|
132
|
+
reader = csv.DictReader(f)
|
|
133
|
+
writer = csv.writer(out)
|
|
134
|
+
writer.writerow(["npi", "state", "issue"])
|
|
135
|
+
for row in reader:
|
|
136
|
+
try:
|
|
137
|
+
r = client.verify_license(npi=row["npi"], state=row["state"])
|
|
138
|
+
except NotFoundError:
|
|
139
|
+
writer.writerow([row["npi"], row["state"], "no record"])
|
|
140
|
+
continue
|
|
141
|
+
if not r.license_active:
|
|
142
|
+
writer.writerow([row["npi"], row["state"], f"inactive: {r.status}"])
|
|
143
|
+
elif r.expiration_date and (r.expiration_date - today) < warn_in:
|
|
144
|
+
writer.writerow([row["npi"], row["state"], f"expires {r.expiration_date}"])
|
|
145
|
+
|
|
146
|
+
print("Done — see alerts.csv", file=sys.stderr)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Webhook receiver (FastAPI) with signature verification
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
import hmac, hashlib, os
|
|
153
|
+
from fastapi import FastAPI, Header, HTTPException, Request
|
|
154
|
+
|
|
155
|
+
SECRET = os.environ["LICENSY_WEBHOOK_SECRET"].encode()
|
|
156
|
+
app = FastAPI()
|
|
157
|
+
|
|
158
|
+
@app.post("/webhooks/licensy")
|
|
159
|
+
async def receive(request: Request, x_licensy_signature: str = Header(...)):
|
|
160
|
+
body = await request.body()
|
|
161
|
+
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
|
|
162
|
+
if not hmac.compare_digest(expected, x_licensy_signature):
|
|
163
|
+
raise HTTPException(401, "bad signature")
|
|
164
|
+
event = await request.json()
|
|
165
|
+
# event.type ∈ {"license.expired", "license.status_changed", ...}
|
|
166
|
+
print(f"{event['type']} for NPI {event['data']['npi']}/{event['data']['state']}")
|
|
167
|
+
return {"ok": True}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Signing-secret format and snippets in 5 languages at <https://licensy.ai/docs#webhook-signing>.
|
|
171
|
+
|
|
172
|
+
### Bulk verification with retry
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
import time
|
|
176
|
+
from licensy import LicensyClient, RateLimitError
|
|
177
|
+
|
|
178
|
+
client = LicensyClient(api_key=os.environ["LICENSY_API_KEY"])
|
|
179
|
+
pairs = [{"npi": n, "state": s} for n, s in roster]
|
|
180
|
+
|
|
181
|
+
while True:
|
|
182
|
+
try:
|
|
183
|
+
results = client.bulk_verify(pairs)
|
|
184
|
+
break
|
|
185
|
+
except RateLimitError as e:
|
|
186
|
+
time.sleep(e.retry_after_seconds)
|
|
187
|
+
|
|
188
|
+
inactive = [r for r in results if not r.license_active]
|
|
189
|
+
print(f"{len(inactive)} inactive of {len(results)}")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Versioning
|
|
193
|
+
|
|
194
|
+
`licensy.__version__` is the package version. The wire protocol is
|
|
195
|
+
versioned at `/api/v1/...`; minor SDK releases stay backward-compatible
|
|
196
|
+
on the API.
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Licensy — real-time U.S. medical-license verification.
|
|
3
|
+
|
|
4
|
+
Quickstart
|
|
5
|
+
==========
|
|
6
|
+
|
|
7
|
+
::
|
|
8
|
+
|
|
9
|
+
from licensy import LicensyClient
|
|
10
|
+
|
|
11
|
+
client = LicensyClient(api_key="sk_live_...")
|
|
12
|
+
|
|
13
|
+
# Verify one physician in one state
|
|
14
|
+
result = client.verify_license(npi="1700013174", state="CA")
|
|
15
|
+
print(result.status, result.expiration_date)
|
|
16
|
+
|
|
17
|
+
# List all licenses for an NPI
|
|
18
|
+
for lic in client.list_physician_licenses(npi="1700013174"):
|
|
19
|
+
print(lic.state, lic.status)
|
|
20
|
+
|
|
21
|
+
Async usage
|
|
22
|
+
===========
|
|
23
|
+
|
|
24
|
+
::
|
|
25
|
+
|
|
26
|
+
from licensy import AsyncLicensyClient
|
|
27
|
+
|
|
28
|
+
async with AsyncLicensyClient(api_key="sk_live_...") as client:
|
|
29
|
+
result = await client.verify_license(npi="1700013174", state="CA")
|
|
30
|
+
|
|
31
|
+
Tier
|
|
32
|
+
====
|
|
33
|
+
|
|
34
|
+
The same client surface works for free and paid tiers. Paid-only fields
|
|
35
|
+
(screenshot URL, license number, board source, etc.) are populated when
|
|
36
|
+
the API key is on a Pro/Enterprise plan and ``None`` otherwise.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from licensy.client import LicensyClient, AsyncLicensyClient
|
|
40
|
+
from licensy.errors import (
|
|
41
|
+
LicensyError,
|
|
42
|
+
AuthError,
|
|
43
|
+
RateLimitError,
|
|
44
|
+
NotFoundError,
|
|
45
|
+
TierUpgradeRequired,
|
|
46
|
+
ValidationError,
|
|
47
|
+
)
|
|
48
|
+
from licensy.models import (
|
|
49
|
+
LicenseResult,
|
|
50
|
+
LicenseListResult,
|
|
51
|
+
LicenseStatus,
|
|
52
|
+
DisciplinaryAction,
|
|
53
|
+
HistoryResult,
|
|
54
|
+
SubscriptionResult,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
__version__ = "0.1.0"
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"LicensyClient",
|
|
61
|
+
"AsyncLicensyClient",
|
|
62
|
+
"LicensyError",
|
|
63
|
+
"AuthError",
|
|
64
|
+
"RateLimitError",
|
|
65
|
+
"NotFoundError",
|
|
66
|
+
"TierUpgradeRequired",
|
|
67
|
+
"ValidationError",
|
|
68
|
+
"LicenseResult",
|
|
69
|
+
"LicenseListResult",
|
|
70
|
+
"LicenseStatus",
|
|
71
|
+
"DisciplinaryAction",
|
|
72
|
+
"HistoryResult",
|
|
73
|
+
"SubscriptionResult",
|
|
74
|
+
"__version__",
|
|
75
|
+
]
|