internetdata 2.4.1__tar.gz → 2.5.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.
Files changed (95) hide show
  1. {internetdata-2.4.1 → internetdata-2.5.0}/PKG-INFO +2 -2
  2. {internetdata-2.4.1 → internetdata-2.5.0}/README.md +1 -1
  3. {internetdata-2.4.1 → internetdata-2.5.0}/spec/openapi.yaml +64 -15
  4. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/__init__.py +1 -1
  5. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/authorization/oauth_authorize.py +25 -9
  6. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/authorization/oauth_token.py +20 -4
  7. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/__init__.py +2 -0
  8. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/database_metadata.py +30 -0
  9. internetdata-2.5.0/src/internetdata/_generated/models/database_metadata_sample_size.py +47 -0
  10. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/database_version.py +22 -0
  11. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/download.py +8 -0
  12. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/oauth_metadata.py +14 -0
  13. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/token_request.py +11 -1
  14. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/token_response.py +10 -7
  15. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/models.py +19 -0
  16. {internetdata-2.4.1 → internetdata-2.5.0}/testdata/testdata.json +2 -2
  17. {internetdata-2.4.1 → internetdata-2.5.0}/tests/test_client.py +3 -0
  18. {internetdata-2.4.1 → internetdata-2.5.0}/.gitignore +0 -0
  19. {internetdata-2.4.1 → internetdata-2.5.0}/LICENSE +0 -0
  20. {internetdata-2.4.1 → internetdata-2.5.0}/pyproject.toml +0 -0
  21. {internetdata-2.4.1 → internetdata-2.5.0}/scripts/download-spec.sh +0 -0
  22. {internetdata-2.4.1 → internetdata-2.5.0}/scripts/generate.sh +0 -0
  23. {internetdata-2.4.1 → internetdata-2.5.0}/scripts/publish.sh +0 -0
  24. {internetdata-2.4.1 → internetdata-2.5.0}/scripts/v2_subset.py +0 -0
  25. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_core.py +0 -0
  26. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/__init__.py +0 -0
  27. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/__init__.py +0 -0
  28. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/account/__init__.py +0 -0
  29. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/account/account_identity.py +0 -0
  30. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/api_keys/__init__.py +0 -0
  31. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/api_keys/account_create_apikey.py +0 -0
  32. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/api_keys/account_list_apikeys.py +0 -0
  33. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/api_keys/account_reveal_apikey.py +0 -0
  34. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/api_keys/account_revoke_apikey.py +0 -0
  35. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/api_keys/account_rotate_apikey.py +0 -0
  36. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/authorization/__init__.py +0 -0
  37. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/authorization/oauth_device_authorization.py +0 -0
  38. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/authorization/oauth_metadata.py +0 -0
  39. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/authorization/oauth_revoke.py +0 -0
  40. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/database_v_2/__init__.py +0 -0
  41. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/database_v_2/database_checksum_v2.py +0 -0
  42. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/database_v_2/database_metadata_v2.py +0 -0
  43. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/database_v_2/download_database_v2.py +0 -0
  44. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/database_v_2/list_databases.py +0 -0
  45. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/database_v_2/list_downloads.py +0 -0
  46. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/organization/__init__.py +0 -0
  47. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/organization/account_org.py +0 -0
  48. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/api/organization/account_org_members.py +0 -0
  49. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/client.py +0 -0
  50. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/errors.py +0 -0
  51. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/account_create_apikey_request.py +0 -0
  52. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/account_created_apikey.py +0 -0
  53. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/account_org.py +0 -0
  54. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/account_org_ref.py +0 -0
  55. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/account_org_wrap.py +0 -0
  56. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/account_rc.py +0 -0
  57. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/account_revealed_apikey.py +0 -0
  58. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/account_user.py +0 -0
  59. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/apikey_detail.py +0 -0
  60. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/apikey_list.py +0 -0
  61. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/database.py +0 -0
  62. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/database_checksum_v2_response_200.py +0 -0
  63. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/database_format.py +0 -0
  64. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/database_license_type_type_1.py +0 -0
  65. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/database_license_type_type_2_type_1.py +0 -0
  66. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/database_license_type_type_3_type_1.py +0 -0
  67. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/database_metadata_column.py +0 -0
  68. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/database_metadata_sample.py +0 -0
  69. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/database_metadata_sample_additional_property_item.py +0 -0
  70. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/database_metadata_schema.py +0 -0
  71. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/database_metadata_size.py +0 -0
  72. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/db_checksums.py +0 -0
  73. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/device_authorization.py +0 -0
  74. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/device_authorization_request.py +0 -0
  75. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/download_outcome.py +0 -0
  76. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/error.py +0 -0
  77. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/identity.py +0 -0
  78. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/list_databases_response_200.py +0 -0
  79. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/list_downloads_response_200.py +0 -0
  80. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/oauth_authorize_code_challenge_method.py +0 -0
  81. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/oauth_authorize_response_type.py +0 -0
  82. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/oauth_error.py +0 -0
  83. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/oauth_revoke_response_200.py +0 -0
  84. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/revoke_request.py +0 -0
  85. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/models/standing.py +0 -0
  86. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/_generated/types.py +0 -0
  87. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/aio.py +0 -0
  88. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/client.py +0 -0
  89. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/errors.py +0 -0
  90. {internetdata-2.4.1 → internetdata-2.5.0}/src/internetdata/py.typed +0 -0
  91. {internetdata-2.4.1 → internetdata-2.5.0}/tests/conftest.py +0 -0
  92. {internetdata-2.4.1 → internetdata-2.5.0}/tests/helpers.py +0 -0
  93. {internetdata-2.4.1 → internetdata-2.5.0}/tests/test_conformance.py +0 -0
  94. {internetdata-2.4.1 → internetdata-2.5.0}/tests/test_download.py +0 -0
  95. {internetdata-2.4.1 → internetdata-2.5.0}/tests/test_oauth.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: internetdata
3
- Version: 2.4.1
3
+ Version: 2.5.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
@@ -211,7 +211,7 @@ There are official InternetData client libraries available for many languages in
211
211
 
212
212
  InternetData: IP and network intelligence databases, published as CSV and MMDB, licensed by contract and downloadable on demand.
213
213
 
214
- [<img src="https://s3.internetdata.io/internetdata-public/brand/mark.svg" alt="InternetData" width="96"/>](https://internetdata.io/)
214
+ [<img src="https://s3.internetdata.io/internetdata-public/brand/mark.svg" alt="InternetData" height="64"/>](https://internetdata.io/)
215
215
 
216
216
  ## License
217
217
 
@@ -177,7 +177,7 @@ There are official InternetData client libraries available for many languages in
177
177
 
178
178
  InternetData: IP and network intelligence databases, published as CSV and MMDB, licensed by contract and downloadable on demand.
179
179
 
180
- [<img src="https://s3.internetdata.io/internetdata-public/brand/mark.svg" alt="InternetData" width="96"/>](https://internetdata.io/)
180
+ [<img src="https://s3.internetdata.io/internetdata-public/brand/mark.svg" alt="InternetData" height="64"/>](https://internetdata.io/)
181
181
 
182
182
  ## License
183
183
 
@@ -12,7 +12,7 @@ info:
12
12
  email: support@internetdata.io
13
13
  url: https://internetdata.io
14
14
  termsOfService: https://internetdata.io/terms
15
- version: 2026.09.21
15
+ version: 2026.09.26
16
16
  servers:
17
17
  - url: https://internetdata.io
18
18
  tags:
@@ -658,6 +658,14 @@ paths:
658
658
  The browser entry point for the authorization-code flow. This is a
659
659
  redirect target, not something to call from code.
660
660
 
661
+ Any client may sign in without registering first by using a Client ID
662
+ Metadata Document: make `client_id` an https URL that serves your
663
+ client's metadata as JSON, naming that same URL as its `client_id`,
664
+ with `token_endpoint_auth_method` `none` and your `redirect_uris`. An
665
+ https redirect URI must be on the same origin as the `client_id`; a
666
+ loopback one (`http://127.0.0.1`, `http://[::1]`, `http://localhost`)
667
+ matches on any port. Such a client is granted `apikeys.use` at most.
668
+
661
669
  An unknown `client_id` or an unregistered `redirect_uri` is shown to the
662
670
  USER and never redirected, because sending an error to an address we
663
671
  have not verified belongs to you is how an open redirector works.
@@ -689,12 +697,12 @@ paths:
689
697
  type: string
690
698
  - name: code_challenge_method
691
699
  in: query
692
- required: false
700
+ required: true
693
701
  schema:
694
702
  type: string
695
703
  enum:
696
704
  - S256
697
- description: S256 only. `plain` is refused rather than downgraded.
705
+ description: S256, named explicitly. `plain`, or no method at all, is refused rather than downgraded.
698
706
  - name: scope
699
707
  in: query
700
708
  required: false
@@ -711,12 +719,18 @@ paths:
711
719
  required: false
712
720
  schema:
713
721
  type: string
714
- description: RFC 8707. What the token is FOR, so it cannot be replayed elsewhere.
722
+ description: |
723
+ RFC 8707. What the token is FOR, so it cannot be replayed elsewhere:
724
+ this brand's MCP server, the only resource it issues tokens for.
725
+ Anything else answers `invalid_target`. A client that names none is
726
+ given that server.
715
727
  responses:
716
728
  '302':
717
729
  description: A redirect to the consent screen, or back to your redirect_uri with an error.
718
730
  '400':
719
731
  description: Shown to the user. An unverified redirect target is never sent an error.
732
+ '429':
733
+ description: Shown to the user. Too many authorizations from one address.
720
734
  /oauth/token:
721
735
  post:
722
736
  operationId: oauthToken
@@ -732,10 +746,14 @@ paths:
732
746
  `slow_down`, which means widen your interval and keep it widened.
733
747
 
734
748
  `authorization_code` exchanges a code from `/oauth/authorize`, with the
735
- `code_verifier` matching the challenge you sent.
749
+ `code_verifier` matching the challenge you sent and the same
750
+ `redirect_uri`. A code works once: presenting it again also revokes
751
+ what the first exchange issued.
736
752
 
737
753
  `refresh_token` exchanges a refresh token. The presented token is
738
754
  consumed whatever happens next, so store the new one before using it.
755
+ Presenting a consumed refresh token again ends the whole authorization,
756
+ since it means the token was copied.
739
757
  security: []
740
758
  requestBody:
741
759
  required: true
@@ -757,8 +775,10 @@ paths:
757
775
  `access_denied` means the person refused, and `expired_token` that
758
776
  the device code is no longer valid: it expired, or it was already
759
777
  exchanged or refused. `invalid_grant` means the code, device code or
760
- refresh token is not valid for this `client_id`. `slow_down` also
761
- answers any grant when this address sends too many requests.
778
+ refresh token is not valid for this `client_id`. `invalid_target`
779
+ means `resource` names something other than what was authorized.
780
+ `slow_down` also answers any grant when this address sends too many
781
+ requests.
762
782
  content:
763
783
  application/json:
764
784
  schema:
@@ -981,6 +1001,11 @@ components:
981
1001
  MMDB exists for them.
982
1002
  items:
983
1003
  $ref: '#/components/schemas/DatabaseFormat'
1004
+ sample_formats:
1005
+ type: array
1006
+ description: The formats an evaluation sample is published in, if any.
1007
+ items:
1008
+ $ref: '#/components/schemas/DatabaseFormat'
984
1009
  Download:
985
1010
  type: object
986
1011
  description: One download ATTEMPT, refusals included.
@@ -988,6 +1013,7 @@ components:
988
1013
  - dataset_id
989
1014
  - format
990
1015
  - outcome
1016
+ - sample
991
1017
  - bytes
992
1018
  - http_status
993
1019
  - apikey_id
@@ -1008,6 +1034,9 @@ components:
1008
1034
  - expired
1009
1035
  - unknown
1010
1036
  - unavailable
1037
+ sample:
1038
+ type: boolean
1039
+ description: The evaluation sample rather than the database itself.
1011
1040
  bytes:
1012
1041
  type: integer
1013
1042
  format: int64
@@ -1088,6 +1117,15 @@ components:
1088
1117
  additionalProperties:
1089
1118
  type: integer
1090
1119
  format: int64
1120
+ sample_size:
1121
+ type: object
1122
+ description: Bytes per format of the evaluation sample, where one is published.
1123
+ additionalProperties:
1124
+ type: integer
1125
+ format: int64
1126
+ sample_entries:
1127
+ type: integer
1128
+ description: Row count in the evaluation sample.
1091
1129
  DbChecksumSuccessResponse:
1092
1130
  type: object
1093
1131
  required:
@@ -1425,6 +1463,9 @@ components:
1425
1463
  type: array
1426
1464
  items:
1427
1465
  type: string
1466
+ client_id_metadata_document_supported:
1467
+ type: boolean
1468
+ description: Any client may sign in with an https `client_id` serving its own metadata.
1428
1469
  token_endpoint_auth_methods_supported:
1429
1470
  type: array
1430
1471
  items:
@@ -1499,7 +1540,12 @@ components:
1499
1540
  description: Required by the authorization code grant.
1500
1541
  redirect_uri:
1501
1542
  type: string
1502
- description: 'Authorization code grant: the `redirect_uri` the code was issued against, exactly.'
1543
+ description: >-
1544
+ Required by the authorization code grant: the `redirect_uri` the code was issued
1545
+ against, exactly.
1546
+ resource:
1547
+ type: string
1548
+ description: 'Authorization code grant, optional: RFC 8707, and it must name what was authorized.'
1503
1549
  refresh_token:
1504
1550
  type: string
1505
1551
  description: Required by the refresh token grant.
@@ -1526,16 +1572,19 @@ components:
1526
1572
  mslm:apikey_id:
1527
1573
  type: string
1528
1574
  description: |
1529
- Not part of OAuth. The ID of the API key the person picked when they
1530
- approved, returned by every grant while this authorization may still
1531
- read that key back. Absent when no key was picked, or when the
1532
- person's role no longer allows reading keys back.
1575
+ Not part of OAuth, and only for our own clients. The ID of the API
1576
+ key the person picked when they approved, returned by every grant
1577
+ while this authorization may still read that key back. Absent when
1578
+ no key was picked, or when the person's role no longer allows
1579
+ reading keys back.
1533
1580
  mslm:apikey:
1534
1581
  type: string
1535
1582
  description: |
1536
- Not part of OAuth. The API key itself, so a device ends up holding an
1537
- ordinary key. Returned by the device code and authorization code
1538
- grants only, never by a refresh, and only alongside
1583
+ Not part of OAuth, and only for our own clients: a client that signed
1584
+ in with a Client ID Metadata Document never receives a key. The API
1585
+ key itself, so a device ends up holding an ordinary key. Returned by
1586
+ the device code and authorization code grants only, never by a
1587
+ refresh, and only alongside
1539
1588
  `mslm:apikey_id`. Absent when that key's secret cannot be read back,
1540
1589
  which is the case for a key created before keys could be shown again
1541
1590
  in the console; a rotated key can be.
@@ -38,7 +38,7 @@ from .models import (
38
38
  TokenResponse,
39
39
  )
40
40
 
41
- __version__ = "2.4.1"
41
+ __version__ = "2.5.0"
42
42
 
43
43
  __all__ = [
44
44
  "DATABASE_FORMATS",
@@ -16,7 +16,7 @@ def _get_kwargs(
16
16
  redirect_uri: str,
17
17
  response_type: OauthAuthorizeResponseType,
18
18
  code_challenge: str,
19
- code_challenge_method: OauthAuthorizeCodeChallengeMethod | Unset = UNSET,
19
+ code_challenge_method: OauthAuthorizeCodeChallengeMethod,
20
20
  scope: str | Unset = UNSET,
21
21
  state: str | Unset = UNSET,
22
22
  resource: str | Unset = UNSET,
@@ -33,10 +33,7 @@ def _get_kwargs(
33
33
 
34
34
  params["code_challenge"] = code_challenge
35
35
 
36
- json_code_challenge_method: str | Unset = UNSET
37
- if not isinstance(code_challenge_method, Unset):
38
- json_code_challenge_method = code_challenge_method.value
39
-
36
+ json_code_challenge_method = code_challenge_method.value
40
37
  params["code_challenge_method"] = json_code_challenge_method
41
38
 
42
39
  params["scope"] = scope
@@ -65,6 +62,9 @@ def _parse_response(
65
62
  if response.status_code == 400:
66
63
  return None
67
64
 
65
+ if response.status_code == 429:
66
+ return None
67
+
68
68
  if client.raise_on_unexpected_status:
69
69
  raise errors.UnexpectedStatus(response.status_code, response.content)
70
70
  else:
@@ -89,7 +89,7 @@ def sync_detailed(
89
89
  redirect_uri: str,
90
90
  response_type: OauthAuthorizeResponseType,
91
91
  code_challenge: str,
92
- code_challenge_method: OauthAuthorizeCodeChallengeMethod | Unset = UNSET,
92
+ code_challenge_method: OauthAuthorizeCodeChallengeMethod,
93
93
  scope: str | Unset = UNSET,
94
94
  state: str | Unset = UNSET,
95
95
  resource: str | Unset = UNSET,
@@ -99,6 +99,14 @@ def sync_detailed(
99
99
  The browser entry point for the authorization-code flow. This is a
100
100
  redirect target, not something to call from code.
101
101
 
102
+ Any client may sign in without registering first by using a Client ID
103
+ Metadata Document: make `client_id` an https URL that serves your
104
+ client's metadata as JSON, naming that same URL as its `client_id`,
105
+ with `token_endpoint_auth_method` `none` and your `redirect_uris`. An
106
+ https redirect URI must be on the same origin as the `client_id`; a
107
+ loopback one (`http://127.0.0.1`, `http://[::1]`, `http://localhost`)
108
+ matches on any port. Such a client is granted `apikeys.use` at most.
109
+
102
110
  An unknown `client_id` or an unregistered `redirect_uri` is shown to the
103
111
  USER and never redirected, because sending an error to an address we
104
112
  have not verified belongs to you is how an open redirector works.
@@ -110,7 +118,7 @@ def sync_detailed(
110
118
  redirect_uri (str):
111
119
  response_type (OauthAuthorizeResponseType):
112
120
  code_challenge (str):
113
- code_challenge_method (OauthAuthorizeCodeChallengeMethod | Unset):
121
+ code_challenge_method (OauthAuthorizeCodeChallengeMethod):
114
122
  scope (str | Unset):
115
123
  state (str | Unset):
116
124
  resource (str | Unset):
@@ -148,7 +156,7 @@ async def asyncio_detailed(
148
156
  redirect_uri: str,
149
157
  response_type: OauthAuthorizeResponseType,
150
158
  code_challenge: str,
151
- code_challenge_method: OauthAuthorizeCodeChallengeMethod | Unset = UNSET,
159
+ code_challenge_method: OauthAuthorizeCodeChallengeMethod,
152
160
  scope: str | Unset = UNSET,
153
161
  state: str | Unset = UNSET,
154
162
  resource: str | Unset = UNSET,
@@ -158,6 +166,14 @@ async def asyncio_detailed(
158
166
  The browser entry point for the authorization-code flow. This is a
159
167
  redirect target, not something to call from code.
160
168
 
169
+ Any client may sign in without registering first by using a Client ID
170
+ Metadata Document: make `client_id` an https URL that serves your
171
+ client's metadata as JSON, naming that same URL as its `client_id`,
172
+ with `token_endpoint_auth_method` `none` and your `redirect_uris`. An
173
+ https redirect URI must be on the same origin as the `client_id`; a
174
+ loopback one (`http://127.0.0.1`, `http://[::1]`, `http://localhost`)
175
+ matches on any port. Such a client is granted `apikeys.use` at most.
176
+
161
177
  An unknown `client_id` or an unregistered `redirect_uri` is shown to the
162
178
  USER and never redirected, because sending an error to an address we
163
179
  have not verified belongs to you is how an open redirector works.
@@ -169,7 +185,7 @@ async def asyncio_detailed(
169
185
  redirect_uri (str):
170
186
  response_type (OauthAuthorizeResponseType):
171
187
  code_challenge (str):
172
- code_challenge_method (OauthAuthorizeCodeChallengeMethod | Unset):
188
+ code_challenge_method (OauthAuthorizeCodeChallengeMethod):
173
189
  scope (str | Unset):
174
190
  state (str | Unset):
175
191
  resource (str | Unset):
@@ -79,10 +79,14 @@ def sync_detailed(
79
79
  `slow_down`, which means widen your interval and keep it widened.
80
80
 
81
81
  `authorization_code` exchanges a code from `/oauth/authorize`, with the
82
- `code_verifier` matching the challenge you sent.
82
+ `code_verifier` matching the challenge you sent and the same
83
+ `redirect_uri`. A code works once: presenting it again also revokes
84
+ what the first exchange issued.
83
85
 
84
86
  `refresh_token` exchanges a refresh token. The presented token is
85
87
  consumed whatever happens next, so store the new one before using it.
88
+ Presenting a consumed refresh token again ends the whole authorization,
89
+ since it means the token was copied.
86
90
 
87
91
  Args:
88
92
  body (TokenRequest):
@@ -121,10 +125,14 @@ def sync(
121
125
  `slow_down`, which means widen your interval and keep it widened.
122
126
 
123
127
  `authorization_code` exchanges a code from `/oauth/authorize`, with the
124
- `code_verifier` matching the challenge you sent.
128
+ `code_verifier` matching the challenge you sent and the same
129
+ `redirect_uri`. A code works once: presenting it again also revokes
130
+ what the first exchange issued.
125
131
 
126
132
  `refresh_token` exchanges a refresh token. The presented token is
127
133
  consumed whatever happens next, so store the new one before using it.
134
+ Presenting a consumed refresh token again ends the whole authorization,
135
+ since it means the token was copied.
128
136
 
129
137
  Args:
130
138
  body (TokenRequest):
@@ -158,10 +166,14 @@ async def asyncio_detailed(
158
166
  `slow_down`, which means widen your interval and keep it widened.
159
167
 
160
168
  `authorization_code` exchanges a code from `/oauth/authorize`, with the
161
- `code_verifier` matching the challenge you sent.
169
+ `code_verifier` matching the challenge you sent and the same
170
+ `redirect_uri`. A code works once: presenting it again also revokes
171
+ what the first exchange issued.
162
172
 
163
173
  `refresh_token` exchanges a refresh token. The presented token is
164
174
  consumed whatever happens next, so store the new one before using it.
175
+ Presenting a consumed refresh token again ends the whole authorization,
176
+ since it means the token was copied.
165
177
 
166
178
  Args:
167
179
  body (TokenRequest):
@@ -198,10 +210,14 @@ async def asyncio(
198
210
  `slow_down`, which means widen your interval and keep it widened.
199
211
 
200
212
  `authorization_code` exchanges a code from `/oauth/authorize`, with the
201
- `code_verifier` matching the challenge you sent.
213
+ `code_verifier` matching the challenge you sent and the same
214
+ `redirect_uri`. A code works once: presenting it again also revokes
215
+ what the first exchange issued.
202
216
 
203
217
  `refresh_token` exchanges a refresh token. The presented token is
204
218
  consumed whatever happens next, so store the new one before using it.
219
+ Presenting a consumed refresh token again ends the whole authorization,
220
+ since it means the token was copied.
205
221
 
206
222
  Args:
207
223
  body (TokenRequest):
@@ -22,6 +22,7 @@ from .database_metadata_sample import DatabaseMetadataSample
22
22
  from .database_metadata_sample_additional_property_item import (
23
23
  DatabaseMetadataSampleAdditionalPropertyItem,
24
24
  )
25
+ from .database_metadata_sample_size import DatabaseMetadataSampleSize
25
26
  from .database_metadata_schema import DatabaseMetadataSchema
26
27
  from .database_metadata_size import DatabaseMetadataSize
27
28
  from .database_version import DatabaseVersion
@@ -65,6 +66,7 @@ __all__ = (
65
66
  "DatabaseMetadataColumn",
66
67
  "DatabaseMetadataSample",
67
68
  "DatabaseMetadataSampleAdditionalPropertyItem",
69
+ "DatabaseMetadataSampleSize",
68
70
  "DatabaseMetadataSchema",
69
71
  "DatabaseMetadataSize",
70
72
  "DatabaseVersion",
@@ -11,6 +11,7 @@ from ..types import UNSET, Unset
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from ..models.database_metadata_sample import DatabaseMetadataSample
14
+ from ..models.database_metadata_sample_size import DatabaseMetadataSampleSize
14
15
  from ..models.database_metadata_schema import DatabaseMetadataSchema
15
16
  from ..models.database_metadata_size import DatabaseMetadataSize
16
17
 
@@ -30,6 +31,9 @@ class DatabaseMetadata:
30
31
  size (DatabaseMetadataSize): Bytes per format.
31
32
  update_freq (str | Unset): How often a new build is published.
32
33
  sample (DatabaseMetadataSample | Unset): A few real rows, keyed by format.
34
+ sample_size (DatabaseMetadataSampleSize | Unset): Bytes per format of the evaluation sample, where one is
35
+ published.
36
+ sample_entries (int | Unset): Row count in the evaluation sample.
33
37
  """
34
38
 
35
39
  id: str
@@ -39,6 +43,8 @@ class DatabaseMetadata:
39
43
  size: DatabaseMetadataSize
40
44
  update_freq: str | Unset = UNSET
41
45
  sample: DatabaseMetadataSample | Unset = UNSET
46
+ sample_size: DatabaseMetadataSampleSize | Unset = UNSET
47
+ sample_entries: int | Unset = UNSET
42
48
  additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
43
49
 
44
50
  def to_dict(self) -> dict[str, Any]:
@@ -58,6 +64,12 @@ class DatabaseMetadata:
58
64
  if not isinstance(self.sample, Unset):
59
65
  sample = self.sample.to_dict()
60
66
 
67
+ sample_size: dict[str, Any] | Unset = UNSET
68
+ if not isinstance(self.sample_size, Unset):
69
+ sample_size = self.sample_size.to_dict()
70
+
71
+ sample_entries = self.sample_entries
72
+
61
73
  field_dict: dict[str, Any] = {}
62
74
  field_dict.update(self.additional_properties)
63
75
  field_dict.update(
@@ -73,12 +85,19 @@ class DatabaseMetadata:
73
85
  field_dict["update_freq"] = update_freq
74
86
  if sample is not UNSET:
75
87
  field_dict["sample"] = sample
88
+ if sample_size is not UNSET:
89
+ field_dict["sample_size"] = sample_size
90
+ if sample_entries is not UNSET:
91
+ field_dict["sample_entries"] = sample_entries
76
92
 
77
93
  return field_dict
78
94
 
79
95
  @classmethod
80
96
  def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
81
97
  from ..models.database_metadata_sample import DatabaseMetadataSample
98
+ from ..models.database_metadata_sample_size import (
99
+ DatabaseMetadataSampleSize,
100
+ )
82
101
  from ..models.database_metadata_schema import DatabaseMetadataSchema
83
102
  from ..models.database_metadata_size import DatabaseMetadataSize
84
103
 
@@ -102,6 +121,15 @@ class DatabaseMetadata:
102
121
  else:
103
122
  sample = DatabaseMetadataSample.from_dict(_sample)
104
123
 
124
+ _sample_size = d.pop("sample_size", UNSET)
125
+ sample_size: DatabaseMetadataSampleSize | Unset
126
+ if isinstance(_sample_size, Unset):
127
+ sample_size = UNSET
128
+ else:
129
+ sample_size = DatabaseMetadataSampleSize.from_dict(_sample_size)
130
+
131
+ sample_entries = d.pop("sample_entries", UNSET)
132
+
105
133
  database_metadata = cls(
106
134
  id=id,
107
135
  updated=updated,
@@ -110,6 +138,8 @@ class DatabaseMetadata:
110
138
  size=size,
111
139
  update_freq=update_freq,
112
140
  sample=sample,
141
+ sample_size=sample_size,
142
+ sample_entries=sample_entries,
113
143
  )
114
144
 
115
145
  database_metadata.additional_properties = d
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any, Self, TypeVar
5
+
6
+ from attrs import define as _attrs_define
7
+ from attrs import field as _attrs_field
8
+
9
+ T = TypeVar("T", bound="DatabaseMetadataSampleSize")
10
+
11
+
12
+ @_attrs_define
13
+ class DatabaseMetadataSampleSize:
14
+ """Bytes per format of the evaluation sample, where one is published."""
15
+
16
+ additional_properties: dict[str, int] = _attrs_field(init=False, factory=dict)
17
+
18
+ def to_dict(self) -> dict[str, Any]:
19
+
20
+ field_dict: dict[str, Any] = {}
21
+ field_dict.update(self.additional_properties)
22
+
23
+ return field_dict
24
+
25
+ @classmethod
26
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
27
+ d = dict(src_dict)
28
+ database_metadata_sample_size = cls()
29
+
30
+ database_metadata_sample_size.additional_properties = d
31
+ return database_metadata_sample_size
32
+
33
+ @property
34
+ def additional_keys(self) -> list[str]:
35
+ return list(self.additional_properties.keys())
36
+
37
+ def __getitem__(self, key: str) -> int:
38
+ return self.additional_properties[key]
39
+
40
+ def __setitem__(self, key: str, value: int) -> None:
41
+ self.additional_properties[key] = value
42
+
43
+ def __delitem__(self, key: str) -> None:
44
+ del self.additional_properties[key]
45
+
46
+ def __contains__(self, key: str) -> bool:
47
+ return key in self.additional_properties
@@ -7,6 +7,7 @@ from attrs import define as _attrs_define
7
7
  from attrs import field as _attrs_field
8
8
 
9
9
  from ..models.database_format import DatabaseFormat
10
+ from ..types import UNSET, Unset
10
11
 
11
12
  T = TypeVar("T", bound="DatabaseVersion")
12
13
 
@@ -21,12 +22,14 @@ class DatabaseVersion:
21
22
  formats (list[DatabaseFormat]): The formats this version is BUILT in. Asking for another is a 400,
22
23
  not a gap - the `_provider` catalogs are keyed by provider id, so no
23
24
  MMDB exists for them.
25
+ sample_formats (list[DatabaseFormat] | Unset): The formats an evaluation sample is published in, if any.
24
26
  """
25
27
 
26
28
  id: str
27
29
  version: int
28
30
  summary: str
29
31
  formats: list[DatabaseFormat]
32
+ sample_formats: list[DatabaseFormat] | Unset = UNSET
30
33
  additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
31
34
 
32
35
  def to_dict(self) -> dict[str, Any]:
@@ -41,6 +44,13 @@ class DatabaseVersion:
41
44
  formats_item = formats_item_data.value
42
45
  formats.append(formats_item)
43
46
 
47
+ sample_formats: list[str] | Unset = UNSET
48
+ if not isinstance(self.sample_formats, Unset):
49
+ sample_formats = []
50
+ for sample_formats_item_data in self.sample_formats:
51
+ sample_formats_item = sample_formats_item_data.value
52
+ sample_formats.append(sample_formats_item)
53
+
44
54
  field_dict: dict[str, Any] = {}
45
55
  field_dict.update(self.additional_properties)
46
56
  field_dict.update(
@@ -51,6 +61,8 @@ class DatabaseVersion:
51
61
  "formats": formats,
52
62
  }
53
63
  )
64
+ if sample_formats is not UNSET:
65
+ field_dict["sample_formats"] = sample_formats
54
66
 
55
67
  return field_dict
56
68
 
@@ -70,11 +82,21 @@ class DatabaseVersion:
70
82
 
71
83
  formats.append(formats_item)
72
84
 
85
+ _sample_formats = d.pop("sample_formats", UNSET)
86
+ sample_formats: list[DatabaseFormat] | Unset = UNSET
87
+ if _sample_formats is not UNSET:
88
+ sample_formats = []
89
+ for sample_formats_item_data in _sample_formats:
90
+ sample_formats_item = DatabaseFormat(sample_formats_item_data)
91
+
92
+ sample_formats.append(sample_formats_item)
93
+
73
94
  database_version = cls(
74
95
  id=id,
75
96
  version=version,
76
97
  summary=summary,
77
98
  formats=formats,
99
+ sample_formats=sample_formats,
78
100
  )
79
101
 
80
102
  database_version.additional_properties = d
@@ -20,6 +20,7 @@ class Download:
20
20
  dataset_id (str):
21
21
  format_ (str):
22
22
  outcome (DownloadOutcome):
23
+ sample (bool): The evaluation sample rather than the database itself.
23
24
  bytes_ (int | None): Object size at redirect time, NOT bytes delivered: the transfer is a
24
25
  presigned redirect straight to object storage, so we never observe
25
26
  how much of it was taken.
@@ -33,6 +34,7 @@ class Download:
33
34
  dataset_id: str
34
35
  format_: str
35
36
  outcome: DownloadOutcome
37
+ sample: bool
36
38
  bytes_: int | None
37
39
  http_status: int | None
38
40
  apikey_id: None | str
@@ -48,6 +50,8 @@ class Download:
48
50
 
49
51
  outcome = self.outcome.value
50
52
 
53
+ sample = self.sample
54
+
51
55
  bytes_: int | None
52
56
  bytes_ = self.bytes_
53
57
 
@@ -72,6 +76,7 @@ class Download:
72
76
  "dataset_id": dataset_id,
73
77
  "format": format_,
74
78
  "outcome": outcome,
79
+ "sample": sample,
75
80
  "bytes": bytes_,
76
81
  "http_status": http_status,
77
82
  "apikey_id": apikey_id,
@@ -92,6 +97,8 @@ class Download:
92
97
 
93
98
  outcome = DownloadOutcome(d.pop("outcome"))
94
99
 
100
+ sample = d.pop("sample")
101
+
95
102
  def _parse_bytes_(data: object) -> int | None:
96
103
  if data is None:
97
104
  return data
@@ -133,6 +140,7 @@ class Download:
133
140
  dataset_id=dataset_id,
134
141
  format_=format_,
135
142
  outcome=outcome,
143
+ sample=sample,
136
144
  bytes_=bytes_,
137
145
  http_status=http_status,
138
146
  apikey_id=apikey_id,
@@ -24,6 +24,8 @@ class OauthMetadata:
24
24
  response_types_supported (list[str] | Unset):
25
25
  grant_types_supported (list[str] | Unset):
26
26
  code_challenge_methods_supported (list[str] | Unset):
27
+ client_id_metadata_document_supported (bool | Unset): Any client may sign in with an https `client_id` serving
28
+ its own metadata.
27
29
  token_endpoint_auth_methods_supported (list[str] | Unset): Always `none`. Every client is public and has no
28
30
  secret.
29
31
  authorization_response_iss_parameter_supported (bool | Unset): RFC 9207. A redirect back from the authorization
@@ -40,6 +42,7 @@ class OauthMetadata:
40
42
  response_types_supported: list[str] | Unset = UNSET
41
43
  grant_types_supported: list[str] | Unset = UNSET
42
44
  code_challenge_methods_supported: list[str] | Unset = UNSET
45
+ client_id_metadata_document_supported: bool | Unset = UNSET
43
46
  token_endpoint_auth_methods_supported: list[str] | Unset = UNSET
44
47
  authorization_response_iss_parameter_supported: bool | Unset = UNSET
45
48
  service_documentation: str | Unset = UNSET
@@ -72,6 +75,8 @@ class OauthMetadata:
72
75
  if not isinstance(self.code_challenge_methods_supported, Unset):
73
76
  code_challenge_methods_supported = self.code_challenge_methods_supported
74
77
 
78
+ client_id_metadata_document_supported = self.client_id_metadata_document_supported
79
+
75
80
  token_endpoint_auth_methods_supported: list[str] | Unset = UNSET
76
81
  if not isinstance(self.token_endpoint_auth_methods_supported, Unset):
77
82
  token_endpoint_auth_methods_supported = self.token_endpoint_auth_methods_supported
@@ -103,6 +108,10 @@ class OauthMetadata:
103
108
  field_dict["grant_types_supported"] = grant_types_supported
104
109
  if code_challenge_methods_supported is not UNSET:
105
110
  field_dict["code_challenge_methods_supported"] = code_challenge_methods_supported
111
+ if client_id_metadata_document_supported is not UNSET:
112
+ field_dict["client_id_metadata_document_supported"] = (
113
+ client_id_metadata_document_supported
114
+ )
106
115
  if token_endpoint_auth_methods_supported is not UNSET:
107
116
  field_dict["token_endpoint_auth_methods_supported"] = (
108
117
  token_endpoint_auth_methods_supported
@@ -139,6 +148,10 @@ class OauthMetadata:
139
148
  list[str], d.pop("code_challenge_methods_supported", UNSET)
140
149
  )
141
150
 
151
+ client_id_metadata_document_supported = d.pop(
152
+ "client_id_metadata_document_supported", UNSET
153
+ )
154
+
142
155
  token_endpoint_auth_methods_supported = cast(
143
156
  list[str], d.pop("token_endpoint_auth_methods_supported", UNSET)
144
157
  )
@@ -159,6 +172,7 @@ class OauthMetadata:
159
172
  response_types_supported=response_types_supported,
160
173
  grant_types_supported=grant_types_supported,
161
174
  code_challenge_methods_supported=code_challenge_methods_supported,
175
+ client_id_metadata_document_supported=client_id_metadata_document_supported,
162
176
  token_endpoint_auth_methods_supported=token_endpoint_auth_methods_supported,
163
177
  authorization_response_iss_parameter_supported=authorization_response_iss_parameter_supported,
164
178
  service_documentation=service_documentation,
@@ -20,7 +20,9 @@ class TokenRequest:
20
20
  device_code (str | Unset): Required by the device code grant.
21
21
  code (str | Unset): Required by the authorization code grant.
22
22
  code_verifier (str | Unset): Required by the authorization code grant.
23
- redirect_uri (str | Unset): Authorization code grant: the `redirect_uri` the code was issued against, exactly.
23
+ redirect_uri (str | Unset): Required by the authorization code grant: the `redirect_uri` the code was issued
24
+ against, exactly.
25
+ resource (str | Unset): Authorization code grant, optional: RFC 8707, and it must name what was authorized.
24
26
  refresh_token (str | Unset): Required by the refresh token grant.
25
27
  """
26
28
 
@@ -30,6 +32,7 @@ class TokenRequest:
30
32
  code: str | Unset = UNSET
31
33
  code_verifier: str | Unset = UNSET
32
34
  redirect_uri: str | Unset = UNSET
35
+ resource: str | Unset = UNSET
33
36
  refresh_token: str | Unset = UNSET
34
37
  additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
35
38
 
@@ -46,6 +49,8 @@ class TokenRequest:
46
49
 
47
50
  redirect_uri = self.redirect_uri
48
51
 
52
+ resource = self.resource
53
+
49
54
  refresh_token = self.refresh_token
50
55
 
51
56
  field_dict: dict[str, Any] = {}
@@ -64,6 +69,8 @@ class TokenRequest:
64
69
  field_dict["code_verifier"] = code_verifier
65
70
  if redirect_uri is not UNSET:
66
71
  field_dict["redirect_uri"] = redirect_uri
72
+ if resource is not UNSET:
73
+ field_dict["resource"] = resource
67
74
  if refresh_token is not UNSET:
68
75
  field_dict["refresh_token"] = refresh_token
69
76
 
@@ -84,6 +91,8 @@ class TokenRequest:
84
91
 
85
92
  redirect_uri = d.pop("redirect_uri", UNSET)
86
93
 
94
+ resource = d.pop("resource", UNSET)
95
+
87
96
  refresh_token = d.pop("refresh_token", UNSET)
88
97
 
89
98
  token_request = cls(
@@ -93,6 +102,7 @@ class TokenRequest:
93
102
  code=code,
94
103
  code_verifier=code_verifier,
95
104
  redirect_uri=redirect_uri,
105
+ resource=resource,
96
106
  refresh_token=refresh_token,
97
107
  )
98
108
 
@@ -20,13 +20,16 @@ class TokenResponse:
20
20
  expires_in (int): Seconds until the access token expires.
21
21
  refresh_token (str | Unset): Always returned. A refresh consumes the token it presents, so keep this one.
22
22
  scope (str | Unset): What was actually granted, which may be narrower than what was asked for.
23
- mslmapikey_id (str | Unset): Not part of OAuth. The ID of the API key the person picked when they
24
- approved, returned by every grant while this authorization may still
25
- read that key back. Absent when no key was picked, or when the
26
- person's role no longer allows reading keys back.
27
- mslmapikey (str | Unset): Not part of OAuth. The API key itself, so a device ends up holding an
28
- ordinary key. Returned by the device code and authorization code
29
- grants only, never by a refresh, and only alongside
23
+ mslmapikey_id (str | Unset): Not part of OAuth, and only for our own clients. The ID of the API
24
+ key the person picked when they approved, returned by every grant
25
+ while this authorization may still read that key back. Absent when
26
+ no key was picked, or when the person's role no longer allows
27
+ reading keys back.
28
+ mslmapikey (str | Unset): Not part of OAuth, and only for our own clients: a client that signed
29
+ in with a Client ID Metadata Document never receives a key. The API
30
+ key itself, so a device ends up holding an ordinary key. Returned by
31
+ the device code and authorization code grants only, never by a
32
+ refresh, and only alongside
30
33
  `mslm:apikey_id`. Absent when that key's secret cannot be read back,
31
34
  which is the case for a key created before keys could be shown again
32
35
  in the console; a rotated key can be.
@@ -77,6 +77,8 @@ class DatabaseVersion:
77
77
  version: int
78
78
  summary: str
79
79
  formats: tuple[Format, ...]
80
+ # The formats an evaluation sample is published in, None when there is none.
81
+ sample_formats: tuple[Format, ...] | None = None
80
82
 
81
83
 
82
84
  @dataclass(frozen=True, slots=True)
@@ -133,6 +135,9 @@ class DatabaseMetadata:
133
135
  update_freq: str | None = None
134
136
  sample: dict[str, tuple[dict[str, Any], ...]] = field(default_factory=dict)
135
137
  raw: dict[str, Any] = field(default_factory=dict)
138
+ # Bytes per format and row count of the evaluation sample, where one is published.
139
+ sample_size: dict[str, int] | None = None
140
+ sample_entries: int | None = None
136
141
 
137
142
 
138
143
  @dataclass(frozen=True, slots=True)
@@ -153,6 +158,10 @@ class Download:
153
158
  apikey_id: str | None = None
154
159
  client_ip: str | None = None
155
160
  user_agent: str | None = None
161
+ # Whether this was the evaluation sample rather than the database itself. Keyword-only
162
+ # with a default, so a Download built by hand before 2.5.0 still builds; the parser
163
+ # always sets it.
164
+ sample: bool = field(default=False, kw_only=True)
156
165
 
157
166
 
158
167
  def to_database(body: dict[str, Any]) -> Database:
@@ -177,6 +186,7 @@ def to_version(body: dict[str, Any]) -> DatabaseVersion:
177
186
  version=body["version"],
178
187
  summary=body["summary"],
179
188
  formats=tuple(body["formats"]),
189
+ sample_formats=_optional_tuple(body.get("sample_formats")),
180
190
  )
181
191
 
182
192
 
@@ -192,6 +202,8 @@ def to_metadata(body: dict[str, Any]) -> DatabaseMetadata:
192
202
  update_freq=body.get("update_freq"),
193
203
  sample={fmt: tuple(rows) for fmt, rows in body.get("sample", {}).items()},
194
204
  raw=body,
205
+ sample_size=dict(body["sample_size"]) if body.get("sample_size") is not None else None,
206
+ sample_entries=body.get("sample_entries"),
195
207
  )
196
208
 
197
209
 
@@ -210,9 +222,14 @@ def to_download(body: dict[str, Any]) -> Download:
210
222
  apikey_id=body["apikey_id"],
211
223
  client_ip=body["client_ip"],
212
224
  user_agent=body["user_agent"],
225
+ sample=body["sample"],
213
226
  )
214
227
 
215
228
 
229
+ def _optional_tuple(value: Any) -> tuple[Any, ...] | None:
230
+ return tuple(value) if value is not None else None
231
+
232
+
216
233
  def _datetime(value: Any) -> datetime.datetime | None:
217
234
  """A NULLABLE wire timestamp. A license with no end date carries `expires: null`.
218
235
 
@@ -242,6 +259,7 @@ class OauthMetadata:
242
259
  token_endpoint_auth_methods_supported: tuple[str, ...] | None = None
243
260
  authorization_response_iss_parameter_supported: bool | None = None
244
261
  service_documentation: str | None = None
262
+ client_id_metadata_document_supported: bool | None = None
245
263
 
246
264
 
247
265
  @dataclass(frozen=True, slots=True)
@@ -308,6 +326,7 @@ _OAUTH_METADATA: dict[str, _Member] = {
308
326
  "token_endpoint_auth_methods_supported": ("strs", False),
309
327
  "authorization_response_iss_parameter_supported": ("bool", False),
310
328
  "service_documentation": ("str", False),
329
+ "client_id_metadata_document_supported": ("bool", False),
311
330
  }
312
331
 
313
332
  _DEVICE_AUTHORIZATION: dict[str, _Member] = {
@@ -158,7 +158,7 @@
158
158
  ]
159
159
  },
160
160
  "oauth": {
161
- "why": "The public OAuth accessor (docs/sdk/oauth.md). Arguments use the canonical parameter names; form fields and response members are WIRE names, mapped by each SDK exactly as it maps any other response. mslm:apikey_id and mslm:apikey surface as apikey_id and apikey.",
161
+ "why": "The public OAuth accessor. Arguments use the canonical parameter names; form fields and response members are WIRE names, mapped by each SDK exactly as it maps any other response. mslm:apikey_id and mslm:apikey surface as apikey_id and apikey.",
162
162
  "endpoints": {
163
163
  "metadata": {
164
164
  "method": "GET",
@@ -1219,7 +1219,7 @@
1219
1219
  ]
1220
1220
  },
1221
1221
  "deferred": {
1222
- "why": "Vectors for authorizationUrl, exchangeAuthorizationCode, createPkce and pkceChallenge, which ship only once the server can complete the authorization-code flow (docs/sdk/oauth.md, Deferred). A device-flow runner never reads this key; nothing outside it names a deferred operation. emit.mjs still verifies these vectors so they are right when the methods ship.",
1222
+ "why": "Vectors for authorizationUrl, exchangeAuthorizationCode, createPkce and pkceChallenge, which ship only once the server can complete the authorization-code flow. A device-flow runner never reads this key; nothing outside it names a deferred operation. emit.mjs still verifies these vectors so they are right when the methods ship.",
1223
1223
  "pkce": {
1224
1224
  "why": "RFC 7636 Appendix B. A generated verifier is 32 CSPRNG bytes as unpadded base64url.",
1225
1225
  "verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
@@ -87,6 +87,7 @@ DOWNLOADS = {
87
87
  "apikey_id": "ak_1",
88
88
  "client_ip": "203.0.113.4",
89
89
  "user_agent": "curl/8.5.0",
90
+ "sample": False,
90
91
  "created": "2026-09-04T10:00:00.000Z",
91
92
  },
92
93
  {
@@ -98,6 +99,7 @@ DOWNLOADS = {
98
99
  "apikey_id": None,
99
100
  "client_ip": None,
100
101
  "user_agent": None,
102
+ "sample": True,
101
103
  "created": "2026-09-04T09:00:00.000Z",
102
104
  },
103
105
  ]
@@ -199,6 +201,7 @@ def test_a_refusal_is_listed_beside_a_success(make_client: ClientFactory) -> Non
199
201
  assert denied.bytes is None, "a refusal moved no bytes, which is not zero bytes"
200
202
  assert denied.http_status == 403
201
203
  assert denied.apikey_id is None and denied.client_ip is None
204
+ assert [a.sample for a in attempts] == [False, True]
202
205
 
203
206
 
204
207
  def test_the_downloads_limit_reaches_the_wire(make_client: ClientFactory) -> None:
File without changes
File without changes