@voxgig/sdkgen 1.5.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/dist/cmp/AgentGuideContent.js +5 -0
- package/dist/cmp/AgentGuideContent.js.map +1 -1
- package/dist/cmp/Feature.js +13 -0
- package/dist/cmp/Feature.js.map +1 -1
- package/dist/helpers/canonType.d.ts +3 -1
- package/dist/helpers/canonType.js +44 -1
- package/dist/helpers/canonType.js.map +1 -1
- package/dist/helpers/packageMeta.js +7 -0
- package/dist/helpers/packageMeta.js.map +1 -1
- package/dist/sdkgen.d.ts +3 -3
- package/dist/sdkgen.js +4 -1
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -6
- package/project/.sdk/model/feature/paging.aontu +7 -0
- package/project/.sdk/model/target/py-data.aontu +64 -0
- package/project/.sdk/src/cmp/c/fragment/Main.fragment.c +89 -1
- package/project/.sdk/src/cmp/clojure/Main_clojure.ts +1 -0
- package/project/.sdk/src/cmp/csharp/fragment/Main.fragment.cs +97 -0
- package/project/.sdk/src/cmp/dart/fragment/Main.fragment.dart +78 -0
- package/project/.sdk/src/cmp/elixir/fragment/Main.fragment.ex +84 -0
- package/project/.sdk/src/cmp/go/fragment/Main.fragment.go +87 -0
- package/project/.sdk/src/cmp/js/fragment/Main.fragment.js +75 -0
- package/project/.sdk/src/cmp/lua/fragment/Main.fragment.lua +85 -0
- package/project/.sdk/src/cmp/ocaml/Main_ocaml.ts +4 -0
- package/project/.sdk/src/cmp/perl/fragment/Main.fragment.pm +80 -0
- package/project/.sdk/src/cmp/php/fragment/Main.fragment.php +86 -0
- package/project/.sdk/src/cmp/py/Entity_py.ts +9 -2
- package/project/.sdk/src/cmp/py/MainEntity_py.ts +1 -1
- package/project/.sdk/src/cmp/py/Main_py.ts +34 -6
- package/project/.sdk/src/cmp/py/Package_py.ts +6 -9
- package/project/.sdk/src/cmp/py/TestDirect_py.ts +2 -2
- package/project/.sdk/src/cmp/py/TestEntity_py.ts +3 -3
- package/project/.sdk/src/cmp/py/fragment/Entity.fragment.py +2 -2
- package/project/.sdk/src/cmp/py/fragment/EntityCreateOp.fragment.py +2 -2
- package/project/.sdk/src/cmp/py/fragment/EntityLoadOp.fragment.py +2 -2
- package/project/.sdk/src/cmp/py/fragment/EntityRemoveOp.fragment.py +2 -2
- package/project/.sdk/src/cmp/py/fragment/EntityUpdateOp.fragment.py +2 -2
- package/project/.sdk/src/cmp/py/fragment/Main.fragment.py +75 -8
- package/project/.sdk/src/cmp/py-data/Main_py-data.ts +987 -0
- package/project/.sdk/src/cmp/rb/fragment/Main.fragment.rb +71 -0
- package/project/.sdk/src/cmp/rust/fragment/Main.fragment.rs +95 -0
- package/project/.sdk/src/cmp/swift/fragment/Main.fragment.swift +86 -0
- package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +75 -0
- package/project/.sdk/src/cmp/zig/fragment/Main.fragment.zig +95 -0
- package/project/.sdk/tm/c/core/sdk.h +7 -0
- package/project/.sdk/tm/c/feature/paging.c +141 -2
- package/project/.sdk/tm/c/utility/graphql.c +178 -0
- package/project/.sdk/tm/c/utility/make_options.c +1 -1
- package/project/.sdk/tm/c/utility/make_response.c +6 -0
- package/project/.sdk/tm/c/utility/make_spec.c +23 -3
- package/project/.sdk/tm/clojure/src/sdk/client.clj +64 -1
- package/project/.sdk/tm/clojure/src/sdk/core.clj +126 -3
- package/project/.sdk/tm/clojure/src/sdk/features.clj +80 -14
- package/project/.sdk/tm/cpp/core/types.hpp +83 -0
- package/project/.sdk/tm/cpp/feature/paging.hpp +103 -1
- package/project/.sdk/tm/cpp/utility/pipeline.hpp +152 -3
- package/project/.sdk/tm/csharp/core/Utility.cs +4 -0
- package/project/.sdk/tm/csharp/feature/PagingFeature.cs +118 -1
- package/project/.sdk/tm/csharp/utility/Graphql.cs +190 -0
- package/project/.sdk/tm/csharp/utility/MakeOptions.cs +1 -1
- package/project/.sdk/tm/csharp/utility/MakeResponse.cs +7 -0
- package/project/.sdk/tm/csharp/utility/MakeSpec.cs +21 -2
- package/project/.sdk/tm/csharp/utility/Register.cs +2 -0
- package/project/.sdk/tm/dart/lib/Point.dart +7 -0
- package/project/.sdk/tm/dart/lib/feature/paging/PagingFeature.dart +99 -4
- package/project/.sdk/tm/dart/lib/utility/GraphqlUtility.dart +162 -0
- package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +1 -1
- package/project/.sdk/tm/dart/lib/utility/MakeResponseUtility.dart +5 -0
- package/project/.sdk/tm/dart/lib/utility/MakeSpecUtility.dart +18 -2
- package/project/.sdk/tm/dart/lib/utility/PrepareMethodUtility.dart +6 -1
- package/project/.sdk/tm/dart/lib/utility/Utility.dart +9 -0
- package/project/.sdk/tm/elixir/lib/projectname/feature/paging.ex +136 -13
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +187 -3
- package/project/.sdk/tm/go/core/utility_type.go +2 -0
- package/project/.sdk/tm/go/feature/paging_feature.go +87 -1
- package/project/.sdk/tm/go/utility/graphql.go +168 -0
- package/project/.sdk/tm/go/utility/make_options.go +1 -1
- package/project/.sdk/tm/go/utility/make_response.go +6 -0
- package/project/.sdk/tm/go/utility/make_spec.go +19 -2
- package/project/.sdk/tm/go/utility/register.go +2 -0
- package/project/.sdk/tm/haskell/src/SdkFeatures.hs +146 -7
- package/project/.sdk/tm/haskell/src/SdkRuntime.hs +153 -3
- package/project/.sdk/tm/java/core/SdkClient.java +93 -0
- package/project/.sdk/tm/java/core/Utility.java +4 -0
- package/project/.sdk/tm/java/feature/PagingFeature.java +100 -1
- package/project/.sdk/tm/java/utility/Graphql.java +191 -0
- package/project/.sdk/tm/java/utility/MakeOptions.java +1 -1
- package/project/.sdk/tm/java/utility/MakeResponse.java +6 -0
- package/project/.sdk/tm/java/utility/MakeSpec.java +17 -2
- package/project/.sdk/tm/java/utility/Register.java +2 -0
- package/project/.sdk/tm/js/src/Point.js +5 -0
- package/project/.sdk/tm/js/src/feature/paging/PagingFeature.js +82 -3
- package/project/.sdk/tm/js/src/feature/test/TestFeature.js +14 -2
- package/project/.sdk/tm/js/src/utility/GraphqlUtility.js +148 -0
- package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +1 -1
- package/project/.sdk/tm/js/src/utility/MakeResponseUtility.js +7 -0
- package/project/.sdk/tm/js/src/utility/MakeSpecUtility.js +19 -2
- package/project/.sdk/tm/js/src/utility/Utility.js +4 -0
- package/project/.sdk/tm/kotlin/core/SdkClient.kt +84 -0
- package/project/.sdk/tm/kotlin/core/Utility.kt +2 -0
- package/project/.sdk/tm/kotlin/feature/PagingFeature.kt +92 -1
- package/project/.sdk/tm/kotlin/utility/Graphql.kt +145 -0
- package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +1 -1
- package/project/.sdk/tm/kotlin/utility/MakeReqRes.kt +6 -0
- package/project/.sdk/tm/kotlin/utility/MakeSpecUrl.kt +17 -2
- package/project/.sdk/tm/kotlin/utility/Register.kt +2 -0
- package/project/.sdk/tm/lean/src/SdkUtility.lean +134 -1
- package/project/.sdk/tm/lua/feature/paging_feature.lua +97 -2
- package/project/.sdk/tm/lua/utility/graphql.lua +163 -0
- package/project/.sdk/tm/lua/utility/make_options.lua +1 -1
- package/project/.sdk/tm/lua/utility/make_response.lua +6 -0
- package/project/.sdk/tm/lua/utility/make_spec.lua +18 -2
- package/project/.sdk/tm/lua/utility/register.lua +3 -0
- package/project/.sdk/tm/ocaml/sdk_features.ml +164 -20
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +143 -4
- package/project/.sdk/tm/ocaml/sdk_types.ml +2 -0
- package/project/.sdk/tm/perl/feature/paging_feature.pm +91 -3
- package/project/.sdk/tm/perl/utility/graphql.pm +150 -0
- package/project/.sdk/tm/perl/utility/make_options.pm +1 -1
- package/project/.sdk/tm/perl/utility/make_response.pm +6 -0
- package/project/.sdk/tm/perl/utility/make_spec.pm +20 -2
- package/project/.sdk/tm/perl/utility/register.pm +1 -0
- package/project/.sdk/tm/php/core/UtilityType.php +2 -0
- package/project/.sdk/tm/php/feature/PagingFeature.php +106 -4
- package/project/.sdk/tm/php/utility/Graphql.php +176 -0
- package/project/.sdk/tm/php/utility/MakeOptions.php +1 -1
- package/project/.sdk/tm/php/utility/MakeResponse.php +7 -0
- package/project/.sdk/tm/php/utility/MakeSpec.php +20 -2
- package/project/.sdk/tm/php/utility/Register.php +3 -0
- package/project/.sdk/tm/py/pkg/core/__init__.py +0 -0
- package/project/.sdk/tm/py/{core → pkg/core}/context.py +8 -8
- package/project/.sdk/tm/py/{core → pkg/core}/operation.py +1 -1
- package/project/.sdk/tm/py/{core → pkg/core}/response.py +1 -1
- package/project/.sdk/tm/py/{core → pkg/core}/result.py +1 -1
- package/project/.sdk/tm/py/{core → pkg/core}/utility_type.py +2 -0
- package/project/.sdk/tm/py/pkg/entity/__init__.py +0 -0
- package/project/.sdk/tm/py/pkg/feature/__init__.py +0 -0
- package/project/.sdk/tm/py/{feature → pkg/feature}/audit_feature.py +1 -1
- package/project/.sdk/tm/py/{feature → pkg/feature}/cache_feature.py +1 -1
- package/project/.sdk/tm/py/{feature → pkg/feature}/clienttrack_feature.py +1 -1
- package/project/.sdk/tm/py/{feature → pkg/feature}/debug_feature.py +1 -1
- package/project/.sdk/tm/py/{feature → pkg/feature}/idempotency_feature.py +1 -1
- package/project/.sdk/tm/py/{feature → pkg/feature}/log_feature.py +1 -1
- package/project/.sdk/tm/py/{feature → pkg/feature}/metrics_feature.py +1 -1
- package/project/.sdk/tm/py/{feature → pkg/feature}/netsim_feature.py +1 -1
- package/project/.sdk/tm/py/{feature → pkg/feature}/paging_feature.py +87 -6
- package/project/.sdk/tm/py/{feature → pkg/feature}/proxy_feature.py +1 -1
- package/project/.sdk/tm/py/{feature → pkg/feature}/ratelimit_feature.py +1 -1
- package/project/.sdk/tm/py/{feature → pkg/feature}/rbac_feature.py +1 -1
- package/project/.sdk/tm/py/{feature → pkg/feature}/retry_feature.py +1 -1
- package/project/.sdk/tm/py/{feature → pkg/feature}/streaming_feature.py +1 -1
- package/project/.sdk/tm/py/{feature → pkg/feature}/telemetry_feature.py +1 -1
- package/project/.sdk/tm/py/{feature → pkg/feature}/test_feature.py +2 -2
- package/project/.sdk/tm/py/{feature → pkg/feature}/timeout_feature.py +1 -1
- package/project/.sdk/tm/py/pkg/utility/__init__.py +0 -0
- package/project/.sdk/tm/py/{utility → pkg/utility}/feature_init.py +1 -1
- package/project/.sdk/tm/py/{utility → pkg/utility}/fetcher.py +1 -1
- package/project/.sdk/tm/py/pkg/utility/graphql.py +145 -0
- package/project/.sdk/tm/py/{utility → pkg/utility}/make_context.py +1 -1
- package/project/.sdk/tm/py/{utility → pkg/utility}/make_error.py +5 -5
- package/project/.sdk/tm/py/{utility → pkg/utility}/make_fetch_def.py +2 -2
- package/project/.sdk/tm/py/{utility → pkg/utility}/make_options.py +2 -2
- package/project/.sdk/tm/py/{utility → pkg/utility}/make_point.py +2 -2
- package/project/.sdk/tm/py/{utility → pkg/utility}/make_request.py +2 -2
- package/project/.sdk/tm/py/{utility → pkg/utility}/make_response.py +6 -0
- package/project/.sdk/tm/py/{utility → pkg/utility}/make_spec.py +19 -4
- package/project/.sdk/tm/py/{utility → pkg/utility}/make_url.py +1 -1
- package/project/.sdk/tm/py/{utility → pkg/utility}/param.py +2 -2
- package/project/.sdk/tm/py/{utility → pkg/utility}/prepare_auth.py +1 -1
- package/project/.sdk/tm/py/{utility → pkg/utility}/prepare_headers.py +1 -1
- package/project/.sdk/tm/py/{utility → pkg/utility}/prepare_method.py +1 -1
- package/project/.sdk/tm/py/{utility → pkg/utility}/prepare_params.py +1 -1
- package/project/.sdk/tm/py/{utility → pkg/utility}/prepare_path.py +1 -1
- package/project/.sdk/tm/py/{utility → pkg/utility}/prepare_query.py +1 -1
- package/project/.sdk/tm/py/pkg/utility/register.py +71 -0
- package/project/.sdk/tm/py/{utility → pkg/utility}/transform_request.py +2 -2
- package/project/.sdk/tm/py/{utility → pkg/utility}/transform_response.py +2 -2
- package/project/.sdk/tm/py/test/feature_harness.py +6 -6
- package/project/.sdk/tm/py/test/runner.py +1 -1
- package/project/.sdk/tm/py/test/struct_runner.py +1 -1
- package/project/.sdk/tm/py/test/test_pipeline.py +5 -5
- package/project/.sdk/tm/py/test/test_primary_utility.py +8 -8
- package/project/.sdk/tm/py/test/test_struct_utility.py +3 -3
- package/project/.sdk/tm/py-data/LICENSE +22 -0
- package/project/.sdk/tm/py-data/Makefile +57 -0
- package/project/.sdk/tm/py-data/pkg/auth.py +131 -0
- package/project/.sdk/tm/py-data/pkg/fetch.py +178 -0
- package/project/.sdk/tm/py-data/pkg/frames.py +236 -0
- package/project/.sdk/tm/py-data/src/feature/README.md +8 -0
- package/project/.sdk/tm/py-data/src/feature/debug/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/idempotency/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/log/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/metrics/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/netsim/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/paging/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/proxy/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/ratelimit/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/rbac/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/retry/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/streaming/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/telemetry/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/test/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/timeout/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/test/__init__.py +0 -0
- package/project/.sdk/tm/py-data/test/test_auth.py +95 -0
- package/project/.sdk/tm/py-data/test/test_fetch.py +152 -0
- package/project/.sdk/tm/py-data/test/test_frames.py +250 -0
- package/project/.sdk/tm/rb/core/utility_type.rb +1 -0
- package/project/.sdk/tm/rb/feature/paging_feature.rb +89 -3
- package/project/.sdk/tm/rb/utility/graphql.rb +131 -0
- package/project/.sdk/tm/rb/utility/make_options.rb +1 -1
- package/project/.sdk/tm/rb/utility/make_response.rb +6 -0
- package/project/.sdk/tm/rb/utility/make_spec.rb +18 -2
- package/project/.sdk/tm/rb/utility/register.rb +3 -0
- package/project/.sdk/tm/rust/feature/paging.rs +124 -2
- package/project/.sdk/tm/rust/utility/graphql.rs +193 -0
- package/project/.sdk/tm/rust/utility/make_options.rs +1 -1
- package/project/.sdk/tm/rust/utility/make_response.rs +6 -0
- package/project/.sdk/tm/rust/utility/make_spec.rs +32 -4
- package/project/.sdk/tm/rust/utility/mod.rs +1 -0
- package/project/.sdk/tm/scala/core/SdkClient.scala +90 -0
- package/project/.sdk/tm/scala/core/Utility.scala +4 -0
- package/project/.sdk/tm/scala/feature/PagingFeature.scala +104 -3
- package/project/.sdk/tm/scala/utility/Graphql.scala +160 -0
- package/project/.sdk/tm/scala/utility/Make.scala +7 -1
- package/project/.sdk/tm/scala/utility/MakeSpec.scala +22 -2
- package/project/.sdk/tm/scala/utility/Register.scala +2 -0
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/core/Utility.swift +4 -0
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/PagingFeature.swift +86 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Graphql.swift +142 -0
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Make.swift +24 -2
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +1 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Register.swift +2 -0
- package/project/.sdk/tm/ts/src/Point.ts +7 -0
- package/project/.sdk/tm/ts/src/feature/paging/PagingFeature.ts +82 -3
- package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +14 -2
- package/project/.sdk/tm/ts/src/utility/GraphqlUtility.ts +152 -0
- package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +1 -1
- package/project/.sdk/tm/ts/src/utility/MakeResponseUtility.ts +7 -0
- package/project/.sdk/tm/ts/src/utility/MakeSpecUtility.ts +19 -2
- package/project/.sdk/tm/ts/src/utility/Utility.ts +4 -0
- package/project/.sdk/tm/zig/core/utility.zig +198 -3
- package/project/.sdk/tm/zig/feature/paging.zig +128 -1
- package/src/cmp/AgentGuideContent.ts +5 -0
- package/src/cmp/Feature.ts +13 -0
- package/src/helpers/canonType.ts +46 -0
- package/src/helpers/packageMeta.ts +7 -0
- package/src/sdkgen.ts +6 -1
- package/project/.sdk/tm/py/utility/register.py +0 -68
- /package/project/.sdk/tm/py/{core → pkg/core}/control.py +0 -0
- /package/project/.sdk/tm/py/{core → pkg/core}/error.py +0 -0
- /package/project/.sdk/tm/py/{core → pkg/core}/helpers.py +0 -0
- /package/project/.sdk/tm/py/{core → pkg/core}/spec.py +0 -0
- /package/project/.sdk/tm/py/{feature → pkg/feature}/base_feature.py +0 -0
- /package/project/.sdk/tm/py/{utility → pkg/utility}/clean.py +0 -0
- /package/project/.sdk/tm/py/{utility → pkg/utility}/done.py +0 -0
- /package/project/.sdk/tm/py/{utility → pkg/utility}/feature_add.py +0 -0
- /package/project/.sdk/tm/py/{utility → pkg/utility}/feature_hook.py +0 -0
- /package/project/.sdk/tm/py/{utility → pkg/utility}/make_result.py +0 -0
- /package/project/.sdk/tm/py/{utility → pkg/utility}/prepare_body.py +0 -0
- /package/project/.sdk/tm/py/{utility → pkg/utility}/result_basic.py +0 -0
- /package/project/.sdk/tm/py/{utility → pkg/utility}/result_body.py +0 -0
- /package/project/.sdk/tm/py/{utility → pkg/utility}/result_headers.py +0 -0
- /package/project/.sdk/tm/py/{utility → pkg/utility}/voxgig_struct/__init__.py +0 -0
- /package/project/.sdk/tm/py/{utility → pkg/utility}/voxgig_struct/voxgig_struct.py +0 -0
- /package/project/.sdk/tm/{py/core/__init__.py → py-data/src/feature/audit/.gitkeep} +0 -0
- /package/project/.sdk/tm/{py/entity/__init__.py → py-data/src/feature/base/.gitkeep} +0 -0
- /package/project/.sdk/tm/{py/feature/__init__.py → py-data/src/feature/cache/.gitkeep} +0 -0
- /package/project/.sdk/tm/{py/utility/__init__.py → py-data/src/feature/clienttrack/.gitkeep} +0 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# ProjectName Data — records to DataFrame.
|
|
2
|
+
#
|
|
3
|
+
# The one genuinely new runtime component in this package. Everything else
|
|
4
|
+
# either delegates to the sibling ProjectName SDK or is generated.
|
|
5
|
+
#
|
|
6
|
+
# Pipeline, per accessor call:
|
|
7
|
+
# 1. normalise — SDK dataclasses / dicts / scalars -> plain dicts
|
|
8
|
+
# 2. flatten — one level of nesting -> dotted columns (json_normalize)
|
|
9
|
+
# 3. dtype — model-declared nullable pandas dtypes, coerced safely
|
|
10
|
+
# 4. order — model field order first, extras appended
|
|
11
|
+
#
|
|
12
|
+
# Step 3 uses NULLABLE dtypes ('Int64', not 'int64') throughout. See the
|
|
13
|
+
# PANDAS_DTYPE note in sdkgen's canonType.ts: API payloads omit optional
|
|
14
|
+
# fields freely, so a column that looks integral in one page can arrive with
|
|
15
|
+
# nulls in the next. NumPy's int64/bool cannot hold NA and would silently
|
|
16
|
+
# upcast to float64/object mid-fetch, making a column's dtype depend on which
|
|
17
|
+
# rows happened to come back.
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import dataclasses
|
|
22
|
+
from typing import Any, Iterable
|
|
23
|
+
|
|
24
|
+
import pandas as pd
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Flatten depth accepted by the accessors: 1 (default, one level), 0/"none"
|
|
28
|
+
# (no flattening), or "full" (recursive).
|
|
29
|
+
FLATTEN_DEFAULT = 1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def to_record(item: Any) -> Any:
|
|
33
|
+
"""One SDK result item -> a plain dict (or the value itself if scalar).
|
|
34
|
+
|
|
35
|
+
The ops are NOT symmetric, and this is the load-bearing detail:
|
|
36
|
+
|
|
37
|
+
* `list()` yields ENTITY OBJECTS (ProjectNameXEntity), whose record is
|
|
38
|
+
held privately and read through the public `data_get()`.
|
|
39
|
+
* `load()` / `create()` yield a plain dict.
|
|
40
|
+
|
|
41
|
+
So an entity is unwrapped via `data_get()` before anything else. An
|
|
42
|
+
earlier version fell through to the `__dict__` branch, which filters
|
|
43
|
+
underscore-prefixed attributes and therefore returned `{}` for every
|
|
44
|
+
row — a frame with the right number of rows and NO columns.
|
|
45
|
+
"""
|
|
46
|
+
if item is None:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
# SDK entity object: read the record through its public accessor rather
|
|
50
|
+
# than reaching into `_data`, so feature hooks (GetData) still fire.
|
|
51
|
+
getter = getattr(item, "data_get", None)
|
|
52
|
+
if callable(getter):
|
|
53
|
+
try:
|
|
54
|
+
rec = getter()
|
|
55
|
+
if isinstance(rec, dict):
|
|
56
|
+
return rec
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
if dataclasses.is_dataclass(item) and not isinstance(item, type):
|
|
61
|
+
# asdict() recurses into nested dataclasses, which is what we want:
|
|
62
|
+
# flattening below then decides how deep to expose them as columns.
|
|
63
|
+
return dataclasses.asdict(item)
|
|
64
|
+
if isinstance(item, dict):
|
|
65
|
+
return item
|
|
66
|
+
# Objects with a __dict__ (defensive: a hand-written entity, a namespace).
|
|
67
|
+
d = getattr(item, "__dict__", None)
|
|
68
|
+
if isinstance(d, dict):
|
|
69
|
+
return {k: v for k, v in d.items() if not k.startswith("_")}
|
|
70
|
+
return item
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def to_records(items: Any) -> list:
|
|
74
|
+
"""An op result -> a list of plain records.
|
|
75
|
+
|
|
76
|
+
A `list` op yields a list; a `load`/`create` op yields one record. Both
|
|
77
|
+
are accepted so the same engine serves frame and series accessors.
|
|
78
|
+
"""
|
|
79
|
+
if items is None:
|
|
80
|
+
return []
|
|
81
|
+
if isinstance(items, dict):
|
|
82
|
+
return [to_record(items)]
|
|
83
|
+
if isinstance(items, (list, tuple)):
|
|
84
|
+
return [to_record(i) for i in items]
|
|
85
|
+
if isinstance(items, Iterable) and not isinstance(items, (str, bytes)):
|
|
86
|
+
return [to_record(i) for i in items]
|
|
87
|
+
return [to_record(items)]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _max_level(flatten: Any) -> Any:
|
|
91
|
+
"""Accessor `flatten=` kwarg -> json_normalize max_level.
|
|
92
|
+
|
|
93
|
+
1 (default) is deliberate. Full recursive flattening turns a deeply
|
|
94
|
+
nested payload into an unusable 400-column frame; one level gives the
|
|
95
|
+
dotted columns people actually group by ('model_card.author') and leaves
|
|
96
|
+
genuinely deep structures boxed in object columns, still reachable with
|
|
97
|
+
normal pandas.
|
|
98
|
+
"""
|
|
99
|
+
if flatten is None or flatten is False or flatten == 0 or flatten == "none":
|
|
100
|
+
return 0
|
|
101
|
+
if flatten is True or flatten == "full":
|
|
102
|
+
return None # json_normalize: no limit
|
|
103
|
+
if isinstance(flatten, int):
|
|
104
|
+
return flatten
|
|
105
|
+
return FLATTEN_DEFAULT
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def build_frame(
|
|
109
|
+
items: Any,
|
|
110
|
+
*,
|
|
111
|
+
dtypes: dict | None = None,
|
|
112
|
+
order: list | None = None,
|
|
113
|
+
flatten: Any = FLATTEN_DEFAULT,
|
|
114
|
+
dtype: bool = True,
|
|
115
|
+
parse_dates: Any = None,
|
|
116
|
+
) -> pd.DataFrame:
|
|
117
|
+
"""Records -> DataFrame, typed and ordered from the entity model.
|
|
118
|
+
|
|
119
|
+
dtypes / order are supplied by the generated accessor from the model's
|
|
120
|
+
field list; callers passing raw records (records_df) get neither, which
|
|
121
|
+
is fine — pandas' own inference then applies.
|
|
122
|
+
"""
|
|
123
|
+
records = to_records(items)
|
|
124
|
+
|
|
125
|
+
if len(records) == 0:
|
|
126
|
+
# An empty result still returns the RIGHT columns with the RIGHT
|
|
127
|
+
# dtypes, so downstream code (df.empty checks, concat, groupby on a
|
|
128
|
+
# known column) behaves identically to a populated frame. A bare
|
|
129
|
+
# pd.DataFrame() would raise KeyError on the first column access.
|
|
130
|
+
cols = list(order or [])
|
|
131
|
+
empty = pd.DataFrame({c: pd.Series(dtype=(dtypes or {}).get(c, "object"))
|
|
132
|
+
for c in cols})
|
|
133
|
+
return empty
|
|
134
|
+
|
|
135
|
+
# Scalars (a list op returning bare strings) -> a single 'value' column.
|
|
136
|
+
if not isinstance(records[0], dict):
|
|
137
|
+
return pd.DataFrame({"value": records})
|
|
138
|
+
|
|
139
|
+
max_level = _max_level(flatten)
|
|
140
|
+
if max_level == 0:
|
|
141
|
+
df = pd.DataFrame(records)
|
|
142
|
+
else:
|
|
143
|
+
df = pd.json_normalize(records, sep=".", max_level=max_level)
|
|
144
|
+
|
|
145
|
+
if dtype and dtypes:
|
|
146
|
+
df = apply_dtypes(df, dtypes)
|
|
147
|
+
|
|
148
|
+
if parse_dates:
|
|
149
|
+
df = apply_dates(df, parse_dates)
|
|
150
|
+
|
|
151
|
+
if order:
|
|
152
|
+
df = order_columns(df, order)
|
|
153
|
+
|
|
154
|
+
return df
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def apply_dtypes(df: pd.DataFrame, dtypes: dict) -> pd.DataFrame:
|
|
158
|
+
"""Coerce declared columns to their model dtype, one column at a time.
|
|
159
|
+
|
|
160
|
+
Per-column rather than a single df.astype(): one unconvertible column
|
|
161
|
+
must not fail the whole frame. A column that will not convert is LEFT AS
|
|
162
|
+
IS — an analyst with a usable frame and one object column is far better
|
|
163
|
+
served than one holding an exception. The API is the authority on its own
|
|
164
|
+
data; our model is a hint.
|
|
165
|
+
"""
|
|
166
|
+
for col, want in dtypes.items():
|
|
167
|
+
if col not in df.columns:
|
|
168
|
+
continue
|
|
169
|
+
if str(df[col].dtype) == want:
|
|
170
|
+
continue
|
|
171
|
+
try:
|
|
172
|
+
df[col] = df[col].astype(want)
|
|
173
|
+
except (TypeError, ValueError):
|
|
174
|
+
# Numeric rescue: an API that sends "42" for an integer field is
|
|
175
|
+
# common enough to be worth handling.
|
|
176
|
+
#
|
|
177
|
+
# But to_numeric(errors="coerce") turns anything it cannot parse
|
|
178
|
+
# into NA, which would DESTROY a genuinely non-numeric column
|
|
179
|
+
# while appearing to succeed. So the rescue is only accepted when
|
|
180
|
+
# it is lossless: every value that was non-null before must still
|
|
181
|
+
# be non-null after. Otherwise the column is left exactly as it
|
|
182
|
+
# came from the API.
|
|
183
|
+
if want in ("Int64", "Float64"):
|
|
184
|
+
coerced = pd.to_numeric(df[col], errors="coerce")
|
|
185
|
+
lost = coerced.isna() & df[col].notna()
|
|
186
|
+
if not lost.any():
|
|
187
|
+
try:
|
|
188
|
+
df[col] = coerced.astype(want)
|
|
189
|
+
except (TypeError, ValueError):
|
|
190
|
+
pass
|
|
191
|
+
return df
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def apply_dates(df: pd.DataFrame, parse_dates: Any) -> pd.DataFrame:
|
|
195
|
+
"""Parse the named columns as UTC datetimes.
|
|
196
|
+
|
|
197
|
+
EXPLICIT opt-in, never inferred. The model carries no string formats —
|
|
198
|
+
apidef has no $DATE sentinel — so guessing which string columns are dates
|
|
199
|
+
would be a heuristic on column names, and a wrong guess mangles data
|
|
200
|
+
silently. `errors="coerce"` turns unparseable values into NaT rather than
|
|
201
|
+
raising, keeping the frame usable.
|
|
202
|
+
"""
|
|
203
|
+
cols = [parse_dates] if isinstance(parse_dates, str) else list(parse_dates)
|
|
204
|
+
for col in cols:
|
|
205
|
+
if col in df.columns:
|
|
206
|
+
df[col] = pd.to_datetime(df[col], errors="coerce", utc=True)
|
|
207
|
+
return df
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def order_columns(df: pd.DataFrame, order: list) -> pd.DataFrame:
|
|
211
|
+
"""Model-declared column order first, everything else appended as-is.
|
|
212
|
+
|
|
213
|
+
Stable column order across calls beats alphabetical: an analyst's
|
|
214
|
+
df.iloc[:, :5] should mean the same thing on Monday and Tuesday, and the
|
|
215
|
+
model lists required fields first.
|
|
216
|
+
"""
|
|
217
|
+
known = [c for c in order if c in df.columns]
|
|
218
|
+
extra = [c for c in df.columns if c not in known]
|
|
219
|
+
return df[known + extra]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def records_df(items: Any, **kwargs) -> pd.DataFrame:
|
|
223
|
+
"""Shape ANY op result into a DataFrame.
|
|
224
|
+
|
|
225
|
+
The public escape hatch for endpoints without a generated accessor —
|
|
226
|
+
action ops, non-entity endpoints, or a raw call made through `.sdk`.
|
|
227
|
+
"""
|
|
228
|
+
return build_frame(items, **kwargs)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def to_series(item: Any, **kwargs) -> pd.Series:
|
|
232
|
+
"""One record -> a Series (the natural shape for a single `load`)."""
|
|
233
|
+
df = build_frame(item, **kwargs)
|
|
234
|
+
if len(df) == 0:
|
|
235
|
+
return pd.Series(dtype="object")
|
|
236
|
+
return df.iloc[0]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# py-data features
|
|
2
|
+
|
|
3
|
+
The py-data target does not generate per-feature source. Features are
|
|
4
|
+
applied to the sibling Python SDK by the `py` target; this package
|
|
5
|
+
consumes that SDK as a single dependency and reads the state its
|
|
6
|
+
features publish (notably `paging`, whose recorded cursor drives the
|
|
7
|
+
eager fetch in `fetch.py`). This file exists only to satisfy the
|
|
8
|
+
standard `tm/<target>/src/feature/` folder convention.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# ProjectName Data — credential discovery tests.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from projectname_data import auth
|
|
10
|
+
from projectname_data.auth import resolve, from_env, missing_token_message, missing_base_message, NOT_FOUND
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestResolve:
|
|
14
|
+
|
|
15
|
+
def test_explicit_argument_wins(self, monkeypatch):
|
|
16
|
+
monkeypatch.setenv("ACME_APIKEY", "from-env")
|
|
17
|
+
tok, base, src = resolve("ACME", token="explicit")
|
|
18
|
+
assert tok == "explicit"
|
|
19
|
+
assert src == "argument"
|
|
20
|
+
|
|
21
|
+
def test_env_fallback(self, monkeypatch):
|
|
22
|
+
monkeypatch.setenv("ACME_APIKEY", "from-env")
|
|
23
|
+
tok, base, src = resolve("ACME")
|
|
24
|
+
assert tok == "from-env"
|
|
25
|
+
assert src == "env"
|
|
26
|
+
|
|
27
|
+
def test_missing_returns_none(self, monkeypatch):
|
|
28
|
+
monkeypatch.delenv("ACME_APIKEY", raising=False)
|
|
29
|
+
tok, base, src = resolve("ACME")
|
|
30
|
+
assert tok is None
|
|
31
|
+
assert src is None
|
|
32
|
+
|
|
33
|
+
def test_base_url_from_env(self, monkeypatch):
|
|
34
|
+
monkeypatch.setenv("ACME_APIKEY", "k")
|
|
35
|
+
monkeypatch.setenv("ACME_BASE", "https://tenant.example.com")
|
|
36
|
+
tok, base, src = resolve("ACME")
|
|
37
|
+
assert base == "https://tenant.example.com"
|
|
38
|
+
|
|
39
|
+
def test_explicit_base_wins(self, monkeypatch):
|
|
40
|
+
monkeypatch.setenv("ACME_BASE", "https://env.example.com")
|
|
41
|
+
tok, base, src = resolve("ACME", token="k", base_url="https://arg.example.com")
|
|
42
|
+
assert base == "https://arg.example.com"
|
|
43
|
+
|
|
44
|
+
def test_custom_keys(self, monkeypatch):
|
|
45
|
+
monkeypatch.setenv("WEIRD_TOKEN", "k")
|
|
46
|
+
tok, base, src = resolve("ACME", token_key="WEIRD_TOKEN")
|
|
47
|
+
assert tok == "k"
|
|
48
|
+
|
|
49
|
+
def test_empty_string_is_not_a_credential(self, monkeypatch):
|
|
50
|
+
monkeypatch.setenv("ACME_APIKEY", "")
|
|
51
|
+
tok, base, src = resolve("ACME")
|
|
52
|
+
assert tok is None
|
|
53
|
+
|
|
54
|
+
def test_colab_is_tried_before_env(self, monkeypatch):
|
|
55
|
+
monkeypatch.setenv("ACME_APIKEY", "from-env")
|
|
56
|
+
monkeypatch.setattr(auth, "from_colab",
|
|
57
|
+
lambda key: "from-colab" if key == "ACME_APIKEY" else NOT_FOUND)
|
|
58
|
+
tok, base, src = resolve("ACME")
|
|
59
|
+
assert tok == "from-colab"
|
|
60
|
+
assert src == "colab"
|
|
61
|
+
|
|
62
|
+
def test_colab_failure_falls_through_to_env(self, monkeypatch):
|
|
63
|
+
# Outside Colab the import fails; inside Colab an ungranted secret
|
|
64
|
+
# raises. Neither is an error here — both mean "try the next source".
|
|
65
|
+
monkeypatch.setenv("ACME_APIKEY", "from-env")
|
|
66
|
+
monkeypatch.setattr(auth, "from_colab", lambda key: NOT_FOUND)
|
|
67
|
+
tok, base, src = resolve("ACME")
|
|
68
|
+
assert tok == "from-env"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TestFromEnv:
|
|
72
|
+
|
|
73
|
+
def test_present(self, monkeypatch):
|
|
74
|
+
monkeypatch.setenv("X_Y", "v")
|
|
75
|
+
assert from_env("X_Y") == "v"
|
|
76
|
+
|
|
77
|
+
def test_absent(self, monkeypatch):
|
|
78
|
+
monkeypatch.delenv("X_Y", raising=False)
|
|
79
|
+
assert from_env("X_Y") is NOT_FOUND
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TestMessages:
|
|
83
|
+
|
|
84
|
+
def test_token_message_names_the_real_key(self):
|
|
85
|
+
msg = missing_token_message("ACME")
|
|
86
|
+
assert "ACME_APIKEY" in msg
|
|
87
|
+
# It must teach, not just report: both environments, copy-pasteable.
|
|
88
|
+
assert "userdata.set" in msg
|
|
89
|
+
assert "os.environ" in msg
|
|
90
|
+
assert "data(token=" in msg
|
|
91
|
+
|
|
92
|
+
def test_base_message_names_the_real_key(self):
|
|
93
|
+
msg = missing_base_message("ACME")
|
|
94
|
+
assert "ACME_BASE" in msg
|
|
95
|
+
assert "base_url=" in msg
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# ProjectName Data — paging drain tests.
|
|
2
|
+
#
|
|
3
|
+
# Driven with a fake client rather than the SDK: the drain loop's contract is
|
|
4
|
+
# with the paging state the SDK's paging feature publishes on the client
|
|
5
|
+
# (`client._paging["last"]`), so a fake that publishes the same shape tests
|
|
6
|
+
# the real contract without a network or a generated SDK in the loop.
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from projectname_data.fetch import drain, _next_ctrl, _last_paging, MAX_PAGES_DEFAULT
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FakeClient:
|
|
16
|
+
"""Publishes paging state exactly as the SDK's paging feature does."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, pages):
|
|
19
|
+
self.pages = pages
|
|
20
|
+
self.calls = []
|
|
21
|
+
self._paging = None
|
|
22
|
+
|
|
23
|
+
def call(self, ctrl):
|
|
24
|
+
self.calls.append(ctrl)
|
|
25
|
+
idx = len(self.calls) - 1
|
|
26
|
+
if idx >= len(self.pages):
|
|
27
|
+
return []
|
|
28
|
+
rows, paging = self.pages[idx]
|
|
29
|
+
self._paging = {"last": paging}
|
|
30
|
+
return rows
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def paging(has_more=False, cursor=None, next_page=None):
|
|
34
|
+
return {
|
|
35
|
+
"page": None, "totalCount": None, "nextPage": next_page,
|
|
36
|
+
"next": None, "cursor": cursor, "hasMore": has_more,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestDrain:
|
|
41
|
+
|
|
42
|
+
def test_single_page_makes_one_call(self):
|
|
43
|
+
c = FakeClient([([{"a": 1}], paging(has_more=False))])
|
|
44
|
+
rows = drain(c.call, c, quiet=True)
|
|
45
|
+
assert rows == [{"a": 1}]
|
|
46
|
+
assert len(c.calls) == 1
|
|
47
|
+
|
|
48
|
+
def test_cursor_paging_drains_every_page(self):
|
|
49
|
+
c = FakeClient([
|
|
50
|
+
([{"a": 1}], paging(has_more=True, cursor="c1")),
|
|
51
|
+
([{"a": 2}], paging(has_more=True, cursor="c2")),
|
|
52
|
+
([{"a": 3}], paging(has_more=False)),
|
|
53
|
+
])
|
|
54
|
+
rows = drain(c.call, c, quiet=True)
|
|
55
|
+
assert [r["a"] for r in rows] == [1, 2, 3]
|
|
56
|
+
assert c.calls[1] == {"paging": {"cursor": "c1"}}
|
|
57
|
+
assert c.calls[2] == {"paging": {"cursor": "c2"}}
|
|
58
|
+
|
|
59
|
+
def test_page_number_paging(self):
|
|
60
|
+
c = FakeClient([
|
|
61
|
+
([{"a": 1}], paging(has_more=True, next_page=2)),
|
|
62
|
+
([{"a": 2}], paging(has_more=False)),
|
|
63
|
+
])
|
|
64
|
+
rows = drain(c.call, c, quiet=True)
|
|
65
|
+
assert len(rows) == 2
|
|
66
|
+
assert c.calls[1] == {"paging": {"page": 2}}
|
|
67
|
+
|
|
68
|
+
def test_cursor_wins_over_page_number(self):
|
|
69
|
+
# When a server offers both, the cursor is the one with consistency
|
|
70
|
+
# guarantees.
|
|
71
|
+
nxt = _next_ctrl(paging(has_more=True, cursor="c1", next_page=5))
|
|
72
|
+
assert nxt == {"paging": {"cursor": "c1"}}
|
|
73
|
+
|
|
74
|
+
def test_limit_truncates_and_stops_early(self):
|
|
75
|
+
c = FakeClient([
|
|
76
|
+
([{"a": 1}, {"a": 2}], paging(has_more=True, cursor="c1")),
|
|
77
|
+
([{"a": 3}], paging(has_more=False)),
|
|
78
|
+
])
|
|
79
|
+
rows = drain(c.call, c, limit=2, quiet=True)
|
|
80
|
+
assert len(rows) == 2
|
|
81
|
+
assert len(c.calls) == 1
|
|
82
|
+
|
|
83
|
+
def test_no_paging_state_degrades_to_one_call(self):
|
|
84
|
+
# An SDK with the paging feature inactive genuinely has one page.
|
|
85
|
+
# That is correct degradation, not an error.
|
|
86
|
+
class NoPaging:
|
|
87
|
+
def __init__(self):
|
|
88
|
+
self.n = 0
|
|
89
|
+
|
|
90
|
+
def call(self, ctrl):
|
|
91
|
+
self.n += 1
|
|
92
|
+
return [{"a": 1}]
|
|
93
|
+
|
|
94
|
+
c = NoPaging()
|
|
95
|
+
rows = drain(c.call, c, quiet=True)
|
|
96
|
+
assert len(rows) == 1
|
|
97
|
+
assert c.n == 1
|
|
98
|
+
|
|
99
|
+
def test_empty_page_claiming_more_stops(self):
|
|
100
|
+
c = FakeClient([
|
|
101
|
+
([], paging(has_more=True, cursor="c1")),
|
|
102
|
+
([{"a": 1}], paging(has_more=False)),
|
|
103
|
+
])
|
|
104
|
+
rows = drain(c.call, c, quiet=True)
|
|
105
|
+
assert rows == []
|
|
106
|
+
assert len(c.calls) == 1
|
|
107
|
+
|
|
108
|
+
def test_repeated_cursor_stops_and_warns(self):
|
|
109
|
+
# A server that never advances its cursor would otherwise hang the
|
|
110
|
+
# notebook cell forever.
|
|
111
|
+
c = FakeClient([
|
|
112
|
+
([{"a": 1}], paging(has_more=True, cursor="same")),
|
|
113
|
+
([{"a": 2}], paging(has_more=True, cursor="same")),
|
|
114
|
+
([{"a": 3}], paging(has_more=True, cursor="same")),
|
|
115
|
+
])
|
|
116
|
+
with pytest.warns(UserWarning, match="stopped advancing"):
|
|
117
|
+
rows = drain(c.call, c, quiet=True)
|
|
118
|
+
assert len(rows) == 2
|
|
119
|
+
|
|
120
|
+
def test_max_pages_backstop_warns(self):
|
|
121
|
+
pages = [([{"a": i}], paging(has_more=True, cursor=f"c{i}"))
|
|
122
|
+
for i in range(10)]
|
|
123
|
+
c = FakeClient(pages)
|
|
124
|
+
with pytest.warns(UserWarning, match="max_pages"):
|
|
125
|
+
rows = drain(c.call, c, max_pages=3, quiet=True)
|
|
126
|
+
assert len(rows) == 3
|
|
127
|
+
|
|
128
|
+
def test_has_more_without_a_usable_token_stops(self):
|
|
129
|
+
# A Link: rel="next" URL with no cursor/page is not something we can
|
|
130
|
+
# turn into a ctrl; stopping beats guessing at query parsing.
|
|
131
|
+
c = FakeClient([([{"a": 1}], paging(has_more=True))])
|
|
132
|
+
rows = drain(c.call, c, quiet=True)
|
|
133
|
+
assert len(rows) == 1
|
|
134
|
+
assert len(c.calls) == 1
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class TestPagingHelpers:
|
|
138
|
+
|
|
139
|
+
def test_last_paging_absent(self):
|
|
140
|
+
class Bare:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
assert _last_paging(Bare()) is None
|
|
144
|
+
|
|
145
|
+
def test_last_paging_present(self):
|
|
146
|
+
class C:
|
|
147
|
+
_paging = {"last": {"hasMore": True}}
|
|
148
|
+
|
|
149
|
+
assert _last_paging(C()) == {"hasMore": True}
|
|
150
|
+
|
|
151
|
+
def test_max_pages_default_is_a_backstop_not_a_limit(self):
|
|
152
|
+
assert MAX_PAGES_DEFAULT >= 1000
|