@voxgig/sdkgen 4.23.0 → 4.24.1

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 (163) hide show
  1. package/bin/voxgig-sdkgen +18 -2
  2. package/dist/action/doctor.d.ts +1 -1
  3. package/dist/action/doctor.js +12 -10
  4. package/dist/action/doctor.js.map +1 -1
  5. package/dist/action/edition.js +4 -0
  6. package/dist/action/edition.js.map +1 -1
  7. package/dist/action/feature.js +5 -1
  8. package/dist/action/feature.js.map +1 -1
  9. package/dist/action/remove.d.ts +16 -0
  10. package/dist/action/remove.js +345 -0
  11. package/dist/action/remove.js.map +1 -0
  12. package/dist/action/target.js +6 -1
  13. package/dist/action/target.js.map +1 -1
  14. package/dist/cmp/Deploy.js +9 -6
  15. package/dist/cmp/Deploy.js.map +1 -1
  16. package/dist/helpers/semver.js +3 -1
  17. package/dist/helpers/semver.js.map +1 -1
  18. package/dist/tsconfig.tsbuildinfo +1 -1
  19. package/package.json +2 -2
  20. package/project/.sdk/tm/c/core/context.c +3 -1
  21. package/project/.sdk/tm/c/feature/paging.c +37 -3
  22. package/project/.sdk/tm/c/feature/test.c +51 -27
  23. package/project/.sdk/tm/c/utility/prepare_query.c +2 -1
  24. package/project/.sdk/tm/c/utility/transform_request.c +25 -3
  25. package/project/.sdk/tm/clojure/src/sdk/core.clj +21 -7
  26. package/project/.sdk/tm/clojure/src/sdk/features.clj +68 -18
  27. package/project/.sdk/tm/cpp/core/types.hpp +1 -1
  28. package/project/.sdk/tm/cpp/feature/paging.hpp +36 -3
  29. package/project/.sdk/tm/cpp/feature/test.hpp +41 -13
  30. package/project/.sdk/tm/cpp/utility/pipeline.hpp +16 -4
  31. package/project/.sdk/tm/csharp/core/Context.cs +1 -1
  32. package/project/.sdk/tm/csharp/feature/PagingFeature.cs +58 -4
  33. package/project/.sdk/tm/csharp/feature/TestFeature.cs +13 -21
  34. package/project/.sdk/tm/csharp/utility/Fetcher.cs +19 -12
  35. package/project/.sdk/tm/csharp/utility/PrepareQuery.cs +1 -1
  36. package/project/.sdk/tm/csharp/utility/TransformRequest.cs +23 -3
  37. package/project/.sdk/tm/elixir/lib/projectname/context.ex +1 -1
  38. package/project/.sdk/tm/elixir/lib/projectname/feature/paging.ex +52 -10
  39. package/project/.sdk/tm/elixir/lib/projectname/feature/test.ex +47 -17
  40. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +30 -9
  41. package/project/.sdk/tm/go/core/context.go +13 -13
  42. package/project/.sdk/tm/go/feature/paging_feature.go +40 -2
  43. package/project/.sdk/tm/go/feature/test_feature.go +3 -15
  44. package/project/.sdk/tm/go/test/feature_corpus_test.go +1 -0
  45. package/project/.sdk/tm/go/test/feature_test.go +54 -0
  46. package/project/.sdk/tm/go/test/pipeline_test.go +163 -57
  47. package/project/.sdk/tm/go/test/primary_utility_test.go +53 -0
  48. package/project/.sdk/tm/go/utility/fetcher.go +41 -20
  49. package/project/.sdk/tm/go/utility/prepare_query.go +1 -1
  50. package/project/.sdk/tm/go/utility/transform_request.go +22 -3
  51. package/project/.sdk/tm/java/core/Context.java +2 -1
  52. package/project/.sdk/tm/java/feature/PagingFeature.java +46 -4
  53. package/project/.sdk/tm/java/feature/TestFeature.java +10 -13
  54. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +3 -0
  55. package/project/.sdk/tm/java/test/FeatureTest.java +42 -0
  56. package/project/.sdk/tm/java/test/PipelineTest.java +141 -48
  57. package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +67 -6
  58. package/project/.sdk/tm/java/utility/Fetcher.java +36 -28
  59. package/project/.sdk/tm/java/utility/PrepareQuery.java +1 -1
  60. package/project/.sdk/tm/java/utility/TransformRequest.java +18 -3
  61. package/project/.sdk/tm/js/src/Context.js +7 -2
  62. package/project/.sdk/tm/js/src/feature/paging/PagingFeature.js +24 -6
  63. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +7 -5
  64. package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +1 -1
  65. package/project/.sdk/tm/js/test/feature/Corpus.test.js +4 -0
  66. package/project/.sdk/tm/js/test/pipeline.test.js +88 -22
  67. package/project/.sdk/tm/js/test/utility/PrimaryUtility.test.js +47 -1
  68. package/project/.sdk/tm/kotlin/core/Context.kt +1 -1
  69. package/project/.sdk/tm/kotlin/feature/PagingFeature.kt +36 -4
  70. package/project/.sdk/tm/kotlin/feature/TestFeature.kt +11 -14
  71. package/project/.sdk/tm/kotlin/utility/Fetcher.kt +33 -24
  72. package/project/.sdk/tm/kotlin/utility/Prepare.kt +1 -1
  73. package/project/.sdk/tm/kotlin/utility/ResultTransform.kt +18 -3
  74. package/project/.sdk/tm/lua/core/context.lua +8 -1
  75. package/project/.sdk/tm/lua/core/control.lua +3 -0
  76. package/project/.sdk/tm/lua/feature/paging_feature.lua +44 -5
  77. package/project/.sdk/tm/lua/feature/test_feature.lua +32 -14
  78. package/project/.sdk/tm/lua/test/omni.lua +8 -1
  79. package/project/.sdk/tm/lua/test/pipeline_test.lua +107 -28
  80. package/project/.sdk/tm/lua/test/primary_utility_test.lua +83 -3
  81. package/project/.sdk/tm/lua/utility/prepare_query.lua +2 -1
  82. package/project/.sdk/tm/lua/utility/transform_request.lua +19 -3
  83. package/project/.sdk/tm/ocaml/sdk_features.ml +72 -21
  84. package/project/.sdk/tm/ocaml/sdk_runtime.ml +23 -8
  85. package/project/.sdk/tm/perl/core/context.pm +2 -1
  86. package/project/.sdk/tm/perl/feature/paging_feature.pm +31 -5
  87. package/project/.sdk/tm/perl/feature/test_feature.pm +46 -16
  88. package/project/.sdk/tm/perl/feature/timeout_feature.pm +17 -13
  89. package/project/.sdk/tm/perl/t/feature_corpus.t +14 -1
  90. package/project/.sdk/tm/perl/t/omni.pm +7 -1
  91. package/project/.sdk/tm/perl/t/pipeline.t +124 -27
  92. package/project/.sdk/tm/perl/t/primary_utility.t +73 -0
  93. package/project/.sdk/tm/perl/utility/fetcher.pm +15 -1
  94. package/project/.sdk/tm/perl/utility/prepare_query.pm +1 -0
  95. package/project/.sdk/tm/perl/utility/transform_request.pm +13 -3
  96. package/project/.sdk/tm/php/core/Context.php +5 -1
  97. package/project/.sdk/tm/php/core/Control.php +5 -0
  98. package/project/.sdk/tm/php/feature/PagingFeature.php +26 -5
  99. package/project/.sdk/tm/php/feature/TestFeature.php +27 -42
  100. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +17 -1
  101. package/project/.sdk/tm/php/test/FeatureTest.php +44 -0
  102. package/project/.sdk/tm/php/test/PipelineTest.php +143 -26
  103. package/project/.sdk/tm/php/test/PrimaryUtilityTest.php +80 -2
  104. package/project/.sdk/tm/php/utility/Fetcher.php +17 -3
  105. package/project/.sdk/tm/php/utility/PrepareQuery.php +1 -1
  106. package/project/.sdk/tm/php/utility/TransformRequest.php +15 -3
  107. package/project/.sdk/tm/py/pkg/core/context.py +2 -1
  108. package/project/.sdk/tm/py/pkg/feature/paging_feature.py +29 -4
  109. package/project/.sdk/tm/py/pkg/feature/test_feature.py +29 -12
  110. package/project/.sdk/tm/py/pkg/utility/fetcher.py +63 -65
  111. package/project/.sdk/tm/py/pkg/utility/prepare_query.py +2 -1
  112. package/project/.sdk/tm/py/pkg/utility/transform_request.py +11 -3
  113. package/project/.sdk/tm/py/test/test_feature.py +27 -0
  114. package/project/.sdk/tm/py/test/test_feature_corpus.py +11 -1
  115. package/project/.sdk/tm/py/test/test_pipeline.py +108 -31
  116. package/project/.sdk/tm/py/test/test_primary_utility.py +76 -1
  117. package/project/.sdk/tm/rb/core/context.rb +1 -1
  118. package/project/.sdk/tm/rb/feature/paging_feature.rb +27 -5
  119. package/project/.sdk/tm/rb/feature/test_feature.rb +33 -7
  120. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -1
  121. package/project/.sdk/tm/rb/test/feature_test.rb +38 -0
  122. package/project/.sdk/tm/rb/test/pipeline_test.rb +100 -24
  123. package/project/.sdk/tm/rb/test/primary_utility_test.rb +59 -1
  124. package/project/.sdk/tm/rb/utility/fetcher.rb +49 -3
  125. package/project/.sdk/tm/rb/utility/prepare_query.rb +1 -1
  126. package/project/.sdk/tm/rb/utility/transform_request.rb +10 -3
  127. package/project/.sdk/tm/rust/core/context.rs +2 -6
  128. package/project/.sdk/tm/rust/feature/paging.rs +44 -3
  129. package/project/.sdk/tm/rust/feature/secrets.rs +1 -1
  130. package/project/.sdk/tm/rust/feature/test.rs +66 -33
  131. package/project/.sdk/tm/rust/utility/fetcher.rs +41 -14
  132. package/project/.sdk/tm/rust/utility/prepare_query.rs +1 -1
  133. package/project/.sdk/tm/rust/utility/transform_request.rs +31 -5
  134. package/project/.sdk/tm/scala/core/Context.scala +2 -1
  135. package/project/.sdk/tm/scala/feature/PagingFeature.scala +41 -4
  136. package/project/.sdk/tm/scala/feature/TestFeature.scala +11 -12
  137. package/project/.sdk/tm/scala/utility/Fetcher.scala +34 -13
  138. package/project/.sdk/tm/scala/utility/Prepare.scala +1 -1
  139. package/project/.sdk/tm/scala/utility/Transform.scala +19 -3
  140. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/core/Context.swift +1 -1
  141. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/PagingFeature.swift +31 -4
  142. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +12 -9
  143. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Fetcher.swift +22 -3
  144. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +1 -1
  145. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/ResultUtil.swift +14 -3
  146. package/project/.sdk/tm/ts/src/Context.ts +7 -2
  147. package/project/.sdk/tm/ts/src/feature/paging/PagingFeature.ts +19 -3
  148. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +3 -0
  149. package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +1 -1
  150. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +1 -0
  151. package/project/.sdk/tm/ts/test/pipeline.test.ts +52 -11
  152. package/project/.sdk/tm/zig/core/context.zig +1 -1
  153. package/project/.sdk/tm/zig/core/utility.zig +19 -5
  154. package/project/.sdk/tm/zig/feature/paging.zig +34 -4
  155. package/project/.sdk/tm/zig/feature/test.zig +54 -26
  156. package/project/sdkgen-package.json +1 -1
  157. package/src/action/doctor.ts +12 -9
  158. package/src/action/edition.ts +8 -0
  159. package/src/action/feature.ts +7 -1
  160. package/src/action/remove.ts +492 -0
  161. package/src/action/target.ts +8 -1
  162. package/src/cmp/Deploy.ts +11 -6
  163. package/src/helpers/semver.ts +3 -1
@@ -12,6 +12,22 @@ class ProjectNameFetcher
12
12
  // Users can override by passing a User-Agent header in fetchdef.
13
13
  public const DEFAULT_USER_AGENT = 'Mozilla/5.0 (compatible; ProjectNameSDK/1.0)';
14
14
 
15
+ // One cURL handle per process, reset between calls rather than closed:
16
+ // curl_reset keeps the connection cache, so the next request to the
17
+ // same host reuses the open connection.
18
+ private static $curl = null;
19
+
20
+ private static function curlHandle(string $fullurl)
21
+ {
22
+ if (self::$curl === null) {
23
+ self::$curl = curl_init();
24
+ } else {
25
+ curl_reset(self::$curl);
26
+ }
27
+ curl_setopt(self::$curl, CURLOPT_URL, $fullurl);
28
+ return self::$curl;
29
+ }
30
+
15
31
  public static function defaultHttpFetch(string $fullurl, array $fetchdef): array
16
32
  {
17
33
  $method_str = strtoupper($fetchdef['method'] ?? 'GET');
@@ -130,7 +146,7 @@ class ProjectNameFetcher
130
146
  ];
131
147
  }
132
148
 
133
- $ch = curl_init($fullurl);
149
+ $ch = self::curlHandle($fullurl);
134
150
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
135
151
  curl_setopt($ch, CURLOPT_HEADER, false);
136
152
  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
@@ -157,7 +173,6 @@ class ProjectNameFetcher
157
173
  $response_body = curl_exec($ch);
158
174
  if ($response_body === false) {
159
175
  $err = curl_error($ch) ?: 'curl_exec failed';
160
- curl_close($ch);
161
176
  return [
162
177
  [
163
178
  'status' => 0,
@@ -169,7 +184,6 @@ class ProjectNameFetcher
169
184
  $err,
170
185
  ];
171
186
  }
172
- curl_close($ch);
173
187
 
174
188
  $status = 0;
175
189
  $status_text = '';
@@ -22,7 +22,7 @@ class ProjectNamePrepareQuery
22
22
  foreach ($items as $item) {
23
23
  $key = $item[0];
24
24
  $val = $item[1];
25
- if ($val !== null && is_string($key) && !in_array($key, $params, true)) {
25
+ if ($val !== null && is_string($key) && '$action' !== $key && !in_array($key, $params, true)) {
26
26
  $out[$key] = $val;
27
27
  }
28
28
  }
@@ -16,12 +16,24 @@ class ProjectNameTransformRequest
16
16
  }
17
17
  $transform = ProjectNameHelpers::to_map(\Voxgig\Struct\Struct::getprop($point, 'transform'));
18
18
  if (!$transform) {
19
- return $ctx->reqdata;
19
+ return self::strip_action($ctx->reqdata);
20
20
  }
21
21
  $reqform = \Voxgig\Struct\Struct::getprop($transform, 'req');
22
22
  if (!$reqform) {
23
- return $ctx->reqdata;
23
+ return self::strip_action($ctx->reqdata);
24
24
  }
25
- return \Voxgig\Struct\Struct::transform(['reqdata' => $ctx->reqdata], $reqform);
25
+ return self::strip_action(\Voxgig\Struct\Struct::transform(['reqdata' => $ctx->reqdata], $reqform));
26
+ }
27
+
28
+ // `$action` selects the point (see MakePoint); it is never an API field,
29
+ // so the body is a copy without it. Arrays are values, so the unset
30
+ // never reaches the caller's copy.
31
+ private static function strip_action(mixed $reqdata): mixed
32
+ {
33
+ if (!is_array($reqdata) || !array_key_exists('$action', $reqdata)) {
34
+ return $reqdata;
35
+ }
36
+ unset($reqdata['$action']);
37
+ return $reqdata;
26
38
  }
27
39
  }
@@ -53,7 +53,8 @@ class ProjectNameContext:
53
53
  self.ctrl.actor = ctrl_raw["actor"]
54
54
  if isinstance(ctrl_raw.get("paging"), dict):
55
55
  self.ctrl.paging = ctrl_raw["paging"]
56
- elif basectx is not None and basectx.ctrl is not None:
56
+ elif (basectx is not None and basectx.ctrl is not None
57
+ and get_ctx_prop(ctxmap, "opname") is None):
57
58
  self.ctrl = basectx.ctrl
58
59
 
59
60
  # Meta
@@ -12,8 +12,8 @@ from projectname_sdk.feature.base_feature import ProjectNameBaseFeature
12
12
  # stamps page/limit (or a cursor) into the request query; on the way back
13
13
  # (PreResult) it reads the server's pagination signals — a `Link:
14
14
  # rel="next"` header, `X-Page`/`X-Next-Page`/`X-Total-Count` headers, or
15
- # `next`/`cursor`/`nextCursor`/`hasMore` fields in the body — and records
16
- # them on `ctx.result.paging`. A per-call cursor/page supplied via ctrl
15
+ # `next`/`cursor`/`nextCursor`/`hasMore` fields in the body (snake_case forms
16
+ # too) — and records them on `ctx.result.paging` and the caller's ctrl paging. A per-call cursor/page supplied via ctrl
17
17
  # paging takes priority (used by auto-iteration). Parameter names
18
18
  # (`pageParam`/`limitParam`/`cursorParam`), `startPage` and page size
19
19
  # (`limit`) are configurable.
@@ -66,7 +66,11 @@ class ProjectNamePagingFeature(ProjectNameBaseFeature):
66
66
  if paging.get("cursor") is not None:
67
67
  spec.query[cursor_param] = paging["cursor"]
68
68
  elif spec.query.get(page_param) is None:
69
- page = paging.get("page")
69
+ # A record written back by PreResult holds the page just fetched
70
+ # as "page" and the one to fetch as "nextPage", so nextPage wins.
71
+ page = paging.get("nextPage")
72
+ if page is None:
73
+ page = paging.get("page")
70
74
  if page is None:
71
75
  page = self.options.get("startPage") or 1
72
76
  spec.query[page_param] = page
@@ -166,14 +170,25 @@ class ProjectNamePagingFeature(ProjectNameBaseFeature):
166
170
  paging["hasMore"] = more
167
171
  explicit_more = True
168
172
 
169
- # Body-level cursors.
173
+ # Body-level signals; snake_case forms are read first so camelCase wins.
170
174
  if isinstance(body, dict):
171
175
  if body.get("next") is not None:
172
176
  paging["next"] = paging["next"] or body["next"]
177
+ if body.get("next_cursor") is not None:
178
+ paging["cursor"] = body["next_cursor"]
173
179
  if body.get("cursor") is not None:
174
180
  paging["cursor"] = body["cursor"]
175
181
  if body.get("nextCursor") is not None:
176
182
  paging["cursor"] = body["nextCursor"]
183
+ if paging["nextPage"] is None:
184
+ np = body.get("nextPage")
185
+ if np is None:
186
+ np = body.get("next_page")
187
+ if isinstance(np, (int, float, str)) and not isinstance(np, bool):
188
+ paging["nextPage"] = np
189
+ if isinstance(body.get("has_more"), bool):
190
+ paging["hasMore"] = body["has_more"]
191
+ explicit_more = True
177
192
  if isinstance(body.get("hasMore"), bool):
178
193
  paging["hasMore"] = body["hasMore"]
179
194
  explicit_more = True
@@ -191,6 +206,16 @@ class ProjectNamePagingFeature(ProjectNameBaseFeature):
191
206
 
192
207
  result.paging = paging
193
208
 
209
+ ctrl = ctx.ctrl
210
+ if ctrl is not None:
211
+ # The Context shares the caller's paging dict by reference, so
212
+ # refill it in place; a fresh dict on ctrl never reaches the caller.
213
+ if isinstance(getattr(ctrl, "paging", None), dict):
214
+ ctrl.paging.clear()
215
+ ctrl.paging.update(paging)
216
+ else:
217
+ ctrl.paging = paging
218
+
194
219
  self.client._paging = {"last": paging}
195
220
 
196
221
  def _is_list(self, ctx):
@@ -143,18 +143,11 @@ class ProjectNameTestFeature(ProjectNameBaseFeature):
143
143
  args = test_self.build_args(fctx, op, update_match)
144
144
  found = vs.select(entmap, args)
145
145
  ent = vs.getelem(found, 0)
146
- if ent is None and isinstance(entmap, dict):
147
- for e in entmap.values():
148
- if isinstance(e, dict):
149
- ent = e
150
- break
151
146
  if ent is None:
147
+ # update miss: 404, never another record
152
148
  return respond(404, None, {"statusText": "Not found"})
153
- if isinstance(ent, dict):
154
- reqdata = fctx.reqdata
155
- if reqdata is not None:
156
- for k, v in reqdata.items():
157
- ent[k] = v
149
+ if isinstance(ent, dict) and isinstance(fctx.reqdata, dict):
150
+ vs.merge([ent, fctx.reqdata])
158
151
  vs.delprop(ent, "$KEY")
159
152
  out = vs.clone(ent)
160
153
  return respond(200, out)
@@ -260,12 +253,36 @@ class ProjectNameTestFeature(ProjectNameBaseFeature):
260
253
 
261
254
  return netsim_fetcher
262
255
 
256
+ # The entity's own endpoint: a terminal `{param}` marks a record route,
257
+ # and among equals the shallower path wins (the same rule as make_point).
258
+ @staticmethod
259
+ def pick_point(points):
260
+ if not isinstance(points, list) or len(points) == 0:
261
+ return None
262
+
263
+ def terminal(p):
264
+ parts = vs.getprop(p, "parts")
265
+ return isinstance(parts, list) and len(parts) > 0 and \
266
+ isinstance(parts[-1], str) and parts[-1].startswith("{")
267
+
268
+ def depth(p):
269
+ parts = vs.getprop(p, "parts")
270
+ return len(parts) if isinstance(parts, list) else 0
271
+
272
+ point = points[0]
273
+ for cand in points[1:]:
274
+ if terminal(cand) != terminal(point):
275
+ if terminal(cand):
276
+ point = cand
277
+ elif depth(cand) < depth(point):
278
+ point = cand
279
+ return point
280
+
263
281
  def build_args(self, ctx, op, args):
264
282
  opname = op.name
265
283
 
266
- # Get last point from config.
267
284
  points = vs.getpath(ctx.config, "entity." + ctx.entity.get_name() + ".op." + opname + ".points")
268
- point = vs.getelem(points, -1)
285
+ point = self.pick_point(points)
269
286
 
270
287
  # Path AND query: a path-only read misses a query-addressed record
271
288
  # (e.g. GET /result?trace_id=), which has no path param at all.
@@ -2,21 +2,37 @@
2
2
 
3
3
  from __future__ import annotations
4
4
  import json
5
+ import threading
6
+ from http.cookiejar import DefaultCookiePolicy
5
7
  from projectname_sdk.utility.voxgig_struct import voxgig_struct as vs
6
8
 
7
9
 
8
10
  # Default User-Agent — many CDNs (notably Cloudflare) reject requests with
9
- # Python's default urllib UA ("Python-urllib/3.x"), returning 403 before
10
- # the request even reaches the origin. Set a Mozilla-shaped UA so the SDK
11
- # behaves like every other HTTP client by default. Users can still override
12
- # by passing a User-Agent header in fetchdef.
11
+ # a library's default UA, returning 403 before the request even reaches
12
+ # the origin. Set a Mozilla-shaped UA so the SDK behaves like every other
13
+ # HTTP client by default. Users can still override by passing a User-Agent
14
+ # header in fetchdef.
13
15
  _DEFAULT_USER_AGENT = "Mozilla/5.0 (compatible; ProjectNameSDK/1.0)"
14
16
 
15
17
 
16
- def _default_http_fetch(fullurl, fetchdef):
17
- import urllib.request
18
- import urllib.error
18
+ _SESSION = None
19
+ _SESSION_LOCK = threading.Lock()
20
+
21
+
22
+ def _session():
23
+ # Pool connections without retaining response cookies between calls.
24
+ global _SESSION
25
+ if _SESSION is None:
26
+ import requests
27
+ with _SESSION_LOCK:
28
+ if _SESSION is None:
29
+ session = requests.Session()
30
+ session.cookies.set_policy(DefaultCookiePolicy(allowed_domains=[]))
31
+ _SESSION = session
32
+ return _SESSION
33
+
19
34
 
35
+ def _default_http_fetch(fullurl, fetchdef):
20
36
  method = fetchdef.get("method", "GET")
21
37
  body_str = fetchdef.get("body")
22
38
  headers = fetchdef.get("headers", {})
@@ -26,76 +42,58 @@ def _default_http_fetch(fullurl, fetchdef):
26
42
 
27
43
  data = body_str.encode("utf-8") if body_str is not None else None
28
44
 
29
- req = urllib.request.Request(fullurl, data=data, method=method)
45
+ req_headers = {}
30
46
  has_ua = False
31
47
  for k, v in headers.items():
32
48
  if k.lower() == "user-agent":
33
49
  has_ua = True
34
- req.add_header(k, v)
50
+ req_headers[k] = v
35
51
  if not has_ua:
36
- req.add_header("User-Agent", _DEFAULT_USER_AGENT)
52
+ req_headers["User-Agent"] = _DEFAULT_USER_AGENT
37
53
 
38
54
  # Manual redirects: fetchdef["redirect"] == "manual" surfaces a 3xx as
39
- # an ordinary response instead of auto-following it — urllib would
40
- # otherwise replay the request, headers included, against whatever host
41
- # Location names. Set by the station feature's transport middleware
42
- # under an egress hosts policy (mirrors the ts fetch option).
43
- opener = None
44
- if fetchdef.get("redirect") == "manual":
45
- class _NoRedirect(urllib.request.HTTPRedirectHandler):
46
- def redirect_request(self, req, fp, code, msg, headers, newurl):
47
- return None
48
- opener = urllib.request.build_opener(_NoRedirect)
55
+ # an ordinary response instead of auto-following it, which would replay
56
+ # the request, headers included, against whatever host Location names.
57
+ # Set by the station feature's transport middleware under an egress
58
+ # hosts policy (mirrors the ts fetch option).
59
+ kwargs = {"allow_redirects": fetchdef.get("redirect") != "manual"}
60
+
61
+ proxy = fetchdef.get("proxy")
62
+ if isinstance(proxy, str) and proxy:
63
+ kwargs["proxies"] = {"http": proxy, "https": proxy}
64
+
65
+ timeout = fetchdef.get("timeout")
66
+ if isinstance(timeout, (int, float)) and timeout > 0:
67
+ kwargs["timeout"] = timeout
49
68
 
50
69
  try:
51
- resp = opener.open(req) if opener is not None else urllib.request.urlopen(req)
52
- body = resp.read().decode("utf-8")
53
- resp_headers = {}
54
- for k, v in resp.getheaders():
55
- resp_headers[k.lower()] = v
56
-
57
- json_body = None
58
- if len(body) > 0:
59
- try:
60
- json_body = json.loads(body)
61
- except Exception:
62
- pass
63
-
64
- status = resp.getcode()
65
- status_text = "OK" if status < 400 else "Error"
66
-
67
- return {
68
- "status": status,
69
- "statusText": status_text,
70
- "headers": resp_headers,
71
- "json": lambda: json_body,
72
- "body": body,
73
- }, None
74
- except urllib.error.HTTPError as e:
75
- body = e.read().decode("utf-8")
76
- resp_headers = {}
77
- for k, v in e.headers.items():
78
- resp_headers[k.lower()] = v
79
-
80
- json_body = None
81
- if len(body) > 0:
82
- try:
83
- json_body = json.loads(body)
84
- except Exception:
85
- pass
86
-
87
- status_text = "OK" if e.code < 400 else "Error"
88
-
89
- return {
90
- "status": e.code,
91
- "statusText": status_text,
92
- "headers": resp_headers,
93
- "json": lambda: json_body,
94
- "body": body,
95
- }, None
70
+ resp = _session().request(method, fullurl, data=data, headers=req_headers, **kwargs)
71
+ body = resp.content.decode("utf-8")
96
72
  except Exception as e:
97
73
  return None, str(e)
98
74
 
75
+ resp_headers = {}
76
+ for k, v in resp.headers.items():
77
+ resp_headers[k.lower()] = v
78
+
79
+ json_body = None
80
+ if len(body) > 0:
81
+ try:
82
+ json_body = json.loads(body)
83
+ except Exception:
84
+ pass
85
+
86
+ status = resp.status_code
87
+ status_text = resp.reason or ("OK" if status < 400 else "Error")
88
+
89
+ return {
90
+ "status": status,
91
+ "statusText": status_text,
92
+ "headers": resp_headers,
93
+ "json": lambda: json_body,
94
+ "body": body,
95
+ }, None
96
+
99
97
 
100
98
  def fetcher_util(ctx, fullurl, fetchdef):
101
99
  if ctx.client.mode != "live":
@@ -27,7 +27,8 @@ def prepare_query_util(ctx):
27
27
  for item in reqmatch_items:
28
28
  key = item[0]
29
29
  val = item[1]
30
- if val is not None and isinstance(key, str) and not _contains_param(params, key):
30
+ if val is not None and isinstance(key, str) and key != "$action" \
31
+ and not _contains_param(params, key):
31
32
  out[key] = val
32
33
 
33
34
  return out
@@ -5,6 +5,14 @@ from projectname_sdk.utility.voxgig_struct import voxgig_struct as vs
5
5
  from projectname_sdk.core.helpers import to_map
6
6
 
7
7
 
8
+ # `$action` selects the point (see make_point_util); it is never an API field,
9
+ # so the body is a copy without it. The caller's dict is left untouched.
10
+ def _strip_action(reqdata):
11
+ if not isinstance(reqdata, dict) or "$action" not in reqdata:
12
+ return reqdata
13
+ return {k: v for k, v in reqdata.items() if k != "$action"}
14
+
15
+
8
16
  def transform_request_util(ctx):
9
17
  spec = ctx.spec
10
18
  point = ctx.point
@@ -14,14 +22,14 @@ def transform_request_util(ctx):
14
22
 
15
23
  transform = to_map(vs.getprop(point, "transform"))
16
24
  if transform is None:
17
- return ctx.reqdata
25
+ return _strip_action(ctx.reqdata)
18
26
 
19
27
  reqform = vs.getprop(transform, "req")
20
28
  if reqform is None:
21
- return ctx.reqdata
29
+ return _strip_action(ctx.reqdata)
22
30
 
23
31
  reqdata = vs.transform({
24
32
  "reqdata": ctx.reqdata,
25
33
  }, reqform)
26
34
 
27
- return reqdata
35
+ return _strip_action(reqdata)
@@ -724,6 +724,33 @@ class TestPagingFeature:
724
724
  assert res["result"].paging["cursor"] == "abc"
725
725
  assert res["result"].paging["hasMore"] is True
726
726
 
727
+ def test_snake_case_signals_and_ctrl_write_back(self):
728
+ server, calls = recording_server(lambda n, _fd: make_response(
729
+ 200, {"has_more": True, "next_cursor": "c2"} if n == 1
730
+ else {"has_more": False}))
731
+ h = make_client([{"name": "paging"}], server=server)
732
+ pg = {}
733
+ ctrl = {"paging": pg}
734
+ res = h.op(op="list", path="/w", ctrl=ctrl)
735
+ assert res["result"].paging["hasMore"] is True
736
+ assert res["result"].paging["cursor"] == "c2"
737
+ assert pg["cursor"] == "c2" and pg["hasMore"] is True
738
+ h.op(op="list", path="/w", ctrl=ctrl)
739
+ assert re.search(r"[?&]cursor=c2(&|$)", calls[1]["url"])
740
+ assert pg["hasMore"] is False and pg["cursor"] is None
741
+
742
+ def test_continues_from_written_back_next_page(self):
743
+ server, calls = recording_server(lambda n, _fd: make_response(
744
+ 200, {"next_page": 2} if n == 1 else {}, {"x-page": str(n)}))
745
+ h = make_client([{"name": "paging"}], server=server)
746
+ pg = {}
747
+ ctrl = {"paging": pg}
748
+ h.op(op="list", path="/w", ctrl=ctrl)
749
+ assert pg["page"] == 1 and pg["nextPage"] == 2 and pg["hasMore"] is True
750
+ h.op(op="list", path="/w", ctrl=ctrl)
751
+ assert re.search(r"[?&]page=2(&|$)", calls[1]["url"])
752
+ assert pg["hasMore"] is False
753
+
727
754
  def test_non_list_op_is_not_paged(self):
728
755
  server, calls = recording_server()
729
756
  h = make_client([{"name": "paging"}], server=server)
@@ -240,6 +240,14 @@ def _subset(actual, expect, path):
240
240
  "{}: got {!r}, want {!r}".format(path, actual, expect)
241
241
 
242
242
 
243
+ # Whether the SDK built the feature at all. The activity record is the
244
+ # wrong probe: most features create theirs on first use, so an idle client
245
+ # has none and every section but the eager ones read as inert.
246
+ def _present(client, name):
247
+ return any(getattr(f, "name", None) == name
248
+ for f in (getattr(client, "features", None) or []))
249
+
250
+
243
251
  def _record(client, name):
244
252
  return getattr(client, "_" + name, None)
245
253
 
@@ -280,7 +288,9 @@ class TestFeatureCorpus:
280
288
  # Probed by ACTIVATING it: the feature defaults to inactive, so an
281
289
  # idle client never builds it and its absence says nothing.
282
290
  probe = _client({"feature": [{"name": name, "active": True}]})
283
- if _record(probe, name) is None:
291
+ if not _present(probe, name):
292
+ # The one line every runner prints for an inert section.
293
+ print("feature.{}: inert (this SDK does not generate the feature)".format(name))
284
294
  pytest.skip("this SDK was generated without the {} feature".format(name))
285
295
 
286
296
  ops = _usable_ops(2)