internetdata 2.8.0__tar.gz → 2.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.
- {internetdata-2.8.0 → internetdata-2.9.0}/PKG-INFO +15 -1
- {internetdata-2.8.0 → internetdata-2.9.0}/README.md +14 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/__init__.py +3 -1
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_core.py +36 -5
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/aio.py +29 -4
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/client.py +39 -4
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/models.py +35 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/testdata/testdata.json +491 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/tests/helpers.py +3 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/tests/test_oauth.py +123 -1
- {internetdata-2.8.0 → internetdata-2.9.0}/.gitignore +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/LICENSE +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/pyproject.toml +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/scripts/download-spec.sh +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/scripts/generate.sh +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/scripts/publish.sh +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/scripts/v2_subset.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/spec/openapi.yaml +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/__init__.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/__init__.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/account/__init__.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/account/account_identity.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/api_keys/__init__.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/api_keys/account_create_apikey.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/api_keys/account_list_apikeys.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/api_keys/account_reveal_apikey.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/api_keys/account_revoke_apikey.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/api_keys/account_rotate_apikey.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/authorization/__init__.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/authorization/oauth_authorize.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/authorization/oauth_device_authorization.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/authorization/oauth_metadata.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/authorization/oauth_register.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/authorization/oauth_revoke.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/authorization/oauth_token.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/database_v_2/__init__.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/database_v_2/database_checksum_v2.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/database_v_2/database_metadata_v2.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/database_v_2/download_database_v2.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/database_v_2/list_databases.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/database_v_2/list_downloads.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/organization/__init__.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/organization/account_org.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/api/organization/account_org_members.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/client.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/errors.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/__init__.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/account_create_apikey_request.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/account_created_apikey.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/account_org.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/account_org_ref.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/account_org_wrap.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/account_rc.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/account_revealed_apikey.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/account_user.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/apikey_detail.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/apikey_list.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/client_registration.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/client_registration_request.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/database.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/database_checksum_v2_response_200.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/database_format.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/database_license_type_type_1.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/database_license_type_type_2_type_1.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/database_license_type_type_3_type_1.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/database_metadata.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/database_metadata_column.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/database_metadata_sample.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/database_metadata_sample_additional_property_item.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/database_metadata_sample_size.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/database_metadata_schema.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/database_metadata_size.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/database_version.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/db_checksums.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/device_authorization.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/device_authorization_request.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/download.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/download_outcome.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/error.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/identity.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/list_databases_response_200.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/list_downloads_response_200.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/oauth_authorize_code_challenge_method.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/oauth_authorize_response_type.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/oauth_error.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/oauth_metadata.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/oauth_revoke_response_200.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/revoke_request.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/standing.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/token_request.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/models/token_response.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/_generated/types.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/errors.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/src/internetdata/py.typed +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/tests/conftest.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/tests/test_client.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/tests/test_conformance.py +0 -0
- {internetdata-2.8.0 → internetdata-2.9.0}/tests/test_download.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: internetdata
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.0
|
|
4
4
|
Summary: Official Python client library for the InternetData API. Download and verify licensed IP datasets.
|
|
5
5
|
Project-URL: Homepage, https://internetdata.io
|
|
6
6
|
Project-URL: Documentation, https://docs.internetdata.io
|
|
@@ -223,6 +223,20 @@ tokens = signin.oauth.exchange_authorization_code("your-client-id", code, pkce.v
|
|
|
223
223
|
|
|
224
224
|
Check that `state` came back as you sent it before you exchange `code`, which works once. The client ID can also be the https URL of a client metadata document your app serves, and such an app is never handed a key, so `tokens.apikey` stays `None`.
|
|
225
225
|
|
|
226
|
+
### Register a client
|
|
227
|
+
|
|
228
|
+
An app with no client ID, and no client metadata document to use as one, registers itself and signs in with the `client_id` it gets back:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
client = InternetData()
|
|
232
|
+
registration = client.oauth.register(
|
|
233
|
+
["http://127.0.0.1:8765/callback"], client_name="Your App"
|
|
234
|
+
)
|
|
235
|
+
# registration.client_id is the client ID for authorization_url and exchange_authorization_code.
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Each redirect URI must be https on a public host name, or http on a loopback address, which then matches any port. Nothing is kept until a person approves the client, so an app with no client ID saved can simply register again.
|
|
239
|
+
|
|
226
240
|
## Other Libraries
|
|
227
241
|
|
|
228
242
|
There are official InternetData client libraries available for many languages including PHP, Python, Go, Java, and Ruby. See our GitHub at https://github.com/internetdata for more.
|
|
@@ -189,6 +189,20 @@ tokens = signin.oauth.exchange_authorization_code("your-client-id", code, pkce.v
|
|
|
189
189
|
|
|
190
190
|
Check that `state` came back as you sent it before you exchange `code`, which works once. The client ID can also be the https URL of a client metadata document your app serves, and such an app is never handed a key, so `tokens.apikey` stays `None`.
|
|
191
191
|
|
|
192
|
+
### Register a client
|
|
193
|
+
|
|
194
|
+
An app with no client ID, and no client metadata document to use as one, registers itself and signs in with the `client_id` it gets back:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
client = InternetData()
|
|
198
|
+
registration = client.oauth.register(
|
|
199
|
+
["http://127.0.0.1:8765/callback"], client_name="Your App"
|
|
200
|
+
)
|
|
201
|
+
# registration.client_id is the client ID for authorization_url and exchange_authorization_code.
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Each redirect URI must be https on a public host name, or http on a loopback address, which then matches any port. Nothing is kept until a person approves the client, so an app with no client ID saved can simply register again.
|
|
205
|
+
|
|
192
206
|
## Other Libraries
|
|
193
207
|
|
|
194
208
|
There are official InternetData client libraries available for many languages including PHP, Python, Go, Java, and Ruby. See our GitHub at https://github.com/internetdata for more.
|
|
@@ -24,6 +24,7 @@ from .models import (
|
|
|
24
24
|
DATABASE_FORMATS,
|
|
25
25
|
LICENSE_TYPES,
|
|
26
26
|
STANDINGS,
|
|
27
|
+
ClientRegistration,
|
|
27
28
|
Database,
|
|
28
29
|
DatabaseMetadata,
|
|
29
30
|
DatabaseVersion,
|
|
@@ -39,7 +40,7 @@ from .models import (
|
|
|
39
40
|
TokenResponse,
|
|
40
41
|
)
|
|
41
42
|
|
|
42
|
-
__version__ = "2.
|
|
43
|
+
__version__ = "2.9.0"
|
|
43
44
|
|
|
44
45
|
__all__ = [
|
|
45
46
|
"DATABASE_FORMATS",
|
|
@@ -49,6 +50,7 @@ __all__ = [
|
|
|
49
50
|
"AsyncDatabaseApi",
|
|
50
51
|
"AsyncInternetData",
|
|
51
52
|
"AsyncOauthApi",
|
|
53
|
+
"ClientRegistration",
|
|
52
54
|
"Database",
|
|
53
55
|
"DatabaseApi",
|
|
54
56
|
"DatabaseMetadata",
|
|
@@ -14,7 +14,7 @@ import os
|
|
|
14
14
|
import secrets
|
|
15
15
|
import threading
|
|
16
16
|
import time
|
|
17
|
-
from collections.abc import Callable, Iterator
|
|
17
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from types import ModuleType
|
|
20
20
|
from typing import IO, Any, TypeVar, cast
|
|
@@ -36,6 +36,7 @@ OAUTH_DEVICE_AUTHORIZATION_PATH = "/oauth/device_authorization"
|
|
|
36
36
|
OAUTH_TOKEN_PATH = "/oauth/token"
|
|
37
37
|
OAUTH_REVOKE_PATH = "/oauth/revoke"
|
|
38
38
|
OAUTH_AUTHORIZE_PATH = "/oauth/authorize"
|
|
39
|
+
OAUTH_REGISTER_PATH = "/oauth/register"
|
|
39
40
|
DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code"
|
|
40
41
|
|
|
41
42
|
# One chunk of a transfer, and therefore the ceiling on what a download of any size
|
|
@@ -348,10 +349,13 @@ def oauth_request(
|
|
|
348
349
|
path: str,
|
|
349
350
|
form: dict[str, str] | None,
|
|
350
351
|
bound: float | None,
|
|
352
|
+
body: dict[str, Any] | None = None,
|
|
351
353
|
) -> httpx.Response:
|
|
352
|
-
"""One attempt at an OAuth endpoint, carrying no credential, its failure raised.
|
|
354
|
+
"""One attempt at an OAuth endpoint, carrying no credential, its failure raised. `body`
|
|
355
|
+
goes as JSON, for `register`; every other one sends a form or nothing."""
|
|
353
356
|
http = client.get_httpx_client()
|
|
354
|
-
|
|
357
|
+
req = _oauth_build(http, method, path, form, bound, body)
|
|
358
|
+
return oauth_checked(exchange(http, req, bound))
|
|
355
359
|
|
|
356
360
|
|
|
357
361
|
async def oauth_request_async(
|
|
@@ -360,13 +364,39 @@ async def oauth_request_async(
|
|
|
360
364
|
path: str,
|
|
361
365
|
form: dict[str, str] | None,
|
|
362
366
|
bound: float | None,
|
|
367
|
+
body: dict[str, Any] | None = None,
|
|
363
368
|
) -> httpx.Response:
|
|
364
369
|
"""`oauth_request`, awaited."""
|
|
365
370
|
http = client.get_async_httpx_client()
|
|
366
|
-
req = _oauth_build(http, method, path, form, bound)
|
|
371
|
+
req = _oauth_build(http, method, path, form, bound, body)
|
|
367
372
|
return oauth_checked(await exchange_async(http, req, bound))
|
|
368
373
|
|
|
369
374
|
|
|
375
|
+
def registration_body(redirect_uris: Sequence[str], client_name: str | None) -> dict[str, Any]:
|
|
376
|
+
"""`register`'s JSON body: the redirect URIs in the order given, then `client_name`
|
|
377
|
+
unless it is None or empty. No URI at all, an empty one, or one with no UTF-8 (a lone
|
|
378
|
+
surrogate) is refused before anything is sent."""
|
|
379
|
+
if isinstance(redirect_uris, str) or not redirect_uris:
|
|
380
|
+
raise ValueError(f"redirect_uris must list at least one URI, not {redirect_uris!r}")
|
|
381
|
+
uris = list(redirect_uris)
|
|
382
|
+
for i, uri in enumerate(uris):
|
|
383
|
+
_check_text(f"redirect_uris[{i}]", uri)
|
|
384
|
+
body: dict[str, Any] = {"redirect_uris": uris}
|
|
385
|
+
if client_name:
|
|
386
|
+
_check_text("client_name", client_name)
|
|
387
|
+
body["client_name"] = client_name
|
|
388
|
+
return body
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _check_text(name: str, value: Any) -> None:
|
|
392
|
+
if not isinstance(value, str) or value == "":
|
|
393
|
+
raise ValueError(f"{name} must be a non-empty string, not {value!r}")
|
|
394
|
+
try:
|
|
395
|
+
value.encode("utf-8")
|
|
396
|
+
except UnicodeEncodeError as exc:
|
|
397
|
+
raise ValueError(f"{name} has no UTF-8 to send: {value!r}") from exc
|
|
398
|
+
|
|
399
|
+
|
|
370
400
|
def authorization_url(
|
|
371
401
|
base_url: str,
|
|
372
402
|
client_id: str,
|
|
@@ -571,8 +601,9 @@ def _oauth_build(
|
|
|
571
601
|
path: str,
|
|
572
602
|
form: dict[str, str] | None,
|
|
573
603
|
bound: float | None,
|
|
604
|
+
body: dict[str, Any] | None = None,
|
|
574
605
|
) -> httpx.Request:
|
|
575
|
-
req = http.build_request(method, path, data=form, timeout=httpx.Timeout(bound))
|
|
606
|
+
req = http.build_request(method, path, data=form, json=body, timeout=httpx.Timeout(bound))
|
|
576
607
|
req.headers.pop("authorization", None)
|
|
577
608
|
return req
|
|
578
609
|
|
|
@@ -10,9 +10,9 @@ from __future__ import annotations
|
|
|
10
10
|
import asyncio
|
|
11
11
|
import builtins
|
|
12
12
|
import os
|
|
13
|
-
from collections.abc import Awaitable, Callable
|
|
13
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
14
14
|
from types import TracebackType
|
|
15
|
-
from typing import Self, TypeVar
|
|
15
|
+
from typing import Any, Self, TypeVar
|
|
16
16
|
|
|
17
17
|
import httpx
|
|
18
18
|
|
|
@@ -24,6 +24,7 @@ from ._core import (
|
|
|
24
24
|
DEVICE_CODE_GRANT,
|
|
25
25
|
OAUTH_DEVICE_AUTHORIZATION_PATH,
|
|
26
26
|
OAUTH_METADATA_PATH,
|
|
27
|
+
OAUTH_REGISTER_PATH,
|
|
27
28
|
OAUTH_REVOKE_PATH,
|
|
28
29
|
OAUTH_TOKEN_PATH,
|
|
29
30
|
POLL_DEADLINE_CAP,
|
|
@@ -45,6 +46,7 @@ from ._core import (
|
|
|
45
46
|
part_file,
|
|
46
47
|
pkce_challenge,
|
|
47
48
|
redirect_location,
|
|
49
|
+
registration_body,
|
|
48
50
|
request_async,
|
|
49
51
|
retry_delay,
|
|
50
52
|
storage_refusal,
|
|
@@ -60,6 +62,7 @@ from ._generated.client import AuthenticatedClient
|
|
|
60
62
|
from ._generated.models.database_format import DatabaseFormat
|
|
61
63
|
from .errors import InternetDataError, OauthError, OauthExpiredTokenError
|
|
62
64
|
from .models import (
|
|
65
|
+
ClientRegistration,
|
|
63
66
|
Database,
|
|
64
67
|
DatabaseMetadata,
|
|
65
68
|
DeviceAuthorization,
|
|
@@ -68,6 +71,7 @@ from .models import (
|
|
|
68
71
|
OauthMetadata,
|
|
69
72
|
Pkce,
|
|
70
73
|
TokenResponse,
|
|
74
|
+
to_client_registration,
|
|
71
75
|
to_device_authorization,
|
|
72
76
|
to_metadata,
|
|
73
77
|
to_oauth_metadata,
|
|
@@ -434,6 +438,22 @@ class AsyncOauthApi:
|
|
|
434
438
|
"""The `S256` challenge for a PKCE verifier: its SHA-256, as unpadded base64url."""
|
|
435
439
|
return pkce_challenge(verifier)
|
|
436
440
|
|
|
441
|
+
async def register(
|
|
442
|
+
self,
|
|
443
|
+
redirect_uris: Sequence[str],
|
|
444
|
+
*,
|
|
445
|
+
client_name: str | None = None,
|
|
446
|
+
timeout: float | None = None,
|
|
447
|
+
) -> ClientRegistration:
|
|
448
|
+
"""Register a client for the authorization code flow; see `OauthApi.register`."""
|
|
449
|
+
body = registration_body(redirect_uris, client_name)
|
|
450
|
+
|
|
451
|
+
async def call() -> ClientRegistration:
|
|
452
|
+
res = await self._send("POST", OAUTH_REGISTER_PATH, None, timeout, body)
|
|
453
|
+
return to_client_registration(oauth_body(res), res.status_code)
|
|
454
|
+
|
|
455
|
+
return await self._owner._retrying(call, self._owner._retries)
|
|
456
|
+
|
|
437
457
|
async def revoke(self, client_id: str, token: str, *, timeout: float | None = None) -> None:
|
|
438
458
|
"""End a token; revoking the refresh token signs the machine out."""
|
|
439
459
|
|
|
@@ -480,7 +500,12 @@ class AsyncOauthApi:
|
|
|
480
500
|
return await self._owner._retrying(call, 0)
|
|
481
501
|
|
|
482
502
|
async def _send(
|
|
483
|
-
self,
|
|
503
|
+
self,
|
|
504
|
+
method: str,
|
|
505
|
+
path: str,
|
|
506
|
+
form: dict[str, str] | None,
|
|
507
|
+
timeout: float | None,
|
|
508
|
+
body: dict[str, Any] | None = None,
|
|
484
509
|
) -> httpx.Response:
|
|
485
510
|
bound = self._owner._bound(timeout)
|
|
486
|
-
return await oauth_request_async(self._owner._client, method, path, form, bound)
|
|
511
|
+
return await oauth_request_async(self._owner._client, method, path, form, bound, body)
|
|
@@ -5,9 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
import builtins
|
|
6
6
|
import os
|
|
7
7
|
import time
|
|
8
|
-
from collections.abc import Callable
|
|
8
|
+
from collections.abc import Callable, Sequence
|
|
9
9
|
from types import TracebackType
|
|
10
|
-
from typing import Self, TypeVar
|
|
10
|
+
from typing import Any, Self, TypeVar
|
|
11
11
|
|
|
12
12
|
import httpx
|
|
13
13
|
|
|
@@ -19,6 +19,7 @@ from ._core import (
|
|
|
19
19
|
DEVICE_CODE_GRANT,
|
|
20
20
|
OAUTH_DEVICE_AUTHORIZATION_PATH,
|
|
21
21
|
OAUTH_METADATA_PATH,
|
|
22
|
+
OAUTH_REGISTER_PATH,
|
|
22
23
|
OAUTH_REVOKE_PATH,
|
|
23
24
|
OAUTH_TOKEN_PATH,
|
|
24
25
|
POLL_DEADLINE_CAP,
|
|
@@ -40,6 +41,7 @@ from ._core import (
|
|
|
40
41
|
part_file,
|
|
41
42
|
pkce_challenge,
|
|
42
43
|
redirect_location,
|
|
44
|
+
registration_body,
|
|
43
45
|
request,
|
|
44
46
|
retry_delay,
|
|
45
47
|
storage_refusal,
|
|
@@ -55,6 +57,7 @@ from ._generated.client import AuthenticatedClient
|
|
|
55
57
|
from ._generated.models.database_format import DatabaseFormat
|
|
56
58
|
from .errors import InternetDataError, OauthError, OauthExpiredTokenError
|
|
57
59
|
from .models import (
|
|
60
|
+
ClientRegistration,
|
|
58
61
|
Database,
|
|
59
62
|
DatabaseMetadata,
|
|
60
63
|
DeviceAuthorization,
|
|
@@ -63,6 +66,7 @@ from .models import (
|
|
|
63
66
|
OauthMetadata,
|
|
64
67
|
Pkce,
|
|
65
68
|
TokenResponse,
|
|
69
|
+
to_client_registration,
|
|
66
70
|
to_device_authorization,
|
|
67
71
|
to_metadata,
|
|
68
72
|
to_oauth_metadata,
|
|
@@ -470,6 +474,31 @@ class OauthApi:
|
|
|
470
474
|
"""The `S256` challenge for a PKCE verifier: its SHA-256, as unpadded base64url."""
|
|
471
475
|
return pkce_challenge(verifier)
|
|
472
476
|
|
|
477
|
+
def register(
|
|
478
|
+
self,
|
|
479
|
+
redirect_uris: Sequence[str],
|
|
480
|
+
*,
|
|
481
|
+
client_name: str | None = None,
|
|
482
|
+
timeout: float | None = None,
|
|
483
|
+
) -> ClientRegistration:
|
|
484
|
+
"""Register a client for the authorization code flow, for a program that cannot
|
|
485
|
+
serve a Client ID Metadata Document, and get its `client_id`.
|
|
486
|
+
|
|
487
|
+
Each redirect URI must be https on a public host name, or http on a loopback
|
|
488
|
+
address, which then matches on any port; the server refuses any other with the
|
|
489
|
+
`OauthError` `invalid_redirect_uri`. Nothing is kept until a person approves the
|
|
490
|
+
client, so registering again costs nothing, and this is retried like a lookup. No
|
|
491
|
+
URI at all, an empty one, or one with no UTF-8 raises `ValueError` before anything
|
|
492
|
+
is sent.
|
|
493
|
+
"""
|
|
494
|
+
body = registration_body(redirect_uris, client_name)
|
|
495
|
+
|
|
496
|
+
def call() -> ClientRegistration:
|
|
497
|
+
res = self._send("POST", OAUTH_REGISTER_PATH, None, timeout, body)
|
|
498
|
+
return to_client_registration(oauth_body(res), res.status_code)
|
|
499
|
+
|
|
500
|
+
return self._owner._retrying(call, self._owner._retries)
|
|
501
|
+
|
|
473
502
|
def revoke(self, client_id: str, token: str, *, timeout: float | None = None) -> None:
|
|
474
503
|
"""End a token. A refresh token ends the whole sign-in and every token it issued, so
|
|
475
504
|
revoking it is how a program signs the machine out."""
|
|
@@ -524,6 +553,12 @@ class OauthApi:
|
|
|
524
553
|
return self._owner._retrying(call, 0)
|
|
525
554
|
|
|
526
555
|
def _send(
|
|
527
|
-
self,
|
|
556
|
+
self,
|
|
557
|
+
method: str,
|
|
558
|
+
path: str,
|
|
559
|
+
form: dict[str, str] | None,
|
|
560
|
+
timeout: float | None,
|
|
561
|
+
body: dict[str, Any] | None = None,
|
|
528
562
|
) -> httpx.Response:
|
|
529
|
-
|
|
563
|
+
bound = self._owner._bound(timeout)
|
|
564
|
+
return oauth_request(self._owner._client, method, path, form, bound, body)
|
|
@@ -361,6 +361,26 @@ class TokenResponse:
|
|
|
361
361
|
apikey: str | None = field(default=None, repr=False)
|
|
362
362
|
|
|
363
363
|
|
|
364
|
+
@dataclass(frozen=True, slots=True)
|
|
365
|
+
class ClientRegistration:
|
|
366
|
+
"""A registered client, public and with no secret: it proves each sign-in with PKCE.
|
|
367
|
+
|
|
368
|
+
`client_id` goes to `authorization_url` and `exchange_authorization_code`.
|
|
369
|
+
`client_id_issued_at` is Unix seconds, and `client_name` the name the person will see,
|
|
370
|
+
a placeholder when none was given or none can be shown. `token_endpoint_auth_method` is
|
|
371
|
+
always `none`, and `scope` the most this client may be granted.
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
client_id: str
|
|
375
|
+
client_id_issued_at: int
|
|
376
|
+
client_name: str
|
|
377
|
+
redirect_uris: tuple[str, ...]
|
|
378
|
+
token_endpoint_auth_method: str
|
|
379
|
+
grant_types: tuple[str, ...] | None = None
|
|
380
|
+
response_types: tuple[str, ...] | None = None
|
|
381
|
+
scope: str | None = None
|
|
382
|
+
|
|
383
|
+
|
|
364
384
|
def to_oauth_metadata(body: Any, status: int) -> OauthMetadata:
|
|
365
385
|
return OauthMetadata(**_members(body, _OAUTH_METADATA, status))
|
|
366
386
|
|
|
@@ -373,6 +393,10 @@ def to_token_response(body: Any, status: int) -> TokenResponse:
|
|
|
373
393
|
return TokenResponse(**_members(body, _TOKEN_RESPONSE, status))
|
|
374
394
|
|
|
375
395
|
|
|
396
|
+
def to_client_registration(body: Any, status: int) -> ClientRegistration:
|
|
397
|
+
return ClientRegistration(**_members(body, _CLIENT_REGISTRATION, status))
|
|
398
|
+
|
|
399
|
+
|
|
376
400
|
# A member's type on the wire, and whether a 2xx without it is malformed.
|
|
377
401
|
_Member = tuple[Literal["str", "int", "bool", "strs"], bool]
|
|
378
402
|
|
|
@@ -412,6 +436,17 @@ _TOKEN_RESPONSE: dict[str, _Member] = {
|
|
|
412
436
|
"apikey": ("str", False),
|
|
413
437
|
}
|
|
414
438
|
|
|
439
|
+
_CLIENT_REGISTRATION: dict[str, _Member] = {
|
|
440
|
+
"client_id": ("str", True),
|
|
441
|
+
"client_id_issued_at": ("int", True),
|
|
442
|
+
"client_name": ("str", True),
|
|
443
|
+
"redirect_uris": ("strs", True),
|
|
444
|
+
"token_endpoint_auth_method": ("str", True),
|
|
445
|
+
"grant_types": ("strs", False),
|
|
446
|
+
"response_types": ("strs", False),
|
|
447
|
+
"scope": ("str", False),
|
|
448
|
+
}
|
|
449
|
+
|
|
415
450
|
_WIRE_NAMES = {"apikey_id": "mslm:apikey_id", "apikey": "mslm:apikey"}
|
|
416
451
|
|
|
417
452
|
|