redblack-python-sdk 0.1.0__py3-none-any.whl
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.
- redblack/__init__.py +105 -0
- redblack/_generated/__init__.py +39 -0
- redblack/_generated/contract.py +212 -0
- redblack/_generated/operation_specs.py +5115 -0
- redblack/_version.py +4 -0
- redblack/auth.py +352 -0
- redblack/client.py +512 -0
- redblack/config.py +576 -0
- redblack/downloads.py +410 -0
- redblack/errors.py +483 -0
- redblack/models/__init__.py +105 -0
- redblack/models/base.py +150 -0
- redblack/models/generated.py +11736 -0
- redblack/odata.py +75 -0
- redblack/pagination.py +184 -0
- redblack/polling.py +163 -0
- redblack/py.typed +1 -0
- redblack/raw.py +102 -0
- redblack/resources/__init__.py +65 -0
- redblack/resources/_base.py +212 -0
- redblack/resources/_collections.py +80 -0
- redblack/resources/_exports.py +215 -0
- redblack/resources/accounts.py +132 -0
- redblack/resources/cash_actions.py +260 -0
- redblack/resources/equivalents.py +144 -0
- redblack/resources/households.py +138 -0
- redblack/resources/orders.py +118 -0
- redblack/resources/restrictions.py +146 -0
- redblack/resources/review_sets.py +889 -0
- redblack/resources/securities.py +175 -0
- redblack/resources/tasks.py +291 -0
- redblack/resources/users.py +88 -0
- redblack/response.py +118 -0
- redblack/transport.py +606 -0
- redblack_python_sdk-0.1.0.dist-info/METADATA +151 -0
- redblack_python_sdk-0.1.0.dist-info/RECORD +38 -0
- redblack_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- redblack_python_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Shared protocols and mechanics for hand-written resource facades."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import asdict, dataclass, is_dataclass
|
|
7
|
+
from typing import Any, Generic, Literal, Protocol, TypeAlias, TypeVar, cast, runtime_checkable
|
|
8
|
+
|
|
9
|
+
from ..odata import ODataQuery
|
|
10
|
+
from ..pagination import Page
|
|
11
|
+
from ..response import ApiResponse, HttpMetadata
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
HttpInfoMode = TypeVar("HttpInfoMode", Literal[False], Literal[True])
|
|
15
|
+
ApiResponseProtocol: TypeAlias = ApiResponse[T]
|
|
16
|
+
HttpMetadataProtocol: TypeAlias = HttpMetadata
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@runtime_checkable
|
|
20
|
+
class ClientProtocol(Protocol):
|
|
21
|
+
"""The narrow client interface consumed by every resource group."""
|
|
22
|
+
|
|
23
|
+
def request(
|
|
24
|
+
self,
|
|
25
|
+
operation_id: str,
|
|
26
|
+
*,
|
|
27
|
+
path_params: Mapping[str, object] | None = None,
|
|
28
|
+
query: Mapping[str, object] | None = None,
|
|
29
|
+
headers: Mapping[str, str] | None = None,
|
|
30
|
+
json: object | None = None,
|
|
31
|
+
stream: bool = False,
|
|
32
|
+
timeout_budget: float | None = None,
|
|
33
|
+
) -> ApiResponse[Any]: ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read_member(value: object, *names: str) -> Any:
|
|
37
|
+
if isinstance(value, Mapping):
|
|
38
|
+
for name in names:
|
|
39
|
+
if name in value:
|
|
40
|
+
return value[name]
|
|
41
|
+
return None
|
|
42
|
+
for name in names:
|
|
43
|
+
if hasattr(value, name):
|
|
44
|
+
return getattr(value, name)
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def enum_value(value: object, *field_names: str) -> int | None:
|
|
49
|
+
wrapper = read_member(value, *field_names)
|
|
50
|
+
raw = read_member(wrapper, "value") if wrapper is not None else None
|
|
51
|
+
if raw is None and isinstance(wrapper, int) and not isinstance(wrapper, bool):
|
|
52
|
+
raw = wrapper
|
|
53
|
+
if isinstance(raw, bool) or not isinstance(raw, int):
|
|
54
|
+
return None
|
|
55
|
+
return raw
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def response_header(response: ApiResponse[Any], name: str) -> str | None:
|
|
59
|
+
if name.casefold() == "location":
|
|
60
|
+
location = getattr(response, "location", None)
|
|
61
|
+
if location:
|
|
62
|
+
return str(location)
|
|
63
|
+
location = getattr(response.http, "location", None)
|
|
64
|
+
if location:
|
|
65
|
+
return str(location)
|
|
66
|
+
if name.casefold() == "etag":
|
|
67
|
+
etag = getattr(response.http, "etag", None)
|
|
68
|
+
if etag:
|
|
69
|
+
return str(etag)
|
|
70
|
+
target = name.casefold()
|
|
71
|
+
for key, value in response.headers.items():
|
|
72
|
+
if key.casefold() == target:
|
|
73
|
+
return value
|
|
74
|
+
for key, value in response.http.headers.items():
|
|
75
|
+
if key.casefold() == target:
|
|
76
|
+
return value
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def body_mapping(value: object) -> dict[str, Any]:
|
|
81
|
+
"""Convert a request model or mapping when a facade must add safe fields."""
|
|
82
|
+
|
|
83
|
+
if isinstance(value, Mapping):
|
|
84
|
+
return dict(value)
|
|
85
|
+
model_dump = getattr(value, "model_dump", None)
|
|
86
|
+
if callable(model_dump):
|
|
87
|
+
return dict(
|
|
88
|
+
cast(Mapping[str, Any], model_dump(by_alias=True, exclude_unset=True, mode="python"))
|
|
89
|
+
)
|
|
90
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
91
|
+
return asdict(value)
|
|
92
|
+
raise TypeError("request value must be a mapping, dataclass, or Pydantic model")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def body_value(value: object) -> object:
|
|
96
|
+
"""Serialize nested request models without losing omitted/null semantics."""
|
|
97
|
+
|
|
98
|
+
if isinstance(value, Mapping):
|
|
99
|
+
return {key: body_value(item) for key, item in value.items()}
|
|
100
|
+
if isinstance(value, list | tuple):
|
|
101
|
+
return [body_value(item) for item in value]
|
|
102
|
+
model_dump = getattr(value, "model_dump", None)
|
|
103
|
+
if callable(model_dump):
|
|
104
|
+
return model_dump(by_alias=True, exclude_unset=True, mode="python")
|
|
105
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
106
|
+
return {key: body_value(item) for key, item in asdict(value).items()}
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True, slots=True)
|
|
111
|
+
class VersionedResource(Generic[T]):
|
|
112
|
+
"""A resource value paired with the exact server ETag used for writes."""
|
|
113
|
+
|
|
114
|
+
data: T
|
|
115
|
+
etag: str | None
|
|
116
|
+
http: HttpMetadataProtocol
|
|
117
|
+
|
|
118
|
+
def __getattr__(self, name: str) -> Any:
|
|
119
|
+
return getattr(self.data, name)
|
|
120
|
+
|
|
121
|
+
def __getitem__(self, key: object) -> Any:
|
|
122
|
+
return self.data[key] # type: ignore[index]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Resource(Generic[HttpInfoMode]):
|
|
126
|
+
OPERATION_IDS: frozenset[str] = frozenset()
|
|
127
|
+
|
|
128
|
+
def __init__(self, client: ClientProtocol, *, with_http_info: bool = False) -> None:
|
|
129
|
+
self._client = client
|
|
130
|
+
self._with_http_info = with_http_info
|
|
131
|
+
|
|
132
|
+
def _request(
|
|
133
|
+
self,
|
|
134
|
+
operation_id: str,
|
|
135
|
+
*,
|
|
136
|
+
path_params: Mapping[str, object] | None = None,
|
|
137
|
+
query: Mapping[str, object] | None = None,
|
|
138
|
+
headers: Mapping[str, str] | None = None,
|
|
139
|
+
json: object | None = None,
|
|
140
|
+
stream: bool = False,
|
|
141
|
+
timeout_budget: float | None = None,
|
|
142
|
+
) -> ApiResponse[Any]:
|
|
143
|
+
if timeout_budget is None:
|
|
144
|
+
return self._client.request(
|
|
145
|
+
operation_id,
|
|
146
|
+
path_params=path_params,
|
|
147
|
+
query=query,
|
|
148
|
+
headers=headers,
|
|
149
|
+
json=json,
|
|
150
|
+
stream=stream,
|
|
151
|
+
)
|
|
152
|
+
return self._client.request(
|
|
153
|
+
operation_id,
|
|
154
|
+
path_params=path_params,
|
|
155
|
+
query=query,
|
|
156
|
+
headers=headers,
|
|
157
|
+
json=json,
|
|
158
|
+
stream=stream,
|
|
159
|
+
timeout_budget=timeout_budget,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def _page(
|
|
163
|
+
self,
|
|
164
|
+
operation_id: str,
|
|
165
|
+
*,
|
|
166
|
+
odata: ODataQuery,
|
|
167
|
+
path_params: Mapping[str, object] | None = None,
|
|
168
|
+
extra_query: Mapping[str, object] | None = None,
|
|
169
|
+
) -> Page[Any] | ApiResponse[Page[Any]]:
|
|
170
|
+
response = self._request(
|
|
171
|
+
operation_id,
|
|
172
|
+
path_params=path_params,
|
|
173
|
+
query={**odata.to_params(), **(extra_query or {})},
|
|
174
|
+
)
|
|
175
|
+
page = Page.from_data(response.data, query=odata, http=response.http)
|
|
176
|
+
return self._finish(response, page)
|
|
177
|
+
|
|
178
|
+
def _page_value(
|
|
179
|
+
self,
|
|
180
|
+
operation_id: str,
|
|
181
|
+
*,
|
|
182
|
+
odata: ODataQuery,
|
|
183
|
+
path_params: Mapping[str, object] | None = None,
|
|
184
|
+
extra_query: Mapping[str, object] | None = None,
|
|
185
|
+
) -> Page[Any]:
|
|
186
|
+
"""Return an internal page regardless of the public response view."""
|
|
187
|
+
|
|
188
|
+
response = self._request(
|
|
189
|
+
operation_id,
|
|
190
|
+
path_params=path_params,
|
|
191
|
+
query={**odata.to_params(), **(extra_query or {})},
|
|
192
|
+
)
|
|
193
|
+
return Page.from_data(response.data, query=odata, http=response.http)
|
|
194
|
+
|
|
195
|
+
def _finish(self, response: ApiResponse[Any], value: T) -> T | ApiResponse[T]:
|
|
196
|
+
if self._with_http_info:
|
|
197
|
+
return response.with_data(value)
|
|
198
|
+
return value
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def require_positive_identifier(name: str, value: int) -> None:
|
|
202
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
203
|
+
raise TypeError(f"{name} must be an integer")
|
|
204
|
+
if value <= 0:
|
|
205
|
+
raise ValueError(f"{name} must be greater than zero")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def require_etag(if_match: str) -> None:
|
|
209
|
+
if not isinstance(if_match, str):
|
|
210
|
+
raise TypeError("if_match must be a string")
|
|
211
|
+
if not if_match.strip():
|
|
212
|
+
raise ValueError("if_match must not be empty")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Reusable collection method implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator, Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..odata import DEFAULT_PAGE_SIZE, ODataQuery
|
|
9
|
+
from ..pagination import Page, Paginator
|
|
10
|
+
from ._base import ApiResponseProtocol, HttpInfoMode, Resource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CollectionResource(Resource[HttpInfoMode]):
|
|
14
|
+
LIST_OPERATION_ID: str
|
|
15
|
+
|
|
16
|
+
def _list(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
filter: str | None = None,
|
|
20
|
+
order_by: str | None = None,
|
|
21
|
+
top: int | None = None,
|
|
22
|
+
skip: int | None = None,
|
|
23
|
+
count: bool | None = None,
|
|
24
|
+
path_params: Mapping[str, object] | None = None,
|
|
25
|
+
extra_query: Mapping[str, object] | None = None,
|
|
26
|
+
) -> Page[Any] | ApiResponseProtocol[Page[Any]]:
|
|
27
|
+
query = ODataQuery(filter=filter, order_by=order_by, top=top, skip=skip, count=count)
|
|
28
|
+
return self._page(
|
|
29
|
+
self.LIST_OPERATION_ID, odata=query, path_params=path_params, extra_query=extra_query
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def _iter_pages(
|
|
33
|
+
self,
|
|
34
|
+
*,
|
|
35
|
+
filter: str | None = None,
|
|
36
|
+
order_by: str | None = None,
|
|
37
|
+
count: bool | None = None,
|
|
38
|
+
start_skip: int = 0,
|
|
39
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
40
|
+
max_items: int | None = None,
|
|
41
|
+
path_params: Mapping[str, object] | None = None,
|
|
42
|
+
extra_query: Mapping[str, object] | None = None,
|
|
43
|
+
) -> Iterator[Page[Any]]:
|
|
44
|
+
base = ODataQuery(filter=filter, order_by=order_by, skip=start_skip, count=count)
|
|
45
|
+
paginator = Paginator(
|
|
46
|
+
fetch_page=lambda query: self._page_value(
|
|
47
|
+
self.LIST_OPERATION_ID,
|
|
48
|
+
odata=query,
|
|
49
|
+
path_params=path_params,
|
|
50
|
+
extra_query=extra_query,
|
|
51
|
+
),
|
|
52
|
+
query=base,
|
|
53
|
+
page_size=page_size,
|
|
54
|
+
max_items=max_items,
|
|
55
|
+
)
|
|
56
|
+
return paginator.iter_pages()
|
|
57
|
+
|
|
58
|
+
def _iter_all(
|
|
59
|
+
self,
|
|
60
|
+
*,
|
|
61
|
+
filter: str | None = None,
|
|
62
|
+
order_by: str | None = None,
|
|
63
|
+
count: bool | None = None,
|
|
64
|
+
start_skip: int = 0,
|
|
65
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
66
|
+
max_items: int | None = None,
|
|
67
|
+
path_params: Mapping[str, object] | None = None,
|
|
68
|
+
extra_query: Mapping[str, object] | None = None,
|
|
69
|
+
) -> Iterator[Any]:
|
|
70
|
+
pages = self._iter_pages(
|
|
71
|
+
filter=filter,
|
|
72
|
+
order_by=order_by,
|
|
73
|
+
count=count,
|
|
74
|
+
start_skip=start_skip,
|
|
75
|
+
page_size=page_size,
|
|
76
|
+
max_items=max_items,
|
|
77
|
+
path_params=path_params,
|
|
78
|
+
extra_query=extra_query,
|
|
79
|
+
)
|
|
80
|
+
return (item for page in pages for item in page.items)
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Shared implementation for the four documented export operation families."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from os import PathLike
|
|
8
|
+
from threading import Event
|
|
9
|
+
from typing import Any, Generic, TypeVar
|
|
10
|
+
|
|
11
|
+
from ..downloads import (
|
|
12
|
+
AddressResolver,
|
|
13
|
+
Downloader,
|
|
14
|
+
DownloadResult,
|
|
15
|
+
StorageUrlPolicy,
|
|
16
|
+
validate_storage_url,
|
|
17
|
+
)
|
|
18
|
+
from ..errors import RedBlackError
|
|
19
|
+
from ..polling import Poller, PollOutcome
|
|
20
|
+
from ._base import (
|
|
21
|
+
ApiResponseProtocol,
|
|
22
|
+
ClientProtocol,
|
|
23
|
+
HttpInfoMode,
|
|
24
|
+
Resource,
|
|
25
|
+
enum_value,
|
|
26
|
+
read_member,
|
|
27
|
+
require_positive_identifier,
|
|
28
|
+
response_header,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
T = TypeVar("T")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class WorkflowProtocolError(RedBlackError):
|
|
35
|
+
"""A successful workflow response omitted data required by its contract."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class ExportOperations:
|
|
40
|
+
create: str
|
|
41
|
+
status: str
|
|
42
|
+
file: str
|
|
43
|
+
body_required: bool
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse_datetime(value: object) -> datetime | None:
|
|
47
|
+
if isinstance(value, datetime):
|
|
48
|
+
return value
|
|
49
|
+
if not isinstance(value, str) or not value:
|
|
50
|
+
return None
|
|
51
|
+
normalized = value[:-1] + "+00:00" if value.endswith("Z") else value
|
|
52
|
+
try:
|
|
53
|
+
return datetime.fromisoformat(normalized)
|
|
54
|
+
except ValueError:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(slots=True)
|
|
59
|
+
class ExportJobHandle(Generic[T]):
|
|
60
|
+
"""A client-bound handle for one RedBlack export job."""
|
|
61
|
+
|
|
62
|
+
_client: ClientProtocol
|
|
63
|
+
job_id: int
|
|
64
|
+
_operations: ExportOperations
|
|
65
|
+
latest: T | None = None
|
|
66
|
+
|
|
67
|
+
def __post_init__(self) -> None:
|
|
68
|
+
require_positive_identifier("job_id", self.job_id)
|
|
69
|
+
|
|
70
|
+
def _refresh_with_timeout(self, timeout_budget: float | None) -> T:
|
|
71
|
+
if timeout_budget is None:
|
|
72
|
+
return self.refresh()
|
|
73
|
+
response = self._client.request(
|
|
74
|
+
self._operations.status,
|
|
75
|
+
path_params={"jobId": self.job_id},
|
|
76
|
+
timeout_budget=timeout_budget,
|
|
77
|
+
)
|
|
78
|
+
self.latest = response.data
|
|
79
|
+
return response.data
|
|
80
|
+
|
|
81
|
+
def refresh(self) -> T:
|
|
82
|
+
response = self._client.request(self._operations.status, path_params={"jobId": self.job_id})
|
|
83
|
+
self.latest = response.data
|
|
84
|
+
return response.data
|
|
85
|
+
|
|
86
|
+
status = refresh
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _classify(value: object) -> PollOutcome:
|
|
90
|
+
status = enum_value(value, "job_status", "jobStatus", "export_status", "exportStatus")
|
|
91
|
+
if status == 3:
|
|
92
|
+
return PollOutcome.SUCCEEDED
|
|
93
|
+
if status in {4, 5}:
|
|
94
|
+
return PollOutcome.FAILED
|
|
95
|
+
return PollOutcome.PENDING
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def _suggested_delay(value: object) -> float | None:
|
|
99
|
+
target = _parse_datetime(
|
|
100
|
+
read_member(value, "suggested_next_poll_time", "suggestedNextPollTime")
|
|
101
|
+
)
|
|
102
|
+
if target is None:
|
|
103
|
+
return None
|
|
104
|
+
if target.tzinfo is None:
|
|
105
|
+
target = target.replace(tzinfo=UTC)
|
|
106
|
+
return max(0.0, (target - datetime.now(UTC)).total_seconds())
|
|
107
|
+
|
|
108
|
+
def wait(
|
|
109
|
+
self,
|
|
110
|
+
*,
|
|
111
|
+
timeout: float | None = 300.0,
|
|
112
|
+
poll_interval: float = 1.0,
|
|
113
|
+
max_interval: float = 30.0,
|
|
114
|
+
backoff_factor: float = 1.5,
|
|
115
|
+
stop_event: Event | None = None,
|
|
116
|
+
raise_on_failure: bool = True,
|
|
117
|
+
) -> T:
|
|
118
|
+
return Poller(
|
|
119
|
+
refresh=self.refresh,
|
|
120
|
+
classify=self._classify,
|
|
121
|
+
suggested_delay=self._suggested_delay,
|
|
122
|
+
refresh_with_timeout=self._refresh_with_timeout,
|
|
123
|
+
).wait(
|
|
124
|
+
timeout=timeout,
|
|
125
|
+
poll_interval=poll_interval,
|
|
126
|
+
max_interval=max_interval,
|
|
127
|
+
backoff_factor=backoff_factor,
|
|
128
|
+
stop_event=stop_event,
|
|
129
|
+
raise_on_failure=raise_on_failure,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def file_response(self) -> ApiResponseProtocol[None]:
|
|
133
|
+
"""Request the RedBlack file endpoint without following its redirect."""
|
|
134
|
+
|
|
135
|
+
return self._client.request(
|
|
136
|
+
self._operations.file,
|
|
137
|
+
path_params={"jobId": self.job_id},
|
|
138
|
+
stream=True,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def download_url(
|
|
142
|
+
self,
|
|
143
|
+
*,
|
|
144
|
+
policy: StorageUrlPolicy | None = None,
|
|
145
|
+
resolver: AddressResolver | None = None,
|
|
146
|
+
) -> str:
|
|
147
|
+
"""Obtain and validate the five-minute storage URL."""
|
|
148
|
+
|
|
149
|
+
response = self.file_response()
|
|
150
|
+
location = response_header(response, "Location")
|
|
151
|
+
if not location:
|
|
152
|
+
raise WorkflowProtocolError("export file response omitted Location")
|
|
153
|
+
return validate_storage_url(location, policy=policy, resolver=resolver)
|
|
154
|
+
|
|
155
|
+
def download_to(
|
|
156
|
+
self,
|
|
157
|
+
destination: str | PathLike[str],
|
|
158
|
+
*,
|
|
159
|
+
overwrite: bool = False,
|
|
160
|
+
policy: StorageUrlPolicy | None = None,
|
|
161
|
+
download_client: object | None = None,
|
|
162
|
+
resolver: AddressResolver | None = None,
|
|
163
|
+
) -> DownloadResult:
|
|
164
|
+
"""Download with a credential-free client and atomic file publication."""
|
|
165
|
+
|
|
166
|
+
active_policy = policy or StorageUrlPolicy()
|
|
167
|
+
url = self.download_url(policy=active_policy, resolver=resolver)
|
|
168
|
+
downloader = Downloader(policy=active_policy, client=download_client, resolver=resolver)
|
|
169
|
+
return downloader.download_to(url, destination, overwrite=overwrite)
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def has_failures(self) -> bool:
|
|
173
|
+
if self.latest is None:
|
|
174
|
+
return False
|
|
175
|
+
fault_reason = read_member(self.latest, "fault_reason", "faultReason")
|
|
176
|
+
return bool(fault_reason) or self._classify(self.latest) is PollOutcome.FAILED
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class ExportResource(Resource[HttpInfoMode]):
|
|
180
|
+
EXPORT_OPERATIONS: ExportOperations
|
|
181
|
+
|
|
182
|
+
def _create_export(
|
|
183
|
+
self, *, filter: str | None = None
|
|
184
|
+
) -> ExportJobHandle[Any] | ApiResponseProtocol[ExportJobHandle[Any]]:
|
|
185
|
+
if filter is not None:
|
|
186
|
+
if not isinstance(filter, str):
|
|
187
|
+
raise TypeError("filter must be a string")
|
|
188
|
+
if not filter.strip():
|
|
189
|
+
raise ValueError("filter must not be empty")
|
|
190
|
+
body: object | None = {"filter": filter}
|
|
191
|
+
elif self.EXPORT_OPERATIONS.body_required:
|
|
192
|
+
# Restriction/equivalent requestBody is required even though its
|
|
193
|
+
# only property is optional.
|
|
194
|
+
body = {}
|
|
195
|
+
else:
|
|
196
|
+
body = None
|
|
197
|
+
response = self._request(self.EXPORT_OPERATIONS.create, json=body)
|
|
198
|
+
job_id = read_member(response.data, "id")
|
|
199
|
+
if isinstance(job_id, bool) or not isinstance(job_id, int):
|
|
200
|
+
raise WorkflowProtocolError("export create response omitted integer id")
|
|
201
|
+
handle = ExportJobHandle(
|
|
202
|
+
self._client,
|
|
203
|
+
job_id,
|
|
204
|
+
self.EXPORT_OPERATIONS,
|
|
205
|
+
latest=response.data,
|
|
206
|
+
)
|
|
207
|
+
return self._finish(response, handle)
|
|
208
|
+
|
|
209
|
+
def _get_export(
|
|
210
|
+
self, job_id: int
|
|
211
|
+
) -> ExportJobHandle[Any] | ApiResponseProtocol[ExportJobHandle[Any]]:
|
|
212
|
+
require_positive_identifier("job_id", job_id)
|
|
213
|
+
response = self._request(self.EXPORT_OPERATIONS.status, path_params={"jobId": job_id})
|
|
214
|
+
handle = ExportJobHandle(self._client, job_id, self.EXPORT_OPERATIONS, latest=response.data)
|
|
215
|
+
return self._finish(response, handle)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Accounts resource operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from typing import Literal, overload
|
|
7
|
+
|
|
8
|
+
from ..models import Account, AccountExportJob
|
|
9
|
+
from ..odata import DEFAULT_PAGE_SIZE
|
|
10
|
+
from ..pagination import Page
|
|
11
|
+
from ._base import ApiResponseProtocol, HttpInfoMode
|
|
12
|
+
from ._collections import CollectionResource
|
|
13
|
+
from ._exports import ExportJobHandle, ExportOperations, ExportResource
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AccountsResource(CollectionResource[HttpInfoMode], ExportResource[HttpInfoMode]):
|
|
17
|
+
LIST_OPERATION_ID = "Accounts_QueryAccounts"
|
|
18
|
+
EXPORT_OPERATIONS = ExportOperations(
|
|
19
|
+
create="Accounts_Export",
|
|
20
|
+
status="Accounts_GetBulkExportSessionStatus",
|
|
21
|
+
file="Accounts_GetBulkExportFile",
|
|
22
|
+
body_required=False,
|
|
23
|
+
)
|
|
24
|
+
OPERATION_IDS = frozenset(
|
|
25
|
+
{
|
|
26
|
+
LIST_OPERATION_ID,
|
|
27
|
+
EXPORT_OPERATIONS.create,
|
|
28
|
+
EXPORT_OPERATIONS.status,
|
|
29
|
+
EXPORT_OPERATIONS.file,
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@overload
|
|
34
|
+
def list(
|
|
35
|
+
self: AccountsResource[Literal[False]],
|
|
36
|
+
*,
|
|
37
|
+
filter: str | None = None,
|
|
38
|
+
order_by: str | None = None,
|
|
39
|
+
top: int | None = None,
|
|
40
|
+
skip: int | None = None,
|
|
41
|
+
count: bool | None = None,
|
|
42
|
+
) -> Page[Account]: ...
|
|
43
|
+
|
|
44
|
+
@overload
|
|
45
|
+
def list(
|
|
46
|
+
self: AccountsResource[Literal[True]],
|
|
47
|
+
*,
|
|
48
|
+
filter: str | None = None,
|
|
49
|
+
order_by: str | None = None,
|
|
50
|
+
top: int | None = None,
|
|
51
|
+
skip: int | None = None,
|
|
52
|
+
count: bool | None = None,
|
|
53
|
+
) -> ApiResponseProtocol[Page[Account]]: ...
|
|
54
|
+
|
|
55
|
+
def list(
|
|
56
|
+
self,
|
|
57
|
+
*,
|
|
58
|
+
filter: str | None = None,
|
|
59
|
+
order_by: str | None = None,
|
|
60
|
+
top: int | None = None,
|
|
61
|
+
skip: int | None = None,
|
|
62
|
+
count: bool | None = None,
|
|
63
|
+
) -> Page[Account] | ApiResponseProtocol[Page[Account]]:
|
|
64
|
+
return self._list(filter=filter, order_by=order_by, top=top, skip=skip, count=count)
|
|
65
|
+
|
|
66
|
+
def iter_pages(
|
|
67
|
+
self,
|
|
68
|
+
*,
|
|
69
|
+
filter: str | None = None,
|
|
70
|
+
order_by: str | None = None,
|
|
71
|
+
count: bool | None = None,
|
|
72
|
+
start_skip: int = 0,
|
|
73
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
74
|
+
max_items: int | None = None,
|
|
75
|
+
) -> Iterator[Page[Account]]:
|
|
76
|
+
return self._iter_pages(
|
|
77
|
+
filter=filter,
|
|
78
|
+
order_by=order_by,
|
|
79
|
+
count=count,
|
|
80
|
+
start_skip=start_skip,
|
|
81
|
+
page_size=page_size,
|
|
82
|
+
max_items=max_items,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def iter_all(
|
|
86
|
+
self,
|
|
87
|
+
*,
|
|
88
|
+
filter: str | None = None,
|
|
89
|
+
order_by: str | None = None,
|
|
90
|
+
count: bool | None = None,
|
|
91
|
+
start_skip: int = 0,
|
|
92
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
93
|
+
max_items: int | None = None,
|
|
94
|
+
) -> Iterator[Account]:
|
|
95
|
+
return self._iter_all(
|
|
96
|
+
filter=filter,
|
|
97
|
+
order_by=order_by,
|
|
98
|
+
count=count,
|
|
99
|
+
start_skip=start_skip,
|
|
100
|
+
page_size=page_size,
|
|
101
|
+
max_items=max_items,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@overload
|
|
105
|
+
def create_export(
|
|
106
|
+
self: AccountsResource[Literal[False]], *, filter: str | None = None
|
|
107
|
+
) -> ExportJobHandle[AccountExportJob]: ...
|
|
108
|
+
|
|
109
|
+
@overload
|
|
110
|
+
def create_export(
|
|
111
|
+
self: AccountsResource[Literal[True]], *, filter: str | None = None
|
|
112
|
+
) -> ApiResponseProtocol[ExportJobHandle[AccountExportJob]]: ...
|
|
113
|
+
|
|
114
|
+
def create_export(
|
|
115
|
+
self, *, filter: str | None = None
|
|
116
|
+
) -> ExportJobHandle[AccountExportJob] | ApiResponseProtocol[ExportJobHandle[AccountExportJob]]:
|
|
117
|
+
return self._create_export(filter=filter)
|
|
118
|
+
|
|
119
|
+
@overload
|
|
120
|
+
def get_export(
|
|
121
|
+
self: AccountsResource[Literal[False]], job_id: int
|
|
122
|
+
) -> ExportJobHandle[AccountExportJob]: ...
|
|
123
|
+
|
|
124
|
+
@overload
|
|
125
|
+
def get_export(
|
|
126
|
+
self: AccountsResource[Literal[True]], job_id: int
|
|
127
|
+
) -> ApiResponseProtocol[ExportJobHandle[AccountExportJob]]: ...
|
|
128
|
+
|
|
129
|
+
def get_export(
|
|
130
|
+
self, job_id: int
|
|
131
|
+
) -> ExportJobHandle[AccountExportJob] | ApiResponseProtocol[ExportJobHandle[AccountExportJob]]:
|
|
132
|
+
return self._get_export(job_id)
|