@apollo-deploy/tesseract 1.8.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.
Files changed (194) hide show
  1. package/README.md +218 -110
  2. package/dist/adapters/csharp/index.d.ts +5 -8
  3. package/dist/adapters/csharp/index.d.ts.map +1 -1
  4. package/dist/adapters/csharp/index.js +314 -74
  5. package/dist/adapters/csharp/index.js.map +1 -1
  6. package/dist/adapters/documentation.d.ts +17 -0
  7. package/dist/adapters/documentation.d.ts.map +1 -0
  8. package/dist/adapters/documentation.js +268 -0
  9. package/dist/adapters/documentation.js.map +1 -0
  10. package/dist/adapters/go/index.d.ts +5 -8
  11. package/dist/adapters/go/index.d.ts.map +1 -1
  12. package/dist/adapters/go/index.js +197 -101
  13. package/dist/adapters/go/index.js.map +1 -1
  14. package/dist/adapters/kotlin/index.d.ts +5 -9
  15. package/dist/adapters/kotlin/index.d.ts.map +1 -1
  16. package/dist/adapters/kotlin/index.js +280 -212
  17. package/dist/adapters/kotlin/index.js.map +1 -1
  18. package/dist/adapters/php/index.d.ts +5 -15
  19. package/dist/adapters/php/index.d.ts.map +1 -1
  20. package/dist/adapters/php/index.js +362 -90
  21. package/dist/adapters/php/index.js.map +1 -1
  22. package/dist/adapters/python/index.d.ts +5 -8
  23. package/dist/adapters/python/index.d.ts.map +1 -1
  24. package/dist/adapters/python/index.js +240 -109
  25. package/dist/adapters/python/index.js.map +1 -1
  26. package/dist/adapters/ruby/index.d.ts +3 -3
  27. package/dist/adapters/ruby/index.d.ts.map +1 -1
  28. package/dist/adapters/ruby/index.js +235 -46
  29. package/dist/adapters/ruby/index.js.map +1 -1
  30. package/dist/adapters/rust/index.d.ts +5 -9
  31. package/dist/adapters/rust/index.d.ts.map +1 -1
  32. package/dist/adapters/rust/index.js +237 -169
  33. package/dist/adapters/rust/index.js.map +1 -1
  34. package/dist/adapters/shared.d.ts +7 -0
  35. package/dist/adapters/shared.d.ts.map +1 -0
  36. package/dist/adapters/shared.js +68 -0
  37. package/dist/adapters/shared.js.map +1 -0
  38. package/dist/adapters/type-utils.d.ts +13 -0
  39. package/dist/adapters/type-utils.d.ts.map +1 -0
  40. package/dist/adapters/type-utils.js +105 -0
  41. package/dist/adapters/type-utils.js.map +1 -0
  42. package/dist/adapters/types.d.ts.map +1 -1
  43. package/dist/adapters/typescript/emitter/emitSchema.d.ts +2 -2
  44. package/dist/adapters/typescript/emitter/emitSchema.js +39 -27
  45. package/dist/adapters/typescript/emitter/emitSchema.js.map +1 -1
  46. package/dist/adapters/typescript/emitter/emitTypeScriptModels.d.ts +2 -2
  47. package/dist/adapters/typescript/emitter/emitTypeScriptModels.js +15 -15
  48. package/dist/adapters/typescript/emitter/importCollector.d.ts +2 -4
  49. package/dist/adapters/typescript/emitter/importCollector.d.ts.map +1 -1
  50. package/dist/adapters/typescript/emitter/importCollector.js +40 -50
  51. package/dist/adapters/typescript/emitter/importCollector.js.map +1 -1
  52. package/dist/adapters/typescript/emitter/parsers.d.ts +1 -1
  53. package/dist/adapters/typescript/emitter/parsers.js +13 -13
  54. package/dist/adapters/typescript/emitter/typeHoister.d.ts +1 -1
  55. package/dist/adapters/typescript/emitter/typeHoister.d.ts.map +1 -1
  56. package/dist/adapters/typescript/emitter/typeHoister.js +12 -20
  57. package/dist/adapters/typescript/emitter/typeHoister.js.map +1 -1
  58. package/dist/adapters/typescript/index.d.ts +3 -3
  59. package/dist/adapters/typescript/index.d.ts.map +1 -1
  60. package/dist/adapters/typescript/index.js +148 -166
  61. package/dist/adapters/typescript/index.js.map +1 -1
  62. package/dist/adapters/typescript/shared.d.ts +2 -2
  63. package/dist/adapters/typescript/shared.js +12 -12
  64. package/dist/cli.js +50 -37
  65. package/dist/cli.js.map +1 -1
  66. package/dist/collector.d.ts +77 -3
  67. package/dist/collector.d.ts.map +1 -1
  68. package/dist/collector.js +198 -20
  69. package/dist/collector.js.map +1 -1
  70. package/dist/elysia.js +2 -2
  71. package/dist/elysia.js.map +1 -1
  72. package/dist/express.d.ts.map +1 -1
  73. package/dist/express.js.map +1 -1
  74. package/dist/fastify.d.ts +8 -9
  75. package/dist/fastify.d.ts.map +1 -1
  76. package/dist/fastify.js +105 -164
  77. package/dist/fastify.js.map +1 -1
  78. package/dist/helpers/handlebars.d.ts +7 -16
  79. package/dist/helpers/handlebars.d.ts.map +1 -1
  80. package/dist/helpers/handlebars.js +127 -177
  81. package/dist/helpers/handlebars.js.map +1 -1
  82. package/dist/hono.d.ts.map +1 -1
  83. package/dist/hono.js.map +1 -1
  84. package/dist/index.d.ts +14 -12
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +32 -30
  87. package/dist/index.js.map +1 -1
  88. package/dist/koa.d.ts.map +1 -1
  89. package/dist/koa.js.map +1 -1
  90. package/dist/nestjs.d.ts.map +1 -1
  91. package/dist/nestjs.js +41 -14
  92. package/dist/nestjs.js.map +1 -1
  93. package/dist/pipeline/enrich.d.ts +2 -13
  94. package/dist/pipeline/enrich.d.ts.map +1 -1
  95. package/dist/pipeline/enrich.js +157 -212
  96. package/dist/pipeline/enrich.js.map +1 -1
  97. package/dist/pipeline/intake.d.ts +3 -3
  98. package/dist/pipeline/intake.d.ts.map +1 -1
  99. package/dist/pipeline/intake.js +342 -238
  100. package/dist/pipeline/intake.js.map +1 -1
  101. package/dist/pipeline/write.d.ts +1 -14
  102. package/dist/pipeline/write.d.ts.map +1 -1
  103. package/dist/pipeline/write.js +73 -113
  104. package/dist/pipeline/write.js.map +1 -1
  105. package/dist/types/config.d.ts +14 -26
  106. package/dist/types/config.d.ts.map +1 -1
  107. package/dist/types/config.js +16 -12
  108. package/dist/types/config.js.map +1 -1
  109. package/dist/types/ir.d.ts +12 -8
  110. package/dist/types/ir.d.ts.map +1 -1
  111. package/dist/types/manifest.d.ts +10 -11
  112. package/dist/types/manifest.d.ts.map +1 -1
  113. package/dist/types/manifest.js +54 -14
  114. package/dist/types/manifest.js.map +1 -1
  115. package/dist/types/sdk-module.d.ts +2 -2
  116. package/dist/types/sdk-module.js +1 -1
  117. package/dist/utils/format.d.ts +1 -3
  118. package/dist/utils/format.d.ts.map +1 -1
  119. package/dist/utils/format.js +3 -10
  120. package/dist/utils/format.js.map +1 -1
  121. package/dist/utils/naming.d.ts.map +1 -1
  122. package/dist/utils/naming.js +174 -184
  123. package/dist/utils/naming.js.map +1 -1
  124. package/dist/utils/npm-token.d.ts +1 -17
  125. package/dist/utils/npm-token.d.ts.map +1 -1
  126. package/dist/utils/npm-token.js +27 -76
  127. package/dist/utils/npm-token.js.map +1 -1
  128. package/dist/utils/version.d.ts +14 -0
  129. package/dist/utils/version.d.ts.map +1 -0
  130. package/dist/utils/version.js +60 -0
  131. package/dist/utils/version.js.map +1 -0
  132. package/package.json +4 -3
  133. package/templates/csharp/client.hbs +26 -49
  134. package/templates/csharp/csproj.hbs +7 -0
  135. package/templates/csharp/domain.hbs +48 -130
  136. package/templates/csharp/errors.hbs +17 -25
  137. package/templates/csharp/readme.hbs +31 -44
  138. package/templates/csharp/transport.hbs +238 -136
  139. package/templates/csharp/types.hbs +30 -76
  140. package/templates/go/client.hbs +44 -109
  141. package/templates/go/domain.hbs +83 -25
  142. package/templates/go/go-mod.hbs +1 -4
  143. package/templates/go/readme.hbs +32 -29
  144. package/templates/go/transport.hbs +174 -173
  145. package/templates/go/types.hbs +17 -53
  146. package/templates/go/utils.hbs +3 -2
  147. package/templates/kotlin/build-gradle.hbs +1 -1
  148. package/templates/kotlin/client.hbs +18 -15
  149. package/templates/kotlin/domain.hbs +54 -51
  150. package/templates/kotlin/errors.hbs +1 -1
  151. package/templates/kotlin/readme.hbs +35 -137
  152. package/templates/kotlin/transport.hbs +140 -123
  153. package/templates/kotlin/types.hbs +30 -81
  154. package/templates/php/client.hbs +8 -46
  155. package/templates/php/composer.hbs +2 -1
  156. package/templates/php/domain.hbs +86 -117
  157. package/templates/php/errors.hbs +1 -1
  158. package/templates/php/readme.hbs +42 -49
  159. package/templates/php/transport.hbs +192 -145
  160. package/templates/php/types.hbs +63 -60
  161. package/templates/python/client.hbs +15 -2
  162. package/templates/python/domain.hbs +50 -108
  163. package/templates/python/pyproject.hbs +5 -0
  164. package/templates/python/readme.hbs +47 -30
  165. package/templates/python/resources-init.hbs +1 -1
  166. package/templates/python/transport.hbs +167 -53
  167. package/templates/python/types.hbs +22 -58
  168. package/templates/ruby/client.hbs +10 -6
  169. package/templates/ruby/domain.hbs +48 -67
  170. package/templates/ruby/errors.hbs +3 -3
  171. package/templates/ruby/gemspec.hbs +3 -10
  172. package/templates/ruby/index.hbs +3 -3
  173. package/templates/ruby/readme.hbs +33 -70
  174. package/templates/ruby/transport.hbs +129 -103
  175. package/templates/ruby/types.hbs +11 -16
  176. package/templates/ruby/version.hbs +1 -1
  177. package/templates/rust/cargo.hbs +2 -0
  178. package/templates/rust/client.hbs +10 -1
  179. package/templates/rust/domain.hbs +69 -52
  180. package/templates/rust/error.hbs +3 -1
  181. package/templates/rust/lib.hbs +4 -5
  182. package/templates/rust/readme.hbs +33 -46
  183. package/templates/rust/transport.hbs +170 -159
  184. package/templates/rust/types.hbs +34 -111
  185. package/templates/typescript/domain-class.hbs +14 -6
  186. package/templates/typescript/domain.hbs +13 -5
  187. package/templates/typescript/package-json.hbs +3 -1
  188. package/templates/typescript/readme.hbs +42 -42
  189. package/templates/typescript/transport.hbs +46 -8
  190. package/templates/typescript/types-errors.hbs +1 -1
  191. package/dist/adapters/typescript/model-emitter.d.ts +0 -13
  192. package/dist/adapters/typescript/model-emitter.d.ts.map +0 -1
  193. package/dist/adapters/typescript/model-emitter.js +0 -415
  194. package/dist/adapters/typescript/model-emitter.js.map +0 -1
@@ -1,11 +1,13 @@
1
1
  """{{interfaceName}} operations."""
2
2
 
3
- from typing import Optional, Dict, Any, List
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 {{#each imports}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}
8
+ from ..types import {{join imports ", "}}
6
9
  {{/if}}
7
- from ..transport import SDKTransport, RequestOptions
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
- {{#if isEventStream}}
19
- def {{snakeCase name}}(
20
- self,
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
- {{name}}: str,
27
+ path = path.replace("{{placeholder}}", quote(str({{parameterName}}), safe=""))
23
28
  {{/each}}
24
- {{#if (and queryType (not queryParams.length))}}
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
- {{name}}: Optional[{{pythonType type}}] = None,
32
- {{/each}}
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
- def {{snakeCase name}}(
64
- self,
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
- # Build query parameters
98
- _query: Dict[str, Any] = {}
99
- {{#each queryParams}}
100
- if {{name}} is not None:
101
- _query["{{originalName}}"] = {{name}}
102
- {{/each}}
103
- {{#if queryType}}
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
- {{#if (hasRequestBody this)}}
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=_path,
125
- {{#if (or queryParams.length queryType)}}
126
- query=_query if _query else None,
127
- {{/if}}
128
- {{#if (hasRequestBody this)}}
65
+ path=path,
66
+ query=query or None,
67
+ {{#if requestBody}}
129
68
  body=body,
130
- content_type=_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
- return response.json()
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}}
@@ -24,3 +24,8 @@ dev = [
24
24
 
25
25
  [tool.setuptools.packages.find]
26
26
  include = ["{{moduleName}}*"]
27
+
28
+ [tool.setuptools.data-files]
29
+ "." = ["README.md", "LICENSE"]
30
+ "docs" = ["docs/*.md"]
31
+ "docs/domains" = ["docs/domains/*.md"]
@@ -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
- > Auto-generated by Tesseract.
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 Start
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
- {{#if (eq type "apiKey")}}
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
- {{#with groups.[0]}}
33
- # Example: {{interfaceName}}
34
- result = client.{{snakeCase name}}.{{snakeCase operations.[0].name}}()
35
- {{/with}}
26
+ {{#if groups.length}}
27
+ api = client.{{groups.[0].accessorName}}
28
+ {{/if}}
36
29
  ```
37
30
 
38
- ## API Reference
31
+ The client owns an `httpx.Client`. Close it when the application shuts down, or use it as a context manager.
39
32
 
40
- {{#each groups}}
41
- ### {{interfaceName}}
33
+ ## Configuration and reliability
42
34
 
43
- {{#if description}}
44
- {{description}}
45
- {{/if}}
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
- | Method | HTTP | Path | Description |
48
- |--------|------|------|-------------|
49
- {{#each operations}}
50
- | `{{name}}` | {{httpMethod}} | `{{path}}` | {{#if description}}{{description}}{{else}}-{{/if}} |
51
- {{/each}}
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 Handling
63
+ ## Error handling
56
64
 
57
65
  ```python
58
66
  from {{moduleName}}.errors import SDKError
59
67
 
60
68
  try:
61
- result = client.some_api.some_method()
62
- except SDKError as e:
63
- print(f"Error {e.status}: {e.message} ({e.code})")
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,7 +1,7 @@
1
1
  """API resource modules for {{moduleName}}."""
2
2
 
3
3
  {{#each groups}}
4
- from .{{fileName}} import {{interfaceName}}
4
+ from .{{fileModule}} import {{interfaceName}}
5
5
  {{/each}}
6
6
 
7
7
  __all__ = [
@@ -1,30 +1,42 @@
1
1
  """HTTP transport layer for {{moduleName}}."""
2
2
 
3
- import time
3
+ from __future__ import annotations
4
+
5
+ import email.utils
4
6
  import json
5
7
  import random
6
- from typing import Optional, Dict, Any, Callable
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 options."""
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
- signal: Optional[Any] = None,
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.signal = signal
34
+ self.retries = retries
35
+ self.retry_unsafe_requests = retry_unsafe_requests
24
36
 
25
37
 
26
38
  class SDKTransport:
27
- """HTTP transport with retry, auth, and error handling."""
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.default_headers = default_headers or {}
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 = {**self.default_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 an HTTP request with retry logic."""
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
- timeout = (options.timeout_ms if options and options.timeout_ms else self.timeout_ms) / 1000.0
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 = {k: v for k, v in query.items() if v is not None}
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
- data = None
140
+ content = None
98
141
  if body is not None:
99
142
  if content_type == "application/json":
100
- data = json.dumps(body, default=str)
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
- data = body
151
+ content = body
152
+ headers["Content-Type"] = content_type
104
153
 
105
- last_error = None
106
- for attempt in range(self.max_retries + 1):
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=data,
116
- timeout=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 >= 400:
123
- self._handle_error_response(response)
173
+ if response.status_code < 400:
174
+ return response
124
175
 
125
- return response
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
- except httpx.TimeoutException as e:
128
- last_error = SDKError("Request timed out: " + str(e), 0, "timeout")
129
- except httpx.NetworkError as e:
130
- last_error = SDKError("Network error: " + str(e), 0, "network_error")
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 e:
134
- last_error = SDKError("Request failed: " + str(e), 0, "unknown_error")
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
- if attempt < self.max_retries:
137
- delay = min(1000 * (2 ** attempt) + random.uniform(0, 500), 30000) / 1000.0
138
- time.sleep(delay)
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
- if self.on_error and last_error:
141
- self.on_error(last_error)
142
- raise last_error
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
- def _handle_error_response(self, response: httpx.Response) -> None:
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
- error_code = body.get("error", "unknown_error")
148
- message = body.get("message", response.text)
149
- request_id = body.get("request_id")
150
- raise SDKError(message, response.status_code, error_code, request_id)
151
- except (json.JSONDecodeError, KeyError):
152
- raise SDKError(
153
- "HTTP " + str(response.status_code) + ": " + str(response.text)[:200],
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
- def close(self):
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, *args):
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,