@apollo-deploy/tesseract 1.9.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +218 -110
- package/dist/adapters/csharp/index.d.ts +5 -8
- package/dist/adapters/csharp/index.d.ts.map +1 -1
- package/dist/adapters/csharp/index.js +314 -74
- package/dist/adapters/csharp/index.js.map +1 -1
- package/dist/adapters/documentation.d.ts +17 -0
- package/dist/adapters/documentation.d.ts.map +1 -0
- package/dist/adapters/documentation.js +268 -0
- package/dist/adapters/documentation.js.map +1 -0
- package/dist/adapters/go/index.d.ts +5 -8
- package/dist/adapters/go/index.d.ts.map +1 -1
- package/dist/adapters/go/index.js +197 -101
- package/dist/adapters/go/index.js.map +1 -1
- package/dist/adapters/kotlin/index.d.ts +5 -9
- package/dist/adapters/kotlin/index.d.ts.map +1 -1
- package/dist/adapters/kotlin/index.js +280 -212
- package/dist/adapters/kotlin/index.js.map +1 -1
- package/dist/adapters/php/index.d.ts +5 -15
- package/dist/adapters/php/index.d.ts.map +1 -1
- package/dist/adapters/php/index.js +362 -90
- package/dist/adapters/php/index.js.map +1 -1
- package/dist/adapters/python/index.d.ts +5 -8
- package/dist/adapters/python/index.d.ts.map +1 -1
- package/dist/adapters/python/index.js +240 -109
- package/dist/adapters/python/index.js.map +1 -1
- package/dist/adapters/ruby/index.d.ts +3 -3
- package/dist/adapters/ruby/index.d.ts.map +1 -1
- package/dist/adapters/ruby/index.js +235 -46
- package/dist/adapters/ruby/index.js.map +1 -1
- package/dist/adapters/rust/index.d.ts +5 -9
- package/dist/adapters/rust/index.d.ts.map +1 -1
- package/dist/adapters/rust/index.js +237 -169
- package/dist/adapters/rust/index.js.map +1 -1
- package/dist/adapters/shared.d.ts +7 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/adapters/shared.js +68 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/type-utils.d.ts +13 -0
- package/dist/adapters/type-utils.d.ts.map +1 -0
- package/dist/adapters/type-utils.js +105 -0
- package/dist/adapters/type-utils.js.map +1 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/adapters/typescript/emitter/emitSchema.d.ts +2 -2
- package/dist/adapters/typescript/emitter/emitSchema.js +39 -27
- package/dist/adapters/typescript/emitter/emitSchema.js.map +1 -1
- package/dist/adapters/typescript/emitter/emitTypeScriptModels.d.ts +2 -2
- package/dist/adapters/typescript/emitter/emitTypeScriptModels.js +15 -15
- package/dist/adapters/typescript/emitter/importCollector.d.ts +2 -4
- package/dist/adapters/typescript/emitter/importCollector.d.ts.map +1 -1
- package/dist/adapters/typescript/emitter/importCollector.js +40 -50
- package/dist/adapters/typescript/emitter/importCollector.js.map +1 -1
- package/dist/adapters/typescript/emitter/parsers.d.ts +1 -1
- package/dist/adapters/typescript/emitter/parsers.js +13 -13
- package/dist/adapters/typescript/emitter/typeHoister.d.ts +1 -1
- package/dist/adapters/typescript/emitter/typeHoister.d.ts.map +1 -1
- package/dist/adapters/typescript/emitter/typeHoister.js +12 -20
- package/dist/adapters/typescript/emitter/typeHoister.js.map +1 -1
- package/dist/adapters/typescript/index.d.ts +3 -3
- package/dist/adapters/typescript/index.d.ts.map +1 -1
- package/dist/adapters/typescript/index.js +148 -166
- package/dist/adapters/typescript/index.js.map +1 -1
- package/dist/adapters/typescript/shared.d.ts +2 -2
- package/dist/adapters/typescript/shared.js +12 -12
- package/dist/cli.js +50 -37
- package/dist/cli.js.map +1 -1
- package/dist/collector.d.ts +13 -3
- package/dist/collector.d.ts.map +1 -1
- package/dist/collector.js +69 -32
- package/dist/collector.js.map +1 -1
- package/dist/elysia.js +2 -2
- package/dist/elysia.js.map +1 -1
- package/dist/express.d.ts.map +1 -1
- package/dist/express.js.map +1 -1
- package/dist/fastify.d.ts +7 -0
- package/dist/fastify.d.ts.map +1 -1
- package/dist/fastify.js +39 -20
- package/dist/fastify.js.map +1 -1
- package/dist/helpers/handlebars.d.ts +7 -16
- package/dist/helpers/handlebars.d.ts.map +1 -1
- package/dist/helpers/handlebars.js +127 -177
- package/dist/helpers/handlebars.js.map +1 -1
- package/dist/hono.d.ts.map +1 -1
- package/dist/hono.js.map +1 -1
- package/dist/index.d.ts +14 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -30
- package/dist/index.js.map +1 -1
- package/dist/koa.d.ts.map +1 -1
- package/dist/koa.js.map +1 -1
- package/dist/nestjs.d.ts.map +1 -1
- package/dist/nestjs.js +41 -14
- package/dist/nestjs.js.map +1 -1
- package/dist/pipeline/enrich.d.ts +2 -13
- package/dist/pipeline/enrich.d.ts.map +1 -1
- package/dist/pipeline/enrich.js +157 -212
- package/dist/pipeline/enrich.js.map +1 -1
- package/dist/pipeline/intake.d.ts +3 -3
- package/dist/pipeline/intake.d.ts.map +1 -1
- package/dist/pipeline/intake.js +342 -238
- package/dist/pipeline/intake.js.map +1 -1
- package/dist/pipeline/write.d.ts +1 -14
- package/dist/pipeline/write.d.ts.map +1 -1
- package/dist/pipeline/write.js +73 -113
- package/dist/pipeline/write.js.map +1 -1
- package/dist/types/config.d.ts +14 -26
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +16 -12
- package/dist/types/config.js.map +1 -1
- package/dist/types/ir.d.ts +12 -8
- package/dist/types/ir.d.ts.map +1 -1
- package/dist/types/manifest.d.ts +10 -11
- package/dist/types/manifest.d.ts.map +1 -1
- package/dist/types/manifest.js +54 -14
- package/dist/types/manifest.js.map +1 -1
- package/dist/types/sdk-module.d.ts +2 -2
- package/dist/types/sdk-module.js +1 -1
- package/dist/utils/format.d.ts +1 -3
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +3 -10
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/naming.d.ts.map +1 -1
- package/dist/utils/naming.js +174 -184
- package/dist/utils/naming.js.map +1 -1
- package/dist/utils/npm-token.d.ts +1 -17
- package/dist/utils/npm-token.d.ts.map +1 -1
- package/dist/utils/npm-token.js +27 -76
- package/dist/utils/npm-token.js.map +1 -1
- package/dist/utils/version.d.ts +14 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +60 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +4 -3
- package/templates/csharp/client.hbs +26 -49
- package/templates/csharp/csproj.hbs +7 -0
- package/templates/csharp/domain.hbs +48 -130
- package/templates/csharp/errors.hbs +17 -25
- package/templates/csharp/readme.hbs +31 -44
- package/templates/csharp/transport.hbs +238 -136
- package/templates/csharp/types.hbs +30 -76
- package/templates/go/client.hbs +44 -109
- package/templates/go/domain.hbs +83 -25
- package/templates/go/go-mod.hbs +1 -4
- package/templates/go/readme.hbs +32 -29
- package/templates/go/transport.hbs +174 -173
- package/templates/go/types.hbs +17 -53
- package/templates/go/utils.hbs +3 -2
- package/templates/kotlin/build-gradle.hbs +1 -1
- package/templates/kotlin/client.hbs +18 -15
- package/templates/kotlin/domain.hbs +54 -51
- package/templates/kotlin/errors.hbs +1 -1
- package/templates/kotlin/readme.hbs +35 -137
- package/templates/kotlin/transport.hbs +140 -123
- package/templates/kotlin/types.hbs +30 -81
- package/templates/php/client.hbs +8 -46
- package/templates/php/composer.hbs +2 -1
- package/templates/php/domain.hbs +86 -117
- package/templates/php/errors.hbs +1 -1
- package/templates/php/readme.hbs +42 -49
- package/templates/php/transport.hbs +192 -145
- package/templates/php/types.hbs +63 -60
- package/templates/python/client.hbs +15 -2
- package/templates/python/domain.hbs +50 -108
- package/templates/python/pyproject.hbs +5 -0
- package/templates/python/readme.hbs +47 -30
- package/templates/python/resources-init.hbs +1 -1
- package/templates/python/transport.hbs +167 -53
- package/templates/python/types.hbs +22 -58
- package/templates/ruby/client.hbs +10 -6
- package/templates/ruby/domain.hbs +48 -67
- package/templates/ruby/errors.hbs +3 -3
- package/templates/ruby/gemspec.hbs +3 -10
- package/templates/ruby/index.hbs +3 -3
- package/templates/ruby/readme.hbs +33 -70
- package/templates/ruby/transport.hbs +129 -103
- package/templates/ruby/types.hbs +11 -16
- package/templates/ruby/version.hbs +1 -1
- package/templates/rust/cargo.hbs +2 -0
- package/templates/rust/client.hbs +10 -1
- package/templates/rust/domain.hbs +69 -52
- package/templates/rust/error.hbs +3 -1
- package/templates/rust/lib.hbs +4 -5
- package/templates/rust/readme.hbs +33 -46
- package/templates/rust/transport.hbs +170 -159
- package/templates/rust/types.hbs +34 -111
- package/templates/typescript/domain-class.hbs +14 -6
- package/templates/typescript/domain.hbs +13 -5
- package/templates/typescript/package-json.hbs +3 -1
- package/templates/typescript/readme.hbs +42 -42
- package/templates/typescript/transport.hbs +46 -8
- package/templates/typescript/types-errors.hbs +1 -1
- package/dist/adapters/typescript/model-emitter.d.ts +0 -13
- package/dist/adapters/typescript/model-emitter.d.ts.map +0 -1
- package/dist/adapters/typescript/model-emitter.js +0 -415
- package/dist/adapters/typescript/model-emitter.js.map +0 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"""{{interfaceName}} operations."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Any, Dict, Iterator, List, Literal, Optional, Union
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from pydantic import TypeAdapter
|
|
4
7
|
{{#if imports.length}}
|
|
5
|
-
from ..types import {{
|
|
8
|
+
from ..types import {{join imports ", "}}
|
|
6
9
|
{{/if}}
|
|
7
|
-
from ..transport import
|
|
8
|
-
from ..errors import SDKError
|
|
10
|
+
from ..transport import RequestOptions, SDKTransport
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class {{interfaceName}}:
|
|
@@ -15,125 +17,65 @@ class {{interfaceName}}:
|
|
|
15
17
|
self._transport = transport
|
|
16
18
|
|
|
17
19
|
{{#each operations}}
|
|
18
|
-
{{
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
def {{methodName}}(
|
|
21
|
+
self{{#if signature}},
|
|
22
|
+
{{{signature}}}{{/if}},
|
|
23
|
+
) -> {{{targetResponseType}}}:
|
|
24
|
+
"""{{#if summary}}{{summary}}{{else}}{{description}}{{/if}}"""
|
|
25
|
+
path = "{{path}}"
|
|
21
26
|
{{#each pathParams}}
|
|
22
|
-
{{
|
|
27
|
+
path = path.replace("{{placeholder}}", quote(str({{parameterName}}), safe=""))
|
|
23
28
|
{{/each}}
|
|
24
|
-
|
|
25
|
-
*,
|
|
26
|
-
query: Optional[Dict[str, Any]] = None,
|
|
27
|
-
{{/if}}
|
|
28
|
-
{{#if queryParams.length}}
|
|
29
|
-
*,
|
|
29
|
+
query: Dict[str, Any] = {}
|
|
30
30
|
{{#each queryParams}}
|
|
31
|
-
|
|
32
|
-
{{
|
|
33
|
-
{{/if}}
|
|
34
|
-
options: Optional[RequestOptions] = None,
|
|
35
|
-
):
|
|
36
|
-
"""{{#if summary}}{{summary}}{{#if description}} - {{description}}{{/if}}{{else if description}}{{description}}{{/if}} (SSE stream)
|
|
37
|
-
"""
|
|
38
|
-
{{#if deprecated}}
|
|
39
|
-
# @deprecated {{deprecationMessage}}
|
|
40
|
-
{{/if}}
|
|
41
|
-
# Build path with parameter substitution
|
|
42
|
-
_path = "{{path}}"
|
|
43
|
-
{{#each pathParams}}
|
|
44
|
-
_path = _path.replace("{" + "{{originalName}}" + "}", str({{name}}))
|
|
45
|
-
{{/each}}
|
|
46
|
-
|
|
47
|
-
# Build query parameters
|
|
48
|
-
_query: Dict[str, Any] = {}
|
|
49
|
-
{{#each queryParams}}
|
|
50
|
-
if query is not None and hasattr(query, "{{name}}") and getattr(query, "{{name}}") is not None:
|
|
51
|
-
_query["{{originalName}}"] = getattr(query, "{{name}}")
|
|
52
|
-
{{/each}}
|
|
53
|
-
|
|
54
|
-
response = self._transport.request(
|
|
55
|
-
method="{{httpMethod}}",
|
|
56
|
-
path=_path,
|
|
57
|
-
query=_query if _query else None,
|
|
58
|
-
options=options,
|
|
59
|
-
)
|
|
60
|
-
return response.json()
|
|
61
|
-
|
|
31
|
+
{{#if required}}
|
|
32
|
+
query["{{originalName}}"] = {{parameterName}}
|
|
62
33
|
{{else}}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
{{#each pathParams}}
|
|
66
|
-
{{name}}: str,
|
|
67
|
-
{{/each}}
|
|
68
|
-
{{#if requestBody}}
|
|
69
|
-
body: {{pythonType requestBody.type}},
|
|
34
|
+
if {{parameterName}} is not None:
|
|
35
|
+
query["{{originalName}}"] = {{parameterName}}
|
|
70
36
|
{{/if}}
|
|
71
|
-
{{#if (and queryType (not queryParams.length))}}
|
|
72
|
-
*,
|
|
73
|
-
query: Optional[Dict[str, Any]] = None,
|
|
74
|
-
{{/if}}
|
|
75
|
-
{{#if queryParams.length}}
|
|
76
|
-
*,
|
|
77
|
-
{{#each queryParams}}
|
|
78
|
-
{{name}}: Optional[{{pythonType type}}] = None,
|
|
79
37
|
{{/each}}
|
|
38
|
+
headers: Dict[str, str] = {}
|
|
39
|
+
{{#each headerParams}}
|
|
40
|
+
{{#if required}}
|
|
41
|
+
headers["{{originalName}}"] = str({{parameterName}})
|
|
42
|
+
{{else}}
|
|
43
|
+
if {{parameterName}} is not None:
|
|
44
|
+
headers["{{originalName}}"] = str({{parameterName}})
|
|
80
45
|
{{/if}}
|
|
81
|
-
{{#if headerType}}
|
|
82
|
-
headers: Optional[{{pythonType headerType}}] = None,
|
|
83
|
-
{{/if}}
|
|
84
|
-
options: Optional[RequestOptions] = None,
|
|
85
|
-
) -> {{pythonType responseType}}:
|
|
86
|
-
"""{{#if summary}}{{summary}}{{#if description}} - {{description}}{{/if}}{{else if description}}{{description}}{{/if}}
|
|
87
|
-
"""
|
|
88
|
-
{{#if deprecated}}
|
|
89
|
-
# @deprecated {{deprecationMessage}}
|
|
90
|
-
{{/if}}
|
|
91
|
-
# Build path with parameter substitution
|
|
92
|
-
_path = "{{path}}"
|
|
93
|
-
{{#each pathParams}}
|
|
94
|
-
_path = _path.replace("{" + "{{originalName}}" + "}", str({{name}}))
|
|
95
46
|
{{/each}}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
{{#
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
{{
|
|
103
|
-
{{
|
|
104
|
-
if query is not None:
|
|
105
|
-
_items = query.__dict__.items() if hasattr(query, "__dict__") else []
|
|
106
|
-
for k, v in _items:
|
|
107
|
-
if v is not None:
|
|
108
|
-
_query[k] = v
|
|
47
|
+
{{#if cookieParams.length}}
|
|
48
|
+
cookies = []
|
|
49
|
+
{{#each cookieParams}}
|
|
50
|
+
{{#if required}}
|
|
51
|
+
cookies.append("{{originalName}}=" + str({{parameterName}}))
|
|
52
|
+
{{else}}
|
|
53
|
+
if {{parameterName}} is not None:
|
|
54
|
+
cookies.append("{{originalName}}=" + str({{parameterName}}))
|
|
109
55
|
{{/if}}
|
|
110
|
-
|
|
111
|
-
# Build headers
|
|
112
|
-
_headers: Dict[str, str] = {}
|
|
113
|
-
{{#each headerParams}}
|
|
114
|
-
if headers is not None and hasattr(headers, "{{name}}") and getattr(headers, "{{name}}") is not None:
|
|
115
|
-
_headers["{{originalName}}"] = str(getattr(headers, "{{name}}"))
|
|
116
56
|
{{/each}}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
_content_type = "{{requestBody.contentType}}"
|
|
57
|
+
if cookies:
|
|
58
|
+
headers["Cookie"] = "; ".join(cookies)
|
|
120
59
|
{{/if}}
|
|
121
|
-
|
|
60
|
+
request_options = options or RequestOptions()
|
|
61
|
+
if headers:
|
|
62
|
+
request_options.headers.update(headers)
|
|
122
63
|
response = self._transport.request(
|
|
123
64
|
method="{{httpMethod}}",
|
|
124
|
-
path=
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
{{/if}}
|
|
128
|
-
{{#if (hasRequestBody this)}}
|
|
65
|
+
path=path,
|
|
66
|
+
query=query or None,
|
|
67
|
+
{{#if requestBody}}
|
|
129
68
|
body=body,
|
|
130
|
-
content_type=
|
|
131
|
-
{{/if}}
|
|
132
|
-
{{#if (or headerParams.length (hasRequestBody this))}}
|
|
133
|
-
options=RequestOptions(headers=_headers if _headers else None),
|
|
69
|
+
content_type="{{requestBody.contentType}}",
|
|
134
70
|
{{/if}}
|
|
71
|
+
options=request_options,
|
|
135
72
|
)
|
|
136
|
-
|
|
137
|
-
|
|
73
|
+
{{#if isEventStream}}
|
|
74
|
+
return response.iter_lines()
|
|
75
|
+
{{else if isVoid}}
|
|
76
|
+
return None
|
|
77
|
+
{{else}}
|
|
78
|
+
return TypeAdapter({{{targetResponseType}}}).validate_python(response.json())
|
|
138
79
|
{{/if}}
|
|
80
|
+
|
|
139
81
|
{{/each}}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# {{title}} Python SDK
|
|
2
2
|
|
|
3
|
-
{{#if description}}
|
|
4
|
-
{{description}}
|
|
5
|
-
{{/if}}
|
|
3
|
+
{{#if description}}{{description}}{{else}}Python client for the {{title}} API.{{/if}}
|
|
6
4
|
|
|
7
|
-
>
|
|
5
|
+
> Generated by Tesseract. Generated source files should not be edited manually.
|
|
8
6
|
|
|
9
7
|
## Installation
|
|
10
8
|
|
|
@@ -12,7 +10,7 @@
|
|
|
12
10
|
pip install {{packageName}}
|
|
13
11
|
```
|
|
14
12
|
|
|
15
|
-
## Quick
|
|
13
|
+
## Quick start
|
|
16
14
|
|
|
17
15
|
```python
|
|
18
16
|
from {{moduleName}} import create_{{snakeCase clientName}}_client
|
|
@@ -20,45 +18,64 @@ from {{moduleName}} import create_{{snakeCase clientName}}_client
|
|
|
20
18
|
client = create_{{snakeCase clientName}}_client(
|
|
21
19
|
base_url="{{baseUrl}}",
|
|
22
20
|
{{#each securitySchemes}}
|
|
23
|
-
{{
|
|
24
|
-
{{configKey}}="your-api-key",
|
|
25
|
-
{{/if}}
|
|
26
|
-
{{#if (and (eq type "http") (eq scheme "bearer"))}}
|
|
27
|
-
{{configKey}}="your-bearer-token",
|
|
28
|
-
{{/if}}
|
|
21
|
+
{{configKey}}="your-credential",
|
|
29
22
|
{{/each}}
|
|
23
|
+
timeout_ms=15_000,
|
|
30
24
|
)
|
|
31
25
|
|
|
32
|
-
{{#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{{/with}}
|
|
26
|
+
{{#if groups.length}}
|
|
27
|
+
api = client.{{groups.[0].accessorName}}
|
|
28
|
+
{{/if}}
|
|
36
29
|
```
|
|
37
30
|
|
|
38
|
-
|
|
31
|
+
The client owns an `httpx.Client`. Close it when the application shuts down, or use it as a context manager.
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
### {{interfaceName}}
|
|
33
|
+
## Configuration and reliability
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
35
|
+
- Bounded request timeouts
|
|
36
|
+
- Exponential-backoff retries for transient failures
|
|
37
|
+
- `Retry-After` support for rate limits
|
|
38
|
+
- Default and per-request headers
|
|
39
|
+
- Structured `SDKError` values containing status, code, and request ID
|
|
40
|
+
- Request, response, and error hooks
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
{
|
|
42
|
+
```python
|
|
43
|
+
client = create_{{snakeCase clientName}}_client(
|
|
44
|
+
timeout_ms=30_000,
|
|
45
|
+
retries=3,
|
|
46
|
+
default_headers={"X-Application": "my-service"},
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Unsafe HTTP methods are not retried unless an idempotency key is present or unsafe retries are explicitly enabled.
|
|
51
|
+
|
|
52
|
+
## Documentation
|
|
52
53
|
|
|
54
|
+
- [Documentation index](./docs/README.md)
|
|
55
|
+
- [Generated types](./docs/types.md)
|
|
56
|
+
|
|
57
|
+
| Domain | Description |
|
|
58
|
+
|---|---|
|
|
59
|
+
{{#each groups}}
|
|
60
|
+
| [{{interfaceName}}](./docs/domains/{{fileName}}.md) | {{#if description}}{{description}}{{else}}{{name}} operations{{/if}} |
|
|
53
61
|
{{/each}}
|
|
54
62
|
|
|
55
|
-
## Error
|
|
63
|
+
## Error handling
|
|
56
64
|
|
|
57
65
|
```python
|
|
58
66
|
from {{moduleName}}.errors import SDKError
|
|
59
67
|
|
|
60
68
|
try:
|
|
61
|
-
|
|
62
|
-
except SDKError as
|
|
63
|
-
print(
|
|
69
|
+
pass # Call a domain operation.
|
|
70
|
+
except SDKError as error:
|
|
71
|
+
print(error.status, error.code, error.request_id)
|
|
72
|
+
raise
|
|
64
73
|
```
|
|
74
|
+
|
|
75
|
+
## Requirements
|
|
76
|
+
|
|
77
|
+
- Python 3.9 or later
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
|
@@ -1,30 +1,42 @@
|
|
|
1
1
|
"""HTTP transport layer for {{moduleName}}."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import email.utils
|
|
4
6
|
import json
|
|
5
7
|
import random
|
|
6
|
-
|
|
8
|
+
import time
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import Any, Callable, Dict, Optional
|
|
7
11
|
from urllib.parse import urlencode
|
|
12
|
+
|
|
8
13
|
import httpx
|
|
14
|
+
|
|
9
15
|
from .errors import SDKError
|
|
10
16
|
|
|
11
17
|
|
|
18
|
+
_RETRYABLE_METHODS = frozenset({"GET", "HEAD", "OPTIONS", "PUT", "DELETE"})
|
|
19
|
+
_RETRYABLE_STATUSES = frozenset({408, 425, 429, 500, 502, 503, 504})
|
|
20
|
+
|
|
21
|
+
|
|
12
22
|
class RequestOptions:
|
|
13
|
-
"""Per-request
|
|
23
|
+
"""Per-request transport overrides."""
|
|
14
24
|
|
|
15
25
|
def __init__(
|
|
16
26
|
self,
|
|
17
27
|
timeout_ms: Optional[int] = None,
|
|
18
28
|
headers: Optional[Dict[str, str]] = None,
|
|
19
|
-
|
|
29
|
+
retries: Optional[int] = None,
|
|
30
|
+
retry_unsafe_requests: Optional[bool] = None,
|
|
20
31
|
):
|
|
21
32
|
self.timeout_ms = timeout_ms
|
|
22
33
|
self.headers = headers or {}
|
|
23
|
-
self.
|
|
34
|
+
self.retries = retries
|
|
35
|
+
self.retry_unsafe_requests = retry_unsafe_requests
|
|
24
36
|
|
|
25
37
|
|
|
26
38
|
class SDKTransport:
|
|
27
|
-
"""HTTP transport with
|
|
39
|
+
"""Synchronous HTTP transport with bounded retries and structured errors."""
|
|
28
40
|
|
|
29
41
|
def __init__(
|
|
30
42
|
self,
|
|
@@ -34,25 +46,36 @@ class SDKTransport:
|
|
|
34
46
|
{{/each}}
|
|
35
47
|
timeout_ms: int = 15000,
|
|
36
48
|
retries: int = 3,
|
|
49
|
+
retry_unsafe_requests: bool = False,
|
|
37
50
|
default_headers: Optional[Dict[str, str]] = None,
|
|
38
|
-
on_request: Optional[Callable] = None,
|
|
39
|
-
on_response: Optional[Callable] = None,
|
|
40
|
-
on_error: Optional[Callable] = None,
|
|
51
|
+
on_request: Optional[Callable[..., None]] = None,
|
|
52
|
+
on_response: Optional[Callable[[httpx.Response], None]] = None,
|
|
53
|
+
on_error: Optional[Callable[[SDKError], None]] = None,
|
|
41
54
|
):
|
|
55
|
+
if timeout_ms <= 0:
|
|
56
|
+
raise ValueError("timeout_ms must be greater than zero")
|
|
57
|
+
if retries < 0:
|
|
58
|
+
raise ValueError("retries cannot be negative")
|
|
59
|
+
|
|
42
60
|
self.base_url = base_url.rstrip("/")
|
|
43
61
|
{{#each securitySchemes}}
|
|
44
62
|
self._{{configKey}} = {{configKey}}
|
|
45
63
|
{{/each}}
|
|
46
64
|
self.timeout_ms = timeout_ms
|
|
47
65
|
self.max_retries = retries
|
|
48
|
-
self.
|
|
66
|
+
self.retry_unsafe_requests = retry_unsafe_requests
|
|
67
|
+
self.default_headers = {
|
|
68
|
+
"Accept": "application/json",
|
|
69
|
+
"User-Agent": "{{moduleName}}-python-sdk/{{packageVersion}}",
|
|
70
|
+
**(default_headers or {}),
|
|
71
|
+
}
|
|
49
72
|
self.on_request = on_request
|
|
50
73
|
self.on_response = on_response
|
|
51
74
|
self.on_error = on_error
|
|
52
75
|
self._client = httpx.Client(timeout=timeout_ms / 1000.0)
|
|
53
76
|
|
|
54
77
|
def _build_headers(self, extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
|
|
55
|
-
headers =
|
|
78
|
+
headers = dict(self.default_headers)
|
|
56
79
|
{{#each securitySchemes}}
|
|
57
80
|
{{#if (eq type "apiKey")}}
|
|
58
81
|
if self._{{configKey}}:
|
|
@@ -84,84 +107,173 @@ class SDKTransport:
|
|
|
84
107
|
content_type: str = "application/json",
|
|
85
108
|
options: Optional[RequestOptions] = None,
|
|
86
109
|
) -> httpx.Response:
|
|
87
|
-
"""Execute
|
|
110
|
+
"""Execute a request and retry only transient, idempotent failures."""
|
|
111
|
+
method = method.upper()
|
|
88
112
|
url = self.base_url + path
|
|
89
113
|
headers = self._build_headers(options.headers if options else None)
|
|
90
|
-
|
|
114
|
+
timeout_ms = (
|
|
115
|
+
options.timeout_ms
|
|
116
|
+
if options is not None and options.timeout_ms is not None
|
|
117
|
+
else self.timeout_ms
|
|
118
|
+
)
|
|
119
|
+
retries = (
|
|
120
|
+
options.retries
|
|
121
|
+
if options is not None and options.retries is not None
|
|
122
|
+
else self.max_retries
|
|
123
|
+
)
|
|
124
|
+
retry_unsafe = (
|
|
125
|
+
options.retry_unsafe_requests
|
|
126
|
+
if options is not None and options.retry_unsafe_requests is not None
|
|
127
|
+
else self.retry_unsafe_requests
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if timeout_ms <= 0:
|
|
131
|
+
raise ValueError("timeout_ms must be greater than zero")
|
|
132
|
+
if retries < 0:
|
|
133
|
+
raise ValueError("retries cannot be negative")
|
|
91
134
|
|
|
92
135
|
if query:
|
|
93
|
-
filtered = {
|
|
136
|
+
filtered = {key: value for key, value in query.items() if value is not None}
|
|
94
137
|
if filtered:
|
|
95
|
-
url += "?" + urlencode(filtered)
|
|
138
|
+
url += "?" + urlencode(filtered, doseq=True)
|
|
96
139
|
|
|
97
|
-
|
|
140
|
+
content = None
|
|
98
141
|
if body is not None:
|
|
99
142
|
if content_type == "application/json":
|
|
100
|
-
|
|
143
|
+
payload = (
|
|
144
|
+
body.model_dump(by_alias=True, exclude_none=True)
|
|
145
|
+
if hasattr(body, "model_dump")
|
|
146
|
+
else body
|
|
147
|
+
)
|
|
148
|
+
content = json.dumps(payload, default=str)
|
|
101
149
|
headers["Content-Type"] = "application/json"
|
|
102
150
|
else:
|
|
103
|
-
|
|
151
|
+
content = body
|
|
152
|
+
headers["Content-Type"] = content_type
|
|
104
153
|
|
|
105
|
-
|
|
106
|
-
|
|
154
|
+
can_retry = self._can_retry(method, headers, retry_unsafe)
|
|
155
|
+
last_error: Optional[SDKError] = None
|
|
156
|
+
|
|
157
|
+
for attempt in range(retries + 1):
|
|
107
158
|
try:
|
|
108
159
|
if self.on_request:
|
|
109
|
-
self.on_request(method, url, attempt)
|
|
160
|
+
self.on_request(method=method, url=url, attempt=attempt)
|
|
110
161
|
|
|
111
162
|
response = self._client.request(
|
|
112
163
|
method=method,
|
|
113
164
|
url=url,
|
|
114
165
|
headers=headers,
|
|
115
|
-
content=
|
|
116
|
-
timeout=
|
|
166
|
+
content=content,
|
|
167
|
+
timeout=timeout_ms / 1000.0,
|
|
117
168
|
)
|
|
118
169
|
|
|
119
170
|
if self.on_response:
|
|
120
171
|
self.on_response(response)
|
|
121
172
|
|
|
122
|
-
if response.status_code
|
|
123
|
-
|
|
173
|
+
if response.status_code < 400:
|
|
174
|
+
return response
|
|
124
175
|
|
|
125
|
-
|
|
176
|
+
last_error = self._error_from_response(response)
|
|
177
|
+
if not (
|
|
178
|
+
can_retry
|
|
179
|
+
and attempt < retries
|
|
180
|
+
and response.status_code in _RETRYABLE_STATUSES
|
|
181
|
+
):
|
|
182
|
+
self._notify_error(last_error)
|
|
183
|
+
raise last_error
|
|
126
184
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
last_error = SDKError(
|
|
185
|
+
self._sleep_before_retry(attempt, response.headers.get("Retry-After"))
|
|
186
|
+
except (httpx.TimeoutException, httpx.NetworkError) as error:
|
|
187
|
+
code = "timeout" if isinstance(error, httpx.TimeoutException) else "network_error"
|
|
188
|
+
last_error = SDKError(str(error) or "Request failed", 0, code)
|
|
189
|
+
if not (can_retry and attempt < retries):
|
|
190
|
+
self._notify_error(last_error)
|
|
191
|
+
raise last_error from error
|
|
192
|
+
self._sleep_before_retry(attempt, None)
|
|
131
193
|
except SDKError:
|
|
132
194
|
raise
|
|
133
|
-
except Exception as
|
|
134
|
-
last_error = SDKError("Request failed
|
|
195
|
+
except Exception as error:
|
|
196
|
+
last_error = SDKError(str(error) or "Request failed", 0, "unknown_error")
|
|
197
|
+
self._notify_error(last_error)
|
|
198
|
+
raise last_error from error
|
|
199
|
+
|
|
200
|
+
error = last_error or SDKError("Request failed after retries", 0, "retry_exhausted")
|
|
201
|
+
self._notify_error(error)
|
|
202
|
+
raise error
|
|
135
203
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
204
|
+
def _can_retry(self, method: str, headers: Dict[str, str], retry_unsafe: bool) -> bool:
|
|
205
|
+
if retry_unsafe or method in _RETRYABLE_METHODS:
|
|
206
|
+
return True
|
|
207
|
+
return any(
|
|
208
|
+
name.lower() == "x-idempotency-key" and bool(value)
|
|
209
|
+
for name, value in headers.items()
|
|
210
|
+
)
|
|
139
211
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
212
|
+
def _sleep_before_retry(self, attempt: int, retry_after: Optional[str]) -> None:
|
|
213
|
+
delay = self._parse_retry_after(retry_after)
|
|
214
|
+
if delay is None:
|
|
215
|
+
maximum = min(0.4 * (2**attempt), 30.0)
|
|
216
|
+
delay = random.uniform(0.0, maximum)
|
|
217
|
+
time.sleep(delay)
|
|
143
218
|
|
|
144
|
-
|
|
219
|
+
@staticmethod
|
|
220
|
+
def _parse_retry_after(value: Optional[str]) -> Optional[float]:
|
|
221
|
+
if not value:
|
|
222
|
+
return None
|
|
223
|
+
try:
|
|
224
|
+
return min(max(float(value), 0.0), 30.0)
|
|
225
|
+
except ValueError:
|
|
226
|
+
try:
|
|
227
|
+
parsed = email.utils.parsedate_to_datetime(value)
|
|
228
|
+
if parsed.tzinfo is None:
|
|
229
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
230
|
+
return min(
|
|
231
|
+
max((parsed - datetime.now(timezone.utc)).total_seconds(), 0.0),
|
|
232
|
+
30.0,
|
|
233
|
+
)
|
|
234
|
+
except (TypeError, ValueError, OverflowError):
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
def _error_from_response(self, response: httpx.Response) -> SDKError:
|
|
238
|
+
request_id = response.headers.get("x-request-id") or response.headers.get("request-id")
|
|
145
239
|
try:
|
|
146
240
|
body = response.json()
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
241
|
+
except json.JSONDecodeError:
|
|
242
|
+
body = None
|
|
243
|
+
|
|
244
|
+
if isinstance(body, dict):
|
|
245
|
+
nested = body.get("error") if isinstance(body.get("error"), dict) else None
|
|
246
|
+
code = body.get("code") or (nested or {}).get("code")
|
|
247
|
+
if not code and isinstance(body.get("error"), str):
|
|
248
|
+
code = body.get("error")
|
|
249
|
+
message = body.get("message") or (nested or {}).get("message")
|
|
250
|
+
return SDKError(
|
|
251
|
+
str(message or response.reason_phrase or "Request failed"),
|
|
154
252
|
response.status_code,
|
|
155
|
-
"http_error",
|
|
253
|
+
str(code or "http_error"),
|
|
254
|
+
str(body.get("request_id") or request_id) if body.get("request_id") or request_id else None,
|
|
255
|
+
details=body.get("details"),
|
|
156
256
|
)
|
|
157
257
|
|
|
158
|
-
|
|
258
|
+
text = response.text.strip()
|
|
259
|
+
return SDKError(
|
|
260
|
+
text[:500] or f"HTTP {response.status_code}: {response.reason_phrase}",
|
|
261
|
+
response.status_code,
|
|
262
|
+
"http_error",
|
|
263
|
+
request_id,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def _notify_error(self, error: SDKError) -> None:
|
|
267
|
+
if self.on_error:
|
|
268
|
+
self.on_error(error)
|
|
269
|
+
|
|
270
|
+
def close(self) -> None:
|
|
159
271
|
self._client.close()
|
|
160
272
|
|
|
161
|
-
def __enter__(self):
|
|
273
|
+
def __enter__(self) -> "SDKTransport":
|
|
162
274
|
return self
|
|
163
275
|
|
|
164
|
-
def __exit__(self, *
|
|
276
|
+
def __exit__(self, *_: Any) -> None:
|
|
165
277
|
self.close()
|
|
166
278
|
|
|
167
279
|
|
|
@@ -172,10 +284,11 @@ def create_transport(
|
|
|
172
284
|
{{/each}}
|
|
173
285
|
timeout_ms: int = 15000,
|
|
174
286
|
retries: int = 3,
|
|
287
|
+
retry_unsafe_requests: bool = False,
|
|
175
288
|
default_headers: Optional[Dict[str, str]] = None,
|
|
176
|
-
on_request: Optional[Callable] = None,
|
|
177
|
-
on_response: Optional[Callable] = None,
|
|
178
|
-
on_error: Optional[Callable] = None,
|
|
289
|
+
on_request: Optional[Callable[..., None]] = None,
|
|
290
|
+
on_response: Optional[Callable[[httpx.Response], None]] = None,
|
|
291
|
+
on_error: Optional[Callable[[SDKError], None]] = None,
|
|
179
292
|
) -> SDKTransport:
|
|
180
293
|
return SDKTransport(
|
|
181
294
|
base_url=base_url,
|
|
@@ -184,6 +297,7 @@ def create_transport(
|
|
|
184
297
|
{{/each}}
|
|
185
298
|
timeout_ms=timeout_ms,
|
|
186
299
|
retries=retries,
|
|
300
|
+
retry_unsafe_requests=retry_unsafe_requests,
|
|
187
301
|
default_headers=default_headers,
|
|
188
302
|
on_request=on_request,
|
|
189
303
|
on_response=on_response,
|